Compare commits

...

278 Commits

Author SHA1 Message Date
60b72aabe8 fix test 2018-11-24 13:38:23 +09:00
c8fcb0ab18 Use framework dependent build for ARM 2018-11-23 16:14:13 +09:00
9911d18390 Do not push latest images to dockerhub 2018-11-23 14:12:47 +09:00
e24630ac1e Remove qemu install requirement for the host 2018-11-23 14:08:14 +09:00
4c1fd3edae More comment on ARM build 2018-11-23 14:02:44 +09:00
f65492dd66 Use stretch slim for arm 2018-11-23 14:00:33 +09:00
5d978c7670 Use manifest image for building arm images 2018-11-23 13:58:21 +09:00
11788cece9 No need to create latest tag 2018-11-23 13:18:35 +09:00
1aaa55dc62 Make test less flaky 2018-11-23 13:09:30 +09:00
ce57a2b8fb Do not tag latest 2018-11-23 12:59:48 +09:00
0604cc5bd0 bump 2018-11-23 11:37:05 +09:00
3d2c0bcc6c Use specific sdk and runtime version for arm 2018-11-23 11:23:27 +09:00
0f222979a6 CircleCI multiarch Docker images ()
* Preparing final version of CircleCI docker building

* Removing test job requirement for building Docker images

We'll already monitor build before tagging, would be too many checks

* Adding pushing of manifest for tag

* Easy access to docker/circle config files for edit

* Generalizing script with $DOCKERHUB_REPO variable
2018-11-23 11:21:01 +09:00
a1eb6a14f5 Fix all script because of docker-compose team screwing up (https://github.com/docker/compose/issues/6316) 2018-11-22 16:16:10 +09:00
186ce01022 add pairing code to tokens page after authorize () 2018-11-22 15:13:35 +09:00
0096ec1d12 bump nbxplorer 2018-11-21 20:41:51 +09:00
2929d7bf51 Fixing MONA_BTC rate breaking tests and CircleCI because zaif is down 2018-11-20 15:42:45 -06:00
d90fb5764d Add noindex,nofollow to invoices, checkouts and fix create invoice ui bug ()
* add noindex,nofollow on invoices

* fix create invoice button and add noindex,nofollow to checkout and invoice pages
2018-11-19 13:20:48 +09:00
4dccd0c733 Add better instruction on how to customize the theme 2018-11-17 12:43:11 +09:00
300d912331 bump 2018-11-17 11:43:41 +09:00
9d21c89151 Preserving title with custom amount ()
* Preserving title with custom amount

* Custom button texts for complete localization

* Update tests, now checking custom amount description and button text

* Support for Custom CSS in POS
2018-11-17 11:39:43 +09:00
24a8c4015c Bump 2018-11-17 11:35:20 +09:00
5eb40d6b7f Bugfixing redirect button () 2018-11-17 11:32:31 +09:00
36f486e91b Add test for the parser 2018-11-17 01:45:59 +09:00
5b684ac26e Make really sure we don't generate segwit addresses for non segwit coins 2018-11-17 01:39:32 +09:00
85062725bd bump 2018-11-17 01:21:56 +09:00
401d9c8565 DerivationSchemeParser should not override a label 2018-11-17 01:21:34 +09:00
6f276ac1bc Do not crash if derivation strategy is empty 2018-11-17 01:09:28 +09:00
4350785cef Remove double slash 2018-11-17 00:23:51 +09:00
9a2a85ac3d Update translations 2018-11-17 00:22:18 +09:00
d030a61322 bump 2018-11-17 00:16:31 +09:00
dacb6dca41 bump .net core 2018-11-17 00:13:22 +09:00
c40fc69087 Use the choiceKey of PoS item as ItemCode 2018-11-16 23:16:44 +09:00
eff983135c showcase the custom field in PoS template 2018-11-16 18:36:18 +09:00
479303dd9e Tweaking UI for custom amounts ()
* Tweaking appearance of custom amount card

* Allowing POS items to have custom amounts, good for donations/tips

* Prepending currency symbol in POS

* Fixing regression, thanks unit test
2018-11-16 12:31:38 +09:00
e9b2088f7d change default title for pointofsale 2018-11-14 17:45:46 +09:00
4af5b94013 Add tooling which pull transifex translation automatically, add Slovenčina. (Close ) 2018-11-14 16:48:25 +09:00
441398402d Remove global.json because .403 became suddenly unavailable 2018-11-13 16:41:49 +09:00
258d4fda3f bump 2018-11-13 16:37:43 +09:00
8e667f6c3f Allow empty template (Fix ) 2018-11-13 16:32:13 +09:00
a996cc2e6d Fix margins, change template () 2018-11-13 16:29:18 +09:00
9b8a8690e7 Change links to gitbook 2018-11-13 16:21:58 +09:00
f2387fd6b5 Workaround to compile on circle 2018-11-13 16:16:57 +09:00
888036a99d use docker on Circle CI 2018-11-13 15:55:10 +09:00
539c0ed7f0 show dotnet info on CI 2018-11-13 15:47:25 +09:00
95e065a462 Add tooltip to update store () 2018-11-13 15:36:07 +09:00
087f20cb6c Fix small view error in logs () 2018-11-12 22:25:39 +09:00
7adf321956 Checkout Experience Language Setting ()
* fix check out experience default language validation of preset value not found

* Update CheckoutExperienceViewModel.cs
2018-11-12 22:17:00 +09:00
dc749462ec automatically detect the btcpay server url in btcpay.js 2018-11-10 23:43:48 +09:00
16b57f24a2 Fix 2018-11-10 23:25:11 +09:00
b16b1c3e8b - add item image and description ()
- fix margins
2018-11-10 15:38:26 +09:00
fee56873b5 Handle exception if log file do not exists. 2018-11-09 21:43:10 +09:00
e1b2b72cd2 bump 2018-11-09 21:16:09 +09:00
daf4e5ce6c I am sorry for so many prs <3 ()
* make language loading more solid

* disable browser lang preferences

* pr fix

* pr fixes

* pr fixes

* make sure language files are named correctly

* fix dropdown width issue when in modal form

* fix issue from jquery hell
2018-11-09 21:13:00 +09:00
2ec2c7263f Make language loading more efficient and solid ()
* make language loading more solid

* disable browser lang preferences

* pr fix

* pr fixes

* pr fixes
2018-11-09 19:02:53 +09:00
abfcab552f bump () 2018-11-09 17:34:30 +09:00
cfdf8b1670 Example modal in invoice list () 2018-11-09 17:13:45 +09:00
f23e2a3ec4 async i18n and json translation format ()
* start working on loading locales async and as json

* finish off langs and UI

* fix path

* fix tests
2018-11-09 16:48:38 +09:00
aa1ac3da50 Modal invoice through btcpay.js ()
* Modal through btcpay.js

* Handling close action depending on whether is modal or not

* Tweaking button position

* Stripping trailing slashes if present when setting site root
2018-11-09 16:09:09 +09:00
c9c7316b7d Logs UI in Server Admin ()
* add in ui

* add in logging viewer

* Revert "add in ui"

This reverts commit 9614721fa8a439f7097adca69772b5d41f6585d6.

* finish basic feature

* clean up

* improve and fix build

* add in debug log level command option

* use paging for log file list, use extension to select log files, show message for setting up logging

* make paging a little better

* add very basic UT for logs

* Update ServerController.cs
2018-11-07 22:29:35 +09:00
d152d5cd90 fix build 2018-11-06 16:08:42 +09:00
6fd37710e1 Rename validators namespace 2018-11-06 15:38:07 +09:00
0419a3c19a do not affect Buyer for every paymentid 2018-11-05 17:37:55 +09:00
0c382da561 Show unconf transactions with low opacity 2018-11-05 17:26:49 +09:00
9fc7f287d2 Expose buyer object to conform to bitpay API 2018-11-05 17:02:12 +09:00
dd7c4850f0 bump nbxplorer 2018-11-05 14:12:05 +09:00
93992ec3ed bump 2018-11-05 12:15:05 +09:00
15d9adfbf1 Fix rate fetching for kraken doge and dash 2018-11-05 12:14:39 +09:00
676a914c40 Fix, allow rescan if other crypto nodes are not synched 2018-11-04 22:46:27 +09:00
b423b4eec1 Do not allow rescan of wallet which are not segwit 2018-11-04 14:59:28 +09:00
9784a89112 limit apdu size to ledger 2018-11-04 00:36:48 +09:00
7b596e6d9c Add Bitcore BTX support () 2018-11-03 13:42:17 +09:00
76febcf238 bump NBXplorer.Client ()
NBXplorer.Client Version 1.0.3.5 is available: da7df86019
2018-11-02 21:38:41 +09:00
a57a72de88 bump clightning 2018-11-02 19:02:07 +09:00
235b307b06 bump deps 2018-11-02 18:05:48 +09:00
05b0f6d0f7 Fix invoice search not working on transaction id 2018-11-02 14:26:13 +09:00
1d7081d8b8 bump 2018-11-01 21:51:16 +09:00
c0174c0c2c inverse DASH rate 2018-11-01 16:33:53 +09:00
fa8324c1f9 Fix DASH rate for kraken 2018-11-01 14:48:46 +09:00
4b0951caec trim destination in WalletSend 2018-11-01 12:54:25 +09:00
0d51c99717 Properly configure the logger to log what happen in ConfigureServices, add https profile adapted for debugging ledger wallet. 2018-11-01 12:52:32 +09:00
24623c59d7 Adjusted mechanism for setting https binding configuration option ()
* Adjusted mechanism for setting https binding configuration option.

* Modified the https binding logic to use default bind and port options.

* Removed dedicated https certification config properties and instead used direct access via setting name.
2018-11-01 12:07:28 +09:00
88044f6b76 Decouple Wallet Send screen from Ledger Wallet 2018-11-01 00:19:25 +09:00
38edbf8362 Improve token UX (Fix ) 2018-10-31 17:59:09 +09:00
bc0acf5701 make test more reliable 2018-10-31 16:57:31 +09:00
a82f181126 Reactivate cryptopia 2018-10-31 13:31:03 +09:00
be0139a46f bump 2018-10-31 13:06:36 +09:00
4db5b4f2b1 Wait for the nodes to be fully synched before starting tests 2018-10-31 13:06:17 +09:00
93cefced80 bump .NET core and dependencies 2018-10-31 13:03:12 +09:00
85f586f623 bump dependencies 2018-10-31 11:56:21 +09:00
2be1f97419 Remove cryptopedia as direct provider, add estimated time to wallet rescan page, bump nbx 2018-10-30 15:40:27 +09:00
63014231ab Revert "Added configuration options for BtcPayServer https binding. ()"
This reverts commit 3ac37497ab9b5ff2c28eaab54c7f2a12356659dd.
2018-10-30 00:25:05 +09:00
3ac37497ab Added configuration options for BtcPayServer https binding. () 2018-10-30 00:11:02 +09:00
d0cafb020f Add an invoices list to store list 2018-10-29 12:44:20 +09:00
d3b3198b68 For lightning payments tests, add small delay after creating the invoice before sending the payment 2018-10-29 00:22:30 +09:00
c1f17ff63b Add some test logs to flaky test 2018-10-28 23:43:48 +09:00
dafd958f69 bump 2018-10-28 23:07:58 +09:00
f51af6c61c fix issue with changelly rates and cover with UTs () 2018-10-28 23:07:36 +09:00
254db22063 Change test trait name 2018-10-28 22:51:02 +09:00
8be4256278 Fix unreliable tests 2018-10-28 22:46:03 +09:00
8e8669d63f Warning as errors 2018-10-28 22:15:32 +09:00
4625ff92f1 Run unreliable tests, attempt to make them a bit more reliable 2018-10-28 22:10:37 +09:00
6aa84326af Make sure tests run sequentially 2018-10-28 21:46:12 +09:00
9a384d81fe Run only dev time containers 2018-10-28 21:25:42 +09:00
0cbe36c048 Run reliable tests, remove the docker build 2018-10-28 21:19:18 +09:00
7f16aa8c7e Run only fast tests on CI 2018-10-28 20:59:59 +09:00
872f8a6229 Add circleCI badge 2018-10-28 20:28:16 +09:00
9b261daa6d Add circleci file 2018-10-28 20:06:04 +09:00
c46c15c258 Fix changelly tests 2018-10-28 01:10:07 +09:00
a8ba1ed1ed Removing Kukks changelly credential from the source code 2018-10-28 01:02:24 +09:00
ff4056d4f3 bump 2018-10-27 23:32:04 +09:00
ae152c3ffa bump NBXplorer 2018-10-27 23:30:57 +09:00
e2ff33d7db Document how to test mysql 2018-10-27 23:20:50 +09:00
ce94c05fd3 MySQL Support ()
* MySQL EF support added using Pomelo MySQL provider.

* MySQL EF support added using Pomelo MySQL provider.
2018-10-27 23:15:21 +09:00
9cde4dc7e2 Restart containers if crash 2018-10-27 23:14:26 +09:00
ca571cd756 Add authorization on WalletRescan 2018-10-27 22:52:09 +09:00
e5eb0c79c0 Exposing LND Rest, providing info in Server/Services ()
* Displaying LND Rest connection info in Services

* Code cleanup

* Tweaking UI

* Fix typo
2018-10-27 22:49:39 +09:00
43bd6587d3 re-enable changelly 2018-10-27 22:41:37 +09:00
3bb059ab74 refactor changelly & improve tests () 2018-10-27 22:41:07 +09:00
4c963d6edf bump 2018-10-26 23:10:45 +09:00
396bc7f7b4 Commenting Changelly 2018-10-26 23:10:29 +09:00
2896a9b26f Add ScanUTXOSet support 2018-10-26 23:07:39 +09:00
9267a45449 Remove useless test 2018-10-26 19:07:19 +09:00
c430d470c4 Fix warnings and bump nbxplorer 2018-10-26 19:06:06 +09:00
3921a3ca22 Fix warnings, update libs 2018-10-26 18:36:58 +09:00
1ff0a98d30 Adding Ukrainian Translation () 2018-10-24 15:18:31 +09:00
f0efd52cb7 Adding Kazakh Language () 2018-10-24 15:17:09 +09:00
bb8fa88688 Adding Vietnamese () 2018-10-24 15:16:30 +09:00
4b976c13c1 Changelly v2 ()
* Disable shapeshift and use changelly

* UI to manage changelly payment method

* wip on changelly api

* Add in Vue component for changelly and remove target currency from payment method

* add changelly merhcant id

* Small fixes to get Conversion to load

* wip fixing the component

* fix merge conflict

* fixes to UI

* remove debug, fix fee calc and move changelly to own partials

* Update ChangellyController.cs

* move original vue setup back to checkout

* Update core.js

* Extracting Changelly component to js file

* Proposal for loading spinner

* remove zone

* imrpove changelly ui

* add in changelly config checks

* try new method to calculate amount + remove to currency from list

* abstract changelly lofgic to provider and reduce dependency on js component

* Add UTs for Changelly

* refactor changelly backend

* fix failing UT

* add shitcoin tax

* pr changes

* pr changes

* WIP: getting rid of changelly dependency

* client caching, compiling code, cleaner code

* Cleaner changelly

* fiat!

* updat i18n, css and error styler

* default keys

* pr changes part 1

* part2

* fix tests

* fix loader alignment and retry button responsiveness

* final pr change
2018-10-24 14:52:19 +09:00
f68d4efcdd update to 0.17.0 2018-10-19 19:05:12 +09:00
fea247b218 Fixing broken link in Wallets/WalletSend.cshtml ()
Removing the earlier Yubico link, since it's broken and the article no longer exists.
Furthermore, I tested this integration with other U2F supporting browsers (Firefox Nightly, Firefox) and it only works in Google Chrome, so I suggest we only suggest what works, and for now, that's Chrome only.
2018-10-18 12:43:41 +09:00
f419c56a3c Revert "Changelly Support ()"
This reverts commit a5fca7a1c43c4d23ad1a825253fa1fe3ab26677c.
2018-10-18 12:27:46 +09:00
a5fca7a1c4 Changelly Support ()
* Disable shapeshift and use changelly

* UI to manage changelly payment method

* wip on changelly api

* Add in Vue component for changelly and remove target currency from payment method

* add changelly merhcant id

* Small fixes to get Conversion to load

* wip fixing the component

* fix merge conflict

* fixes to UI

* remove debug, fix fee calc and move changelly to own partials

* Update ChangellyController.cs

* move original vue setup back to checkout

* Update core.js

* Extracting Changelly component to js file

* Proposal for loading spinner

* remove zone

* imrpove changelly ui

* add in changelly config checks

* try new method to calculate amount + remove to currency from list

* abstract changelly lofgic to provider and reduce dependency on js component

* Add UTs for Changelly

* refactor changelly backend

* fix failing UT

* add shitcoin tax

* pr changes

* pr changes
2018-10-18 12:13:39 +09:00
e18d0b5d51 Updating Yaml () 2018-10-17 13:30:43 +09:00
9952cdca7f bump 2018-10-17 12:06:37 +09:00
6278145374 Removing old QR update code () 2018-10-17 11:55:49 +09:00
84018a5caa Bugfixing race condition for QR code switch ()
Ref: 
2018-10-17 11:49:30 +09:00
d7785fe2d2 Added Serilog file logger for debug file support. () 2018-10-16 00:37:42 +09:00
e1751c4d91 [Fix] Querying rate with authenticated request should be successfull 2018-10-15 18:11:20 +09:00
913da79ff4 Remove dups lang 2018-10-14 21:35:21 +09:00
a4fbb2de7e creating italian localization file () 2018-10-14 21:34:15 +09:00
b5601ed5e6 fix typo () 2018-10-14 21:28:09 +09:00
42c4f15f22 fixed typos () 2018-10-14 21:26:47 +09:00
6fbd9b2628 bump 2018-10-12 14:02:27 +09:00
d04bfb58a2 Resolving issue with long translations breaking layout () 2018-10-12 14:01:44 +09:00
cded2548f5 bump 2018-10-12 13:32:04 +09:00
dcc859a86a Disable export to JSON 2018-10-12 13:17:38 +09:00
9bec38559f Nepali Translation ()
* Nepali Language

* Delete Checkout.cshtml

* Delete LanguageService.cs

* add np.js

* Match Language File Style
2018-10-12 10:11:03 +09:00
2856454d41 [langs-fr] Correction and some minor improvement () 2018-10-12 10:10:42 +09:00
b3c4fc4003 Added Russian () 2018-10-12 10:10:20 +09:00
c2bbc04c4c Various bugfixes ()
* NotifyEmail field on Invoice, sending email when triggered

* Styling invoices page

* Exporting Invoices in JSON

* Recoding based on feedback

* Fixing image breaking responsive layout on mobile

* Reducing amount of data sent in email notification

* Turning bundling on by default
2018-10-12 10:09:13 +09:00
db40c7bc32 Solving the new version of btcpayserver caused btcpay-python not to create an order problem () 2018-10-11 23:50:28 +09:00
60707fdbd1 Add simplified chinese translation ()
* Add Chinese Simplified translation

* Add Chinese simplified translation
2018-10-11 14:25:16 +09:00
f05614f4da bump 2018-10-11 00:51:43 +09:00
a10c382bd4 [Tests] return WalletId when registering scheme 2018-10-10 00:13:37 +09:00
da2fb876cb Can pass pre filled amount and address to Send Wallet 2018-10-09 23:48:14 +09:00
3c58bff803 Comparable WalletId 2018-10-09 23:44:32 +09:00
a28814bc0e Fix RateRules crash if dups 2018-10-09 23:43:03 +09:00
3cff8261ae Set the id for apps only to 20 bytes, and output the Id in the controller so we can use it in tests 2018-10-09 23:38:56 +09:00
b16e8f7b76 Move GetAppDataIfOwner inside AppHelper 2018-10-09 23:34:51 +09:00
57ab001c0c [Tests] Pass Port to the fake http context 2018-10-09 23:32:47 +09:00
3d85dace38 Move currency display method into CurrencyNameTable 2018-10-09 23:30:26 +09:00
7a04c2974f Center QR Authenticator QR Code 2018-10-09 23:30:26 +09:00
d459839bf7 Displaying Node Info as QR code for Lightning payments ()
* Styling elements required for Node info

* Allowing switching QR between bolt11 and node info for lightning

* Equal width for Bolt11 and Node info buttons

* Certain languages were too verbose for display of "Pay with"

Fixes: 
2018-10-08 07:19:55 +09:00
657cfe1b23 bump 2018-10-06 23:21:33 +09:00
f4eaa0f01f Make sure X-Forwarded-Port does not override ExternalUrl 2018-10-06 23:20:32 +09:00
1e2ffcadf0 Add GRS lightning image () 2018-10-03 11:25:52 +09:00
dcc05af02e fix typo 2018-10-02 22:11:01 +09:00
4b7f78f38b document .net version update 2018-10-02 22:06:06 +09:00
f94ff4cc74 Update dotnet version + nbxplorer 2018-10-02 19:33:25 +09:00
b750663a1f update lnd/clightning 2018-09-28 17:08:51 +09:00
4c4b76e995 Remove Bitpay direct provider 2018-09-28 17:08:51 +09:00
da19d2c1a7 Bugfixing display of custom amount entry in POS ()
Ref: 
2018-09-28 13:31:59 +09:00
fb15c5b354 Increase timeout for wallet, update references 2018-09-28 10:15:35 +09:00
6ffe1cfcab bump LedgerWallet lib 2018-09-27 16:31:33 +09:00
87678c58ac Fix error message for ledger signing 2018-09-27 15:43:34 +09:00
feab4cc48a update ledgerwebsocket 2018-09-27 15:21:26 +09:00
712946f512 bump 2018-09-22 05:16:24 -05:00
a7bfceae05 Reverting to 2.1.0 until we update docker images 2018-09-22 05:14:36 -05:00
8a26cd549a bump 2018-09-22 01:11:14 -05:00
1cf3ce0617 Footer with server version now visible only for logged in users
Per 
2018-09-22 01:11:14 -05:00
73c65fada2 Fixing display on Lockout page 2018-09-22 01:11:14 -05:00
92ea923c03 Updating reference to Bitcoin docker image 2018-09-22 01:11:14 -05:00
e7db453717 Removing network fee line item if fee is 0
Per discussion in 
2018-09-22 01:11:14 -05:00
10ee09f052 Bugfixing broken link in footer
Div overlay caused it, fixes 
2018-09-22 01:11:14 -05:00
be7d91a138 VueJs cloak until loading is finished
Addressing 
2018-09-22 01:11:14 -05:00
3278c80d3f Fixing problem with View dynamic recompiling
The type 'RazorViewAttribute' exists in both 'Microsoft.AspNetCore.Mvc.Razor' error resolved
2018-09-22 01:11:14 -05:00
65e1edb0b8 Merge pull request from Kukks/feature/lockout
enable account lockout
2018-09-22 00:42:33 -05:00
e05c88370f enable account lockout 2018-09-12 13:36:44 +02:00
15c29f8419 Update dependencies 2018-09-09 23:04:16 +09:00
fc722731d3 bump 2018-09-08 14:54:23 +09:00
1c9c564e90 Merge branch 'rockstardev-master' 2018-09-08 14:54:06 +09:00
872b60f8ea Merge branch 'master' of github.com:btcpayserver/btcpayserver 2018-09-08 14:53:53 +09:00
0d3364b3da Change button path to api/v1/invoices 2018-09-08 14:53:42 +09:00
fed53661b3 Add btcpay.store.cancreateinvoice claim, and use that for the store 2018-09-08 14:53:41 +09:00
e86b4d89ca remove paybuttontest 2018-09-08 14:53:41 +09:00
c5cb32f6dd Providing option to disable Pay Button at later date 2018-09-08 14:53:41 +09:00
deb56e16ec Confirmation page for enabling Pay Button 2018-09-08 14:53:41 +09:00
b5626ef01c Validating that Store has Pay Button enabled 2018-09-08 14:53:41 +09:00
e39d9067f2 Updating Unit tests 2018-09-08 14:53:41 +09:00
43d34d5d35 Now requiring Authorization on AppsController 2018-09-08 14:53:41 +09:00
7341be76bb Extracting public portion of app controller 2018-09-08 14:53:41 +09:00
0abd62dfe8 Moving PayButton handler to public controller 2018-09-08 14:53:41 +09:00
735012e3d7 Refactoring Invoice to cleanup unused code 2018-09-08 14:53:41 +09:00
b5efb8d2e6 Add exchange name to expired rate 2018-09-08 14:53:40 +09:00
1dbeabb716 Update broken link of third-party host () 2018-09-08 14:05:37 +09:00
671f9e56e2 Merge branch 'master' of github.com:btcpayserver/btcpayserver 2018-09-04 19:04:47 +09:00
dc6c189948 Update broken link ()
Current link goes to a 404 page, updated to current .NET Core SDK 2.1 page
2018-09-03 22:04:14 +09:00
4501824f3f Updating Readme ()
* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md

reduced merchant guide link for better readability

* Update README.md

* Update README.md

* Update README.md
2018-09-03 22:03:50 +09:00
4568d2a98e Add exchange name to expired rate 2018-08-31 10:45:21 +09:00
f5d81334f8 Remove Lightning Specific logic from BTCPay, and use BTCPayServer.Lightning packages instead 2018-08-30 12:24:00 +09:00
f3ed90399b Merge pull request from dalijolijo/master
Bring supported coins in alphabetic order
2018-08-30 10:45:01 +09:00
fada01cec9 Bring supported coins in alphabetic order 2018-08-30 01:07:30 +02:00
1b4b9fb4cc bump 2018-08-29 00:31:39 +09:00
6eeef8a866 Remove XFrame on the checkout page 2018-08-29 00:31:23 +09:00
24979a0af2 add lnd trickledelay 2018-08-28 18:13:28 +09:00
be1a44f018 Fix bug in LND client 2018-08-28 18:06:07 +09:00
9fcc2903fc Revert "Disable color in logs"
This reverts commit 06df63b2835d77a83c8897368f91491e008f57b5.
2018-08-28 16:13:26 +09:00
06df63b283 Disable color in logs 2018-08-28 09:56:17 +09:00
0f1efc16f5 fx build 2018-08-27 18:45:05 +09:00
da8a06952c Fix bug about btcpay showing wrong port 0 as public lightning port for charge and clightning 2018-08-27 18:36:08 +09:00
38d810cef7 Fix bundling 2018-08-25 23:08:46 +09:00
393a3a2b8f Remove obsolete code in checkout 2018-08-25 22:50:17 +09:00
aaddc580d1 bump 2018-08-25 21:49:41 +09:00
957d478865 Fixing coinaverage, ValidityTime was faster than the refresh rate 2018-08-25 21:49:20 +09:00
023913a852 Rate limit per IP the number of login attempt 2018-08-25 20:28:46 +09:00
6c51d83f61 Fix tests 2018-08-25 15:49:04 +09:00
0edaedb6ab Report if BackgroundFetcherRateProvider has expired entry 2018-08-25 15:09:42 +09:00
13f21aa0d6 bump 2018-08-25 14:48:06 +09:00
929a0c37bd Better handle errors on BackgroundFetcherRateProvider 2018-08-25 14:44:56 +09:00
058ccf56d0 Fix uncaught exception on when getting rates of invoice 2018-08-25 14:44:55 +09:00
162ac572da Merge pull request from Onurrr/patch-1
Update Checkout.cshtml
2018-08-24 18:37:07 +09:00
c7f3fdb46d Update Checkout.cshtml 2018-08-23 13:11:07 +02:00
29af07b3f9 Make sure we do not return outdated rates 2018-08-23 13:47:56 +09:00
758436a428 bump 2018-08-23 12:16:43 +09:00
e0cadb4f62 Do not send not found if invoices does not belong to logged on user 2018-08-23 12:13:27 +09:00
013dfa1b61 Properly escape attributes, make the preview an actual form rather than an image 2018-08-23 11:55:29 +09:00
e0f1c50534 Making Currency a textbox instead of dropdown 2018-08-23 11:17:54 +09:00
d50dc2e68e ServerIPN is an email 2018-08-23 11:12:25 +09:00
8b5b18c97e Make sure no trailing slash bug 2018-08-23 11:11:39 +09:00
f7383b4cc8 Fixing error on CheckoutExperience if no crypto is set 2018-08-23 11:08:53 +09:00
1bc32285ba Merge branch 'master' of https://github.com/rockstardev/btcpayserver into rockstardev-master 2018-08-23 11:01:48 +09:00
f12114f9aa Poll and cache rates in parallel 2018-08-23 00:24:33 +09:00
2b6faa8d20 Migrating generator to work on store path 2018-08-22 14:05:12 +02:00
45b7df6ac9 Removing traces of PayButton being an app... shhhh... 2018-08-22 14:04:59 +02:00
5a43ce2719 Transfering Pay Button from App directly to Store 2018-08-22 13:57:54 +02:00
fe31dc8606 Setting titles for new pill navigation 2018-08-22 13:56:55 +02:00
f0615482d9 Refactoring pill navigation to use new subnav code with enums 2018-08-22 13:50:29 +02:00
03c47e6f7d Merge remote-tracking branch 'source/master' 2018-08-22 13:01:34 +02:00
4111b8a5a3 Maintaining AppId reference 2018-08-22 12:59:55 +02:00
5b5a2e8c25 Reorganizing Javascript code and references 2018-08-22 11:10:46 +02:00
9ec0c23c52 Folder for pay buttons, donate button 2018-08-22 10:59:24 +02:00
9a5034c13c URL of image for pay button 2018-08-22 10:52:17 +02:00
b1fcf4524a Separate app for PayButton 2018-08-22 10:26:49 +02:00
87d384dba5 Decouple RateProviderFactory with RateFetcher 2018-08-22 16:53:40 +09:00
4f5a8f7953 Use direct provider for Kraken, update packages 2018-08-21 15:59:57 +09:00
8728356698 Use HttpClientFactory for coinaverage 2018-08-21 14:33:13 +09:00
9c30476fc8 Making BTCPayServer a bit faster when creating invoices 2018-08-21 13:54:52 +09:00
09beb57eaf Fixing csproj 2018-08-17 16:27:37 +02:00
76a36d1829 Merge remote-tracking branch 'source/master'
# Conflicts:
#	BTCPayServer/BTCPayServer.csproj
2018-08-17 13:35:56 +02:00
0d4036efa2 Dynamically determining site url 2018-08-17 13:33:47 +02:00
cb4562aad5 Model validation attributes added for email and url 2018-08-17 13:26:33 +02:00
0084d4766b Server side validation of PayButton POST 2018-08-17 13:21:00 +02:00
74ddcfa01e Handling payment button post and providing test form 2018-08-17 12:38:03 +02:00
ed36fba0d7 Defining input names for validation errors 2018-08-17 10:30:49 +02:00
af015d435b Integrating Clipboard.js and implemeting copy code 2018-08-16 22:22:15 +02:00
ec59980e6f Validation and conditional rendering of Generated Code section 2018-08-16 21:55:24 +02:00
f0f4247c5d Field validation 2018-08-15 23:58:39 +02:00
b562094956 Currency dropdown as part of page model 2018-08-14 23:47:41 +02:00
4afd55c441 Rendering button code on load from srvModel 2018-08-14 23:47:28 +02:00
d7404f418d Returning intialized model and inputchange on all attribs 2018-08-14 19:40:57 +02:00
893410911c Button width influenced by generator 2018-08-14 18:59:59 +02:00
556b581b6a Updating display of generated HTML 2018-08-14 14:57:46 +02:00
1685ccaca8 Fixing success message for DNS 2018-08-13 17:04:37 +09:00
522abcfdfd Fix setting up DNS name 2018-08-13 16:48:10 +09:00
ed1827ff28 Fix not showing ssh service if no LND 2018-08-13 15:10:59 +09:00
57daf27fdd Completion of UI for rest of Pay Button gneerator 2018-08-11 13:34:52 +02:00
e698d90e3c Pay Button page foundation 2018-08-10 20:26:51 +02:00
256 changed files with 15122 additions and 14288 deletions
.circleci
BTCPayServer.Tests
BTCPayServer
BTCPayNetworkProvider.Bitcore.csBTCPayNetworkProvider.Groestlcoin.csBTCPayNetworkProvider.Monacoin.csBTCPayNetworkProvider.csBTCPayServer.csproj
Configuration
Controllers
Data
DerivationSchemeParser.csExtensions.cs
Filters
HostedServices
Hosting
JsonConverters
Models
Payments
Program.cs
Properties
Rating
Security
Services
Validation
Views
WalletId.csZoneLimits.csbundleconfig.json
wwwroot
Dockerfile.linuxamd64Dockerfile.linuxarm32v7README.mdbtcpayserver.sln

105
.circleci/config.yml Normal file

@ -0,0 +1,105 @@
version: 2
jobs:
build:
machine:
docker_layer_caching: true
steps:
- checkout
test:
machine: true
steps:
- checkout
- run:
command: |
lsb_release -a
wget -q https://packages.microsoft.com/config/ubuntu/14.04/packages-microsoft-prod.deb
sudo dpkg -i packages-microsoft-prod.deb
sudo apt-get install apt-transport-https
sudo apt-get update
sudo apt-get install dotnet-sdk-2.1
dotnet --info
dotnet build /p:TreatWarningsAsErrors=true
cd BTCPayServer.Tests
dotnet test --filter Fast=Fast
docker-compose up -d dev
dotnet test --filter Integration=Integration
# publish jobs require $DOCKERHUB_REPO, $DOCKERHUB_USER, $DOCKERHUB_PASS defined
publish_docker_linuxamd64:
machine:
docker_layer_caching: true
steps:
- checkout
- run:
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 login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-amd64
publish_docker_linuxarm:
machine:
docker_layer_caching: true
steps:
- checkout
- run:
command: |
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 login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-arm32v7
publish_docker_multiarch:
machine:
enabled: true
image: circleci/classic:201808-01
steps:
- run:
command: |
# Turn on Experimental features
sudo mkdir $HOME/.docker
sudo sh -c 'echo "{ \"experimental\": \"enabled\" }" >> $HOME/.docker/config.json'
#
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
#
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
sudo docker manifest create --amend $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-amd64 $DOCKERHUB_REPO:$LATEST_TAG-arm32v7
sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-amd64 --os linux --arch amd64
sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 --os linux --arch arm --variant v7
sudo docker manifest push $DOCKERHUB_REPO:$LATEST_TAG -p
workflows:
version: 2
build_and_test:
jobs:
- test
publish:
jobs:
- publish_docker_linuxamd64:
filters:
# ignore any commit on any branch by default
branches:
ignore: /.*/
# only act on version tags
tags:
only: /v[1-9]+(\.[0-9]+)*/
- publish_docker_linuxarm:
filters:
branches:
ignore: /.*/
tags:
only: /v[1-9]+(\.[0-9]+)*/
- publish_docker_multiarch:
requires:
- publish_docker_linuxamd64
- publish_docker_linuxarm
filters:
branches:
ignore: /.*/
tags:
only: /v[1-9]+(\.[0-9]+)*/

@ -6,16 +6,16 @@
<IsPackable>false</IsPackable>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
<LangVersion>7.2</LangVersion>
<UserSecretsId>AB0AC1DD-9D26-485B-9416-56A33F268117</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.2" />
<PackageReference Include="xunit" Version="2.3.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BTCPayServer\BTCPayServer.csproj" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
@ -25,6 +25,12 @@
<None Update="docker-compose.yml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="xunit.runner.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BTCPayServer\BTCPayServer.csproj" />
</ItemGroup>
</Project>

@ -1,4 +1,6 @@
using BTCPayServer.Configuration;
using System.Linq;
using BTCPayServer.HostedServices;
using BTCPayServer.Hosting;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
@ -35,6 +37,12 @@ using Xunit;
namespace BTCPayServer.Tests
{
public enum TestDatabases
{
Postgres,
MySQL,
}
public class BTCPayServerTester : IDisposable
{
private string _Directory;
@ -57,6 +65,11 @@ namespace BTCPayServer.Tests
set;
}
public string MySQL
{
get; set;
}
public string Postgres
{
get; set;
@ -68,6 +81,10 @@ namespace BTCPayServer.Tests
get; set;
}
public TestDatabases TestDatabase
{
get; set;
}
public bool MockRates { get; set; } = true;
@ -94,7 +111,9 @@ namespace BTCPayServer.Tests
config.AppendLine($"btc.lightning={IntegratedLightning.AbsoluteUri}");
if (Postgres != null)
if (TestDatabase == TestDatabases.MySQL && !String.IsNullOrEmpty(MySQL))
config.AppendLine($"mysql=" + MySQL);
else if (!String.IsNullOrEmpty(Postgres))
config.AppendLine($"postgres=" + Postgres);
var confPath = Path.Combine(chainDirectory, "settings.config");
File.WriteAllText(confPath, config.ToString());
@ -120,36 +139,72 @@ namespace BTCPayServer.Tests
.Build();
_Host.Start();
InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository));
StoreRepository = (StoreRepository)_Host.Services.GetService(typeof(StoreRepository));
var rateProvider = (BTCPayRateProviderFactory)_Host.Services.GetService(typeof(BTCPayRateProviderFactory));
rateProvider.DirectProviders.Clear();
StoreRepository = (StoreRepository)_Host.Services.GetService(typeof(StoreRepository));
var dashBoard = (NBXplorerDashboard)_Host.Services.GetService(typeof(NBXplorerDashboard));
while(!dashBoard.IsFullySynched())
{
Thread.Sleep(10);
}
var coinAverageMock = new MockRateProvider();
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
if (MockRates)
{
Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("BTC_USD"),
BidAsk = new BidAsk(5000m)
});
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("BTC_CAD"),
BidAsk = new BidAsk(4500m)
});
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("LTC_BTC"),
BidAsk = new BidAsk(0.001m)
});
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("LTC_USD"),
BidAsk = new BidAsk(500m)
});
rateProvider.DirectProviders.Add("coinaverage", coinAverageMock);
var rateProvider = (RateProviderFactory)_Host.Services.GetService(typeof(RateProviderFactory));
rateProvider.Providers.Clear();
var coinAverageMock = new MockRateProvider();
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("BTC_USD"),
BidAsk = new BidAsk(5000m)
});
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("BTC_CAD"),
BidAsk = new BidAsk(4500m)
});
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("LTC_BTC"),
BidAsk = new BidAsk(0.001m)
});
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("LTC_USD"),
BidAsk = new BidAsk(500m)
});
rateProvider.Providers.Add("coinaverage", coinAverageMock);
var bitflyerMock = new MockRateProvider();
bitflyerMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "bitflyer",
CurrencyPair = CurrencyPair.Parse("BTC_JPY"),
BidAsk = new BidAsk(700000m)
});
rateProvider.Providers.Add("bitflyer", bitflyerMock);
var quadrigacx = new MockRateProvider();
quadrigacx.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "quadrigacx",
CurrencyPair = CurrencyPair.Parse("BTC_CAD"),
BidAsk = new BidAsk(6000m)
});
rateProvider.Providers.Add("quadrigacx", quadrigacx);
var bittrex = new MockRateProvider();
bittrex.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "bittrex",
CurrencyPair = CurrencyPair.Parse("DOGE_BTC"),
BidAsk = new BidAsk(0.004m)
});
rateProvider.Providers.Add("bittrex", bittrex);
}
}
public string HostName
@ -167,17 +222,21 @@ namespace BTCPayServer.Tests
return _Host.Services.GetRequiredService<T>();
}
public T GetController<T>(string userId = null, string storeId = null) where T : Controller
public T GetController<T>(string userId = null, string storeId = null, Claim[] additionalClaims = null) where T : Controller
{
var context = new DefaultHttpContext();
context.Request.Host = new HostString("127.0.0.1");
context.Request.Host = new HostString("127.0.0.1", Port);
context.Request.Scheme = "http";
context.Request.Protocol = "http";
if (userId != null)
{
context.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, userId) }, Policies.CookieAuthentication));
List<Claim> claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.NameIdentifier, userId));
if (additionalClaims != null)
claims.AddRange(additionalClaims);
context.User = new ClaimsPrincipal(new ClaimsIdentity(claims.ToArray(), Policies.CookieAuthentication));
}
if(storeId != null)
if (storeId != null)
{
context.SetStoreData(GetService<StoreRepository>().FindStore(storeId, userId).GetAwaiter().GetResult());
}

@ -0,0 +1,253 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments.Changelly;
using BTCPayServer.Payments.Changelly.Models;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Tests.Logging;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Xunit;
using Xunit.Abstractions;
namespace BTCPayServer.Tests
{
public class ChangellyTests
{
public ChangellyTests(ITestOutputHelper helper)
{
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
Logs.LogProvider = new XUnitLogProvider(helper);
}
[Fact]
[Trait("Integration", "Integration")]
public async void CanSetChangellyPaymentMethod()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
var controller = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
var storeBlob = controller.StoreData.GetStoreBlob();
Assert.Null(storeBlob.ChangellySettings);
var updateModel = new UpdateChangellySettingsViewModel()
{
ApiSecret = "secret",
ApiKey = "key",
ApiUrl = "http://gozo.com",
ChangellyMerchantId = "aaa",
};
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
await controller.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
var store = await tester.PayTester.StoreRepository.FindStore(user.StoreId);
storeBlob = controller.StoreData.GetStoreBlob();
Assert.NotNull(storeBlob.ChangellySettings);
Assert.NotNull(storeBlob.ChangellySettings);
Assert.IsType<ChangellySettings>(storeBlob.ChangellySettings);
Assert.Equal(storeBlob.ChangellySettings.ApiKey, updateModel.ApiKey);
Assert.Equal(storeBlob.ChangellySettings.ApiSecret,
updateModel.ApiSecret);
Assert.Equal(storeBlob.ChangellySettings.ApiUrl, updateModel.ApiUrl);
Assert.Equal(storeBlob.ChangellySettings.ChangellyMerchantId,
updateModel.ChangellyMerchantId);
}
}
[Fact]
[Trait("Integration", "Integration")]
public async void CanToggleChangellyPaymentMethod()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
var controller = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
var updateModel = new UpdateChangellySettingsViewModel()
{
ApiSecret = "secret",
ApiKey = "key",
ApiUrl = "http://gozo.com",
ChangellyMerchantId = "aaa",
Enabled = true
};
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
await controller.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
var store = await tester.PayTester.StoreRepository.FindStore(user.StoreId);
Assert.True(store.GetStoreBlob().ChangellySettings.Enabled);
updateModel.Enabled = false;
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
await controller.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
store = await tester.PayTester.StoreRepository.FindStore(user.StoreId);
Assert.False(store.GetStoreBlob().ChangellySettings.Enabled);
}
}
[Fact]
[Trait("Integration", "Integration")]
public async void CannotUseChangellyApiWithoutChangellyPaymentMethodSet()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
var changellyController =
tester.PayTester.GetController<ChangellyController>(user.UserId, user.StoreId);
changellyController.IsTest = true;
//test non existing payment method
Assert.IsType<BitpayErrorModel>(Assert
.IsType<BadRequestObjectResult>(await changellyController.GetCurrencyList(user.StoreId))
.Value);
var updateModel = CreateDefaultChangellyParams(false);
var storesController = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
//set payment method but disabled
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
await storesController.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
Assert.IsType<BitpayErrorModel>(Assert
.IsType<BadRequestObjectResult>(await changellyController.GetCurrencyList(user.StoreId))
.Value);
updateModel.Enabled = true;
//test with enabled method
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
await storesController.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
Assert.IsNotType<BitpayErrorModel>(Assert
.IsType<OkObjectResult>(await changellyController.GetCurrencyList(user.StoreId))
.Value);
}
}
UpdateChangellySettingsViewModel CreateDefaultChangellyParams(bool enabled)
{
return new UpdateChangellySettingsViewModel()
{
ApiKey = "6ed02cdf1b614d89a8c0ceb170eebb61",
ApiSecret = "8fbd66a2af5fd15a6b5f8ed0159c5842e32a18538521ffa145bd6c9e124d3483",
ChangellyMerchantId = "804298eb5753",
Enabled = enabled
};
}
[Fact]
[Trait("Integration", "Integration")]
public async void CanGetCurrencyListFromChangelly()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
//save changelly settings
var updateModel = CreateDefaultChangellyParams(true);
var storesController = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
//confirm saved
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
await storesController.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
var factory = UnitTest1.CreateBTCPayRateFactory();
var fetcher = new RateFetcher(factory);
var httpClientFactory = new MockHttpClientFactory();
var changellyController = new ChangellyController(
new ChangellyClientProvider(tester.PayTester.StoreRepository, httpClientFactory),
tester.NetworkProvider, fetcher);
changellyController.IsTest = true;
var result = Assert
.IsType<OkObjectResult>(await changellyController.GetCurrencyList(user.StoreId))
.Value as IEnumerable<CurrencyFull>;
Assert.True(result.Any());
}
}
[Fact]
[Trait("Integration", "Integration")]
public async void CanCalculateToAmountForChangelly()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
var updateModel = CreateDefaultChangellyParams(true);
var storesController = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
await storesController.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
var factory = UnitTest1.CreateBTCPayRateFactory();
var fetcher = new RateFetcher(factory);
var httpClientFactory = new MockHttpClientFactory();
var changellyController = new ChangellyController(
new ChangellyClientProvider(tester.PayTester.StoreRepository, httpClientFactory),
tester.NetworkProvider, fetcher);
changellyController.IsTest = true;
Assert.IsType<decimal>(Assert
.IsType<OkObjectResult>(await changellyController.CalculateAmount(user.StoreId, "ltc", "btc", 1.0m))
.Value);
}
}
[Fact]
[Trait("Integration", "Integration")]
public void CanComputeBaseAmount()
{
Assert.Equal(1, ChangellyCalculationHelper.ComputeBaseAmount(1, 1));
Assert.Equal(0.5m, ChangellyCalculationHelper.ComputeBaseAmount(1, 0.5m));
Assert.Equal(2, ChangellyCalculationHelper.ComputeBaseAmount(0.5m, 1));
Assert.Equal(4m, ChangellyCalculationHelper.ComputeBaseAmount(1, 4));
}
[Fact]
[Trait("Integration", "Integration")]
public void CanComputeCorrectAmount()
{
Assert.Equal(1, ChangellyCalculationHelper.ComputeCorrectAmount(0.5m, 1, 2));
Assert.Equal(0.25m, ChangellyCalculationHelper.ComputeCorrectAmount(0.5m, 1, 0.5m));
Assert.Equal(20, ChangellyCalculationHelper.ComputeCorrectAmount(10, 1, 2));
}
}
public class MockHttpClientFactory : IHttpClientFactory
{
public HttpClient CreateClient(string name)
{
return new HttpClient();
}
}
}

@ -1,9 +1,9 @@
using System;
using System.Collections.Generic;
using System.Text;
using BTCPayServer.Lightning;
using BTCPayServer.Lightning.Charge;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Payments.Lightning.Charge;
using BTCPayServer.Payments.Lightning.CLightning;
using NBitcoin;
namespace BTCPayServer.Tests

@ -1,4 +1,4 @@
FROM microsoft/dotnet:2.1.300-sdk-alpine3.7
FROM microsoft/dotnet:2.1.403-sdk-alpine3.7
WORKDIR /app
# caches restore result by copying csproj file separately
COPY BTCPayServer.Tests/BTCPayServer.Tests.csproj BTCPayServer.Tests/BTCPayServer.Tests.csproj

@ -2,7 +2,7 @@
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Payments.Lightning.CLightning;
using BTCPayServer.Lightning.CLightning;
using NBitcoin;
namespace BTCPayServer.Tests
@ -13,10 +13,10 @@ namespace BTCPayServer.Tests
public LightningDTester(ServerTester parent, string environmentName, string defaultRPC, string defaultHost, Network network)
{
this.parent = parent;
RPC = new CLightningRPCClient(new Uri(parent.GetEnvironment(environmentName, defaultRPC)), network);
RPC = new CLightningClient(new Uri(parent.GetEnvironment(environmentName, defaultRPC)), network);
}
public CLightningRPCClient RPC { get; }
public CLightningClient RPC { get; }
public string P2PHost { get; }
}

@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using System.Text;
using BTCPayServer.Payments.Lightning.Lnd;
using BTCPayServer.Lightning.LND;
using NBitcoin;
namespace BTCPayServer.Tests.Lnd
@ -16,12 +16,12 @@ namespace BTCPayServer.Tests.Lnd
var url = serverTester.GetEnvironment(environmentName, defaultValue);
Swagger = new LndSwaggerClient(new LndRestSettings(new Uri(url)) { AllowInsecure = true });
Client = new LndInvoiceClient(Swagger);
Client = new LndClient(Swagger, network);
P2PHost = _Parent.GetEnvironment(environmentName + "_HOST", defaultHost);
}
public LndSwaggerClient Swagger { get; set; }
public LndInvoiceClient Client { get; set; }
public LndClient Client { get; set; }
public string P2PHost { get; }
}
}

@ -1,246 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Payments.Lightning.Lnd;
using NBitcoin;
using NBitcoin.RPC;
using Xunit;
using Xunit.Abstractions;
using System.Linq;
using System.Threading;
using NBitpayClient;
using System.Globalization;
using Xunit.Sdk;
namespace BTCPayServer.Tests.Lnd
{
// this depends for now on `docker-compose up devlnd`
public class UnitTests
{
private readonly ITestOutputHelper output;
public UnitTests(ITestOutputHelper output)
{
this.output = output;
initializeEnvironment();
MerchantLnd = new LndSwaggerClient(new LndRestSettings(new Uri("https://127.0.0.1:53280")) { AllowInsecure = true });
InvoiceClient = new LndInvoiceClient(MerchantLnd);
CustomerLnd = new LndSwaggerClient(new LndRestSettings(new Uri("https://127.0.0.1:53281")) { AllowInsecure = true });
}
private LndSwaggerClient MerchantLnd { get; set; }
private LndInvoiceClient InvoiceClient { get; set; }
private LndSwaggerClient CustomerLnd { get; set; }
[Fact]
public async Task GetInfo()
{
var res = await InvoiceClient.GetInfo();
output.WriteLine("Result: " + res.ToJson());
}
[Fact]
public async Task CreateInvoice()
{
var res = await InvoiceClient.CreateInvoice(10000, "Hello world", TimeSpan.FromSeconds(3600));
output.WriteLine("Result: " + res.ToJson());
}
[Fact]
public async Task GetInvoice()
{
var createInvoice = await InvoiceClient.CreateInvoice(10000, "Hello world", TimeSpan.FromSeconds(3600));
var getInvoice = await InvoiceClient.GetInvoice(createInvoice.Id);
Assert.Equal(createInvoice.BOLT11, getInvoice.BOLT11);
}
[Fact]
public void Play()
{
var seq = new System.Buffers.ReadOnlySequence<byte>(new ReadOnlyMemory<byte>(new byte[1000]));
var seq2 = seq.Slice(3);
var pos = seq2.GetPosition(0);
}
// integration tests
[Fact]
public async Task TestWaitListenInvoice()
{
var merchantInvoice = await InvoiceClient.CreateInvoice(10000, "Hello world", TimeSpan.FromSeconds(3600));
var merchantInvoice2 = await InvoiceClient.CreateInvoice(10000, "Hello world", TimeSpan.FromSeconds(3600));
var waitToken = default(CancellationToken);
var listener = await InvoiceClient.Listen(waitToken);
var waitTask = listener.WaitInvoice(waitToken);
await EnsureLightningChannelAsync();
var payResponse = await CustomerLnd.SendPaymentSyncAsync(new LnrpcSendRequest
{
Payment_request = merchantInvoice.BOLT11
});
var invoice = await waitTask;
Assert.True(invoice.PaidAt.HasValue);
var waitTask2 = listener.WaitInvoice(waitToken);
payResponse = await CustomerLnd.SendPaymentSyncAsync(new LnrpcSendRequest
{
Payment_request = merchantInvoice2.BOLT11
});
invoice = await waitTask2;
Assert.True(invoice.PaidAt.HasValue);
var waitTask3 = listener.WaitInvoice(waitToken);
await Task.Delay(100);
listener.Dispose();
Assert.Throws<TaskCanceledException>(() => waitTask3.GetAwaiter().GetResult());
}
[Fact]
public async Task CreateLndInvoiceAndPay()
{
var merchantInvoice = await InvoiceClient.CreateInvoice(10000, "Hello world", TimeSpan.FromSeconds(3600));
await EnsureLightningChannelAsync();
await EventuallyAsync(async () =>
{
var payResponse = await CustomerLnd.SendPaymentSyncAsync(new LnrpcSendRequest
{
Payment_request = merchantInvoice.BOLT11
});
var invoice = await InvoiceClient.GetInvoice(merchantInvoice.Id);
Assert.True(invoice.PaidAt.HasValue);
});
}
private 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);
}
}
}
public async Task<LnrpcChannel> EnsureLightningChannelAsync()
{
var merchantInfo = await WaitLNSynched();
var merchantNodeAddress = new LnrpcLightningAddress
{
Pubkey = merchantInfo.NodeId,
Host = "merchant_lnd:9735"
};
while (true)
{
// if channel is pending generate blocks until confirmed
var pendingResponse = await CustomerLnd.PendingChannelsAsync();
if (pendingResponse.Pending_open_channels?
.Any(a => a.Channel?.Remote_node_pub == merchantNodeAddress.Pubkey) == true)
{
ExplorerNode.Generate(1);
await WaitLNSynched();
continue;
}
// check if channel is established
var chanResponse = await CustomerLnd.ListChannelsAsync(null, null, null, null);
LnrpcChannel channelToMerchant = null;
if (chanResponse != null && chanResponse.Channels != null)
{
channelToMerchant = chanResponse.Channels
.Where(a => a.Remote_pubkey == merchantNodeAddress.Pubkey)
.FirstOrDefault();
}
if (channelToMerchant == null)
{
// create new channel
var isConnected = await CustomerLnd.ListPeersAsync();
if (isConnected.Peers == null ||
!isConnected.Peers.Any(a => a.Pub_key == merchantInfo.NodeId))
{
var connectResp = await CustomerLnd.ConnectPeerAsync(new LnrpcConnectPeerRequest
{
Addr = merchantNodeAddress
});
}
var addressResponse = await CustomerLnd.NewWitnessAddressAsync();
var address = BitcoinAddress.Create(addressResponse.Address, Network.RegTest);
await ExplorerNode.SendToAddressAsync(address, Money.Coins(0.2m));
ExplorerNode.Generate(1);
await WaitLNSynched();
var channelReq = new LnrpcOpenChannelRequest
{
Local_funding_amount = 16777215.ToString(CultureInfo.InvariantCulture),
Node_pubkey_string = merchantInfo.NodeId
};
var channelResp = await CustomerLnd.OpenChannelSyncAsync(channelReq);
}
else
{
// channel exists, return it
ExplorerNode.Generate(1);
await WaitLNSynched();
return channelToMerchant;
}
}
}
private async Task<LightningNodeInformation> WaitLNSynched()
{
while (true)
{
var merchantInfo = await InvoiceClient.GetInfo();
var blockCount = await ExplorerNode.GetBlockCountAsync();
if (merchantInfo.BlockHeight != blockCount)
{
await Task.Delay(500);
}
else
{
return merchantInfo;
}
}
}
//
private void initializeEnvironment()
{
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);
}
public BTCPayNetworkProvider NetworkProvider { get; private set; }
public RPCClient ExplorerNode { get; set; }
internal string GetEnvironment(string variable, string defaultValue)
{
var var = Environment.GetEnvironmentVariable(variable);
return String.IsNullOrEmpty(var) ? defaultValue : var;
}
}
}

@ -29,11 +29,7 @@ If you want to stop, and remove all existing data
docker-compose down --v
```
You can run the tests inside a container by running
```
docker-compose run --rm tests
```
You can run tests on `MySql` database instead of `Postgres` by setting environnement variable `TESTS_DB` equals to `MySql`.
## How to manually test payments

@ -11,6 +11,21 @@ namespace BTCPayServer.Tests
public class RateRulesTest
{
[Fact]
[Trait("Fast", "Fast")]
public void SecondDuplicatedRuleIsIgnored()
{
StringBuilder builder = new StringBuilder();
builder.AppendLine("DOGE_X = 1.1");
builder.AppendLine("DOGE_X = 1.2");
Assert.True(RateRules.TryParse(builder.ToString(), out var rules));
var rule = rules.GetRuleFor(new CurrencyPair("DOGE", "BTC"));
rule.Reevaluate();
Assert.True(!rule.HasError);
Assert.Equal(1.1m, rule.BidAsk.Ask);
}
[Fact]
[Trait("Fast", "Fast")]
public void CanParseRateRules()
{
// Check happy path

@ -18,10 +18,10 @@ using System.Text;
using System.Threading.Tasks;
using System.Threading;
using System.Globalization;
using BTCPayServer.Payments.Lightning.CLightning;
using BTCPayServer.Payments.Lightning.Charge;
using BTCPayServer.Tests.Lnd;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Lightning.CLightning;
using BTCPayServer.Lightning;
namespace BTCPayServer.Tests
{
@ -49,8 +49,8 @@ namespace BTCPayServer.Tests
LTCExplorerClient = new ExplorerClient(NetworkProvider.GetNetwork("LTC").NBXplorerNetwork, new Uri(GetEnvironment("TESTS_LTCNBXPLORERURL", "http://127.0.0.1:32838/")));
var btc = NetworkProvider.GetNetwork("BTC").NBitcoinNetwork;
CustomerLightningD = (CLightningRPCClient)LightningClientFactory.CreateClient(GetEnvironment("TEST_CUSTOMERLIGHTNINGD", "type=clightning;server=tcp://127.0.0.1:30992/"), btc);
MerchantLightningD = (CLightningRPCClient)LightningClientFactory.CreateClient(GetEnvironment("TEST_MERCHANTLIGHTNINGD", "type=clightning;server=tcp://127.0.0.1:30993/"), btc);
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);
MerchantCharge = new ChargeTester(this, "TEST_MERCHANTCHARGE", "type=charge;server=http://127.0.0.1:54938/;api-token=foiewnccewuify", "merchant_lightningd", btc);
@ -60,7 +60,9 @@ namespace BTCPayServer.Tests
{
NBXplorerUri = ExplorerClient.Address,
LTCNBXplorerUri = LTCExplorerClient.Address,
TestDatabase = Enum.Parse<TestDatabases>(GetEnvironment("TESTS_DB", TestDatabases.Postgres.ToString()), true),
Postgres = GetEnvironment("TESTS_POSTGRES", "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver"),
MySQL = GetEnvironment("TESTS_MYSQL", "User ID=root;Host=127.0.0.1;Port=33036;Database=btcpayserver"),
IntegratedLightning = MerchantCharge.Client.Uri
};
PayTester.Port = int.Parse(GetEnvironment("TESTS_PORT", Utils.FreeTcpPort().ToString(CultureInfo.InvariantCulture)), CultureInfo.InvariantCulture);
@ -78,88 +80,24 @@ namespace BTCPayServer.Tests
PayTester.Start();
}
/// <summary>
/// Connect a customer LN node to the merchant LN node
/// </summary>
public void PrepareLightning(LightningConnectionType lndBackend)
{
ILightningInvoiceClient client = MerchantCharge.Client;
if (lndBackend == LightningConnectionType.LndREST)
client = MerchantLnd.Client;
PrepareLightningAsync(client).GetAwaiter().GetResult();
}
private static readonly string[] SKIPPED_STATES =
{ "ONCHAIN", "CHANNELD_SHUTTING_DOWN", "CLOSINGD_SIGEXCHANGE", "CLOSINGD_COMPLETE", "FUNDING_SPEND_SEEN" };
/// <summary>
/// Connect a customer LN node to the merchant LN node
/// </summary>
/// <returns></returns>
private async Task PrepareLightningAsync(ILightningInvoiceClient client)
public Task EnsureChannelsSetup()
{
bool awaitingLocking = false;
while (true)
{
var merchantInfo = await WaitLNSynched(client, CustomerLightningD, MerchantLightningD);
var peers = await CustomerLightningD.ListPeersAsync();
var filteringToTargetedPeers = peers.Where(a => a.Id == merchantInfo.NodeId);
var channel = filteringToTargetedPeers
.SelectMany(p => p.Channels)
.Where(c => !SKIPPED_STATES.Contains(c.State ?? ""))
.FirstOrDefault();
switch (channel?.State)
{
case null:
var address = await CustomerLightningD.NewAddressAsync();
await ExplorerNode.SendToAddressAsync(address, Money.Coins(0.5m));
ExplorerNode.Generate(1);
await WaitLNSynched(client, CustomerLightningD, MerchantLightningD);
await Task.Delay(1000);
var merchantNodeInfo = new NodeInfo(merchantInfo.NodeId, merchantInfo.Address, merchantInfo.P2PPort);
await CustomerLightningD.ConnectAsync(merchantNodeInfo);
await CustomerLightningD.FundChannelAsync(merchantNodeInfo, Money.Satoshis(16777215));
break;
case "CHANNELD_AWAITING_LOCKIN":
ExplorerNode.Generate(awaitingLocking ? 1 : 10);
await WaitLNSynched(client, CustomerLightningD, MerchantLightningD);
awaitingLocking = true;
break;
case "CHANNELD_NORMAL":
return;
default:
throw new NotSupportedException(channel?.State ?? "");
}
}
return BTCPayServer.Lightning.Tests.ConnectChannels.ConnectAll(ExplorerNode, GetLightningSenderClients(), GetLightningDestClients());
}
private async Task<LightningNodeInformation> WaitLNSynched(params ILightningInvoiceClient[] clients)
private IEnumerable<ILightningClient> GetLightningSenderClients()
{
while (true)
{
var blockCount = await ExplorerNode.GetBlockCountAsync();
var synching = clients.Select(c => WaitLNSynchedCore(blockCount, c)).ToArray();
await Task.WhenAll(synching);
if (synching.All(c => c.Result != null))
return synching[0].Result;
await Task.Delay(1000);
}
yield return CustomerLightningD;
}
private async Task<LightningNodeInformation> WaitLNSynchedCore(int blockCount, ILightningInvoiceClient client)
private IEnumerable<ILightningClient> GetLightningDestClients()
{
var merchantInfo = await client.GetInfo();
if (merchantInfo.BlockHeight == blockCount)
{
return merchantInfo;
}
return null;
yield return MerchantLightningD;
yield return MerchantLnd.Client;
}
public void SendLightningPayment(Invoice invoice)
@ -171,12 +109,12 @@ namespace BTCPayServer.Tests
{
var bolt11 = invoice.CryptoInfo.Where(o => o.PaymentUrls.BOLT11 != null).First().PaymentUrls.BOLT11;
bolt11 = bolt11.Replace("lightning:", "", StringComparison.OrdinalIgnoreCase);
await CustomerLightningD.SendAsync(bolt11);
await CustomerLightningD.Pay(bolt11);
}
public CLightningRPCClient CustomerLightningD { get; set; }
public ILightningClient CustomerLightningD { get; set; }
public CLightningRPCClient MerchantLightningD { get; private set; }
public ILightningClient MerchantLightningD { get; private set; }
public ChargeTester MerchantCharge { get; private set; }
public LndMockTester MerchantLnd { get; set; }
@ -218,7 +156,7 @@ namespace BTCPayServer.Tests
public void Dispose()
{
foreach(var store in Stores)
foreach (var store in Stores)
{
Xunit.Assert.True(PayTester.StoreRepository.DeleteStore(store).GetAwaiter().GetResult());
}

@ -14,6 +14,9 @@ using Xunit;
using NBXplorer.DerivationStrategy;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Tests.Logging;
using BTCPayServer.Lightning;
using BTCPayServer.Lightning.CLightning;
namespace BTCPayServer.Tests
{
@ -70,16 +73,16 @@ namespace BTCPayServer.Tests
public BTCPayNetwork SupportedNetwork { get; set; }
public void RegisterDerivationScheme(string crytoCode)
public WalletId RegisterDerivationScheme(string crytoCode, bool segwit = false)
{
RegisterDerivationSchemeAsync(crytoCode).GetAwaiter().GetResult();
return RegisterDerivationSchemeAsync(crytoCode, segwit).GetAwaiter().GetResult();
}
public async Task RegisterDerivationSchemeAsync(string cryptoCode)
public async Task<WalletId> RegisterDerivationSchemeAsync(string cryptoCode, bool segwit = false)
{
SupportedNetwork = parent.NetworkProvider.GetNetwork(cryptoCode);
var store = parent.PayTester.GetController<StoresController>(UserId, StoreId);
ExtKey = new ExtKey().GetWif(SupportedNetwork.NBitcoinNetwork);
DerivationScheme = new DerivationStrategyFactory(SupportedNetwork.NBitcoinNetwork).Parse(ExtKey.Neuter().ToString() + "-[legacy]");
DerivationScheme = new DerivationStrategyFactory(SupportedNetwork.NBitcoinNetwork).Parse(ExtKey.Neuter().ToString() + (segwit ? "" : "-[legacy]"));
var vm = (StoreViewModel)((ViewResult)store.UpdateStore()).Model;
vm.SpeedPolicy = SpeedPolicy.MediumSpeed;
await store.UpdateStore(vm);
@ -89,6 +92,8 @@ namespace BTCPayServer.Tests
DerivationScheme = DerivationScheme.ToString(),
Confirmation = true
}, cryptoCode);
return new WalletId(StoreId, cryptoCode);
}
public DerivationStrategyBase DerivationScheme { get; set; }
@ -132,7 +137,7 @@ namespace BTCPayServer.Tests
if (connectionType == LightningConnectionType.Charge)
connectionString = "type=charge;server=" + parent.MerchantCharge.Client.Uri.AbsoluteUri;
else if (connectionType == LightningConnectionType.CLightning)
connectionString = "type=clightning;server=" + parent.MerchantLightningD.Address.AbsoluteUri;
connectionString = "type=clightning;server=" + ((CLightningClient)parent.MerchantLightningD).Address.AbsoluteUri;
else if (connectionType == LightningConnectionType.LndREST)
connectionString = $"type=lnd-rest;server={parent.MerchantLnd.Swagger.BaseUrl};allowinsecure=true";
else

@ -40,6 +40,13 @@ using BTCPayServer.Rating;
using BTCPayServer.Validation;
using ExchangeSharp;
using System.Security.Cryptography.X509Certificates;
using BTCPayServer.Lightning;
using BTCPayServer.Models.WalletViewModels;
using System.Security.Claims;
using BTCPayServer.Models.ServerViewModels;
using BTCPayServer.Security;
using NBXplorer.Models;
using RatesViewModel = BTCPayServer.Models.StoreViewModels.RatesViewModel;
namespace BTCPayServer.Tests
{
@ -52,6 +59,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Fast", "Fast")]
public void CanHandleUriValidation()
{
var attribute = new UriAttribute();
@ -73,6 +81,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Fast", "Fast")]
public void CanCalculateCryptoDue2()
{
var dummy = new Key().PubKey.GetAddress(Network.RegTest).ToString();
@ -130,6 +139,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Fast", "Fast")]
public void CanCalculateCryptoDue()
{
var entity = new InvoiceEntity();
@ -254,6 +264,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Integration", "Integration")]
public void CanAcceptInvoiceWithTolerance()
{
var entity = new InvoiceEntity();
@ -281,6 +292,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Integration", "Integration")]
public void CanAcceptInvoiceWithTolerance2()
{
using (var tester = ServerTester.Create())
@ -321,6 +333,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Fast", "Fast")]
public void RoundupCurrenciesCorrectly()
{
foreach (var test in new[]
@ -331,12 +344,13 @@ namespace BTCPayServer.Tests
(0.1m, "$0.10 (USD)"),
})
{
var actual = InvoiceController.FormatCurrency(test.Item1, "USD", new CurrencyNameTable());
var actual = new CurrencyNameTable().DisplayFormatCurrency(test.Item1, "USD");
Assert.Equal(test.Item2, actual);
}
}
[Fact]
[Trait("Integration", "Integration")]
public void CanPayUsingBIP70()
{
using (var tester = ServerTester.Create())
@ -387,22 +401,13 @@ namespace BTCPayServer.Tests
}
[Fact]
public void CanUseLightMoney()
{
var light = LightMoney.MilliSatoshis(1);
Assert.Equal("0.00000000001", light.ToString());
light = LightMoney.MilliSatoshis(200000);
Assert.Equal(200m, light.ToDecimal(LightMoneyUnit.Satoshi));
Assert.Equal(0.00000001m * 200m, light.ToDecimal(LightMoneyUnit.BTC));
}
[Fact]
public void CanSetLightningServer()
[Trait("Integration", "Integration")]
public async Task CanSetLightningServer()
{
using (var tester = ServerTester.Create())
{
tester.Start();
await tester.EnsureChannelsSetup();
var user = tester.NewAccount();
user.GrantAccess();
var storeController = user.GetController<StoresController>();
@ -434,136 +439,27 @@ namespace BTCPayServer.Tests
}
[Fact]
public void CanParseLightningURL()
[Trait("Integration", "Integration")]
public async Task CanSendLightningPaymentCLightning()
{
LightningConnectionString conn = null;
Assert.True(LightningConnectionString.TryParse("/test/a", true, out conn));
for (int i = 0; i < 2; i++)
{
if (i == 1)
Assert.True(LightningConnectionString.TryParse(conn.ToString(), false, out conn));
Assert.Equal(i == 0, conn.IsLegacy);
Assert.Equal("type=clightning;server=unix://test/a", conn.ToString());
Assert.Equal("unix://test/a", conn.ToUri(true).AbsoluteUri);
Assert.Equal("unix://test/a", conn.ToUri(false).AbsoluteUri);
Assert.Equal(LightningConnectionType.CLightning, conn.ConnectionType);
}
Assert.True(LightningConnectionString.TryParse("unix://test/a", true, out conn));
for (int i = 0; i < 2; i++)
{
if (i == 1)
Assert.True(LightningConnectionString.TryParse(conn.ToString(), false, out conn));
Assert.Equal("type=clightning;server=unix://test/a", conn.ToString());
Assert.Equal("unix://test/a", conn.ToUri(true).AbsoluteUri);
Assert.Equal("unix://test/a", conn.ToUri(false).AbsoluteUri);
Assert.Equal(LightningConnectionType.CLightning, conn.ConnectionType);
}
Assert.True(LightningConnectionString.TryParse("unix://test/a", true, out conn));
for (int i = 0; i < 2; i++)
{
if (i == 1)
Assert.True(LightningConnectionString.TryParse(conn.ToString(), false, out conn));
Assert.Equal("type=clightning;server=unix://test/a", conn.ToString());
Assert.Equal("unix://test/a", conn.ToUri(true).AbsoluteUri);
Assert.Equal("unix://test/a", conn.ToUri(false).AbsoluteUri);
Assert.Equal(LightningConnectionType.CLightning, conn.ConnectionType);
}
Assert.True(LightningConnectionString.TryParse("tcp://test/a", true, out conn));
for (int i = 0; i < 2; i++)
{
if (i == 1)
Assert.True(LightningConnectionString.TryParse(conn.ToString(), false, out conn));
Assert.Equal("type=clightning;server=tcp://test/a", conn.ToString());
Assert.Equal("tcp://test/a", conn.ToUri(true).AbsoluteUri);
Assert.Equal("tcp://test/a", conn.ToUri(false).AbsoluteUri);
Assert.Equal(LightningConnectionType.CLightning, conn.ConnectionType);
}
Assert.True(LightningConnectionString.TryParse("http://aaa:bbb@test/a", true, out conn));
for (int i = 0; i < 2; i++)
{
if (i == 1)
Assert.True(LightningConnectionString.TryParse(conn.ToString(), false, out conn));
Assert.Equal("type=charge;server=http://aaa:bbb@test/a", conn.ToString());
Assert.Equal("http://aaa:bbb@test/a", conn.ToUri(true).AbsoluteUri);
Assert.Equal("http://test/a", conn.ToUri(false).AbsoluteUri);
Assert.Equal(LightningConnectionType.Charge, conn.ConnectionType);
Assert.Equal("aaa", conn.Username);
Assert.Equal("bbb", conn.Password);
}
Assert.True(LightningConnectionString.TryParse("http://api-token:bbb@test/a", true, out conn));
for (int i = 0; i < 2; i++)
{
if (i == 1)
Assert.True(LightningConnectionString.TryParse(conn.ToString(), false, out conn));
Assert.Equal("type=charge;server=http://test/a;api-token=bbb", conn.ToString());
}
Assert.False(LightningConnectionString.TryParse("lol://aaa:bbb@test/a", true, out conn));
Assert.False(LightningConnectionString.TryParse("https://test/a", true, out conn));
Assert.False(LightningConnectionString.TryParse("unix://dwewoi:dwdwqd@test/a", true, out conn));
Assert.False(LightningConnectionString.TryParse("tcp://test/a", false, out conn));
Assert.False(LightningConnectionString.TryParse("type=charge;server=http://aaa:bbb@test/a;unk=lol", false, out conn));
Assert.False(LightningConnectionString.TryParse("type=charge;server=tcp://aaa:bbb@test/a", false, out conn));
Assert.False(LightningConnectionString.TryParse("type=charge", false, out conn));
Assert.False(LightningConnectionString.TryParse("type=clightning", false, out conn));
Assert.True(LightningConnectionString.TryParse("type=clightning;server=tcp://aaa:bbb@test/a", false, out conn));
Assert.True(LightningConnectionString.TryParse("type=clightning;server=/aaa:bbb@test/a", false, out conn));
Assert.True(LightningConnectionString.TryParse("type=clightning;server=unix://aaa:bbb@test/a", false, out conn));
Assert.False(LightningConnectionString.TryParse("type=clightning;server=wtf://aaa:bbb@test/a", false, out conn));
var macaroon = "0201036c6e640247030a10b0dbbde28f009f83d330bde05075ca251201301a160a0761646472657373120472656164120577726974651a170a08696e766f6963657312047265616412057772697465000006200ae088692e67cf14e767c3d2a4a67ce489150bf810654ff980e1b7a7e263d5e8";
var certthumbprint = "c51bb1d402306d0da00e85581b32aa56166bcbab7eb888ff925d7167eb436d06";
// We get this format from "openssl x509 -noout -fingerprint -sha256 -inform pem -in <certificate>"
var certthumbprint2 = "C5:1B:B1:D4:02:30:6D:0D:A0:0E:85:58:1B:32:AA:56:16:6B:CB:AB:7E:B8:88:FF:92:5D:71:67:EB:43:6D:06";
var lndUri = $"type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/;macaroon={macaroon};certthumbprint={certthumbprint}";
var lndUri2 = $"type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/;macaroon={macaroon};certthumbprint={certthumbprint2}";
var certificateHash = new X509Certificate2(Encoders.Hex.DecodeData("2d2d2d2d2d424547494e2043455254494649434154452d2d2d2d2d0a4d494942396a4343415a7967417749424167495156397a62474252724e54716b4e4b55676d72524d377a414b42676771686b6a4f50515144416a41784d5238770a485159445651514b45785a73626d5167595856306232646c626d56795958526c5a43426a5a584a304d51347744415944565151444577564754304e56557a41650a467730784f4441304d6a55794d7a517a4d6a4261467730784f5441324d6a41794d7a517a4d6a42614d444578487a416442674e5642416f54466d78755a4342680a645852765a3256755a584a686447566b49474e6c636e5178446a414d42674e5642414d5442555a50513156544d466b77457759484b6f5a497a6a3043415159490a4b6f5a497a6a304441516344516741454b7557424568564f75707965434157476130766e713262712f59396b41755a78616865646d454553482b753936436d450a397577486b4b2b4a7667547a66385141783550513741357254637155374b57595170303175364f426c5443426b6a414f42674e56485138424166384542414d430a4171517744775944565230544151482f42415577417745422f7a427642674e56485245456144426d6767564754304e565534494a6247396a5957786f62334e300a6877522f4141414268784141414141414141414141414141414141414141414268775373474f69786877514b41457342687753702f717473687754417141724c0a687753702f6d4a72687753702f754f77687753702f714e59687753702f6874436877514b70514157687753702f6c42514d416f4743437147534d343942414d430a413067414d45554349464866716d595a5043647a4a5178386b47586859473834394c31766541364c784d6f7a4f5774356d726835416945413662756e51556c710a6558553070474168776c3041654d726a4d4974394c7652736179756162565a593278343d0a2d2d2d2d2d454e442043455254494649434154452d2d2d2d2d0a"))
.GetCertHash(System.Security.Cryptography.HashAlgorithmName.SHA256);
Assert.True(LightningConnectionString.TryParse(lndUri, false, out conn));
Assert.True(LightningConnectionString.TryParse(lndUri2, false, out var conn2));
Assert.Equal(conn2.ToString(), conn.ToString());
Assert.Equal(lndUri, conn.ToString());
Assert.Equal(LightningConnectionType.LndREST, conn.ConnectionType);
Assert.Equal(macaroon, Encoders.Hex.EncodeData(conn.Macaroon));
Assert.Equal(certthumbprint.Replace(":", "", StringComparison.OrdinalIgnoreCase).ToLowerInvariant(), Encoders.Hex.EncodeData(conn.CertificateThumbprint));
Assert.True(certificateHash.SequenceEqual(conn.CertificateThumbprint));
// AllowInsecure can be set to allow http
Assert.False(LightningConnectionString.TryParse($"type=lnd-rest;server=http://127.0.0.1:53280/;macaroon={macaroon};allowinsecure=false", false, out conn2));
Assert.True(LightningConnectionString.TryParse($"type=lnd-rest;server=http://127.0.0.1:53280/;macaroon={macaroon};allowinsecure=true", false, out conn2));
Assert.True(LightningConnectionString.TryParse($"type=lnd-rest;server=http://127.0.0.1:53280/;macaroon={macaroon};allowinsecure=true", false, out conn2));
await ProcessLightningPayment(LightningConnectionType.CLightning);
}
[Fact]
public void CanSendLightningPaymentCLightning()
[Trait("Integration", "Integration")]
public async Task CanSendLightningPaymentCharge()
{
ProcessLightningPayment(LightningConnectionType.CLightning);
await ProcessLightningPayment(LightningConnectionType.Charge);
}
[Fact]
public void CanSendLightningPaymentCharge()
[Trait("Integration", "Integration")]
public async Task CanSendLightningPaymentLnd()
{
ProcessLightningPayment(LightningConnectionType.Charge);
await ProcessLightningPayment(LightningConnectionType.LndREST);
}
[Fact]
public void CanSendLightningPaymentLnd()
{
ProcessLightningPayment(LightningConnectionType.LndREST);
}
void ProcessLightningPayment(LightningConnectionType type)
async Task ProcessLightningPayment(LightningConnectionType type)
{
// For easier debugging and testing
// LightningLikePaymentHandler.LIGHTNING_TIMEOUT = int.MaxValue;
@ -571,16 +467,15 @@ namespace BTCPayServer.Tests
using (var tester = ServerTester.Create())
{
tester.Start();
await tester.EnsureChannelsSetup();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterLightningNode("BTC", type);
user.RegisterDerivationScheme("BTC");
tester.PrepareLightning(type);
await CanSendLightningPaymentCore(tester, user);
Task.WaitAll(CanSendLightningPaymentCore(tester, user));
Task.WaitAll(Enumerable.Range(0, 5)
await Task.WhenAll(Enumerable.Range(0, 5)
.Select(_ => CanSendLightningPaymentCore(tester, user))
.ToArray());
}
@ -588,10 +483,6 @@ namespace BTCPayServer.Tests
async Task CanSendLightningPaymentCore(ServerTester tester, TestAccount user)
{
// TODO: If this parameter is less than 1 second we start having concurrency problems
await Task.Delay(TimeSpan.FromMilliseconds(1000));
//
var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice()
{
Price = 0.01m,
@ -600,6 +491,7 @@ namespace BTCPayServer.Tests
OrderId = "orderId",
ItemDesc = "Some description"
});
await Task.Delay(TimeSpan.FromMilliseconds(1000)); // Give time to listen the new invoices
await tester.SendLightningPaymentAsync(invoice);
await EventuallyAsync(async () =>
{
@ -610,6 +502,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Integration", "Integration")]
public void CanUseServerInitiatedPairingCode()
{
using (var tester = ServerTester.Create())
@ -635,6 +528,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Integration", "Integration")]
public void CanSendIPN()
{
using (var callbackServer = new CustomServer())
@ -671,6 +565,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Integration", "Integration")]
public void CantPairTwiceWithSamePubkey()
{
using (var tester = ServerTester.Create())
@ -692,6 +587,92 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Integration", "Integration")]
public void CanSolveTheDogesRatesOnKraken()
{
var provider = new BTCPayNetworkProvider(NetworkType.Mainnet);
var factory = CreateBTCPayRateFactory();
var fetcher = new RateFetcher(factory);
Assert.True(RateRules.TryParse("X_X=kraken(X_BTC) * kraken(BTC_X)", out var rule));
foreach(var pair in new[] { "DOGE_USD", "DOGE_CAD", "DASH_CAD", "DASH_USD", "DASH_EUR" })
{
var result = fetcher.FetchRate(CurrencyPair.Parse(pair), rule).GetAwaiter().GetResult();
Assert.NotNull(result.BidAsk);
Assert.Empty(result.Errors);
}
}
[Fact]
[Trait("Integration", "Integration")]
public void CanRescanWallet()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var acc = tester.NewAccount();
acc.GrantAccess();
acc.RegisterDerivationScheme("BTC", true);
var btcDerivationScheme = acc.DerivationScheme;
acc.RegisterDerivationScheme("LTC", true);
var walletController = tester.PayTester.GetController<WalletsController>(acc.UserId);
WalletId walletId = new WalletId(acc.StoreId, "LTC");
var rescan = Assert.IsType<RescanWalletModel>(Assert.IsType<ViewResult>(walletController.WalletRescan(walletId).Result).Model);
Assert.False(rescan.Ok);
Assert.True(rescan.IsFullySync);
Assert.True(rescan.IsSegwit);
Assert.False(rescan.IsSupportedByCurrency);
Assert.False(rescan.IsServerAdmin);
walletId = new WalletId(acc.StoreId, "BTC");
var serverAdminClaim = new[] { new Claim(Policies.CanModifyServerSettings.Key, "true") };
walletController = tester.PayTester.GetController<WalletsController>(acc.UserId, additionalClaims: serverAdminClaim);
rescan = Assert.IsType<RescanWalletModel>(Assert.IsType<ViewResult>(walletController.WalletRescan(walletId).Result).Model);
Assert.True(rescan.Ok);
Assert.True(rescan.IsFullySync);
Assert.True(rescan.IsSupportedByCurrency);
Assert.True(rescan.IsServerAdmin);
rescan.GapLimit = 100;
// Sending a coin
var txId = tester.ExplorerNode.SendToAddress(btcDerivationScheme.Derive(new KeyPath("0/90")).ScriptPubKey, Money.Coins(1.0m));
tester.ExplorerNode.Generate(1);
var transactions = Assert.IsType<ListTransactionsViewModel>(Assert.IsType<ViewResult>(walletController.WalletTransactions(walletId).Result).Model);
Assert.Empty(transactions.Transactions);
Assert.IsType<RedirectToActionResult>(walletController.WalletRescan(walletId, rescan).Result);
while (true)
{
rescan = Assert.IsType<RescanWalletModel>(Assert.IsType<ViewResult>(walletController.WalletRescan(walletId).Result).Model);
if (rescan.Progress == null && rescan.LastSuccess != null)
{
if (rescan.LastSuccess.Found == 0)
continue;
// Scan over
break;
}
else
{
Assert.Null(rescan.TimeOfScan);
Assert.NotNull(rescan.RemainingTime);
Assert.NotNull(rescan.Progress);
Thread.Sleep(100);
}
}
Assert.Null(rescan.PreviousError);
Assert.NotNull(rescan.TimeOfScan);
Assert.Equal(1, rescan.LastSuccess.Found);
transactions = Assert.IsType<ListTransactionsViewModel>(Assert.IsType<ViewResult>(walletController.WalletTransactions(walletId).Result).Model);
var tx = Assert.Single(transactions.Transactions);
Assert.Equal(tx.Id, txId.ToString());
}
}
[Fact]
[Trait("Integration", "Integration")]
public void CanListInvoices()
{
using (var tester = ServerTester.Create())
@ -733,6 +714,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Integration", "Integration")]
public void CanGetRates()
{
using (var tester = ServerTester.Create())
@ -763,6 +745,13 @@ namespace BTCPayServer.Tests
Assert.NotNull(GetCurrencyPairRateResult);
Assert.NotNull(GetCurrencyPairRateResult.Data);
Assert.Equal("LTC", GetCurrencyPairRateResult.Data.Code);
// Should be OK because the request is signed, so we can know the store
var rates = acc.BitPay.GetRates();
HttpClient client = new HttpClient();
// Unauthentified requests should also be ok
var response = client.GetAsync($"http://127.0.0.1:{tester.PayTester.Port}/api/rates?storeId={acc.StoreId}").GetAwaiter().GetResult();
response.EnsureSuccessStatusCode();
}
}
@ -773,6 +762,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Integration", "Integration")]
public void CanRBFPayment()
{
using (var tester = ServerTester.Create())
@ -788,6 +778,7 @@ namespace BTCPayServer.Tests
}, Facade.Merchant);
var payment1 = invoice.BtcDue + Money.Coins(0.0001m);
var payment2 = invoice.BtcDue;
var tx1 = new uint256(tester.ExplorerNode.SendCommand("sendtoaddress", new object[]
{
invoice.BitcoinAddress,
@ -797,8 +788,10 @@ namespace BTCPayServer.Tests
false, //subtractfeefromamount
true, //replaceable
}).ResultString);
Logs.Tester.LogInformation($"Let's send a first payment of {payment1} for the {invoice.BtcDue} invoice ({tx1})");
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, user.SupportedNetwork.NBitcoinNetwork);
Logs.Tester.LogInformation($"The invoice should be paidOver");
Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
@ -816,9 +809,18 @@ namespace BTCPayServer.Tests
var output = tx.Outputs.First(o => o.Value == payment1);
output.Value = payment2;
output.ScriptPubKey = invoiceAddress.ScriptPubKey;
var replaced = tester.ExplorerNode.SignRawTransaction(tx);
tester.ExplorerNode.SendRawTransaction(replaced);
var test = tester.ExplorerClient.GetUTXOs(user.DerivationScheme, null);
using (var cts = new CancellationTokenSource(10000))
using (var listener = tester.ExplorerClient.CreateWebsocketNotificationSession())
{
listener.ListenAllDerivationSchemes();
var replaced = tester.ExplorerNode.SignRawTransaction(tx);
Thread.Sleep(1000); // Make sure the replacement has a different timestamp
var tx2 = tester.ExplorerNode.SendRawTransaction(replaced);
Logs.Tester.LogInformation($"Let's RBF with a payment of {payment2} ({tx2}), waiting for NBXplorer to pick it up");
Assert.Equal(tx2, ((NewTransactionEvent)listener.NextEvent(cts.Token)).TransactionData.TransactionHash);
}
Logs.Tester.LogInformation($"The invoice should now not be paidOver anymore");
Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
@ -829,6 +831,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Fast", "Fast")]
public void CanParseFilter()
{
var filter = "storeid:abc status:abed blabhbalh ";
@ -850,6 +853,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Fast", "Fast")]
public void CanParseFingerprint()
{
Assert.True(SSH.SSHFingerprint.TryParse("4e343c6fc6cfbf9339c02d06a151e1dd", out var unused));
@ -866,6 +870,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Integration", "Integration")]
public void TestAccessBitpayAPI()
{
using (var tester = ServerTester.Create())
@ -932,6 +937,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Integration", "Integration")]
public void CanUseExchangeSpecificRate()
{
using (var tester = ServerTester.Create())
@ -943,8 +949,8 @@ namespace BTCPayServer.Tests
user.RegisterDerivationScheme("BTC");
List<decimal> rates = new List<decimal>();
rates.Add(CreateInvoice(tester, user, "coinaverage"));
var bitflyer = CreateInvoice(tester, user, "bitflyer");
var bitflyer2 = CreateInvoice(tester, user, "bitflyer");
var bitflyer = CreateInvoice(tester, user, "bitflyer", "JPY");
var bitflyer2 = CreateInvoice(tester, user, "bitflyer", "JPY");
Assert.Equal(bitflyer, bitflyer2); // Should be equal because cache
rates.Add(bitflyer);
@ -955,7 +961,7 @@ namespace BTCPayServer.Tests
}
}
private static decimal CreateInvoice(ServerTester tester, TestAccount user, string exchange)
private static decimal CreateInvoice(ServerTester tester, TestAccount user, string exchange, string currency = "USD")
{
var storeController = user.GetController<StoresController>();
var vm = (RatesViewModel)((ViewResult)storeController.Rates()).Model;
@ -964,7 +970,7 @@ namespace BTCPayServer.Tests
var invoice2 = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5000.0m,
Currency = "USD",
Currency = currency,
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
@ -974,16 +980,15 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Integration", "Integration")]
public void CanTweakRate()
{
using (var tester = ServerTester.Create())
{
tester.PayTester.MockRates = false;
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
// First we try payment with a merchant having only BTC
var invoice1 = user.BitPay.CreateInvoice(new Invoice()
{
@ -994,7 +999,7 @@ namespace BTCPayServer.Tests
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
Assert.Equal(Money.Coins(1.0m), invoice1.BtcPrice);
var storeController = user.GetController<StoresController>();
var vm = (RatesViewModel)((ViewResult)storeController.Rates()).Model;
@ -1013,15 +1018,14 @@ namespace BTCPayServer.Tests
FullNotifications = true
}, Facade.Merchant);
// The rate was 5000 USD per BTC
// Now it should be 3000 USD per BTC
// So the expected price should be
var expected = Money.Coins(5000m / 3000m);
Assert.True(invoice2.BtcPrice.Almost(expected, 0.00001m));
var expectedRate = 5000.0m * 0.6m;
var expectedCoins = invoice2.Price / expectedRate;
Assert.True(invoice2.BtcPrice.Almost(Money.Coins(expectedCoins), 0.00001m));
}
}
[Fact]
[Trait("Integration", "Integration")]
public void CanHaveLTCOnlyStore()
{
using (var tester = ServerTester.Create())
@ -1086,6 +1090,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Integration", "Integration")]
public void CanModifyRates()
{
using (var tester = ServerTester.Create())
@ -1132,7 +1137,7 @@ namespace BTCPayServer.Tests
rateVm.ScriptTest = "BTC_USD,BTC_CAD,DOGE_USD,DOGE_CAD";
rateVm.Script = "DOGE_X = bittrex(DOGE_BTC) * BTC_X;\n" +
"X_CAD = quadrigacx(X_CAD);\n" +
"X_X = gdax(X_X);";
"X_X = coinaverage(X_X);";
rateVm.Spread = 50;
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates(rateVm, "Test").Result).Model);
Assert.True(rateVm.TestRateRules.All(t => !t.Error));
@ -1146,6 +1151,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Integration", "Integration")]
public void CanPayWithTwoCurrencies()
{
using (var tester = ServerTester.Create())
@ -1257,6 +1263,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Fast", "Fast")]
public void CanParseCurrencyValue()
{
Assert.True(CurrencyValue.TryParse("1.50USD", out var result));
@ -1277,6 +1284,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Fast", "Fast")]
public void CanParseDerivationScheme()
{
var parser = new DerivationSchemeParser(Network.TestNet);
@ -1314,9 +1322,23 @@ namespace BTCPayServer.Tests
result = parser.Parse(tpub);
Assert.Equal($"{tpub}-[p2sh]", result.ToString());
parser = new DerivationSchemeParser(Network.RegTest);
var parsed = parser.Parse("xpub6DG1rMYXiQtCc6CfdLFD9CtxqhzzRh7j6Sq6EdE9abgYy3cfDRrniLLv2AdwqHL1exiLnnKR5XXcaoiiexf3Y9R6J6rxkJtqJHzNzMW9QMZ-[p2sh]");
Assert.Equal("tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[p2sh]", parsed.ToString());
// Let's make sure we can't generate segwit with dogecoin
parser = new DerivationSchemeParser(NBitcoin.Altcoins.Dogecoin.Instance.Regtest);
parsed = parser.Parse("xpub6DG1rMYXiQtCc6CfdLFD9CtxqhzzRh7j6Sq6EdE9abgYy3cfDRrniLLv2AdwqHL1exiLnnKR5XXcaoiiexf3Y9R6J6rxkJtqJHzNzMW9QMZ-[p2sh]");
Assert.Equal("tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[legacy]", parsed.ToString());
parser = new DerivationSchemeParser(NBitcoin.Altcoins.Dogecoin.Instance.Regtest);
parsed = parser.Parse("tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[p2sh]");
Assert.Equal("tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[legacy]", parsed.ToString());
}
[Fact]
[Trait("Integration", "Integration")]
public void CanDisablePaymentMethods()
{
using (var tester = ServerTester.Create())
@ -1375,11 +1397,13 @@ namespace BTCPayServer.Tests
}
[Fact]
public void CanSetPaymentMethodLimits()
[Trait("Integration", "Integration")]
public async Task CanSetPaymentMethodLimits()
{
using (var tester = ServerTester.Create())
{
tester.Start();
await tester.EnsureChannelsSetup();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
@ -1418,6 +1442,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Integration", "Integration")]
public void CanUsePoSApp()
{
using (var tester = ServerTester.Create())
@ -1435,31 +1460,54 @@ namespace BTCPayServer.Tests
var vmpos = Assert.IsType<UpdatePointOfSaleViewModel>(Assert.IsType<ViewResult>(apps.UpdatePointOfSale(appId).Result).Model);
vmpos.Title = "hello";
vmpos.Currency = "CAD";
vmpos.Template =
"apple:\n" +
" price: 5.0\n" +
" title: good apple\n" +
"orange:\n" +
" price: 10.0\n";
vmpos.ButtonText = "{0} Purchase";
vmpos.CustomButtonText = "Nicolas Sexy Hair";
vmpos.Template = @"
apple:
price: 5.0
title: good apple
orange:
price: 10.0
donation:
price: 1.02
custom: true
";
Assert.IsType<RedirectToActionResult>(apps.UpdatePointOfSale(appId, vmpos).Result);
vmpos = Assert.IsType<UpdatePointOfSaleViewModel>(Assert.IsType<ViewResult>(apps.UpdatePointOfSale(appId).Result).Model);
Assert.Equal("hello", vmpos.Title);
var vmview = Assert.IsType<ViewPointOfSaleViewModel>(Assert.IsType<ViewResult>(apps.ViewPointOfSale(appId).Result).Model);
var publicApps = user.GetController<AppsPublicController>();
var vmview = Assert.IsType<ViewPointOfSaleViewModel>(Assert.IsType<ViewResult>(publicApps.ViewPointOfSale(appId).Result).Model);
Assert.Equal("hello", vmview.Title);
Assert.Equal(2, vmview.Items.Length);
Assert.Equal(3, vmview.Items.Length);
Assert.Equal("good apple", vmview.Items[0].Title);
Assert.Equal("orange", vmview.Items[1].Title);
Assert.Equal(10.0m, vmview.Items[1].Price.Value);
Assert.Equal("$5.00", vmview.Items[0].Price.Formatted);
Assert.IsType<RedirectResult>(apps.ViewPointOfSale(appId, 0, null, null, null, null, "orange").Result);
var invoice = user.BitPay.GetInvoices().First();
Assert.Equal(10.00m, invoice.Price);
Assert.Equal("CAD", invoice.Currency);
Assert.Equal("orange", invoice.ItemDesc);
Assert.Equal("{0} Purchase", vmview.ButtonText);
Assert.Equal("Nicolas Sexy Hair", vmview.CustomButtonText);
Assert.IsType<RedirectToActionResult>(publicApps.ViewPointOfSale(appId, 0, null, null, null, null, "orange").Result);
//
var invoices = user.BitPay.GetInvoices();
var orangeInvoice = invoices.First();
Assert.Equal(10.00m, orangeInvoice.Price);
Assert.Equal("CAD", orangeInvoice.Currency);
Assert.Equal("orange", orangeInvoice.ItemDesc);
// testing custom amount
var action = Assert.IsType<RedirectToActionResult>(publicApps.ViewPointOfSale(appId, 5, null, null, null, null, "donation").Result);
Assert.Equal(nameof(InvoiceController.Checkout), action.ActionName);
invoices = user.BitPay.GetInvoices();
var donationInvoice = invoices.Single(i => i.Price == 5m);
Assert.NotNull(donationInvoice);
Assert.Equal("CAD", donationInvoice.Currency);
Assert.Equal("donation", donationInvoice.ItemDesc);
}
}
[Fact]
[Trait("Integration", "Integration")]
public void CanCreateAndDeleteApps()
{
using (var tester = ServerTester.Create())
@ -1483,6 +1531,7 @@ namespace BTCPayServer.Tests
Assert.Single(appList.Apps);
Assert.Empty(appList2.Apps);
Assert.Equal("test", appList.Apps[0].AppName);
Assert.Equal(apps.CreatedAppId, appList.Apps[0].Id);
Assert.True(appList.Apps[0].IsOwner);
Assert.Equal(user.StoreId, appList.Apps[0].StoreId);
Assert.IsType<NotFoundResult>(apps2.DeleteApp(appList.Apps[0].Id).Result);
@ -1495,6 +1544,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Integration", "Integration")]
public void InvoiceFlowThroughDifferentStatesCorrectly()
{
using (var tester = ServerTester.Create())
@ -1516,7 +1566,7 @@ namespace BTCPayServer.Tests
var ctx = tester.PayTester.GetService<ApplicationDbContextFactory>().CreateContext();
Assert.Equal(0, invoice.CryptoInfo[0].TxCount);
Assert.True(invoice.MinerFees.ContainsKey("BTC"));
Assert.Equal(100m, invoice.MinerFees["BTC"].SatoshiPerBytes);
Assert.Contains(invoice.MinerFees["BTC"].SatoshiPerBytes, new[] { 100.0m, 20.0m });
Eventually(() =>
{
var textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery()
@ -1628,7 +1678,7 @@ namespace BTCPayServer.Tests
}, Facade.Merchant);
invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
cashCow.SendToAddress(invoiceAddress, invoice.BtcDue + Money.Coins(1));
var txId = cashCow.SendToAddress(invoiceAddress, invoice.BtcDue + Money.Coins(1));
Eventually(() =>
{
@ -1636,6 +1686,13 @@ namespace BTCPayServer.Tests
Assert.Equal("paid", localInvoice.Status);
Assert.Equal(Money.Zero, localInvoice.BtcDue);
Assert.Equal("paidOver", (string)((JValue)localInvoice.ExceptionStatus).Value);
var textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery()
{
StoreId = new[] { user.StoreId },
TextSearch = txId.ToString()
}).GetAwaiter().GetResult();
Assert.Single(textSearchResult);
});
cashCow.Generate(1);
@ -1651,6 +1708,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Integration", "Integration")]
public void CheckQuadrigacxRateProvider()
{
var quadri = new QuadrigacxRateProvider();
@ -1664,17 +1722,20 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Integration", "Integration")]
public void CanQueryDirectProviders()
{
var provider = new BTCPayNetworkProvider(NetworkType.Mainnet);
var factory = CreateBTCPayRateFactory(provider);
var factory = CreateBTCPayRateFactory();
foreach (var result in factory
.DirectProviders
.Select(p => (ExpectedName: p.Key, ResultAsync: p.Value.GetRatesAsync()))
.Providers
.Where(p => p.Value is BackgroundFetcherRateProvider)
.Select(p => (ExpectedName: p.Key, ResultAsync: p.Value.GetRatesAsync(), Fetcher: (BackgroundFetcherRateProvider)p.Value))
.ToList())
{
result.Fetcher.InvalidateCache();
var exchangeRates = result.ResultAsync.Result;
result.Fetcher.InvalidateCache();
Assert.NotNull(exchangeRates);
Assert.NotEmpty(exchangeRates);
Assert.NotEmpty(exchangeRates.ByExchange[result.ExpectedName]);
@ -1687,68 +1748,137 @@ namespace BTCPayServer.Tests
&& e.BidAsk.Bid > 1.0m // 1BTC will always be more than 1USD
);
}
// Kraken emit one request only after first GetRates
factory.Providers["kraken"].GetRatesAsync().GetAwaiter().GetResult();
}
[Fact]
[Trait("Integration", "Integration")]
public void CanGetRateCryptoCurrenciesByDefault()
{
var provider = new BTCPayNetworkProvider(NetworkType.Mainnet);
var factory = CreateBTCPayRateFactory(provider);
var factory = CreateBTCPayRateFactory();
var fetcher = new RateFetcher(factory);
var pairs =
provider.GetAll()
.Select(c => new CurrencyPair(c.CryptoCode, "USD"))
.ToHashSet();
var rules = new StoreBlob().GetDefaultRateRules(provider);
var result = factory.FetchRates(pairs, rules);
var result = fetcher.FetchRates(pairs, rules);
foreach (var value in result)
{
var rateResult = value.Value.GetAwaiter().GetResult();
Assert.NotNull(rateResult.BidAsk);
Logs.Tester.LogInformation($"Testing {value.Key.ToString()}");
Assert.True(rateResult.BidAsk != null, $"Impossible to get the rate {rateResult.EvaluatedRule}");
}
}
private static BTCPayRateProviderFactory CreateBTCPayRateFactory(BTCPayNetworkProvider provider)
public static RateProviderFactory CreateBTCPayRateFactory()
{
return new BTCPayRateProviderFactory(new MemoryCacheOptions() { ExpirationScanFrequency = TimeSpan.FromSeconds(1.0) }, provider, new CoinAverageSettings());
return new RateProviderFactory(CreateMemoryCache(), null, new CoinAverageSettings());
}
private static MemoryCacheOptions CreateMemoryCache()
{
return new MemoryCacheOptions() { ExpirationScanFrequency = TimeSpan.FromSeconds(1.0) };
}
class SpyRateProvider : IRateProvider
{
public bool Hit { get; set; }
public Task<ExchangeRates> GetRatesAsync()
{
Hit = true;
var rates = new ExchangeRates();
rates.Add(new ExchangeRate("coinaverage", CurrencyPair.Parse("BTC_USD"), new BidAsk(5000)));
return Task.FromResult(rates);
}
public void AssertHit()
{
Assert.True(Hit, "Should have hit the provider");
Hit = false;
}
public void AssertNotHit()
{
Assert.False(Hit, "Should have not hit the provider");
Hit = false;
}
}
[Fact]
[Trait("Integration", "Integration")]
public async Task CheckLogsRoute()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
var serverController = user.GetController<ServerController>();
var vm = Assert.IsType<LogsViewModel>(Assert.IsType<ViewResult>(await serverController.LogsView()).Model);
}
}
[Fact]
[Trait("Fast", "Fast")]
public void CheckRatesProvider()
{
var provider = new BTCPayNetworkProvider(NetworkType.Mainnet);
var coinAverage = new CoinAverageRateProvider();
var rates = coinAverage.GetRatesAsync().GetAwaiter().GetResult();
Assert.NotNull(rates.GetRate("coinaverage", new CurrencyPair("BTC", "JPY")));
var ratesBitpay = new BitpayRateProvider(new Bitpay(new Key(), new Uri("https://bitpay.com/"))).GetRatesAsync().GetAwaiter().GetResult();
Assert.NotNull(ratesBitpay.GetRate("bitpay", new CurrencyPair("BTC", "JPY")));
var spy = new SpyRateProvider();
RateRules.TryParse("X_X = coinaverage(X_X);", out var rateRules);
var factory = CreateBTCPayRateFactory(provider);
factory.CacheSpan = TimeSpan.FromSeconds(10);
var factory = CreateBTCPayRateFactory();
factory.Providers.Clear();
factory.Providers.Add("coinaverage", new CachedRateProvider("coinaverage", spy, new MemoryCache(CreateMemoryCache())));
factory.Providers.Add("bittrex", new CachedRateProvider("bittrex", spy, new MemoryCache(CreateMemoryCache())));
factory.CacheSpan = TimeSpan.FromSeconds(1);
var fetchedRate = factory.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
Assert.False(fetchedRate.Cached);
fetchedRate = factory.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
Assert.True(fetchedRate.Cached);
var fetcher = new RateFetcher(factory);
Thread.Sleep(11000);
fetchedRate = factory.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
Assert.False(fetchedRate.Cached);
fetchedRate = factory.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
Assert.True(fetchedRate.Cached);
var fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
spy.AssertHit();
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
spy.AssertNotHit();
Thread.Sleep(3000);
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
spy.AssertHit();
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
spy.AssertNotHit();
// Should cache at exchange level so this should hit the cache
var fetchedRate2 = factory.FetchRate(CurrencyPair.Parse("LTC_USD"), rateRules).GetAwaiter().GetResult();
Assert.True(fetchedRate.Cached);
Assert.NotEqual(fetchedRate.BidAsk.Bid, fetchedRate2.BidAsk.Bid);
var fetchedRate2 = fetcher.FetchRate(CurrencyPair.Parse("LTC_USD"), rateRules).GetAwaiter().GetResult();
spy.AssertNotHit();
Assert.Null(fetchedRate2.BidAsk);
Assert.Equal(RateRulesErrors.RateUnavailable, fetchedRate2.Errors.First());
// Should cache at exchange level this should not hit the cache as it is different exchange
RateRules.TryParse("X_X = bittrex(X_X);", out rateRules);
fetchedRate = factory.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
Assert.False(fetchedRate.Cached);
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
spy.AssertHit();
factory.Providers.Clear();
var fetch = new BackgroundFetcherRateProvider(spy);
fetch.DoNotAutoFetchIfExpired = true;
factory.Providers.Add("bittrex", fetch);
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
spy.AssertHit();
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
spy.AssertNotHit();
fetch.UpdateIfNecessary().GetAwaiter().GetResult();
spy.AssertNotHit();
fetch.RefreshRate = TimeSpan.FromSeconds(1.0);
Thread.Sleep(1020);
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
spy.AssertNotHit();
fetch.ValidatyTime = TimeSpan.FromSeconds(1.0);
fetch.UpdateIfNecessary().GetAwaiter().GetResult();
spy.AssertHit();
fetch.GetRatesAsync().GetAwaiter().GetResult();
Thread.Sleep(1000);
Assert.Throws<InvalidOperationException>(() => fetch.GetRatesAsync().GetAwaiter().GetResult());
}
private static bool IsMapped(Invoice invoice, ApplicationDbContext ctx)

@ -1,48 +0,0 @@
using System;
using NBitcoin;
using Xunit;
namespace BTCPayServer.Tests
{
// Helper class for testing functionality and generating data needed during coding/debuging
public class UnitTestPeusa
{
// Unit test that generates temorary checkout Bitpay page
// https://forkbitpay.slack.com/archives/C7M093Z55/p1508293682000217
// Testnet of Bitpay down
//[Fact]
//public void BitpayCheckout()
//{
// var key = new Key(Encoders.Hex.DecodeData("7b70a06f35562873e3dcb46005ed0fe78e1991ad906e56adaaafa40ba861e056"));
// var url = new Uri("https://test.bitpay.com/");
// var btcpay = new Bitpay(key, url);
// var invoice = btcpay.CreateInvoice(new Invoice()
// {
// Price = 5.0,
// Currency = "USD",
// PosData = "posData",
// OrderId = "cdfd8a5f-6928-4c3b-ba9b-ddf438029e73",
// ItemDesc = "Hello from the otherside"
// }, Facade.Merchant);
// // go to invoice.Url
// Console.WriteLine(invoice.Url);
//}
// Generating Extended public key to use on http://localhost:14142/stores/{storeId}
[Fact]
public void GeneratePubkey()
{
var network = Network.RegTest;
ExtKey masterKey = new ExtKey();
Console.WriteLine("Master key : " + masterKey.ToString(network));
ExtPubKey masterPubKey = masterKey.Neuter();
ExtPubKey pubkey = masterPubKey.Derive(0);
Console.WriteLine("PubKey " + 0 + " : " + pubkey.ToString(network));
}
}
}

@ -0,0 +1,93 @@
using System;
using System.Linq;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using NBitcoin.DataEncoders;
using Newtonsoft.Json.Linq;
using Xunit;
using System.IO;
namespace BTCPayServer.Tests
{
/// <summary>
/// This class hold easy to run utilities for dev time
/// </summary>
public class UtilitiesTests
{
/// <summary>
/// Download transifex transactions and put them in BTCPayServer\wwwroot\locales
/// </summary>
[Trait("Utilities", "Utilities")]
[Fact]
public async Task PullTransifexTranslations()
{
// 1. Generate an API Token on https://www.transifex.com/user/settings/api/
// 2. Run "dotnet user-secrets set TransifexAPIToken <youapitoken>"
var client = new TransifexClient(GetTransifexAPIToken());
var json = await client.GetTransifexAsync("https://api.transifex.com/organizations/btcpayserver/projects/btcpayserver/resources/enjson/");
var langs = ((JObject)json["stats"]).Properties().Select(n => n.Name).ToArray();
var langsDir = Path.Combine(Services.LanguageService.TryGetSolutionDirectoryInfo().FullName, "BTCPayServer", "wwwroot", "locales");
Task.WaitAll(langs.Select(async l =>
{
if (l == "no")
return;
var j = await client.GetTransifexAsync($"https://www.transifex.com/api/2/project/btcpayserver/resource/enjson/translation/{l}/");
var content = j["content"].Value<string>();
if (l == "en_US")
l = "en";
if (l == "ne_NP")
l = "np_NP";
if (l == "zh_CN")
l = "zh-SP";
if (l == "kk")
l = "kk-KZ";
var langCode = l.Replace("_", "-");
var langFile = Path.Combine(langsDir, langCode + ".json");
var jobj = JObject.Parse(content);
jobj["code"] = langCode;
if ((string)jobj["currentLanguage"] == "English")
return;
jobj.AddFirst(new JProperty("NOTICE_WARN", "THIS CODE HAS BEEN AUTOMATICALLY GENERATED FROM TRANSIFEX, IF YOU WISH TO HELP TRANSLATION COME ON THE SLACK http://slack.btcpayserver.org TO REQUEST PERMISSION TO https://www.transifex.com/btcpayserver/btcpayserver/"));
content = jobj.ToString(Newtonsoft.Json.Formatting.Indented);
File.WriteAllText(Path.Combine(langsDir, langFile), content);
}).ToArray());
}
private static string GetTransifexAPIToken()
{
var builder = new ConfigurationBuilder();
builder.AddUserSecrets("AB0AC1DD-9D26-485B-9416-56A33F268117");
var config = builder.Build();
var token = config["TransifexAPIToken"];
Assert.False(token == null, "TransifexAPIToken is not set.\n 1.Generate an API Token on https://www.transifex.com/user/settings/api/ \n 2.Run \"dotnet user-secrets set TransifexAPIToken <youapitoken>\"");
return token;
}
}
public class TransifexClient
{
public TransifexClient(string apiToken)
{
Client = new HttpClient();
APIToken = apiToken;
}
public HttpClient Client { get; }
public string APIToken { get; }
public async Task<JObject> GetTransifexAsync(string uri)
{
var message = new HttpRequestMessage(HttpMethod.Get, uri);
message.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Encoders.Base64.EncodeData(Encoding.ASCII.GetBytes($"api:{APIToken}")));
message.Headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
var response = await Client.SendAsync(message);
return await response.Content.ReadAsAsync<JObject>();
}
}
}

@ -1 +1,2 @@
docker exec -ti btcpayservertests_bitcoind_1 bitcoin-cli -datadir="/data" $args
$bitcoind_container_id=$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=bitcoind)
docker exec -ti $bitcoind_container_id bitcoin-cli -datadir="/data" $args

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

@ -14,7 +14,9 @@ services:
TESTS_LTCRPCCONNECTION: server=http://litecoind:43782;ceiwHEbqWI83:DwubwWsoo3
TESTS_BTCNBXPLORERURL: http://nbxplorer:32838/
TESTS_LTCNBXPLORERURL: http://nbxplorer:32838/
TESTS_DB: "Postgres"
TESTS_POSTGRES: User ID=postgres;Host=postgres;Port=5432;Database=btcpayserver
TESTS_MYSQL: User ID=root;Host=mysql;Port=3306;Database=btcpayserver
TESTS_PORT: 80
TESTS_HOSTNAME: tests
TEST_MERCHANTLIGHTNINGD: "type=clightning;server=/etc/merchant_lightningd_datadir/lightning-rpc"
@ -34,14 +36,16 @@ services:
# The dev container is not actually used, it is just handy to run `docker-compose up dev` to start all services
dev:
image: nicolasdorier/docker-bitcoin:0.16.0
image: nicolasdorier/docker-bitcoin:0.17.0
environment:
BITCOIN_NETWORK: regtest
BITCOIN_EXTRA_ARGS: |
regtest=1
deprecatedrpc=signrawtransaction
connect=bitcoind:39388
links:
- nbxplorer
- postgres
- mysql
- customer_lightningd
- merchant_lightningd
- lightning-charged
@ -49,21 +53,24 @@ services:
- merchant_lnd
devlnd:
image: nicolasdorier/docker-bitcoin:0.16.0
image: nicolasdorier/docker-bitcoin:0.17.0
environment:
BITCOIN_NETWORK: regtest
BITCOIN_EXTRA_ARGS: |
regtest=1
deprecatedrpc=signrawtransaction
connect=bitcoind:39388
links:
- nbxplorer
- postgres
- mysql
- customer_lnd
- merchant_lnd
nbxplorer:
image: nicolasdorier/nbxplorer:1.0.2.14
image: nicolasdorier/nbxplorer:2.0.0.0
restart: unless-stopped
ports:
- "32838:32838"
expose:
@ -87,31 +94,33 @@ services:
- litecoind
bitcoind:
image: nicolasdorier/docker-bitcoin:0.16.0
image: nicolasdorier/docker-bitcoin:0.17.0
environment:
BITCOIN_NETWORK: regtest
BITCOIN_EXTRA_ARGS: |
deprecatedrpc=signrawtransaction
rpcuser=ceiwHEbqWI83
rpcpassword=DwubwWsoo3
regtest=1
server=1
rpcport=43782
port=39388
whitelist=0.0.0.0/0
zmqpubrawtx=tcp://0.0.0.0:28332
zmqpubrawblock=tcp://0.0.0.0:28332
zmqpubrawtxlock=tcp://0.0.0.0:28332
zmqpubhashblock=tcp://0.0.0.0:28332
zmqpubrawtx=tcp://0.0.0.0:28333
ports:
- "43782:43782"
- "28332:28332"
expose:
- "43782" # RPC
- "39388" # P2P
- "28332" # ZMQ
- "28333" # ZMQ
volumes:
- "bitcoin_datadir:/data"
customer_lightningd:
image: nicolasdorier/clightning:v0.6-dev
image: nicolasdorier/clightning:v0.6.2-3-dev
stop_signal: SIGKILL
restart: unless-stopped
environment:
EXPOSE_TCP: "true"
LIGHTNINGD_OPT: |
@ -122,6 +131,7 @@ services:
announce-addr=customer_lightningd
log-level=debug
dev-broadcast-interval=1000
dev-bitcoind-poll=1
ports:
- "30992:9835" # api port
expose:
@ -134,7 +144,8 @@ services:
- bitcoind
lightning-charged:
image: shesek/lightning-charge:0.3.15
image: shesek/lightning-charge:0.4.3
restart: unless-stopped
environment:
NETWORK: regtest
API_TOKEN: foiewnccewuify
@ -153,7 +164,8 @@ services:
- merchant_lightningd
merchant_lightningd:
image: nicolasdorier/clightning:v0.6-dev
image: nicolasdorier/clightning:v0.6.2-3-dev
stop_signal: SIGKILL
environment:
EXPOSE_TCP: "true"
LIGHTNINGD_OPT: |
@ -198,9 +210,19 @@ services:
- "39372:5432"
expose:
- "5432"
mysql:
image: mysql:8.0.12
expose:
- "3306"
ports:
- "33036:3306"
environment:
- MYSQL_ALLOW_EMPTY_PASSWORD=yes
merchant_lnd:
image: btcpayserver/lnd:0.4.2.0
image: btcpayserver/lnd:0.5-beta-2
restart: unless-stopped
environment:
LND_CHAIN: "btc"
LND_ENVIRONMENT: "regtest"
@ -208,11 +230,13 @@ services:
restlisten=0.0.0.0:8080
bitcoin.node=bitcoind
bitcoind.rpchost=bitcoind:43782
bitcoind.zmqpath=tcp://bitcoind:28332
bitcoind.zmqpubrawblock=tcp://bitcoind:28332
bitcoind.zmqpubrawtx=tcp://bitcoind:28333
externalip=merchant_lnd:9735
no-macaroons=1
debuglevel=debug
noencryptwallet=1
noseedbackup=1
trickledelay=1000
ports:
- "53280:8080"
expose:
@ -224,7 +248,8 @@ services:
- bitcoind
customer_lnd:
image: btcpayserver/lnd:0.4.2.0
image: btcpayserver/lnd:0.5-beta-2
restart: unless-stopped
environment:
LND_CHAIN: "btc"
LND_ENVIRONMENT: "regtest"
@ -232,11 +257,13 @@ services:
restlisten=0.0.0.0:8080
bitcoin.node=bitcoind
bitcoind.rpchost=bitcoind:43782
bitcoind.zmqpath=tcp://bitcoind:28332
bitcoind.zmqpubrawblock=tcp://bitcoind:28332
bitcoind.zmqpubrawtx=tcp://bitcoind:28333
externalip=customer_lnd:10009
no-macaroons=1
debuglevel=debug
noencryptwallet=1
noseedbackup=1
trickledelay=1000
ports:
- "53281:8080"
expose:

@ -1 +1,2 @@
docker exec -ti btcpayservertests_customer_lightningd_1 lightning-cli $args
$customer_lightning_container_id=$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=customer_lightningd)
docker exec -ti $customer_lightning_container_id lightning-cli $args

@ -1,3 +1,4 @@
#!/bin/bash
docker exec -ti btcpayservertests_customer_lightningd_1 lightning-cli "$@"
customer_lightning_container_id="$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=customer_lightningd)"
docker exec -ti $customer_lightning_container_id lightning-cli "$@"

@ -1 +1,2 @@
docker exec -ti btcpayservertests_litecoind_1 litecoin-cli -datadir="/data" $args
$litecoind_container_id=$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=litecoind)
docker exec -ti $litecoind_container_id litecoin-cli -datadir="/data" $args

@ -1,3 +1,4 @@
#!/bin/bash
docker exec -ti btcpayservertests_litecoind_1 litecoin-cli -datadir="/data" "$@"
litecoind_container_id="$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=litecoind)"
docker exec -ti "$litecoind_container_id" litecoin-cli -datadir="/data" "$@"

@ -1 +1,2 @@
docker exec -ti btcpayservertests_merchant_lightningd_1 lightning-cli $args
$merchant_lightning_container_id=$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=merchant_lightningd)
docker exec -ti $merchant_lightning_container_id lightning-cli $args

@ -1,3 +1,4 @@
#!/bin/bash
docker exec -ti btcpayservertests_merchant_lightningd_1 lightning-cli "$@"
merchant_lightning_container_id="$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=merchant_lightningd)"
docker exec -ti $merchant_lightning_container_id lightning-cli "$@"

@ -0,0 +1,3 @@
{
"parallelizeTestCollections": false
}

@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
using NBitcoin;
using NBXplorer;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
public void InitBitcore()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("BTX");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Bitcore",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://insight.bitcore.cc/tx/{0}" : "https://insight.bitcore.cc/tx/{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "bitcore",
DefaultRateRules = new[]
{
"BTX_X = BTX_BTC * BTC_X",
"BTX_BTC = cryptopia(BTX_BTC)"
},
CryptoImagePath = "imlegacy/bitcore.svg",
LightningImagePath = "imlegacy/bitcore-lightning.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("160'") : new KeyPath("1'")
});
}
}
}

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

@ -24,7 +24,7 @@ namespace BTCPayServer
DefaultRateRules = new[]
{
"MONA_X = MONA_BTC * BTC_X",
"MONA_BTC = zaif(MONA_BTC)"
"MONA_BTC = bittrex(MONA_BTC)"
},
CryptoImagePath = "imlegacy/monacoin.png",
LightningImagePath = "imlegacy/mona-lightning.svg",

@ -47,6 +47,7 @@ namespace BTCPayServer
NetworkType = networkType;
InitBitcoin();
InitLitecoin();
InitBitcore();
InitDogecoin();
InitBitcoinGold();
InitMonacoin();

@ -2,9 +2,12 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<Version>1.0.2.90</Version>
<Version>1.0.3.17</Version>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup>
<PropertyGroup>
<LangVersion>7.3</LangVersion>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Build\dockerfiles\**" />
<Compile Remove="wwwroot\bundles\jqueryvalidate\**" />
@ -30,33 +33,43 @@
<EmbeddedResource Include="Currencies.txt" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.2" />
<PackageReference Include="BuildBundlerMinifier" Version="2.7.385" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.4.1" />
<PackageReference Include="Hangfire" Version="1.6.19" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.5.3" />
<PackageReference Include="Hangfire" Version="1.6.20" />
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.2" />
<PackageReference Include="Hangfire.PostgreSql" Version="1.4.8.2" />
<PackageReference Include="LedgerWallet" Version="2.0.0" />
<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.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.0" />
<PackageReference Include="NBitcoin" Version="4.1.1.32" />
<PackageReference Include="NBitpayClient" Version="1.0.0.29" />
<PackageReference Include="DBreeze" Version="1.87.0" />
<PackageReference Include="NBXplorer.Client" Version="1.0.2.15" />
<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.71" />
<PackageReference Include="NBitpayClient" Version="1.0.0.30" />
<PackageReference Include="DBreeze" Version="1.92.0" />
<PackageReference Include="NBXplorer.Client" Version="2.0.0" />
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.2" />
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.3" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.16" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.1.0" />
<PackageReference Include="NicolasDorier.RateLimits" Version="1.0.0.3" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.18" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.1.2" />
<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.0" />
<PackageReference Include="Text.Analyzers" Version="2.6.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version=" 2.1.0" PrivateAssets="All" />
<PackageReference Include="YamlDotNet" Version="4.3.1" />
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.6" />
<PackageReference Include="YamlDotNet" Version="5.2.1" />
</ItemGroup>
<ItemGroup>
@ -116,10 +129,29 @@
</ItemGroup>
<ItemGroup>
<Content Update="Views\Server\SSHService.cshtml">
<Content Update="Views\Apps\_ViewImports.cshtml">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\LndRestServices.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\SSHService.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Stores\ShowToken.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Stores\PayButtonEnable.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Stores\PayButton.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\LNDGRPCServices.cshtml">
<Content Update="Views\Public\PayButtonHandle.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\LndGrpcServices.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\Maintenance.cshtml">
@ -131,6 +163,12 @@
<Content Update="Views\Wallets\ListWallets.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\WalletRescan.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\WalletSendLedger.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\WalletTransactions.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
@ -144,10 +182,4 @@
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
</ItemGroup>
<ItemGroup>
<None Update="devtest.pfx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

@ -14,6 +14,9 @@ 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
{
@ -31,6 +34,12 @@ namespace BTCPayServer.Configuration
get; set;
}
public string ConfigurationFile
{
get;
private set;
}
public string LogFile
{
get;
private set;
@ -52,6 +61,16 @@ namespace BTCPayServer.Configuration
set;
} = new List<NBXplorerConnectionSetting>();
public static string GetDebugLog(IConfiguration configuration)
{
return configuration.GetValue<string>("debuglog", null);
}
public static LogEventLevel GetDebugLogLevel(IConfiguration configuration)
{
var raw = configuration.GetValue("debugloglevel", nameof(LogEventLevel.Debug));
return (LogEventLevel)Enum.Parse(typeof(LogEventLevel), raw, true);
}
public void LoadArgs(IConfiguration conf)
{
NetworkType = DefaultConfiguration.GetNetworkType(conf);
@ -77,6 +96,7 @@ namespace BTCPayServer.Configuration
setting.ExplorerUri = conf.GetOrDefault<Uri>($"{net.CryptoCode}.explorer.url", net.NBXplorerNetwork.DefaultSettings.DefaultUrl);
setting.CookieFile = conf.GetOrDefault<string>($"{net.CryptoCode}.explorer.cookiefile", net.NBXplorerNetwork.DefaultSettings.DefaultCookieFile);
NBXplorerConnectionSettings.Add(setting);
{
var lightning = conf.GetOrDefault<string>($"{net.CryptoCode}.lightning", string.Empty);
if (lightning.Length != 0)
@ -98,25 +118,31 @@ namespace BTCPayServer.Configuration
}
}
void externalLnd<T>(string code, string lndType)
{
var lightning = conf.GetOrDefault<string>($"{net.CryptoCode}.external.lnd.grpc", string.Empty);
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 {net.CryptoCode}.external.lnd.grpc, " + Environment.NewLine +
$"lnd server: 'type=lnd-grpc;server=https://lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
$"lnd server: 'type=lnd-grpc;server=https://lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
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);
}
ExternalServicesByCryptoCode.Add(net.CryptoCode, new ExternalLNDGRPC(connectionString));
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");
}
Logs.Configuration.LogInformation("Supported chains: " + String.Join(',', supportedChains.ToArray()));
PostgresConnectionString = conf.GetOrDefault<string>("postgres", null);
MySQLConnectionString = conf.GetOrDefault<string>("mysql", null);
BundleJsCss = conf.GetOrDefault<bool>("bundlejscss", true);
ExternalUrl = conf.GetOrDefault<Uri>("externalurl", null);
@ -126,12 +152,12 @@ namespace BTCPayServer.Configuration
int waitTime = 0;
while (!string.IsNullOrEmpty(sshSettings.KeyFile) && !File.Exists(sshSettings.KeyFile))
{
if(waitTime++ < 5)
if (waitTime++ < 5)
System.Threading.Thread.Sleep(1000);
else
throw new ConfigException($"sshkeyfile does not exist");
}
if (sshSettings.Port > ushort.MaxValue ||
sshSettings.Port < ushort.MinValue)
throw new ConfigException($"ssh port is invalid");
@ -165,6 +191,13 @@ namespace BTCPayServer.Configuration
var old = conf.GetOrDefault<Uri>("internallightningnode", null);
if (old != null)
throw new ConfigException($"internallightningnode should not be used anymore, use btclightning instead");
LogFile = GetDebugLog(conf);
if (!string.IsNullOrEmpty(LogFile))
{
Logs.Configuration.LogInformation("LogFile: " + LogFile);
Logs.Configuration.LogInformation("Log Level: " + GetDebugLogLevel(conf));
}
}
private SSHSettings ParseSSHConfiguration(IConfiguration conf)
@ -223,6 +256,11 @@ namespace BTCPayServer.Configuration
get;
set;
}
public string MySQLConnectionString
{
get;
set;
}
public Uri ExternalUrl
{
get;
@ -249,29 +287,4 @@ namespace BTCPayServer.Configuration
return builder.ToString();
}
}
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
{
}
public class ExternalLNDGRPC : ExternalService
{
public ExternalLNDGRPC(LightningConnectionString connectionString)
{
ConnectionString = connectionString;
}
public LightningConnectionString ConnectionString { get; set; }
}
}

@ -37,6 +37,8 @@ namespace BTCPayServer.Configuration
}
else if (typeof(T) == typeof(string))
return (T)(object)str;
else if (typeof(T) == typeof(IPAddress))
return (T)(object)IPAddress.Parse(str);
else if (typeof(T) == typeof(IPEndPoint))
{
var separator = str.LastIndexOf(":", StringComparison.InvariantCulture);

@ -31,6 +31,7 @@ namespace BTCPayServer.Configuration
app.Option("--regtest | -regtest", $"Use regtest (deprecated, use --network instead)", 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("--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);
@ -39,6 +40,8 @@ namespace BTCPayServer.Configuration
app.Option("--sshkeyfile", "SSH private key file to manage BTCPay (default: empty)", CommandOptionType.SingleValue);
app.Option("--sshkeyfilepassword", "Password of the SSH keyfile (default: empty)", CommandOptionType.SingleValue);
app.Option("--sshtrustedfingerprints", "SSH Host public key fingerprint or sha256 (default: empty, it will allow untrusted connections)", CommandOptionType.SingleValue);
app.Option("--debuglog", "A rolling log file for debug messages.", CommandOptionType.SingleValue);
app.Option("--debugloglevel", "The severity you log (default:information)", CommandOptionType.SingleValue);
foreach (var network in provider.GetAll())
{
var crypto = network.CryptoCode.ToLowerInvariant();
@ -104,9 +107,12 @@ namespace BTCPayServer.Configuration
builder.AppendLine("### Server settings ###");
builder.AppendLine("#port=" + defaultSettings.DefaultPort);
builder.AppendLine("#bind=127.0.0.1");
builder.AppendLine("#httpscertificatefilepath=devtest.pfx");
builder.AppendLine("#httpscertificatefilepassword=toto");
builder.AppendLine();
builder.AppendLine("### Database ###");
builder.AppendLine("#postgres=User ID=root;Password=myPassword;Host=localhost;Port=5432;Database=myDataBase;");
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())

@ -0,0 +1,35 @@
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, LndTypes type)
{
ConnectionString = connectionString;
Type = type;
}
public LndTypes Type { get; set; }
public LightningConnectionString ConnectionString { get; set; }
}
public enum LndTypes
{
gRPC, Rest
}
public class ExternalLndGrpc : ExternalLnd
{
public ExternalLndGrpc(LightningConnectionString connectionString) : base(connectionString, LndTypes.gRPC) { }
}
public class ExternalLndRest : ExternalLnd
{
public ExternalLndRest(LightningConnectionString connectionString) : base(connectionString, LndTypes.Rest) { }
}
}

@ -0,0 +1,22 @@
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
{
}
}

@ -18,6 +18,7 @@ using BTCPayServer.Services.Stores;
using BTCPayServer.Logging;
using BTCPayServer.Security;
using System.Globalization;
using NicolasDorier.RateLimits;
namespace BTCPayServer.Controllers
{
@ -70,6 +71,7 @@ namespace BTCPayServer.Controllers
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
[RateLimitsFilter(ZoneLimits.Login, Scope = RateLimitsScope.RemoteAddress)]
public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
{
ViewData["ReturnUrl"] = returnUrl;
@ -88,7 +90,7 @@ namespace BTCPayServer.Controllers
}
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false);
var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: true);
if (result.Succeeded)
{
_logger.LogInformation("User logged in.");

@ -1,25 +1,11 @@
using System;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Models;
using BTCPayServer.Models.AppViewModels;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using NBitcoin.DataEncoders;
using NBitcoin;
using BTCPayServer.Services.Apps;
using Newtonsoft.Json;
using YamlDotNet.RepresentationModel;
using System.IO;
using BTCPayServer.Services.Rates;
using System.Globalization;
using System.Text;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace BTCPayServer.Controllers
{
@ -29,28 +15,54 @@ namespace BTCPayServer.Controllers
{
public PointOfSaleSettings()
{
Title = "My awesome Point of Sale";
Title = "Tea shop";
Currency = "USD";
Template =
"tea:\n" +
" price: 0.02\n" +
" title: Green Tea # title is optional, defaults to the keys\n\n" +
"coffee:\n" +
" price: 1\n\n" +
"bamba:\n" +
" price: 3\n\n" +
"beer:\n" +
" price: 7\n\n" +
"hat:\n" +
" price: 15\n\n" +
"tshirt:\n" +
" price: 25";
"green tea:\n" +
" price: 1\n" +
" title: Green Tea\n" +
" description: Lovely, fresh and tender, Meng Ding Gan Lu ('sweet dew') is grown in the lush Meng Ding Mountains of the southwestern province of Sichuan where it has been cultivated for over a thousand years.\n" +
" image: https://cdn.pixabay.com/photo/2015/03/26/11/03/green-tea-692339__480.jpg\n\n" +
"black tea:\n" +
" price: 1\n" +
" title: Black Tea\n" +
" description: Tian Jian Tian Jian means 'heavenly tippy tea' in Chinese, and it describes the finest grade of dark tea. Our Tian Jian dark tea is from Hunan province which is famous for making some of the best dark teas available.\n" +
" image: https://cdn.pixabay.com/photo/2016/11/29/13/04/beverage-1869716__480.jpg\n\n" +
"rooibos:\n" +
" price: 1.2\n" +
" title: Rooibos\n" +
" description: Rooibos is a dramatic red tea made from a South African herb that contains polyphenols and flavonoids. Often called 'African redbush tea', Rooibos herbal tea delights the senses and delivers potential health benefits with each caffeine-free sip.\n" +
" image: https://cdn.pixabay.com/photo/2017/01/08/08/14/water-1962388__480.jpg\n\n" +
"pu erh:\n" +
" price: 2\n" +
" title: Pu Erh\n" +
" description: This loose pur-erh tea is produced in Yunnan Province, China. The process in a relatively high humidity environment has mellowed the elemental character of the tea when compared to young Pu-erh.\n" +
" image: https://cdn.pixabay.com/photo/2018/07/21/16/56/tea-cup-3552917__480.jpg\n\n" +
"herbal tea:\n" +
" price: 1.8\n" +
" title: Herbal Tea\n" +
" description: Chamomile tea is made from the flower heads of the chamomile plant. The medicinal use of chamomile dates back to the ancient Egyptians, Romans and Greeks. Pay us what you want!\n" +
" image: https://cdn.pixabay.com/photo/2015/07/02/20/57/chamomile-829538__480.jpg\n" +
" custom: true\n\n" +
"fruit tea:\n" +
" price: 1.5\n" +
" title: Fruit Tea\n" +
" description: The Tibetan Himalayas, the land is majestic and beautiful—a spiritual place where, despite the perilous environment, many journey seeking enlightenment. Pay us what you want!\n" +
" image: https://cdn.pixabay.com/photo/2016/09/16/11/24/darts-1673812__480.jpg\n" +
" custom: true";
ShowCustomAmount = true;
}
public string Title { get; set; }
public string Currency { get; set; }
public string Template { get; set; }
public bool ShowCustomAmount { get; set; }
public const string BUTTON_TEXT_DEF = "Buy for {0}";
public string ButtonText { get; set; } = BUTTON_TEXT_DEF;
public const string CUSTOM_BUTTON_TEXT_DEF = "Pay";
public string CustomButtonText { get; set; } = CUSTOM_BUTTON_TEXT_DEF;
public string CustomCSSLink { get; set; }
}
[HttpGet]
@ -66,7 +78,10 @@ namespace BTCPayServer.Controllers
Title = settings.Title,
ShowCustomAmount = settings.ShowCustomAmount,
Currency = settings.Currency,
Template = settings.Template
Template = settings.Template,
ButtonText = settings.ButtonText ?? PointOfSaleSettings.BUTTON_TEXT_DEF,
CustomButtonText = settings.CustomButtonText ?? PointOfSaleSettings.CUSTOM_BUTTON_TEXT_DEF,
CustomCSSLink = settings.CustomCSSLink
};
if (HttpContext?.Request != null)
{
@ -87,7 +102,7 @@ namespace BTCPayServer.Controllers
}
try
{
var items = Parse(settings.Template, settings.Currency);
var items = _AppsHelper.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\" />");
@ -109,11 +124,11 @@ namespace BTCPayServer.Controllers
[Route("{appId}/settings/pos")]
public async Task<IActionResult> UpdatePointOfSale(string appId, UpdatePointOfSaleViewModel vm)
{
if (_Currencies.GetCurrencyData(vm.Currency, false) == null)
if (_AppsHelper.GetCurrencyData(vm.Currency, false) == null)
ModelState.AddModelError(nameof(vm.Currency), "Invalid currency");
try
{
Parse(vm.Template, vm.Currency);
_AppsHelper.Parse(vm.Template, vm.Currency);
}
catch
{
@ -131,146 +146,16 @@ namespace BTCPayServer.Controllers
Title = vm.Title,
ShowCustomAmount = vm.ShowCustomAmount,
Currency = vm.Currency.ToUpperInvariant(),
Template = vm.Template
Template = vm.Template,
ButtonText = vm.ButtonText,
CustomButtonText = vm.CustomButtonText,
CustomCSSLink = vm.CustomCSSLink
});
await UpdateAppSettings(app);
StatusMessage = "App updated";
return RedirectToAction(nameof(ListApps));
}
[HttpGet]
[Route("{appId}/pos")]
public async Task<IActionResult> ViewPointOfSale(string appId)
{
var app = await GetApp(appId, AppType.PointOfSale);
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
var currency = _Currencies.GetCurrencyData(settings.Currency, false);
double step = currency == null ? 1 : Math.Pow(10, -(currency.Divisibility));
return View(new ViewPointOfSaleViewModel()
{
Title = settings.Title,
Step = step.ToString(CultureInfo.InvariantCulture),
ShowCustomAmount = settings.ShowCustomAmount,
Items = Parse(settings.Template, settings.Currency)
});
}
private async Task<AppData> GetApp(string appId, AppType appType)
{
using (var ctx = _ContextFactory.CreateContext())
{
return await ctx.Apps
.Where(us => us.Id == appId &&
us.AppType == appType.ToString())
.FirstOrDefaultAsync();
}
}
private ViewPointOfSaleViewModel.Item[] Parse(string template, string currency)
{
var input = new StringReader(template);
YamlStream stream = new YamlStream();
stream.Load(input);
var root = (YamlMappingNode)stream.Documents[0].RootNode;
return root
.Children
.Select(kv => new { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlMappingNode })
.Where(kv => kv.Value != null)
.Select(c => new ViewPointOfSaleViewModel.Item()
{
Id = c.Key,
Title = c.Value.Children
.Select(kv => new { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlScalarNode })
.Where(kv => kv.Value != null)
.Where(cc => cc.Key == "title")
.FirstOrDefault()?.Value?.Value ?? c.Key,
Price = c.Value.Children
.Select(kv => new { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlScalarNode })
.Where(kv => kv.Value != null)
.Where(cc => cc.Key == "price")
.Select(cc => new ViewPointOfSaleViewModel.Item.ItemPrice()
{
Value = decimal.Parse(cc.Value.Value, CultureInfo.InvariantCulture),
Formatted = FormatCurrency(cc.Value.Value, currency)
})
.Single()
})
.ToArray();
}
string FormatCurrency(string price, string currency)
{
return decimal.Parse(price, CultureInfo.InvariantCulture).ToString("C", _Currencies.GetCurrencyProvider(currency));
}
[HttpPost]
[Route("{appId}/pos")]
[IgnoreAntiforgeryToken]
[EnableCors(CorsPolicies.All)]
public async Task<IActionResult> ViewPointOfSale(string appId,
decimal amount,
string email,
string orderId,
string notificationUrl,
string redirectUrl,
string choiceKey)
{
var app = await GetApp(appId, AppType.PointOfSale);
if (string.IsNullOrEmpty(choiceKey) && amount <= 0)
{
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
}
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
if (string.IsNullOrEmpty(choiceKey) && !settings.ShowCustomAmount)
{
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
}
string title = null;
var price = 0.0m;
if (!string.IsNullOrEmpty(choiceKey))
{
var choices = Parse(settings.Template, settings.Currency);
var choice = choices.FirstOrDefault(c => c.Id == choiceKey);
if (choice == null)
return NotFound();
title = choice.Title;
price = choice.Price.Value;
}
else
{
if (!settings.ShowCustomAmount)
return NotFound();
price = amount;
title = settings.Title;
}
var store = await GetStore(app);
var invoice = await _InvoiceController.CreateInvoiceCore(new NBitpayClient.Invoice()
{
ItemDesc = title,
Currency = settings.Currency,
Price = price,
BuyerEmail = email,
OrderId = orderId,
NotificationURL = notificationUrl,
RedirectURL = redirectUrl,
FullNotifications = true
}, store, HttpContext.Request.GetAbsoluteRoot());
return Redirect(invoice.Data.Url);
}
private async Task<StoreData> GetStore(AppData app)
{
using (var ctx = _ContextFactory.CreateContext())
{
return await ctx.Stores.FirstOrDefaultAsync(s => s.Id == app.StoreDataId);
}
}
private async Task UpdateAppSettings(AppData app)
{
using (var ctx = _ContextFactory.CreateContext())

@ -1,44 +1,47 @@
using System;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Models;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Security;
using BTCPayServer.Services.Apps;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using NBitcoin.DataEncoders;
using Microsoft.EntityFrameworkCore;
using NBitcoin;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Rates;
using NBitcoin.DataEncoders;
namespace BTCPayServer.Controllers
{
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
[AutoValidateAntiforgeryToken]
[Route("apps")]
public partial class AppsController : Controller
{
ApplicationDbContextFactory _ContextFactory;
UserManager<ApplicationUser> _UserManager;
CurrencyNameTable _Currencies;
InvoiceController _InvoiceController;
[TempData]
public string StatusMessage { get; set; }
public AppsController(
UserManager<ApplicationUser> userManager,
ApplicationDbContextFactory contextFactory,
CurrencyNameTable currencies,
InvoiceController invoiceController)
BTCPayNetworkProvider networkProvider,
AppsHelper appsHelper)
{
_InvoiceController = invoiceController;
_UserManager = userManager;
_ContextFactory = contextFactory;
_Currencies = currencies;
_NetworkProvider = networkProvider;
_AppsHelper = appsHelper;
}
private UserManager<ApplicationUser> _UserManager;
private ApplicationDbContextFactory _ContextFactory;
private BTCPayNetworkProvider _NetworkProvider;
private AppsHelper _AppsHelper;
[TempData]
public string StatusMessage { get; set; }
public string CreatedAppId { get; set; }
public async Task<IActionResult> ListApps()
{
var apps = await GetAllApps();
@ -102,7 +105,7 @@ namespace BTCPayServer.Controllers
StatusMessage = "Error: You are not owner of this store";
return RedirectToAction(nameof(ListApps));
}
var id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(32));
var id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(20));
using (var ctx = _ContextFactory.CreateContext())
{
var appData = new AppData() { Id = id };
@ -113,7 +116,7 @@ namespace BTCPayServer.Controllers
await ctx.SaveChangesAsync();
}
StatusMessage = "App successfully created";
CreatedAppId = id;
if (appType == AppType.PointOfSale)
return RedirectToAction(nameof(UpdatePointOfSale), new { appId = id });
return RedirectToAction(nameof(ListApps));
@ -134,21 +137,9 @@ namespace BTCPayServer.Controllers
});
}
private async Task<AppData> GetOwnedApp(string appId, AppType? type = null)
private Task<AppData> GetOwnedApp(string appId, AppType? type = null)
{
var userId = GetUserId();
using (var ctx = _ContextFactory.CreateContext())
{
var app = await ctx.UserStore
.Where(us => us.ApplicationUserId == userId && us.Role == StoreRoles.Owner)
.SelectMany(us => us.StoreData.Apps.Where(a => a.Id == appId))
.FirstOrDefaultAsync();
if (app == null)
return null;
if (type != null && type.Value.ToString() != app.AppType)
return null;
return app;
}
return _AppsHelper.GetAppDataIfOwner(GetUserId(), appId, type);
}
private async Task<StoreData[]> GetOwnedStores()

@ -0,0 +1,233 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Security;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Rates;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using YamlDotNet.RepresentationModel;
using static BTCPayServer.Controllers.AppsController;
namespace BTCPayServer.Controllers
{
public class AppsPublicController : Controller
{
public AppsPublicController(AppsHelper appsHelper, InvoiceController invoiceController)
{
_AppsHelper = appsHelper;
_InvoiceController = invoiceController;
}
private AppsHelper _AppsHelper;
private InvoiceController _InvoiceController;
[HttpGet]
[Route("/apps/{appId}/pos")]
public async Task<IActionResult> ViewPointOfSale(string appId)
{
var app = await _AppsHelper.GetApp(appId, AppType.PointOfSale);
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
var currency = _AppsHelper.GetCurrencyData(settings.Currency, false);
double step = currency == null ? 1 : Math.Pow(10, -(currency.Divisibility));
return View(new ViewPointOfSaleViewModel()
{
Title = settings.Title,
Step = step.ToString(CultureInfo.InvariantCulture),
ShowCustomAmount = settings.ShowCustomAmount,
CurrencySymbol = currency.Symbol,
Items = _AppsHelper.Parse(settings.Template, settings.Currency),
ButtonText = settings.ButtonText,
CustomButtonText = settings.CustomButtonText,
CustomCSSLink = settings.CustomCSSLink
});
}
[HttpPost]
[Route("/apps/{appId}/pos")]
[IgnoreAntiforgeryToken]
[EnableCors(CorsPolicies.All)]
public async Task<IActionResult> ViewPointOfSale(string appId,
decimal amount,
string email,
string orderId,
string notificationUrl,
string redirectUrl,
string choiceKey)
{
var app = await _AppsHelper.GetApp(appId, AppType.PointOfSale);
if (string.IsNullOrEmpty(choiceKey) && amount <= 0)
{
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
}
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
if (string.IsNullOrEmpty(choiceKey) && !settings.ShowCustomAmount)
{
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
}
string title = null;
var price = 0.0m;
if (!string.IsNullOrEmpty(choiceKey))
{
var choices = _AppsHelper.Parse(settings.Template, settings.Currency);
var choice = choices.FirstOrDefault(c => c.Id == choiceKey);
if (choice == null)
return NotFound();
title = choice.Title;
price = choice.Price.Value;
if (amount > price)
price = amount;
}
else
{
if (!settings.ShowCustomAmount)
return NotFound();
price = amount;
title = settings.Title;
}
var store = await _AppsHelper.GetStore(app);
store.AdditionalClaims.Add(new Claim(Policies.CanCreateInvoice.Key, store.Id));
var invoice = await _InvoiceController.CreateInvoiceCore(new NBitpayClient.Invoice()
{
ItemCode = choiceKey ?? string.Empty,
ItemDesc = title,
Currency = settings.Currency,
Price = price,
BuyerEmail = email,
OrderId = orderId,
NotificationURL = notificationUrl,
RedirectURL = redirectUrl,
FullNotifications = true
}, store, HttpContext.Request.GetAbsoluteRoot());
return RedirectToAction(nameof(InvoiceController.Checkout), "Invoice", new { invoiceId = invoice.Data.Id });
}
}
public class AppsHelper
{
ApplicationDbContextFactory _ContextFactory;
CurrencyNameTable _Currencies;
public AppsHelper(ApplicationDbContextFactory contextFactory, CurrencyNameTable currencies)
{
_ContextFactory = contextFactory;
_Currencies = currencies;
}
public async Task<AppData> GetApp(string appId, AppType appType)
{
using (var ctx = _ContextFactory.CreateContext())
{
return await ctx.Apps
.Where(us => us.Id == appId &&
us.AppType == appType.ToString())
.FirstOrDefaultAsync();
}
}
public async Task<StoreData> GetStore(AppData app)
{
using (var ctx = _ContextFactory.CreateContext())
{
return await ctx.Stores.FirstOrDefaultAsync(s => s.Id == app.StoreDataId);
}
}
public ViewPointOfSaleViewModel.Item[] Parse(string template, string currency)
{
if (string.IsNullOrWhiteSpace(template))
return Array.Empty<ViewPointOfSaleViewModel.Item>();
var input = new StringReader(template);
YamlStream stream = new YamlStream();
stream.Load(input);
var root = (YamlMappingNode)stream.Documents[0].RootNode;
return root
.Children
.Select(kv => new PosHolder { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlMappingNode })
.Where(kv => kv.Value != null)
.Select(c => new ViewPointOfSaleViewModel.Item()
{
Description = c.GetDetailString("description"),
Id = c.Key,
Image = c.GetDetailString("image"),
Title = c.GetDetailString("title") ?? c.Key,
Price = c.GetDetail("price")
.Select(cc => new ViewPointOfSaleViewModel.Item.ItemPrice()
{
Value = decimal.Parse(cc.Value.Value, CultureInfo.InvariantCulture),
Formatted = FormatCurrency(cc.Value.Value, currency)
}).Single(),
Custom = c.GetDetailString("custom") == "true"
})
.ToArray();
}
private class PosHolder
{
public string Key { get; set; }
public YamlMappingNode Value { get; set; }
public IEnumerable<PosScalar> GetDetail(string field)
{
var res = Value.Children
.Where(kv => kv.Value != null)
.Select(kv => new PosScalar { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlScalarNode })
.Where(cc => cc.Key == field);
return res;
}
public string GetDetailString(string field)
{
return GetDetail(field).FirstOrDefault()?.Value?.Value;
}
}
private class PosScalar
{
public string Key { get; set; }
public YamlScalarNode Value { get; set; }
}
public string FormatCurrency(string price, string currency)
{
return decimal.Parse(price, CultureInfo.InvariantCulture).ToString("C", _Currencies.GetCurrencyProvider(currency));
}
public CurrencyData GetCurrencyData(string currency, bool useFallback)
{
return _Currencies.GetCurrencyData(currency, useFallback);
}
public async Task<AppData> GetAppDataIfOwner(string userId, string appId, AppType? type = null)
{
if (userId == null || appId == null)
return null;
using (var ctx = _ContextFactory.CreateContext())
{
var app = await ctx.UserStore
.Where(us => us.ApplicationUserId == userId && us.Role == StoreRoles.Owner)
.SelectMany(us => us.StoreData.Apps.Where(a => a.Id == appId))
.FirstOrDefaultAsync();
if (app == null)
return null;
if (type != null && type.Value.ToString() != app.AppType)
return null;
return app;
}
}
}
}

@ -0,0 +1,119 @@
using System;
using System.Threading.Tasks;
using BTCPayServer.Models;
using BTCPayServer.Payments.Changelly;
using BTCPayServer.Rating;
using BTCPayServer.Services.Rates;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers
{
[Route("[controller]/{storeId}")]
public class ChangellyController : Controller
{
private readonly ChangellyClientProvider _changellyClientProvider;
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly RateFetcher _RateProviderFactory;
public ChangellyController(ChangellyClientProvider changellyClientProvider,
BTCPayNetworkProvider btcPayNetworkProvider,
RateFetcher rateProviderFactory)
{
_RateProviderFactory = rateProviderFactory ?? throw new ArgumentNullException(nameof(rateProviderFactory));
_changellyClientProvider = changellyClientProvider;
_btcPayNetworkProvider = btcPayNetworkProvider;
}
[HttpGet]
[Route("currencies")]
public async Task<IActionResult> GetCurrencyList(string storeId)
{
try
{
var client = await TryGetChangellyClient(storeId);
return Ok(await client.GetCurrenciesFull());
}
catch (Exception e)
{
return BadRequest(new BitpayErrorModel()
{
Error = e.Message
});
}
}
[HttpGet]
[Route("calculate")]
public async Task<IActionResult> CalculateAmount(string storeId, string fromCurrency, string toCurrency,
decimal toCurrencyAmount)
{
try
{
var client = await TryGetChangellyClient(storeId);
if (fromCurrency.Equals("usd", StringComparison.InvariantCultureIgnoreCase)
|| fromCurrency.Equals("eur", StringComparison.InvariantCultureIgnoreCase))
{
return await HandleCalculateFiatAmount(fromCurrency, toCurrency, toCurrencyAmount);
}
var callCounter = 0;
var baseRate = await client.GetExchangeAmount(fromCurrency, toCurrency, 1);
var currentAmount = ChangellyCalculationHelper.ComputeBaseAmount(baseRate, toCurrencyAmount);
while (true)
{
if (callCounter > 10)
{
BadRequest();
}
var computedAmount = await client.GetExchangeAmount(fromCurrency, toCurrency, currentAmount);
callCounter++;
if (computedAmount < toCurrencyAmount)
{
currentAmount =
ChangellyCalculationHelper.ComputeCorrectAmount(currentAmount, computedAmount,
toCurrencyAmount);
}
else
{
return Ok(currentAmount);
}
}
}
catch (Exception e)
{
return BadRequest(new BitpayErrorModel()
{
Error = e.Message
});
}
}
private async Task<Changelly> TryGetChangellyClient(string storeId)
{
var store = IsTest? null: HttpContext.GetStoreData();
storeId = storeId ?? store?.Id;
return await _changellyClientProvider.TryGetChangellyClient(storeId, store);
}
private async Task<IActionResult> HandleCalculateFiatAmount(string fromCurrency, string toCurrency,
decimal toCurrencyAmount)
{
var store = HttpContext.GetStoreData();
var rules = store.GetStoreBlob().GetRateRules(_btcPayNetworkProvider);
var rate = await _RateProviderFactory.FetchRate(new CurrencyPair(toCurrency, fromCurrency), rules);
if (rate.BidAsk == null) return BadRequest();
var flatRate = rate.BidAsk.Center;
return Ok(flatRate * toCurrencyAmount);
}
public bool IsTest { get; set; } = false;
}
}

@ -1,26 +1,20 @@
using BTCPayServer.Authentication;
using Microsoft.Extensions.Logging;
using BTCPayServer.Filters;
using BTCPayServer.Logging;
using BTCPayServer.Models;
using Microsoft.AspNetCore.Mvc;
using NBitpayClient;
using System;
using System.Collections.Generic;
using System;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Cors;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using BTCPayServer.Filters;
using BTCPayServer.Models;
using BTCPayServer.Security;
using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using NBitpayClient;
namespace BTCPayServer.Controllers
{
[EnableCors("BitpayAPI")]
[BitpayAPIConstraint]
[Authorize(Policies.CanUseStore.Key, AuthenticationSchemes = Policies.BitpayAuthentication)]
[Authorize(Policies.CanCreateInvoice.Key, AuthenticationSchemes = Policies.BitpayAuthentication)]
public class InvoiceControllerAPI : Controller
{
private InvoiceController _InvoiceController;

@ -1,16 +1,14 @@
using BTCPayServer.Filters;
using Microsoft.Extensions.Logging;
using BTCPayServer.Logging;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBitcoin.Payment;
using System;
using System.Collections.Generic;
using System;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Filters;
using BTCPayServer.Logging;
using BTCPayServer.Payments;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using NBitcoin;
using NBitcoin.Payment;
namespace BTCPayServer.Controllers
{
@ -84,4 +82,36 @@ namespace BTCPayServer.Controllers
return new PaymentAckActionResult(payment.CreateACK(invoiceId + " is currently processing, thanks for your purchase..."));
}
}
public class PaymentRequestActionResult : IActionResult
{
PaymentRequest req;
public PaymentRequestActionResult(PaymentRequest req)
{
this.req = req;
}
public Task ExecuteResultAsync(ActionContext context)
{
context.HttpContext.Response.Headers["Content-Transfer-Encoding"] = "binary";
context.HttpContext.Response.ContentType = "application/bitcoin-paymentrequest";
req.WriteTo(context.HttpContext.Response.Body);
return Task.CompletedTask;
}
}
public class PaymentAckActionResult : IActionResult
{
PaymentACK req;
public PaymentAckActionResult(PaymentACK req)
{
this.req = req;
}
public Task ExecuteResultAsync(ActionContext context)
{
context.HttpContext.Response.Headers["Content-Transfer-Encoding"] = "binary";
context.HttpContext.Response.ContentType = "application/bitcoin-paymentack";
req.WriteTo(context.HttpContext.Response.Body);
return Task.CompletedTask;
}
}
}

@ -1,28 +1,26 @@
using BTCPayServer.Data;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Filters;
using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Changelly;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Security;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using NBitcoin;
using NBitpayClient;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
using System.Net.WebSockets;
using System.Threading;
using BTCPayServer.Events;
using NBXplorer;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Security;
namespace BTCPayServer.Controllers
{
@ -34,7 +32,6 @@ namespace BTCPayServer.Controllers
{
var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery()
{
UserId = GetUserId(),
InvoiceId = invoiceId,
IncludeAddresses = true,
IncludeEvents = true
@ -61,7 +58,8 @@ namespace BTCPayServer.Controllers
MonitoringDate = invoice.MonitoringExpiration,
OrderId = invoice.OrderId,
BuyerInformation = invoice.BuyerInformation,
Fiat = FormatCurrency((decimal)dto.Price, dto.Currency, _CurrencyNameTable),
Fiat = _CurrencyNameTable.DisplayFormatCurrency((decimal)dto.Price, dto.Currency),
NotificationEmail = invoice.NotificationEmail,
NotificationUrl = invoice.NotificationURL,
RedirectUrl = invoice.RedirectURL,
ProductInformation = invoice.ProductInformation,
@ -176,7 +174,8 @@ namespace BTCPayServer.Controllers
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest", false)]
[XFrameOptionsAttribute(null)]
[ReferrerPolicyAttribute("origin")]
public async Task<IActionResult> Checkout(string invoiceId, string id = null, string paymentMethodId = null)
public async Task<IActionResult> Checkout(string invoiceId, string id = null, string paymentMethodId = null,
[FromQuery]string view = null)
{
//Keep compatibility with Bitpay
invoiceId = invoiceId ?? id;
@ -187,6 +186,8 @@ namespace BTCPayServer.Controllers
if (model == null)
return NotFound();
if (view == "modal")
model.IsModal = true;
_CSP.Add(new ConsentSecurityPolicy("script-src", "'unsafe-eval'")); // Needed by Vue
if (!string.IsNullOrEmpty(model.CustomCSSLink) &&
@ -216,7 +217,6 @@ namespace BTCPayServer.Controllers
paymentMethodIdStr = store.GetDefaultCrypto(_NetworkProvider);
isDefaultCrypto = true;
}
var paymentMethodId = PaymentMethodId.Parse(paymentMethodIdStr);
var network = _NetworkProvider.GetNetwork(paymentMethodId.CryptoCode);
if (network == null && isDefaultCrypto)
@ -232,7 +232,7 @@ namespace BTCPayServer.Controllers
if (!isDefaultCrypto)
return null;
var paymentMethodTemp = invoice.GetPaymentMethods(_NetworkProvider)
.Where(c=> paymentMethodId.CryptoCode == c.GetId().CryptoCode)
.Where(c => paymentMethodId.CryptoCode == c.GetId().CryptoCode)
.FirstOrDefault();
if (paymentMethodTemp == null)
paymentMethodTemp = invoice.GetPaymentMethods(_NetworkProvider).First();
@ -248,6 +248,18 @@ namespace BTCPayServer.Controllers
var storeBlob = store.GetStoreBlob();
var currency = invoice.ProductInformation.Currency;
var accounting = paymentMethod.Calculate();
ChangellySettings changelly = (storeBlob.ChangellySettings != null && storeBlob.ChangellySettings.Enabled &&
storeBlob.ChangellySettings.IsConfigured())
? storeBlob.ChangellySettings
: null;
var changellyAmountDue = changelly != null
? (accounting.Due.ToDecimal(MoneyUnit.BTC) *
(1m + (changelly.AmountMarkupPercentage / 100m)))
: (decimal?)null;
var model = new PaymentModel()
{
CryptoCode = network.CryptoCode,
@ -258,7 +270,7 @@ namespace BTCPayServer.Controllers
ServerUrl = HttpContext.Request.GetAbsoluteRoot(),
OrderId = invoice.OrderId,
InvoiceId = invoice.Id,
DefaultLang = storeBlob.DefaultLang ?? "en-US",
DefaultLang = storeBlob.DefaultLang ?? "en",
HtmlTitle = storeBlob.HtmlTitle ?? "BTCPay Invoice",
CustomCSSLink = storeBlob.CustomCSS?.AbsoluteUri,
CustomLogoLink = storeBlob.CustomLogo?.AbsoluteUri,
@ -287,7 +299,10 @@ namespace BTCPayServer.Controllers
Status = invoice.Status,
NetworkFee = paymentMethodDetails.GetTxFee(),
IsMultiCurrency = invoice.GetPayments().Select(p => p.GetPaymentMethodId()).Concat(new[] { paymentMethod.GetId() }).Distinct().Count() > 1,
AllowCoinConversion = storeBlob.AllowCoinConversion,
ChangellyEnabled = changelly != null,
ChangellyMerchantId = changelly?.ChangellyMerchantId,
ChangellyAmountDue = changellyAmountDue,
StoreId = store.Id,
AvailableCryptos = invoice.GetPaymentMethods(_NetworkProvider)
.Where(i => i.Network != null)
.Select(kv => new PaymentModel.AvailableCrypto()
@ -311,7 +326,7 @@ namespace BTCPayServer.Controllers
private string GetDisplayName(PaymentMethodId paymentMethodId, BTCPayNetwork network)
{
return paymentMethodId.PaymentType == PaymentTypes.BTCLike ?
network.DisplayName : network.DisplayName + " (via Lightning)";
network.DisplayName : network.DisplayName + " (Lightning)";
}
private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network)
@ -327,39 +342,12 @@ namespace BTCPayServer.Controllers
if (cryptoCode == productInformation.Currency)
return null;
return FormatCurrency(productInformation.Price, productInformation.Currency, _CurrencyNameTable);
return _CurrencyNameTable.DisplayFormatCurrency(productInformation.Price, productInformation.Currency);
}
private string ExchangeRate(PaymentMethod paymentMethod)
{
string currency = paymentMethod.ParentEntity.ProductInformation.Currency;
return FormatCurrency(paymentMethod.Rate, currency, _CurrencyNameTable);
}
public static string FormatCurrency(decimal price, string currency, CurrencyNameTable currencies)
{
var provider = currencies.GetNumberFormatInfo(currency, true);
var currencyData = currencies.GetCurrencyData(currency, true);
var divisibility = currencyData.Divisibility;
while (true)
{
var rounded = decimal.Round(price, divisibility, MidpointRounding.AwayFromZero);
if ((Math.Abs(rounded - price) / price) < 0.001m)
{
price = rounded;
break;
}
divisibility++;
}
if (divisibility != provider.CurrencyDecimalDigits)
{
provider = (NumberFormatInfo)provider.Clone();
provider.CurrencyDecimalDigits = divisibility;
}
if (currencyData.Crypto)
return price.ToString("C", provider);
else
return price.ToString("C", provider) + $" ({currency})";
return _CurrencyNameTable.DisplayFormatCurrency(paymentMethod.Rate, currency);
}
[HttpGet]
@ -436,23 +424,17 @@ namespace BTCPayServer.Controllers
[BitpayAPIConstraint(false)]
public async Task<IActionResult> ListInvoices(string searchTerm = null, int skip = 0, int count = 50)
{
var model = new InvoicesModel();
var filterString = new SearchString(searchTerm);
foreach (var invoice in await _InvoiceRepository.GetInvoices(new InvoiceQuery()
var model = new InvoicesModel
{
TextSearch = filterString.TextSearch,
Count = count,
SearchTerm = searchTerm,
Skip = skip,
UserId = GetUserId(),
Unusual = !filterString.Filters.ContainsKey("unusual") ? null
: !bool.TryParse(filterString.Filters["unusual"].First(), out var r) ? (bool?)null
: r,
Status = filterString.Filters.ContainsKey("status") ? filterString.Filters["status"].ToArray() : null,
ExceptionStatus = filterString.Filters.ContainsKey("exceptionstatus") ? filterString.Filters["exceptionstatus"].ToArray() : null,
StoreId = filterString.Filters.ContainsKey("storeid") ? filterString.Filters["storeid"].ToArray() : null
}))
Count = count,
StatusMessage = StatusMessage
};
var list = await ListInvoicesProcess(searchTerm, skip, count);
foreach (var invoice in list)
{
model.SearchTerm = searchTerm;
model.Invoices.Add(new InvoiceModel()
{
Status = invoice.Status + (invoice.ExceptionStatus == null ? string.Empty : $" ({invoice.ExceptionStatus})"),
@ -464,12 +446,29 @@ namespace BTCPayServer.Controllers
AmountCurrency = $"{invoice.ProductInformation.Price.ToString(CultureInfo.InvariantCulture)} {invoice.ProductInformation.Currency}"
});
}
model.Skip = skip;
model.Count = count;
model.StatusMessage = StatusMessage;
return View(model);
}
private async Task<InvoiceEntity[]> ListInvoicesProcess(string searchTerm = null, int skip = 0, int count = 50)
{
var filterString = new SearchString(searchTerm);
var list = await _InvoiceRepository.GetInvoices(new InvoiceQuery()
{
TextSearch = filterString.TextSearch,
Count = count,
Skip = skip,
UserId = GetUserId(),
Unusual = !filterString.Filters.ContainsKey("unusual") ? null
: !bool.TryParse(filterString.Filters["unusual"].First(), out var r) ? (bool?)null
: r,
Status = filterString.Filters.ContainsKey("status") ? filterString.Filters["status"].ToArray() : null,
ExceptionStatus = filterString.Filters.ContainsKey("exceptionstatus") ? filterString.Filters["exceptionstatus"].ToArray() : null,
StoreId = filterString.Filters.ContainsKey("storeid") ? filterString.Filters["storeid"].ToArray() : null
});
return list;
}
[HttpGet]
[Route("invoices/create")]
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
@ -503,7 +502,7 @@ namespace BTCPayServer.Controllers
return View(model);
}
StatusMessage = null;
if (!store.HasClaim(Policies.CanModifyStoreSettings.Key))
if (!store.HasClaim(Policies.CanCreateInvoice.Key))
{
ModelState.AddModelError(nameof(model.StoreId), "You need to be owner of this store to create an invoice");
return View(model);
@ -532,6 +531,7 @@ namespace BTCPayServer.Controllers
PosData = model.PosData,
OrderId = model.OrderId,
//RedirectURL = redirect + "redirect",
NotificationEmail = model.NotificationEmail,
NotificationURL = model.NotificationUrl,
ItemDesc = model.ItemDesc,
FullNotifications = true,

@ -1,47 +1,25 @@
using BTCPayServer.Authentication;
using System.Reflection;
using System.Linq;
using Microsoft.Extensions.Logging;
using BTCPayServer.Logging;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NBitpayClient;
using System;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Models;
using Newtonsoft.Json;
using System.Globalization;
using NBitcoin;
using NBitcoin.DataEncoders;
using BTCPayServer.Filters;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using System.Net;
using Microsoft.AspNetCore.Identity;
using Newtonsoft.Json.Linq;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using NBitcoin.Payment;
using BTCPayServer.Data;
using BTCPayServer.Models.InvoicingModels;
using System.Security.Claims;
using BTCPayServer.Services;
using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Wallets;
using BTCPayServer.Validations;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Mvc.Routing;
using NBXplorer.DerivationStrategy;
using NBXplorer;
using BTCPayServer.HostedServices;
using BTCPayServer.Logging;
using BTCPayServer.Models;
using BTCPayServer.Payments;
using BTCPayServer.Rating;
using BTCPayServer.Security;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
using BTCPayServer.Validation;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBitpayClient;
using Newtonsoft.Json;
namespace BTCPayServer.Controllers
{
@ -49,7 +27,7 @@ namespace BTCPayServer.Controllers
{
InvoiceRepository _InvoiceRepository;
ContentSecurityPolicies _CSP;
BTCPayRateProviderFactory _RateProvider;
RateFetcher _RateProvider;
StoreRepository _StoreRepository;
UserManager<ApplicationUser> _UserManager;
private CurrencyNameTable _CurrencyNameTable;
@ -62,7 +40,7 @@ namespace BTCPayServer.Controllers
InvoiceRepository invoiceRepository,
CurrencyNameTable currencyNameTable,
UserManager<ApplicationUser> userManager,
BTCPayRateProviderFactory rateProvider,
RateFetcher rateProvider,
StoreRepository storeRepository,
EventAggregator eventAggregator,
BTCPayWalletProvider walletProvider,
@ -84,6 +62,8 @@ namespace BTCPayServer.Controllers
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl)
{
if (!store.HasClaim(Policies.CanCreateInvoice.Key))
throw new UnauthorizedAccessException();
InvoiceLogs logs = new InvoiceLogs();
logs.Write("Creation of invoice starting");
var entity = new InvoiceEntity
@ -103,6 +83,7 @@ namespace BTCPayServer.Controllers
entity.FullNotifications = invoice.FullNotifications || invoice.ExtendedNotifications;
entity.ExtendedNotifications = invoice.ExtendedNotifications;
entity.NotificationURL = notificationUri?.AbsoluteUri;
entity.NotificationEmail = invoice.NotificationEmail;
entity.BuyerInformation = Map<Invoice, BuyerInformation>(invoice);
entity.PaymentTolerance = storeBlob.PaymentTolerance;
//Another way of passing buyer info to support
@ -195,12 +176,9 @@ namespace BTCPayServer.Controllers
var allRateRuleErrors = string.Join(", ", rateResult.Errors.ToArray());
logs.Write($"{pair.Key}: Rate rule error ({allRateRuleErrors})");
}
if (rateResult.ExchangeExceptions.Count != 0)
foreach (var ex in rateResult.ExchangeExceptions)
{
foreach (var ex in rateResult.ExchangeExceptions)
{
logs.Write($"{pair.Key}: Exception reaching exchange {ex.ExchangeName} ({ex.Exception.Message})");
}
logs.Write($"{pair.Key}: Exception reaching exchange {ex.ExchangeName} ({ex.Exception.Message})");
}
}).ToArray());
}
@ -210,6 +188,7 @@ namespace BTCPayServer.Controllers
try
{
var storeBlob = store.GetStoreBlob();
var preparePayment = handler.PreparePayment(supportedPaymentMethod, store, network);
var rate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, entity.ProductInformation.Currency)];
if (rate.BidAsk == null)
{
@ -220,7 +199,7 @@ namespace BTCPayServer.Controllers
paymentMethod.Network = network;
paymentMethod.SetId(supportedPaymentMethod.PaymentId);
paymentMethod.Rate = rate.BidAsk.Bid;
var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, store, network);
var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, store, network, preparePayment);
if (storeBlob.NetworkFeeDisabled)
paymentDetails.SetNoTxFee();
paymentMethod.SetPaymentMethodDetails(paymentDetails);

@ -443,7 +443,7 @@ namespace BTCPayServer.Controllers
if (!is2faTokenValid)
{
ModelState.AddModelError("model.Code", "Verification code is invalid.");
ModelState.AddModelError(nameof(model.Code), "Verification code is invalid.");
return View(model);
}

@ -1,40 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using NBitcoin.Payment;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Controllers
{
public class PaymentRequestActionResult : IActionResult
{
PaymentRequest req;
public PaymentRequestActionResult(PaymentRequest req)
{
this.req = req;
}
public Task ExecuteResultAsync(ActionContext context)
{
context.HttpContext.Response.Headers["Content-Transfer-Encoding"] = "binary";
context.HttpContext.Response.ContentType = "application/bitcoin-paymentrequest";
req.WriteTo(context.HttpContext.Response.Body);
return Task.CompletedTask;
}
}
public class PaymentAckActionResult : IActionResult
{
PaymentACK req;
public PaymentAckActionResult(PaymentACK req)
{
this.req = req;
}
public Task ExecuteResultAsync(ActionContext context)
{
context.HttpContext.Response.Headers["Content-Transfer-Encoding"] = "binary";
context.HttpContext.Response.ContentType = "application/bitcoin-paymentack";
req.WriteTo(context.HttpContext.Response.Body);
return Task.CompletedTask;
}
}
}

@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Filters;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers
{
public class PublicController : Controller
{
public PublicController(InvoiceController invoiceController,
StoreRepository storeRepository)
{
_InvoiceController = invoiceController;
_StoreRepository = storeRepository;
}
private InvoiceController _InvoiceController;
private StoreRepository _StoreRepository;
[HttpPost]
[Route("api/v1/invoices")]
[MediaTypeAcceptConstraintAttribute("text/html")]
[IgnoreAntiforgeryToken]
[EnableCors(CorsPolicies.All)]
public async Task<IActionResult> PayButtonHandle([FromForm]PayButtonViewModel model)
{
var store = await _StoreRepository.FindStore(model.StoreId);
if (store == null)
ModelState.AddModelError("Store", "Invalid store");
else
{
var storeBlob = store.GetStoreBlob();
if (!storeBlob.AnyoneCanInvoice)
ModelState.AddModelError("Store", "Store has not enabled Pay Button");
}
if (model == null || model.Price <= 0)
ModelState.AddModelError("Price", "Price must be greater than 0");
if (!ModelState.IsValid)
return View();
var invoice = await _InvoiceController.CreateInvoiceCore(new NBitpayClient.Invoice()
{
Price = model.Price,
Currency = model.Currency,
ItemDesc = model.CheckoutDesc,
OrderId = model.OrderId,
NotificationEmail = model.NotifyEmail,
NotificationURL = model.ServerIpn,
RedirectURL = model.BrowserRedirect,
FullNotifications = true
}, store, HttpContext.Request.GetAbsoluteRoot());
return Redirect(invoice.Data.Url);
}
}
}

@ -10,23 +10,32 @@ using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Rating;
using Newtonsoft.Json;
using Microsoft.AspNetCore.Authorization;
using BTCPayServer.Authentication;
namespace BTCPayServer.Controllers
{
[Authorize(AuthenticationSchemes = Security.Policies.BitpayAuthentication)]
[AllowAnonymous]
public class RateController : Controller
{
BTCPayRateProviderFactory _RateProviderFactory;
RateFetcher _RateProviderFactory;
BTCPayNetworkProvider _NetworkProvider;
CurrencyNameTable _CurrencyNameTable;
StoreRepository _StoreRepo;
public TokenRepository TokenRepository { get; }
public RateController(
BTCPayRateProviderFactory rateProviderFactory,
RateFetcher rateProviderFactory,
BTCPayNetworkProvider networkProvider,
TokenRepository tokenRepository,
StoreRepository storeRepo,
CurrencyNameTable currencyNameTable)
{
_RateProviderFactory = rateProviderFactory ?? throw new ArgumentNullException(nameof(rateProviderFactory));
_NetworkProvider = networkProvider;
TokenRepository = tokenRepository;
_StoreRepo = storeRepo;
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
}
@ -36,7 +45,7 @@ namespace BTCPayServer.Controllers
[BitpayAPIConstraint]
public async Task<IActionResult> GetBaseCurrencyRates(string baseCurrency, string storeId)
{
storeId = storeId ?? this.HttpContext.GetStoreData()?.Id;
storeId = await GetStoreId(storeId);
var store = this.HttpContext.GetStoreData();
if (store == null || store.Id != storeId)
store = await _StoreRepo.FindStore(storeId);
@ -66,7 +75,7 @@ namespace BTCPayServer.Controllers
[BitpayAPIConstraint]
public async Task<IActionResult> GetCurrencyPairRate(string baseCurrency, string currency, string storeId)
{
storeId = storeId ?? this.HttpContext.GetStoreData()?.Id;
storeId = await GetStoreId(storeId);
var result = await GetRates2($"{baseCurrency}_{currency}", storeId);
var rates = (result as JsonResult)?.Value as Rate[];
if (rates == null)
@ -79,7 +88,7 @@ namespace BTCPayServer.Controllers
[BitpayAPIConstraint]
public async Task<IActionResult> GetRates(string currencyPairs, string storeId)
{
storeId = storeId ?? this.HttpContext.GetStoreData()?.Id;
storeId = await GetStoreId(storeId);
var result = await GetRates2(currencyPairs, storeId);
var rates = (result as JsonResult)?.Value as Rate[];
if (rates == null)
@ -87,11 +96,29 @@ namespace BTCPayServer.Controllers
return Json(new DataWrapper<Rate[]>(rates));
}
private async Task<string> GetStoreId(string storeId)
{
if (storeId != null && this.HttpContext.GetStoreData()?.Id == storeId)
return storeId;
if(storeId == null)
{
var tokens = await this.TokenRepository.GetTokens(this.User.GetSIN());
storeId = tokens.Select(s => s.StoreId).Where(s => s != null).FirstOrDefault();
}
if (storeId == null)
return null;
var store = await _StoreRepo.FindStore(storeId);
if (store == null)
return null;
this.HttpContext.SetStoreData(store);
return storeId;
}
[Route("api/rates")]
[HttpGet]
public async Task<IActionResult> GetRates2(string currencyPairs, string storeId)
{
storeId = await GetStoreId(storeId);
if (storeId == null)
{
var result = Json(new BitpayErrorsModel() { Error = "You need to specify storeId (in your store settings)" });

@ -8,7 +8,7 @@ using BTCPayServer.Services;
using BTCPayServer.Services.Mails;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Validations;
using BTCPayServer.Validation;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
@ -17,6 +17,7 @@ using NBitcoin.DataEncoders;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
@ -24,6 +25,8 @@ using System.Net.Mail;
using System.Threading.Tasks;
using Renci.SshNet;
using BTCPayServer.Logging;
using BTCPayServer.Lightning;
using BTCPayServer.Configuration.External;
namespace BTCPayServer.Controllers
{
@ -33,14 +36,14 @@ namespace BTCPayServer.Controllers
private UserManager<ApplicationUser> _UserManager;
SettingsRepository _SettingsRepository;
private readonly NBXplorerDashboard _dashBoard;
private BTCPayRateProviderFactory _RateProviderFactory;
private RateFetcher _RateProviderFactory;
private StoreRepository _StoreRepository;
LightningConfigurationProvider _LnConfigProvider;
BTCPayServerOptions _Options;
public ServerController(UserManager<ApplicationUser> userManager,
Configuration.BTCPayServerOptions options,
BTCPayRateProviderFactory rateProviderFactory,
RateFetcher rateProviderFactory,
SettingsRepository settingsRepository,
NBXplorerDashboard dashBoard,
LightningConfigurationProvider lnConfigProvider,
@ -165,6 +168,7 @@ namespace BTCPayServer.Controllers
vm.DNSDomain = null;
return View(vm);
}
[Route("server/maintenance")]
[HttpPost]
public async Task<IActionResult> Maintenance(MaintenanceViewModel vm, string command)
@ -180,6 +184,8 @@ namespace BTCPayServer.Controllers
return View(vm);
}
vm.DNSDomain = vm.DNSDomain.Trim().ToLowerInvariant();
if (vm.DNSDomain.Equals(this.Request.Host.Host, StringComparison.OrdinalIgnoreCase))
return View(vm);
if (IPAddress.TryParse(vm.DNSDomain, out var unused))
{
ModelState.AddModelError(nameof(vm.DNSDomain), $"This should be a domain name");
@ -200,12 +206,13 @@ namespace BTCPayServer.Controllers
{
builder.Scheme = this.Request.Scheme;
builder.Host = vm.DNSDomain;
if (this.Request.Host.Port != null)
builder.Port = this.Request.Host.Port.Value;
builder.Path = "runid";
builder.Query = $"expected={RunId}";
var response = await client.GetAsync(builder.Uri);
if (!response.IsSuccessStatusCode)
var addresses1 = Dns.GetHostAddressesAsync(this.Request.Host.Host);
var addresses2 = Dns.GetHostAddressesAsync(vm.DNSDomain);
await Task.WhenAll(addresses1, addresses2);
var addressesSet = addresses1.GetAwaiter().GetResult().Select(c => c.ToString()).ToHashSet();
var hasCommonAddress = addresses2.GetAwaiter().GetResult().Select(c => c.ToString()).Any(s => addressesSet.Contains(s));
if (!hasCommonAddress)
{
ModelState.AddModelError(nameof(vm.DNSDomain), $"Invalid host ({vm.DNSDomain} is not pointing to this BTCPay instance)");
return View(vm);
@ -273,7 +280,7 @@ namespace BTCPayServer.Controllers
else
{
e.CanTrust = _Options.IsTrustedFingerprint(e.FingerPrint, e.HostKey);
if(!e.CanTrust)
if (!e.CanTrust)
Logs.Configuration.LogError($"SSH host fingerprint for {e.HostKeyName} is untrusted, start BTCPay with -sshtrustedfingerprints \"{Encoders.Hex.EncodeData(e.FingerPrint)}\"");
}
};
@ -417,17 +424,15 @@ namespace BTCPayServer.Controllers
var result = new ServicesViewModel();
foreach (var cryptoCode in _Options.ExternalServicesByCryptoCode.Keys)
{
int i = 0;
foreach (var grpcService in _Options.ExternalServicesByCryptoCode.GetServices<ExternalLnd>(cryptoCode))
{
int i = 0;
foreach (var grpcService in _Options.ExternalServicesByCryptoCode.GetServices<ExternalLNDGRPC>(cryptoCode))
result.LNDServices.Add(new ServicesViewModel.LNDServiceViewModel()
{
result.LNDServices.Add(new ServicesViewModel.LNDServiceViewModel()
{
Crypto = cryptoCode,
Type = "gRPC",
Index = i++,
});
}
Crypto = cryptoCode,
Type = grpcService.Type,
Index = i++,
});
}
}
result.HasSSH = _Options.SSHSettings != null;
@ -435,17 +440,17 @@ namespace BTCPayServer.Controllers
}
[Route("server/services/lnd-grpc/{cryptoCode}/{index}")]
public IActionResult LNDGRPCServices(string cryptoCode, int index, uint? nonce)
public IActionResult LndGrpcServices(string cryptoCode, int index, uint? nonce)
{
if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
{
StatusMessage = $"Error: {cryptoCode} is not fully synched";
return RedirectToAction(nameof(Services));
}
var external = GetExternalLNDConnectionString(cryptoCode, index);
var external = GetExternalLndConnectionString(cryptoCode, index);
if (external == null)
return NotFound();
var model = new LNDGRPCServicesViewModel();
var model = new LndGrpcServicesViewModel();
model.Host = $"{external.BaseUri.DnsSafeHost}:{external.BaseUri.Port}";
model.SSL = external.BaseUri.Scheme == "https";
@ -489,9 +494,9 @@ namespace BTCPayServer.Controllers
[Route("server/services/lnd-grpc/{cryptoCode}/{index}")]
[HttpPost]
public IActionResult LNDGRPCServicesPOST(string cryptoCode, int index)
public IActionResult LndGrpcServicesPost(string cryptoCode, int index)
{
var external = GetExternalLNDConnectionString(cryptoCode, index);
var external = GetExternalLndConnectionString(cryptoCode, index);
if (external == null)
return NotFound();
LightningConfigurations confs = new LightningConfigurations();
@ -509,12 +514,12 @@ namespace BTCPayServer.Controllers
var nonce = RandomUtils.GetUInt32();
var configKey = GetConfigKey("lnd-grpc", cryptoCode, index, nonce);
_LnConfigProvider.KeepConfig(configKey, confs);
return RedirectToAction(nameof(LNDGRPCServices), new { cryptoCode = cryptoCode, nonce = nonce });
return RedirectToAction(nameof(LndGrpcServices), new { cryptoCode = cryptoCode, nonce = nonce });
}
private LightningConnectionString GetExternalLNDConnectionString(string cryptoCode, int index)
private LightningConnectionString GetExternalLndConnectionString(string cryptoCode, int index)
{
var connectionString = _Options.ExternalServicesByCryptoCode.GetServices<ExternalLNDGRPC>(cryptoCode).Skip(index).Select(c => c.ConnectionString).FirstOrDefault();
var connectionString = _Options.ExternalServicesByCryptoCode.GetServices<ExternalLnd>(cryptoCode).Skip(index).Select(c => c.ConnectionString).FirstOrDefault();
if (connectionString == null)
return null;
connectionString = connectionString.Clone();
@ -527,13 +532,35 @@ namespace BTCPayServer.Controllers
}
catch
{
Logging.Logs.Configuration.LogWarning($"{cryptoCode}: The macaroon file path of the external LND grpc config was not found ({connectionString.MacaroonFilePath})");
Logs.Configuration.LogWarning($"{cryptoCode}: The macaroon file path of the external LND grpc config was not found ({connectionString.MacaroonFilePath})");
return null;
}
}
return connectionString;
}
[Route("server/services/lnd-rest/{cryptoCode}/{index}")]
public IActionResult LndRestServices(string cryptoCode, int index, uint? nonce)
{
if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
{
StatusMessage = $"Error: {cryptoCode} is not fully synched";
return RedirectToAction(nameof(Services));
}
var external = GetExternalLndConnectionString(cryptoCode, index);
if (external == null)
return NotFound();
var model = new LndRestServicesViewModel();
model.BaseApiUrl = external.BaseUri.ToString();
if (external.CertificateThumbprint != null)
model.CertificateThumbprint = Encoders.Hex.EncodeData(external.CertificateThumbprint);
if (external.Macaroon != null)
model.Macaroon = Encoders.Hex.EncodeData(external.Macaroon);
return View(model);
}
[Route("server/services/ssh")]
public IActionResult SSHService(bool downloadKeyFile = false)
{
@ -600,5 +627,66 @@ namespace BTCPayServer.Controllers
return View(model);
}
}
[Route("server/logs/{file?}")]
public async Task<IActionResult> LogsView(string file = null, int offset = 0)
{
if (offset < 0)
{
offset = 0;
}
var vm = new LogsViewModel();
if (string.IsNullOrEmpty(_Options.LogFile))
{
vm.StatusMessage = "Error: File Logging Option not specified. " +
"You need to set debuglog and optionally " +
"debugloglevel in the configuration or through runtime arguments";
}
else
{
var di = Directory.GetParent(_Options.LogFile);
if (di == null)
{
vm.StatusMessage = "Error: Could not load log files";
}
var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(_Options.LogFile);
var fileExtension = Path.GetExtension(_Options.LogFile) ?? string.Empty;
var logFiles = di.GetFiles($"{fileNameWithoutExtension}*{fileExtension}");
vm.LogFileCount = logFiles.Length;
vm.LogFiles = logFiles
.OrderBy(info => info.LastWriteTime)
.Skip(offset)
.Take(5)
.ToList();
vm.LogFileOffset = offset;
if (string.IsNullOrEmpty(file)) return View("Logs", vm);
vm.Log = "";
var path = Path.Combine(di.FullName, file);
try
{
using (var fileStream = new FileStream(
path,
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite))
{
using (var reader = new StreamReader(fileStream))
{
vm.Log = await reader.ReadToEndAsync();
}
}
}
catch
{
return NotFound();
}
}
return View("Logs", vm);
}
}
}

@ -34,13 +34,66 @@ namespace BTCPayServer.Controllers
}
DerivationSchemeViewModel vm = new DerivationSchemeViewModel();
vm.ServerUrl = WalletsController.GetLedgerWebsocketUrl(this.HttpContext, cryptoCode, null);
vm.CryptoCode = cryptoCode;
vm.RootKeyPath = network.GetRootKeyPath();
SetExistingValues(store, vm);
return View(vm);
}
[HttpGet]
[Route("{storeId}/derivations/{cryptoCode}/ledger/ws")]
public async Task<IActionResult> AddDerivationSchemeLedger(
string storeId,
string cryptoCode,
string command,
int account = 0)
{
if (!HttpContext.WebSockets.IsWebSocketRequest)
return NotFound();
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
var hw = new HardwareWalletService(webSocket);
object result = null;
var network = _NetworkProvider.GetNetwork(cryptoCode);
using (var normalOperationTimeout = new CancellationTokenSource())
{
normalOperationTimeout.CancelAfter(TimeSpan.FromMinutes(30));
try
{
if (command == "test")
{
result = await hw.Test(normalOperationTimeout.Token);
}
if (command == "getxpub")
{
var getxpubResult = await hw.GetExtPubKey(network, account, normalOperationTimeout.Token);
result = getxpubResult;
}
}
catch (OperationCanceledException)
{ result = new LedgerTestResult() { Success = false, Error = "Timeout" }; }
catch (Exception ex)
{ result = new LedgerTestResult() { Success = false, Error = ex.Message }; }
finally { hw.Dispose(); }
try
{
if (result != null)
{
UTF8Encoding UTF8NOBOM = new UTF8Encoding(false);
var bytes = UTF8NOBOM.GetBytes(JsonConvert.SerializeObject(result, MvcJsonOptions.Value.SerializerSettings));
await webSocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, new CancellationTokenSource(2000).Token);
}
}
catch { }
finally
{
await webSocket.CloseSocket();
}
}
return new EmptyResult();
}
private void SetExistingValues(StoreData store, DerivationSchemeViewModel vm)
{
vm.DerivationScheme = GetExistingDerivationStrategy(vm.CryptoCode, store)?.DerivationStrategyBase.ToString();
@ -60,7 +113,6 @@ namespace BTCPayServer.Controllers
[Route("{storeId}/derivations/{cryptoCode}")]
public async Task<IActionResult> AddDerivationScheme(string storeId, DerivationSchemeViewModel vm, string cryptoCode)
{
vm.ServerUrl = WalletsController.GetLedgerWebsocketUrl(this.HttpContext, cryptoCode, null);
vm.CryptoCode = cryptoCode;
var store = HttpContext.GetStoreData();
if (store == null)
@ -99,10 +151,19 @@ namespace BTCPayServer.Controllers
vm.Confirmation = false;
return View(vm);
}
var storeBlob = store.GetStoreBlob();
var wasExcluded = storeBlob.GetExcludedPaymentMethods().Match(paymentMethodId);
var willBeExcluded = !vm.Enabled;
var showAddress = (vm.Confirmation && !string.IsNullOrWhiteSpace(vm.HintAddress)) || // Testing hint address
(!vm.Confirmation && strategy != null && exisingStrategy != strategy.DerivationStrategyBase.ToString()); // Checking addresses after setting xpub
var showAddress = // Show addresses if:
// - If the user is testing the hint address in confirmation screen
(vm.Confirmation && !string.IsNullOrWhiteSpace(vm.HintAddress)) ||
// - The user is setting a new derivation scheme
(!vm.Confirmation && strategy != null && exisingStrategy != strategy.DerivationStrategyBase.ToString()) ||
// - The user is clicking on continue without changing anything
(!vm.Confirmation && willBeExcluded == wasExcluded);
showAddress = showAddress && strategy != null;
if (!showAddress)
{
try
@ -110,9 +171,7 @@ namespace BTCPayServer.Controllers
if (strategy != null)
await wallet.TrackAsync(strategy.DerivationStrategyBase);
store.SetSupportedPaymentMethod(paymentMethodId, strategy);
var storeBlob = store.GetStoreBlob();
storeBlob.SetExcluded(paymentMethodId, !vm.Enabled);
storeBlob.SetExcluded(paymentMethodId, willBeExcluded);
store.SetStoreBlob(storeBlob);
}
catch

@ -0,0 +1,96 @@
using System;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments.Changelly;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers
{
public partial class StoresController
{
[HttpGet]
[Route("{storeId}/changelly")]
public IActionResult UpdateChangellySettings(string storeId)
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
UpdateChangellySettingsViewModel vm = new UpdateChangellySettingsViewModel();
SetExistingValues(store, vm);
return View(vm);
}
private void SetExistingValues(StoreData store, UpdateChangellySettingsViewModel vm)
{
var existing = store.GetStoreBlob().ChangellySettings;
if (existing == null) return;
vm.ApiKey = existing.ApiKey;
vm.ApiSecret = existing.ApiSecret;
vm.ApiUrl = existing.ApiUrl;
vm.ChangellyMerchantId = existing.ChangellyMerchantId;
vm.Enabled = existing.Enabled;
vm.AmountMarkupPercentage = existing.AmountMarkupPercentage;
vm.ShowFiat = existing.ShowFiat;
}
[HttpPost]
[Route("{storeId}/changelly")]
public async Task<IActionResult> UpdateChangellySettings(string storeId, UpdateChangellySettingsViewModel vm,
string command)
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
if (vm.Enabled)
{
if (!ModelState.IsValid)
{
return View(vm);
}
}
var changellySettings = new ChangellySettings()
{
ApiKey = vm.ApiKey,
ApiSecret = vm.ApiSecret,
ApiUrl = vm.ApiUrl,
ChangellyMerchantId = vm.ChangellyMerchantId,
Enabled = vm.Enabled,
AmountMarkupPercentage = vm.AmountMarkupPercentage,
ShowFiat = vm.ShowFiat
};
switch (command)
{
case "save":
var storeBlob = store.GetStoreBlob();
storeBlob.ChangellySettings = changellySettings;
store.SetStoreBlob(storeBlob);
await _Repo.UpdateStore(store);
StatusMessage = "Changelly settings modified";
_changellyClientProvider.InvalidateClient(storeId);
return RedirectToAction(nameof(UpdateStore), new {
storeId});
case "test":
try
{
var client = new Changelly(_httpClientFactory, changellySettings.ApiKey, changellySettings.ApiSecret,
changellySettings.ApiUrl);
var result = await client.GetCurrenciesFull();
vm.StatusMessage = "Test Successful";
return View(vm);
}
catch (Exception ex)
{
vm.StatusMessage = $"Error: {ex.Message}";
return View(vm);
}
default:
return View(vm);
}
}
}
}

@ -5,12 +5,12 @@ using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning.CLightning;
using Microsoft.AspNetCore.Mvc;
using BTCPayServer.Payments.Lightning;
using System.Net;
using BTCPayServer.Data;
using System.Threading;
using BTCPayServer.Lightning;
namespace BTCPayServer.Controllers
{
@ -114,7 +114,7 @@ namespace BTCPayServer.Controllers
}
if(!System.IO.File.Exists(connectionString.MacaroonFilePath))
{
ModelState.AddModelError(nameof(vm.ConnectionString), "The macaroonfilepath file does exist");
ModelState.AddModelError(nameof(vm.ConnectionString), "The macaroonfilepath file does not exist");
return View(vm);
}
if(!System.IO.Path.IsPathRooted(connectionString.MacaroonFilePath))

@ -1,9 +1,16 @@
using BTCPayServer.Authentication;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using BTCPayServer.Authentication;
using BTCPayServer.Configuration;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.Models;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments.Changelly;
using BTCPayServer.Rating;
using BTCPayServer.Security;
using BTCPayServer.Services;
@ -11,6 +18,7 @@ using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
@ -18,14 +26,6 @@ using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.Extensions.Options;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBXplorer.DerivationStrategy;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace BTCPayServer.Controllers
{
@ -35,7 +35,7 @@ namespace BTCPayServer.Controllers
[AutoValidateAntiforgeryToken]
public partial class StoresController : Controller
{
BTCPayRateProviderFactory _RateFactory;
RateFetcher _RateFactory;
public string CreatedStoreId { get; set; }
public StoresController(
IServiceProvider serviceProvider,
@ -47,20 +47,25 @@ namespace BTCPayServer.Controllers
AccessTokenController tokenController,
BTCPayWalletProvider walletProvider,
BTCPayNetworkProvider networkProvider,
BTCPayRateProviderFactory rateFactory,
RateFetcher rateFactory,
ExplorerClientProvider explorerProvider,
IFeeProviderFactory feeRateProvider,
LanguageService langService,
IHostingEnvironment env)
ChangellyClientProvider changellyClientProvider,
IOptions<MvcJsonOptions> mvcJsonOptions,
IHostingEnvironment env, IHttpClientFactory httpClientFactory)
{
_RateFactory = rateFactory;
_Repo = repo;
_TokenRepository = tokenRepo;
_UserManager = userManager;
_LangService = langService;
_changellyClientProvider = changellyClientProvider;
MvcJsonOptions = mvcJsonOptions;
_TokenController = tokenController;
_WalletProvider = walletProvider;
_Env = env;
_httpClientFactory = httpClientFactory;
_NetworkProvider = networkProvider;
_ExplorerProvider = explorerProvider;
_FeeRateProvider = feeRateProvider;
@ -80,7 +85,9 @@ namespace BTCPayServer.Controllers
TokenRepository _TokenRepository;
UserManager<ApplicationUser> _UserManager;
private LanguageService _LangService;
private readonly ChangellyClientProvider _changellyClientProvider;
IHostingEnvironment _Env;
private IHttpClientFactory _httpClientFactory;
[TempData]
public string StatusMessage
@ -321,7 +328,6 @@ namespace BTCPayServer.Controllers
vm.SetLanguages(_LangService, storeBlob.DefaultLang);
vm.LightningMaxValue = storeBlob.LightningMaxValue?.ToString() ?? "";
vm.OnChainMinValue = storeBlob.OnChainMinValue?.ToString() ?? "";
vm.AllowCoinConversion = storeBlob.AllowCoinConversion;
vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail;
vm.CustomCSS = storeBlob.CustomCSS?.AbsoluteUri;
vm.CustomLogo = storeBlob.CustomLogo?.AbsoluteUri;
@ -365,7 +371,6 @@ namespace BTCPayServer.Controllers
return View(model);
}
blob.DefaultLang = model.DefaultLang;
blob.AllowCoinConversion = model.AllowCoinConversion;
blob.RequiresRefundEmail = model.RequiresRefundEmail;
blob.LightningMaxValue = lightningMaxValue;
blob.OnChainMinValue = onchainMinValue;
@ -402,6 +407,7 @@ namespace BTCPayServer.Controllers
vm.StoreName = store.StoreName;
vm.StoreWebsite = store.StoreWebsite;
vm.NetworkFee = !storeBlob.NetworkFeeDisabled;
vm.AnyoneCanCreateInvoice = storeBlob.AnyoneCanInvoice;
vm.SpeedPolicy = store.SpeedPolicy;
vm.CanDelete = _Repo.CanDeleteStores();
AddPaymentMethods(store, storeBlob, vm);
@ -449,6 +455,15 @@ namespace BTCPayServer.Controllers
Enabled = !excludeFilters.Match(paymentId)
});
}
var changellyEnabled = storeBlob.ChangellySettings != null && storeBlob.ChangellySettings.Enabled;
vm.ThirdPartyPaymentMethods.Add(new StoreViewModel.ThirdPartyPaymentMethod()
{
Enabled = changellyEnabled,
Action = nameof(UpdateChangellySettings),
Provider = "Changelly"
});
}
[HttpPost]
@ -473,6 +488,7 @@ namespace BTCPayServer.Controllers
}
var blob = StoreData.GetStoreBlob();
blob.AnyoneCanInvoice = model.AnyoneCanCreateInvoice;
blob.NetworkFeeDisabled = !model.NetworkFee;
blob.MonitoringExpiration = model.MonitoringExpiration;
blob.InvoiceExpiration = model.InvoiceExpiration;
@ -521,7 +537,7 @@ namespace BTCPayServer.Controllers
private CoinAverageExchange[] GetSupportedExchanges()
{
return _RateFactory.GetSupportedExchanges()
return _RateFactory.RateProviderFactory.GetSupportedExchanges()
.Select(c => c.Value)
.OrderBy(s => s.Name, StringComparer.OrdinalIgnoreCase)
.ToArray();
@ -557,6 +573,45 @@ namespace BTCPayServer.Controllers
return View(model);
}
[HttpGet]
[Route("{storeId}/tokens/{tokenId}/revoke")]
public async Task<IActionResult> RevokeToken(string tokenId)
{
var token = await _TokenRepository.GetToken(tokenId);
if (token == null || token.StoreId != StoreData.Id)
return NotFound();
return View("Confirm", new ConfirmModel()
{
Action = "Revoke the token",
Title = "Revoke the token",
Description = $"The access token with the label \"{token.Label}\" will be revoked, do you wish to continue?",
ButtonClass = "btn-danger"
});
}
[HttpPost]
[Route("{storeId}/tokens/{tokenId}/revoke")]
public async Task<IActionResult> RevokeTokenConfirm(string tokenId)
{
var token = await _TokenRepository.GetToken(tokenId);
if (token == null ||
token.StoreId != StoreData.Id ||
!await _TokenRepository.DeleteToken(tokenId))
StatusMessage = "Failure to revoke this token";
else
StatusMessage = "Token revoked";
return RedirectToAction(nameof(ListTokens));
}
[HttpGet]
[Route("{storeId}/tokens/{tokenId}")]
public async Task<IActionResult> ShowToken(string tokenId)
{
var token = await _TokenRepository.GetToken(tokenId);
if (token == null || token.StoreId != StoreData.Id)
return NotFound();
return View(token);
}
[HttpPost]
[Route("/api-tokens")]
[Route("{storeId}/Tokens/Create")]
@ -621,6 +676,7 @@ namespace BTCPayServer.Controllers
}
public string GeneratedPairingCode { get; set; }
public IOptions<MvcJsonOptions> MvcJsonOptions { get; }
[HttpGet]
[Route("/api-tokens")]
@ -658,21 +714,6 @@ namespace BTCPayServer.Controllers
return View(model);
}
[HttpPost]
[Route("{storeId}/Tokens/Delete")]
public async Task<IActionResult> DeleteToken(string tokenId)
{
var token = await _TokenRepository.GetToken(tokenId);
if (token == null ||
token.StoreId != StoreData.Id ||
!await _TokenRepository.DeleteToken(tokenId))
StatusMessage = "Failure to revoke this token";
else
StatusMessage = "Token revoked";
return RedirectToAction(nameof(ListTokens));
}
[HttpPost]
[Route("{storeId}/tokens/apikey")]
public async Task<IActionResult> GenerateAPIKey()
@ -748,7 +789,8 @@ namespace BTCPayServer.Controllers
StatusMessage = "Server initiated pairing code: " + pairingCode;
return RedirectToAction(nameof(ListTokens), new
{
storeId = store.Id
storeId = store.Id,
pairingCode = pairingCode
});
}
else
@ -767,5 +809,54 @@ namespace BTCPayServer.Controllers
return null;
return _UserManager.GetUserId(User);
}
// TODO: Need to have talk about how architect default currency implementation
// For now we have also hardcoded USD for Store creation and then Invoice creation
const string DEFAULT_CURRENCY = "USD";
[Route("{storeId}/paybutton")]
public IActionResult PayButton()
{
var store = StoreData;
var storeBlob = store.GetStoreBlob();
if (!storeBlob.AnyoneCanInvoice)
{
return View("PayButtonEnable", null);
}
var appUrl = HttpContext.Request.GetAbsoluteRoot().WithTrailingSlash();
var model = new PayButtonViewModel
{
Price = 10,
Currency = DEFAULT_CURRENCY,
ButtonSize = 2,
UrlRoot = appUrl,
PayButtonImageUrl = appUrl + "img/paybutton/pay.png",
StoreId = store.Id
};
return View(model);
}
[HttpPost]
[Route("{storeId}/paybutton")]
public async Task<IActionResult> PayButton(bool enableStore)
{
var blob = StoreData.GetStoreBlob();
blob.AnyoneCanInvoice = enableStore;
if (StoreData.SetStoreBlob(blob))
{
await _Repo.UpdateStore(StoreData);
StatusMessage = "Store successfully updated";
}
return RedirectToAction(nameof(PayButton), new
{
storeId = StoreData.Id
});
}
}
}

@ -24,6 +24,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using NBitcoin;
using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
using Newtonsoft.Json;
using static BTCPayServer.Controllers.StoresController;
@ -32,17 +33,22 @@ namespace BTCPayServer.Controllers
[Route("wallets")]
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
[AutoValidateAntiforgeryToken]
public class WalletsController : Controller
public partial class WalletsController : Controller
{
private StoreRepository _Repo;
private BTCPayNetworkProvider _NetworkProvider;
public StoreRepository Repository { get; }
public BTCPayNetworkProvider NetworkProvider { get; }
public ExplorerClientProvider ExplorerClientProvider { get; }
private readonly UserManager<ApplicationUser> _userManager;
private readonly IOptions<MvcJsonOptions> _mvcJsonOptions;
private readonly NBXplorerDashboard _dashboard;
private readonly ExplorerClientProvider _explorerProvider;
private readonly IFeeProviderFactory _feeRateProvider;
private readonly BTCPayWalletProvider _walletProvider;
BTCPayRateProviderFactory _RateProvider;
public RateFetcher RateFetcher { get; }
[TempData]
public string StatusMessage { get; set; }
CurrencyNameTable _currencyTable;
public WalletsController(StoreRepository repo,
CurrencyNameTable currencyTable,
@ -50,19 +56,19 @@ namespace BTCPayServer.Controllers
UserManager<ApplicationUser> userManager,
IOptions<MvcJsonOptions> mvcJsonOptions,
NBXplorerDashboard dashboard,
BTCPayRateProviderFactory rateProvider,
RateFetcher rateProvider,
ExplorerClientProvider explorerProvider,
IFeeProviderFactory feeRateProvider,
BTCPayWalletProvider walletProvider)
{
_currencyTable = currencyTable;
_Repo = repo;
_RateProvider = rateProvider;
_NetworkProvider = networkProvider;
Repository = repo;
RateFetcher = rateProvider;
NetworkProvider = networkProvider;
_userManager = userManager;
_mvcJsonOptions = mvcJsonOptions;
_dashboard = dashboard;
_explorerProvider = explorerProvider;
ExplorerClientProvider = explorerProvider;
_feeRateProvider = feeRateProvider;
_walletProvider = walletProvider;
}
@ -70,10 +76,10 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> ListWallets()
{
var wallets = new ListWalletsViewModel();
var stores = await _Repo.GetStoresByUserId(GetUserId());
var stores = await Repository.GetStoresByUserId(GetUserId());
var onChainWallets = stores
.SelectMany(s => s.GetSupportedPaymentMethods(_NetworkProvider)
.SelectMany(s => s.GetSupportedPaymentMethods(NetworkProvider)
.OfType<DerivationStrategy>()
.Select(d => ((Wallet: _walletProvider.GetWallet(d.Network),
DerivationStrategy: d.DerivationStrategyBase,
@ -111,7 +117,7 @@ namespace BTCPayServer.Controllers
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId)
{
var store = await _Repo.FindStore(walletId.StoreId, GetUserId());
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
if (paymentMethod == null)
return NotFound();
@ -120,7 +126,7 @@ namespace BTCPayServer.Controllers
var transactions = await wallet.FetchTransactions(paymentMethod.DerivationStrategyBase);
var model = new ListTransactionsViewModel();
foreach(var tx in transactions.UnconfirmedTransactions.Transactions.Concat(transactions.ConfirmedTransactions.Transactions))
foreach (var tx in transactions.UnconfirmedTransactions.Transactions.Concat(transactions.ConfirmedTransactions.Transactions))
{
var vm = new ListTransactionsViewModel.TransactionViewModel();
model.Transactions.Add(vm);
@ -129,6 +135,7 @@ namespace BTCPayServer.Controllers
vm.Timestamp = tx.Timestamp;
vm.Positive = tx.BalanceChange >= Money.Zero;
vm.Balance = tx.BalanceChange.ToString();
vm.IsConfirmed = tx.Confirmations != 0;
}
model.Transactions = model.Transactions.OrderByDescending(t => t.Timestamp).ToList();
return View(model);
@ -139,29 +146,42 @@ namespace BTCPayServer.Controllers
[Route("{walletId}/send")]
public async Task<IActionResult> WalletSend(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId)
WalletId walletId, string defaultDestination = null, string defaultAmount = null)
{
if (walletId?.StoreId == null)
return NotFound();
var store = await _Repo.FindStore(walletId.StoreId, GetUserId());
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
if (paymentMethod == null)
return NotFound();
var network = this.NetworkProvider.GetNetwork(walletId?.CryptoCode);
if (network == null)
return NotFound();
var storeData = store.GetStoreBlob();
var rateRules = store.GetStoreBlob().GetRateRules(_NetworkProvider);
var rateRules = store.GetStoreBlob().GetRateRules(NetworkProvider);
rateRules.Spread = 0.0m;
var currencyPair = new Rating.CurrencyPair(paymentMethod.PaymentId.CryptoCode, GetCurrencyCode(storeData.DefaultLang) ?? "USD");
WalletModel model = new WalletModel();
model.ServerUrl = GetLedgerWebsocketUrl(this.HttpContext, walletId.CryptoCode, paymentMethod.DerivationStrategyBase);
model.CryptoCurrency = walletId.CryptoCode;
WalletSendModel model = new WalletSendModel()
{
Destination = defaultDestination,
CryptoCode = walletId.CryptoCode
};
if (double.TryParse(defaultAmount, out var amount))
model.Amount = (decimal)amount;
var feeProvider = _feeRateProvider.CreateFeeProvider(network);
var recommendedFees = feeProvider.GetFeeRateAsync();
var balance = _walletProvider.GetWallet(network).GetBalance(paymentMethod.DerivationStrategyBase);
model.CurrentBalance = (await balance).ToDecimal(MoneyUnit.BTC);
model.RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi;
model.FeeSatoshiPerByte = model.RecommendedSatoshiPerByte;
using (CancellationTokenSource cts = new CancellationTokenSource())
{
try
{
cts.CancelAfter(TimeSpan.FromSeconds(5));
var result = await _RateProvider.FetchRate(currencyPair, rateRules).WithCancellation(cts.Token);
var result = await RateFetcher.FetchRate(currencyPair, rateRules).WithCancellation(cts.Token);
if (result.BidAsk != null)
{
model.Rate = result.BidAsk.Center;
@ -173,11 +193,150 @@ namespace BTCPayServer.Controllers
model.RateError = $"{result.EvaluatedRule} ({string.Join(", ", result.Errors.OfType<object>().ToArray())})";
}
}
catch(Exception ex) { model.RateError = ex.Message; }
catch (Exception ex) { model.RateError = ex.Message; }
}
return View(model);
}
[HttpPost]
[Route("{walletId}/send")]
public async Task<IActionResult> WalletSend(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, WalletSendModel vm)
{
if (walletId?.StoreId == null)
return NotFound();
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
if (store == null)
return NotFound();
var network = this.NetworkProvider.GetNetwork(walletId?.CryptoCode);
if (network == null)
return NotFound();
var destination = ParseDestination(vm.Destination, network.NBitcoinNetwork);
if (destination == null)
ModelState.AddModelError(nameof(vm.Destination), "Invalid address");
if (vm.Amount.HasValue)
{
if (vm.CurrentBalance == vm.Amount.Value && !vm.SubstractFees)
ModelState.AddModelError(nameof(vm.Amount), "You are sending all your balance to the same destination, you should substract the fees");
if (vm.CurrentBalance < vm.Amount.Value)
ModelState.AddModelError(nameof(vm.Amount), "You are sending more than what you own");
}
if (!ModelState.IsValid)
return View(vm);
return RedirectToAction(nameof(WalletSendLedger), new WalletSendLedgerModel()
{
Destination = vm.Destination,
Amount = vm.Amount.Value,
SubstractFees = vm.SubstractFees,
FeeSatoshiPerByte = vm.FeeSatoshiPerByte
});
}
[HttpGet]
[Route("{walletId}/send/ledger")]
public async Task<IActionResult> WalletSendLedger(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, WalletSendLedgerModel vm)
{
if (walletId?.StoreId == null)
return NotFound();
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
if (paymentMethod == null)
return NotFound();
var network = this.NetworkProvider.GetNetwork(walletId?.CryptoCode);
if (network == null)
return NotFound();
return View(vm);
}
private IDestination[] ParseDestination(string destination, Network network)
{
try
{
destination = destination?.Trim();
return new IDestination[] { BitcoinAddress.Create(destination, network) };
}
catch
{
return null;
}
}
[HttpGet]
[Route("{walletId}/rescan")]
public async Task<IActionResult> WalletRescan(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId)
{
if (walletId?.StoreId == null)
return NotFound();
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
if (paymentMethod == null)
return NotFound();
var vm = new RescanWalletModel();
vm.IsFullySync = _dashboard.IsFullySynched(walletId.CryptoCode, out var unused);
// We need to ensure it is segwit,
// because hardware wallet support need the parent transactions to sign, which NBXplorer don't have. (Nor does a pruned node)
vm.IsSegwit = paymentMethod.DerivationStrategyBase.IsSegwit();
vm.IsServerAdmin = User.Claims.Any(c => c.Type == Policies.CanModifyServerSettings.Key && c.Value == "true");
vm.IsSupportedByCurrency = _dashboard.Get(walletId.CryptoCode)?.Status?.BitcoinStatus?.Capabilities?.CanScanTxoutSet == true;
var explorer = ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode);
var scanProgress = await explorer.GetScanUTXOSetInformationAsync(paymentMethod.DerivationStrategyBase);
if (scanProgress != null)
{
vm.PreviousError = scanProgress.Error;
if (scanProgress.Status == ScanUTXOStatus.Queued || scanProgress.Status == ScanUTXOStatus.Pending)
{
if (scanProgress.Progress == null)
{
vm.Progress = 0;
}
else
{
vm.Progress = scanProgress.Progress.OverallProgress;
vm.RemainingTime = TimeSpan.FromSeconds(scanProgress.Progress.RemainingSeconds).PrettyPrint();
}
}
if (scanProgress.Status == ScanUTXOStatus.Complete)
{
vm.LastSuccess = scanProgress.Progress;
vm.TimeOfScan = (scanProgress.Progress.CompletedAt.Value - scanProgress.Progress.StartedAt).PrettyPrint();
}
}
return View(vm);
}
[HttpPost]
[Route("{walletId}/rescan")]
[Authorize(Policy = Policies.CanModifyServerSettings.Key)]
public async Task<IActionResult> WalletRescan(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, RescanWalletModel vm)
{
if (walletId?.StoreId == null)
return NotFound();
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
if (paymentMethod == null)
return NotFound();
var explorer = ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode);
try
{
await explorer.ScanUTXOSetAsync(paymentMethod.DerivationStrategyBase, vm.BatchSize, vm.GapLimit, vm.StartingIndex);
}
catch (NBXplorerException ex) when (ex.Error.Code == "scanutxoset-in-progress")
{
}
return RedirectToAction();
}
private string GetCurrencyCode(string defaultLang)
{
if (defaultLang == null)
@ -187,7 +346,7 @@ namespace BTCPayServer.Controllers
var ri = new RegionInfo(defaultLang);
return ri.ISOCurrencySymbol;
}
catch(ArgumentException) { }
catch (ArgumentException) { }
return null;
}
@ -197,7 +356,7 @@ namespace BTCPayServer.Controllers
return null;
var paymentMethod = store
.GetSupportedPaymentMethods(_NetworkProvider)
.GetSupportedPaymentMethods(NetworkProvider)
.OfType<DerivationStrategy>()
.FirstOrDefault(p => p.PaymentId.PaymentType == Payments.PaymentTypes.BTCLike && p.PaymentId.CryptoCode == walletId.CryptoCode);
return paymentMethod;
@ -223,20 +382,25 @@ namespace BTCPayServer.Controllers
return _userManager.GetUserId(User);
}
public static string GetLedgerWebsocketUrl(HttpContext httpContext, string cryptoCode, DerivationStrategyBase derivationStrategy)
[HttpGet]
[Route("{walletId}/send/ledger/success")]
public IActionResult WalletSendLedgerSuccess(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId,
string txid)
{
return $"{httpContext.Request.GetAbsoluteRoot().WithTrailingSlash()}ws/ledger/{cryptoCode}/{derivationStrategy?.ToString() ?? string.Empty}";
StatusMessage = $"Transaction broadcasted ({txid})";
return RedirectToAction(nameof(this.WalletTransactions), new { walletId = walletId.ToString() });
}
[HttpGet]
[Route("/ws/ledger/{cryptoCode}/{derivationScheme?}")]
[Route("{walletId}/send/ledger/ws")]
public async Task<IActionResult> LedgerConnection(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId,
string command,
// getinfo
string cryptoCode = null,
// getxpub
[ModelBinder(typeof(ModelBinders.DerivationSchemeModelBinder))]
DerivationStrategyBase derivationScheme = null,
int account = 0,
// sendtoaddress
string destination = null, string amount = null, string feeRate = null, string substractFees = null
@ -244,6 +408,11 @@ namespace BTCPayServer.Controllers
{
if (!HttpContext.WebSockets.IsWebSocketRequest)
return NotFound();
var cryptoCode = walletId.CryptoCode;
var storeBlob = (await Repository.FindStore(walletId.StoreId, GetUserId()));
var derivationScheme = GetPaymentMethod(walletId, storeBlob).DerivationStrategyBase;
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
using (var normalOperationTimeout = new CancellationTokenSource())
@ -257,7 +426,7 @@ namespace BTCPayServer.Controllers
BTCPayNetwork network = null;
if (cryptoCode != null)
{
network = _NetworkProvider.GetNetwork(cryptoCode);
network = NetworkProvider.GetNetwork(cryptoCode);
if (network == null)
throw new FormatException("Invalid value for crypto code");
}
@ -307,15 +476,6 @@ namespace BTCPayServer.Controllers
}
catch { throw new FormatException("Invalid value for subtract fees"); }
}
if (command == "test")
{
result = await hw.Test(normalOperationTimeout.Token);
}
if (command == "getxpub")
{
var getxpubResult = await hw.GetExtPubKey(network, account, normalOperationTimeout.Token);
result = getxpubResult;
}
if (command == "getinfo")
{
var strategy = GetDirectDerivationStrategy(derivationScheme);
@ -323,13 +483,12 @@ namespace BTCPayServer.Controllers
{
throw new Exception($"This store is not configured to use this ledger");
}
var feeProvider = _feeRateProvider.CreateFeeProvider(network);
var recommendedFees = feeProvider.GetFeeRateAsync();
var balance = _walletProvider.GetWallet(network).GetBalance(derivationScheme);
result = new GetInfoResult() { Balance = (double)(await balance).ToDecimal(MoneyUnit.BTC), RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi };
result = new GetInfoResult();
}
if (command == "test")
{
result = await hw.Test(normalOperationTimeout.Token);
}
if (command == "sendtoaddress")
{
if (!_dashboard.IsFullySynched(network.CryptoCode, out var summary))
@ -361,9 +520,8 @@ namespace BTCPayServer.Controllers
throw new HardwareWalletException($"This store is not configured to use this ledger");
}
TransactionBuilder builder = new TransactionBuilder();
TransactionBuilder builder = network.NBitcoinNetwork.CreateTransactionBuilder();
builder.StandardTransactionPolicy.MinRelayTxFee = summary.Status.BitcoinStatus.MinRelayTxFee;
builder.SetConsensusFactory(network.NBitcoinNetwork);
builder.AddCoins(unspentCoins.Select(c => c.Coin).ToArray());
foreach (var element in send)
@ -386,7 +544,6 @@ namespace BTCPayServer.Controllers
else
builder.SendEstimatedFees(feeRateValue);
}
builder.Shuffle();
var unsigned = builder.BuildTransaction(false);
var keypaths = new Dictionary<Script, KeyPath>();
@ -403,7 +560,7 @@ namespace BTCPayServer.Controllers
if (!strategy.Segwit)
{
var parentHashes = usedCoins.Select(c => c.Outpoint.Hash).ToHashSet();
var explorer = _explorerProvider.GetExplorerClient(network);
var explorer = ExplorerClientProvider.GetExplorerClient(network);
var getTransactionAsyncs = parentHashes.Select(h => (Op: explorer.GetTransactionAsync(h), Hash: h)).ToList();
foreach (var getTransactionAsync in getTransactionAsyncs)
{
@ -476,8 +633,6 @@ namespace BTCPayServer.Controllers
public class GetInfoResult
{
public int RecommendedSatoshiPerByte { get; set; }
public double Balance { get; set; }
}
public class SendToAddressResult

@ -5,7 +5,6 @@ using System.Linq;
using System.Threading.Tasks;
using Hangfire;
using Hangfire.MemoryStorage;
using Hangfire.PostgreSql;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations;
using JetBrains.Annotations;
@ -17,7 +16,8 @@ namespace BTCPayServer.Data
public enum DatabaseType
{
Sqlite,
Postgres
Postgres,
MySQL,
}
public class ApplicationDbContextFactory
{
@ -95,6 +95,8 @@ namespace BTCPayServer.Data
builder
.UseNpgsql(_ConnectionString)
.ReplaceService<IMigrationsSqlGenerator, CustomNpgsqlMigrationsSqlGenerator>();
else if (_Type == DatabaseType.MySQL)
builder.UseMySql(_ConnectionString);
}
public void ConfigureHangfireBuilder(IGlobalConfiguration builder)

@ -17,6 +17,7 @@ using BTCPayServer.JsonConverters;
using System.ComponentModel.DataAnnotations;
using BTCPayServer.Services;
using System.Security.Claims;
using BTCPayServer.Payments.Changelly;
using BTCPayServer.Security;
using BTCPayServer.Rating;
@ -166,24 +167,25 @@ namespace BTCPayServer.Data
public Claim[] GetClaims()
{
List<Claim> claims = new List<Claim>();
claims.AddRange(AdditionalClaims);
#pragma warning disable CS0612 // Type or member is obsolete
var role = Role;
#pragma warning restore CS0612 // Type or member is obsolete
if (role == StoreRoles.Owner)
{
claims.Add(new Claim(Policies.CanModifyStoreSettings.Key, Id));
claims.Add(new Claim(Policies.CanUseStore.Key, Id));
}
if (role == StoreRoles.Guest)
if(role == StoreRoles.Owner || role == StoreRoles.Guest || GetStoreBlob().AnyoneCanInvoice)
{
claims.Add(new Claim(Policies.CanUseStore.Key, Id));
claims.Add(new Claim(Policies.CanCreateInvoice.Key, Id));
}
return claims.ToArray();
}
public bool HasClaim(string claim)
{
return GetClaims().Any(c => c.Type == claim);
return GetClaims().Any(c => c.Type == claim && c.Value == Id);
}
public byte[] StoreBlob
@ -196,10 +198,13 @@ namespace BTCPayServer.Data
public List<PairedSINData> PairedSINs { get; set; }
public IEnumerable<APIKeyData> APIKeys { get; set; }
[NotMapped]
public List<Claim> AdditionalClaims { get; set; } = new List<Claim>();
#pragma warning disable CS0618
public string GetDefaultCrypto(BTCPayNetworkProvider networkProvider = null)
{
return DefaultCrypto ?? (networkProvider == null ? "BTC" : GetSupportedPaymentMethods(networkProvider).First().PaymentId.CryptoCode);
return DefaultCrypto ?? (networkProvider == null ? "BTC" : GetSupportedPaymentMethods(networkProvider).Select(p => p.PaymentId.CryptoCode).FirstOrDefault() ?? "BTC");
}
public void SetDefaultCrypto(string defaultCryptoCurrency)
{
@ -257,11 +262,6 @@ namespace BTCPayServer.Data
{
get; set;
}
public bool AllowCoinConversion
{
get; set;
}
public bool RequiresRefundEmail { get; set; }
public string DefaultLang { get; set; }
@ -302,6 +302,10 @@ namespace BTCPayServer.Data
public string RateScript { get; set; }
public bool AnyoneCanInvoice { get; set; }
public ChangellySettings ChangellySettings { get; set; }
string _LightningDescriptionTemplate;
public string LightningDescriptionTemplate

@ -63,10 +63,18 @@ namespace BTCPayServer
electrumMapping.Add(p2wpkh, Array.Empty<string>());
var parts = str.Split('-');
bool hasLabel = false;
for (int i = 0; i < parts.Length; i++)
{
if (IsLabel(parts[i]))
{
if (!hasLabel)
{
hintedLabels.Clear();
if (!Network.Consensus.SupportSegwit)
hintedLabels.Add("legacy");
}
hasLabel = true;
hintedLabels.Add(parts[i].Substring(1, parts[i].Length - 2).ToLowerInvariant());
continue;
}

@ -32,6 +32,7 @@ using System.Globalization;
using BTCPayServer.Services;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using NBXplorer.DerivationStrategy;
namespace BTCPayServer
{
@ -128,6 +129,19 @@ namespace BTCPayServer
resp.Headers[name] = value;
}
public static bool IsSegwit(this DerivationStrategyBase derivationStrategyBase)
{
if (IsSegwitCore(derivationStrategyBase))
return true;
return (derivationStrategyBase is P2SHDerivationStrategy p2shStrat && IsSegwitCore(p2shStrat.Inner));
}
private static bool IsSegwitCore(DerivationStrategyBase derivationStrategyBase)
{
return (derivationStrategyBase is P2WSHDerivationStrategy) ||
(derivationStrategyBase is DirectDerivationStrategy direct) && direct.Segwit;
}
public static string GetAbsoluteRoot(this HttpRequest request)
{
return string.Concat(

@ -28,6 +28,28 @@ namespace BTCPayServer.Filters
}
}
public class MediaTypeAcceptConstraintAttribute : Attribute, IActionConstraint
{
public MediaTypeAcceptConstraintAttribute(string mediaType)
{
MediaType = mediaType ?? throw new ArgumentNullException(nameof(mediaType));
}
public string MediaType
{
get; set;
}
public int Order => 100;
public bool Accept(ActionConstraintContext context)
{
if (!context.RouteContext.HttpContext.Request.Headers.ContainsKey("Accept"))
return false;
return context.RouteContext.HttpContext.Request.Headers["Accept"].ToString().StartsWith(MediaType, StringComparison.Ordinal);
}
}
public class BitpayAPIConstraintAttribute : Attribute, IActionConstraint
{
public BitpayAPIConstraintAttribute(bool isBitpayAPI = true)

@ -23,7 +23,10 @@ namespace BTCPayServer.Filters
public void OnActionExecuting(ActionExecutingContext context)
{
context.HttpContext.Response.SetHeaderOnStarting("X-Frame-Options", Value);
if (context.IsEffectivePolicy<XFrameOptionsAttribute>(this))
{
context.HttpContext.Response.SetHeaderOnStarting("X-Frame-Options", Value);
}
}
}
}

@ -20,6 +20,7 @@ using BTCPayServer.Events;
using NBXplorer;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Payments;
using BTCPayServer.Services.Mails;
namespace BTCPayServer.HostedServices
{
@ -52,24 +53,44 @@ namespace BTCPayServer.HostedServices
EventAggregator _EventAggregator;
InvoiceRepository _InvoiceRepository;
BTCPayNetworkProvider _NetworkProvider;
IEmailSender _EmailSender;
public InvoiceNotificationManager(
IBackgroundJobClient jobClient,
EventAggregator eventAggregator,
InvoiceRepository invoiceRepository,
BTCPayNetworkProvider networkProvider,
ILogger<InvoiceNotificationManager> logger)
ILogger<InvoiceNotificationManager> logger,
IEmailSender emailSender)
{
Logger = logger as ILogger ?? NullLogger.Instance;
_JobClient = jobClient;
_EventAggregator = eventAggregator;
_InvoiceRepository = invoiceRepository;
_NetworkProvider = networkProvider;
_EmailSender = emailSender;
}
async Task Notify(InvoiceEntity invoice, int? eventCode = null, string name = null)
{
CancellationTokenSource cts = new CancellationTokenSource(10000);
if (!String.IsNullOrEmpty(invoice.NotificationEmail))
{
// just extracting most important data for email body, merchant should query API back for full invoice based on Invoice.Id
var ipn = new
{
invoice.Id,
invoice.Status,
invoice.StoreId
};
// TODO: Consider adding info on ItemDesc and payment info (amount)
var emailBody = NBitcoin.JsonConverters.Serializer.ToString(ipn);
await _EmailSender.SendEmailAsync(
invoice.NotificationEmail, $"BtcPayServer Invoice Notification - ${invoice.StoreId}", emailBody);
}
try
{
if (string.IsNullOrEmpty(invoice.NotificationURL))
@ -203,7 +224,7 @@ namespace BTCPayServer.HostedServices
PaymentTotals = dto.PaymentTotals,
AmountPaid = dto.AmountPaid,
ExchangeRates = dto.ExchangeRates,
};
// We keep backward compatibility with bitpay by passing BTC info to the notification
@ -264,15 +285,15 @@ namespace BTCPayServer.HostedServices
sendRequest()
.ContinueWith(t =>
{
if(t.Status == TaskStatus.RanToCompletion)
{
if (t.Status == TaskStatus.RanToCompletion)
{
completion.TrySetResult(t.Result);
}
if(t.Status == TaskStatus.Faulted)
if (t.Status == TaskStatus.Faulted)
{
completion.TrySetException(t.Exception);
}
if(t.Status == TaskStatus.Canceled)
if (t.Status == TaskStatus.Canceled)
{
completion.TrySetCanceled();
}
@ -289,7 +310,7 @@ namespace BTCPayServer.HostedServices
lock (_SendingRequestsByInvoiceId)
{
_SendingRequestsByInvoiceId.TryGetValue(id, out var executing2);
if(executing2 == sending)
if (executing2 == sending)
_SendingRequestsByInvoiceId.Remove(id);
}
}, TaskScheduler.Default);

@ -47,7 +47,11 @@ namespace BTCPayServer.HostedServices
summary.Status != null &&
summary.Status.IsFullySynched;
}
public NBXplorerSummary Get(string cryptoCode)
{
_Summaries.TryGetValue(cryptoCode, out var summary);
return summary;
}
public IEnumerable<NBXplorerSummary> GetAll()
{
return _Summaries.Values;

@ -18,9 +18,9 @@ namespace BTCPayServer.HostedServices
{
private SettingsRepository _SettingsRepository;
private CoinAverageSettings _coinAverageSettings;
BTCPayRateProviderFactory _RateProviderFactory;
RateProviderFactory _RateProviderFactory;
public RatesHostedService(SettingsRepository repo,
BTCPayRateProviderFactory rateProviderFactory,
RateProviderFactory rateProviderFactory,
CoinAverageSettings coinAverageSettings)
{
this._SettingsRepository = repo;
@ -33,16 +33,41 @@ namespace BTCPayServer.HostedServices
return new[]
{
CreateLoopTask(RefreshCoinAverageSupportedExchanges),
CreateLoopTask(RefreshCoinAverageSettings)
CreateLoopTask(RefreshCoinAverageSettings),
CreateLoopTask(RefreshRates)
};
}
async Task RefreshRates()
{
using (var timeout = CancellationTokenSource.CreateLinkedTokenSource(Cancellation))
{
timeout.CancelAfter(TimeSpan.FromSeconds(20.0));
try
{
await Task.WhenAll(_RateProviderFactory.Providers
.Select(p => (Fetcher: p.Value as BackgroundFetcherRateProvider, ExchangeName: p.Key)).Where(p => p.Fetcher != null)
.Select(p => p.Fetcher.UpdateIfNecessary().ContinueWith(t =>
{
if (t.Result.Exception != null)
{
Logs.PayServer.LogWarning($"Error while contacting {p.ExchangeName}: {t.Result.Exception.Message}");
}
}, TaskScheduler.Default))
.ToArray()).WithCancellation(timeout.Token);
}
catch (OperationCanceledException) when (timeout.IsCancellationRequested)
{
}
}
await Task.Delay(TimeSpan.FromSeconds(30), Cancellation);
}
async Task RefreshCoinAverageSupportedExchanges()
{
await new SynchronizationContextRemover();
var tickers = await new CoinAverageRateProvider() { Authenticator = _coinAverageSettings }.GetExchangeTickersAsync();
var exchanges = new CoinAverageExchanges();
foreach(var item in tickers
foreach (var item in tickers
.Exchanges
.Select(c => new CoinAverageExchange(c.Name, c.DisplayName)))
{
@ -54,7 +79,6 @@ namespace BTCPayServer.HostedServices
async Task RefreshCoinAverageSettings()
{
await new SynchronizationContextRemover();
var rates = (await _SettingsRepository.GetSettingAsync<RatesSetting>()) ?? new RatesSetting();
_RateProviderFactory.CacheSpan = TimeSpan.FromMinutes(rates.CacheInMinutes);
if (!string.IsNullOrWhiteSpace(rates.PrivateKey) && !string.IsNullOrWhiteSpace(rates.PublicKey))

@ -38,9 +38,12 @@ using BTCPayServer.Logging;
using BTCPayServer.HostedServices;
using Meziantou.AspNetCore.BundleTagHelpers;
using System.Security.Claims;
using BTCPayServer.Payments.Changelly;
using BTCPayServer.Security;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using NBXplorer.DerivationStrategy;
using NicolasDorier.RateLimits;
using Npgsql;
namespace BTCPayServer.Hosting
{
@ -53,6 +56,7 @@ namespace BTCPayServer.Hosting
var factory = provider.GetRequiredService<ApplicationDbContextFactory>();
factory.ConfigureBuilder(o);
});
services.AddHttpClient();
services.TryAddSingleton<SettingsRepository>();
services.TryAddSingleton<InvoicePaymentNotification>();
services.TryAddSingleton<BTCPayServerOptions>(o => o.GetRequiredService<IOptions<BTCPayServerOptions>>().Value);
@ -73,20 +77,25 @@ namespace BTCPayServer.Hosting
{
var opts = o.GetRequiredService<BTCPayServerOptions>();
ApplicationDbContextFactory dbContext = null;
if (opts.PostgresConnectionString == null)
if (!String.IsNullOrEmpty(opts.PostgresConnectionString))
{
Logs.Configuration.LogInformation($"Postgres DB used ({opts.PostgresConnectionString})");
dbContext = new ApplicationDbContextFactory(DatabaseType.Postgres, opts.PostgresConnectionString);
}
else if(!String.IsNullOrEmpty(opts.MySQLConnectionString))
{
Logs.Configuration.LogInformation($"MySQL DB used ({opts.MySQLConnectionString})");
dbContext = new ApplicationDbContextFactory(DatabaseType.MySQL, opts.MySQLConnectionString);
}
else
{
var connStr = "Data Source=" + Path.Combine(opts.DataDir, "sqllite.db");
Logs.Configuration.LogInformation($"SQLite DB used ({connStr})");
dbContext = new ApplicationDbContextFactory(DatabaseType.Sqlite, connStr);
}
else
{
Logs.Configuration.LogInformation($"Postgres DB used ({opts.PostgresConnectionString})");
dbContext = new ApplicationDbContextFactory(DatabaseType.Postgres, opts.PostgresConnectionString);
}
return dbContext;
});
services.TryAddSingleton<Payments.Lightning.LightningClientFactory>();
services.TryAddSingleton<BTCPayNetworkProvider>(o =>
{
@ -94,6 +103,8 @@ namespace BTCPayServer.Hosting
return opts.NetworkProvider;
});
services.TryAddSingleton<AppsHelper>();
services.TryAddSingleton<LightningConfigurationProvider>();
services.TryAddSingleton<LanguageService>();
services.TryAddSingleton<NBXplorerDashboard>();
@ -122,6 +133,8 @@ namespace BTCPayServer.Hosting
services.AddSingleton<Payments.IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>, Payments.Lightning.LightningLikePaymentHandler>();
services.AddSingleton<IHostedService, Payments.Lightning.LightningListener>();
services.AddSingleton<ChangellyClientProvider>();
services.AddSingleton<IHostedService, NBXplorerWaiters>();
services.AddSingleton<IHostedService, InvoiceNotificationManager>();
@ -137,7 +150,8 @@ namespace BTCPayServer.Hosting
else
return new Bitpay(new Key(), new Uri("https://test.bitpay.com/"));
});
services.TryAddSingleton<BTCPayRateProviderFactory>();
services.TryAddSingleton<RateProviderFactory>();
services.TryAddSingleton<RateFetcher>();
services.TryAddScoped<IHttpContextAccessor, HttpContextAccessor>();
services.AddTransient<AccessTokenController>();
@ -154,7 +168,7 @@ namespace BTCPayServer.Hosting
{
var opts = provider.GetRequiredService<BTCPayServerOptions>();
var bundle = new BundleOptions();
bundle.UseMinifiedFiles = opts.BundleJsCss;
bundle.UseBundles = opts.BundleJsCss;
bundle.AppendVersion = true;
return bundle;
});
@ -163,6 +177,10 @@ namespace BTCPayServer.Hosting
{
options.AddPolicy(CorsPolicies.All, p=>p.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin());
});
var rateLimits = new RateLimitService();
rateLimits.SetZone($"zone={ZoneLimits.Login} rate=5r/min burst=3 nodelay");
services.AddSingleton(rateLimits);
return services;
}
@ -183,7 +201,7 @@ namespace BTCPayServer.Hosting
static void Retry(Action act)
{
CancellationTokenSource cts = new CancellationTokenSource(10000);
CancellationTokenSource cts = new CancellationTokenSource(1000);
while (true)
{
try
@ -191,7 +209,9 @@ namespace BTCPayServer.Hosting
act();
return;
}
catch when(!cts.IsCancellationRequested)
// Starting up
catch (PostgresException ex) when (ex.SqlState == "57P03") { Thread.Sleep(1000); }
catch when (!cts.IsCancellationRequested)
{
Thread.Sleep(100);
}

@ -83,14 +83,14 @@ namespace BTCPayServer.Hosting
var path = httpContext.Request.Path.Value;
if (
bitpayAuth &&
path == "/invoices" &&
(path == "/invoices" || path == "/invoices/") &&
httpContext.Request.Method == "POST" &&
isJson)
return true;
if (
bitpayAuth &&
path == "/invoices" &&
(path == "/invoices" || path == "/invoices/") &&
httpContext.Request.Method == "GET")
return true;
@ -105,8 +105,8 @@ namespace BTCPayServer.Hosting
return true;
if (
path.Equals("/tokens", StringComparison.Ordinal) &&
( httpContext.Request.Method == "GET" || httpContext.Request.Method == "POST"))
path.Equals("/tokens", StringComparison.Ordinal) &&
(httpContext.Request.Method == "GET" || httpContext.Request.Method == "POST"))
return true;
return false;
@ -140,13 +140,9 @@ namespace BTCPayServer.Hosting
if (reverseProxyScheme != null && _Options.ExternalUrl.Scheme != reverseProxyScheme)
{
if (reverseProxyScheme == "http" && _Options.ExternalUrl.Scheme == "https")
Logs.PayServer.LogWarning($"BTCPay ExternalUrl setting expected to use scheme '{_Options.ExternalUrl.Scheme}' externally, but the reverse proxy uses scheme '{reverseProxyScheme}'");
httpContext.Request.Scheme = reverseProxyScheme;
}
else
{
httpContext.Request.Scheme = _Options.ExternalUrl.Scheme;
Logs.PayServer.LogWarning($"BTCPay ExternalUrl setting expected to use scheme '{_Options.ExternalUrl.Scheme}' externally, but the reverse proxy uses scheme '{reverseProxyScheme}' (X-Forwarded-Port), forcing ExternalUrl");
}
httpContext.Request.Scheme = _Options.ExternalUrl.Scheme;
if (_Options.ExternalUrl.IsDefaultPort)
httpContext.Request.Host = new HostString(_Options.ExternalUrl.Host);
else

@ -57,19 +57,22 @@ namespace BTCPayServer.Hosting
return context.GetHttpContext().User.IsInRole(_Role);
}
}
public Startup(IConfiguration conf, IHostingEnvironment env)
public Startup(IConfiguration conf, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
Configuration = conf;
_Env = env;
LoggerFactory = loggerFactory;
}
IHostingEnvironment _Env;
public IConfiguration Configuration
{
get; set;
}
public ILoggerFactory LoggerFactory { get; }
public void ConfigureServices(IServiceCollection services)
{
Logs.Configure(LoggerFactory);
services.ConfigureBTCPayServer(Configuration);
services.AddMemoryCache();
services.AddIdentity<ApplicationUser, IdentityRole>()
@ -100,6 +103,9 @@ namespace BTCPayServer.Hosting
options.Password.RequireLowercase = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
options.Lockout.MaxFailedAccessAttempts = 5;
options.Lockout.AllowedForNewUsers = true;
});
services.AddHangfire((o) =>
@ -116,14 +122,42 @@ namespace BTCPayServer.Hosting
});
});
// Needed to debug U2F for ledger support
//services.Configure<KestrelServerOptions>(kestrel =>
//{
// kestrel.Listen(IPAddress.Loopback, 5012, l =>
// {
// l.UseHttps("devtest.pfx", "toto");
// });
//});
// If the HTTPS certificate path is not set this logic will NOT be used and the default Kestrel binding logic will be.
string httpsCertificateFilePath = Configuration.GetOrDefault<string>("HttpsCertificateFilePath", null);
bool useDefaultCertificate = Configuration.GetOrDefault<bool>("HttpsUseDefaultCertificate", false);
bool hasCertPath = !String.IsNullOrEmpty(httpsCertificateFilePath);
if (hasCertPath || useDefaultCertificate)
{
var bindAddress = Configuration.GetOrDefault<IPAddress>("bind", IPAddress.Any);
int bindPort = Configuration.GetOrDefault<int>("port", 443);
services.Configure<KestrelServerOptions>(kestrel =>
{
if (hasCertPath && !File.Exists(httpsCertificateFilePath))
{
// Note that by design this is a fatal error condition that will cause the process to exit.
throw new ConfigException($"The https certificate file could not be found at {httpsCertificateFilePath}.");
}
if(hasCertPath && useDefaultCertificate)
{
throw new ConfigException($"Conflicting settings: if HttpsUseDefaultCertificate is true, HttpsCertificateFilePath should not be used");
}
kestrel.Listen(bindAddress, bindPort, l =>
{
if (hasCertPath)
{
Logs.Configuration.LogInformation($"Using HTTPS with the certificate located in {httpsCertificateFilePath}.");
l.UseHttps(httpsCertificateFilePath, Configuration.GetOrDefault<string>("HttpsCertificateFilePassword", null));
}
else
{
Logs.Configuration.LogInformation($"Using HTTPS with the default certificate");
l.UseHttps();
}
});
});
}
}
public void Configure(
@ -133,7 +167,6 @@ namespace BTCPayServer.Hosting
BTCPayServerOptions options,
ILoggerFactory loggerFactory)
{
Logs.Configure(loggerFactory);
Logs.Configuration.LogInformation($"Root Path: {options.RootPath}");
if (options.RootPath.Equals("/", StringComparison.OrdinalIgnoreCase))
{
@ -166,6 +199,7 @@ namespace BTCPayServer.Hosting
Authorization = new[] { new NeedRole(Roles.ServerAdmin) }
});
app.UseWebSockets();
app.UseStatusCodePages();
app.UseMvc(routes =>
{
routes.MapRoute(

@ -1,43 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json;
using System.Reflection;
using BTCPayServer.Payments.Lightning;
using NBitcoin.JsonConverters;
using System.Globalization;
namespace BTCPayServer.JsonConverters
{
public class LightMoneyJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(LightMoneyJsonConverter).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
}
Type longType = typeof(long).GetTypeInfo();
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
try
{
return reader.TokenType == JsonToken.Null ? null :
reader.TokenType == JsonToken.Integer ?
longType.IsAssignableFrom(reader.ValueType) ? new LightMoney((long)reader.Value)
: new LightMoney(long.MaxValue) :
reader.TokenType == JsonToken.String ? new LightMoney(long.Parse((string)reader.Value, CultureInfo.InvariantCulture))
: null;
}
catch (InvalidCastException)
{
throw new JsonObjectException("Money amount should be in millisatoshi", reader);
}
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
writer.WriteValue(((LightMoney)value).MilliSatoshi);
}
}
}

@ -15,6 +15,7 @@ namespace BTCPayServer.Models.AppViewModels
public string AppName { get; set; }
public string AppType { get; set; }
public bool IsOwner { get; set; }
public string UpdateAction { get { return "Update" + AppType; } }
public string ViewAction { get { return "View" + AppType; } }
}

@ -14,7 +14,6 @@ namespace BTCPayServer.Models.AppViewModels
[Required]
[MaxLength(5)]
public string Currency { get; set; }
[Required]
[MaxLength(5000)]
public string Template { get; set; }
@ -24,5 +23,18 @@ namespace BTCPayServer.Models.AppViewModels
public string Example2 { get; internal set; }
public string ExampleCallback { get; internal set; }
public string InvoiceUrl { get; internal set; }
[Required]
[MaxLength(30)]
[Display(Name = "Text to display on each buttons for items with a specific price")]
public string ButtonText { get; set; }
[Required]
[MaxLength(30)]
[Display(Name = "Text to display on buttons next to the input allowing the user to enter a custom amount")]
public string CustomButtonText { get; set; }
[MaxLength(500)]
[Display(Name = "Custom bootstrap CSS file")]
public string CustomCSSLink { get; set; }
}
}

@ -14,13 +14,23 @@ namespace BTCPayServer.Models.AppViewModels
public string Formatted { get; set; }
public decimal Value { get; set; }
}
public string Description { get; set; }
public string Id { get; set; }
public string Image { get; set; }
public ItemPrice Price { get; set; }
public string Title { get; set; }
public bool Custom { get; set; }
}
public bool ShowCustomAmount { get; set; }
public string Step { get; set; }
public string Title { get; set; }
public Item[] Items { get; set; }
public string CurrencySymbol { get; set; }
public string ButtonText { get; set; }
public string CustomButtonText { get; set; }
public string CustomCSSLink { get; set; }
}
}

@ -247,6 +247,8 @@ namespace BTCPayServer.Models
public Dictionary<string, string> Addresses { get; set; }
[JsonProperty("paymentCodes")]
public Dictionary<string, NBitpayClient.InvoicePaymentUrls> PaymentCodes { get; set; }
[JsonProperty("buyer")]
public JObject Buyer { get; set; }
}
public class Flags
{

@ -53,6 +53,12 @@ namespace BTCPayServer.Models.InvoicingModels
get; set;
}
[EmailAddress]
public string NotificationEmail
{
get; set;
}
[Uri]
public string NotificationUrl
{

@ -142,5 +142,6 @@ namespace BTCPayServer.Models.InvoicingModels
public AddressModel[] Addresses { get; set; }
public DateTimeOffset MonitoringDate { get; internal set; }
public List<Data.InvoiceEventData> Events { get; internal set; }
public string NotificationEmail { get; internal set; }
}
}

@ -21,6 +21,7 @@ namespace BTCPayServer.Models.InvoicingModels
public string CustomLogoLink { get; set; }
public string DefaultLang { get; set; }
public List<AvailableCrypto> AvailableCryptos { get; set; } = new List<AvailableCrypto>();
public bool IsModal { get; set; }
public bool IsLightning { get; set; }
public string CryptoCode { get; set; }
public string ServerUrl { get; set; }
@ -55,7 +56,10 @@ namespace BTCPayServer.Models.InvoicingModels
public string PaymentMethodName { get; set; }
public string CryptoImage { get; set; }
public bool AllowCoinConversion { get; set; }
public bool ChangellyEnabled { get; set; }
public string StoreId { get; set; }
public string PeerInfo { get; set; }
public string ChangellyMerchantId { get; set; }
public decimal? ChangellyAmountDue { get; set; }
}
}

@ -1,4 +1,4 @@
using BTCPayServer.Validations;
using BTCPayServer.Validation;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

@ -5,7 +5,7 @@ using System.Threading.Tasks;
namespace BTCPayServer.Models.ServerViewModels
{
public class LNDGRPCServicesViewModel
public class LndGrpcServicesViewModel
{
public string Host { get; set; }
public bool SSL { get; set; }

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models.ServerViewModels
{
public class LndRestServicesViewModel
{
public string BaseApiUrl { get; set; }
public string Macaroon { get; set; }
public string CertificateThumbprint { get; set; }
}
}

@ -0,0 +1,19 @@
using System.Collections.Generic;
using System.IO;
namespace BTCPayServer.Models.ServerViewModels
{
public class LogsViewModel
{
public string StatusMessage
{
get; set;
}
public List<FileInfo> LogFiles { get; set; } = new List<FileInfo>();
public string Log { get; set; }
public int LogFileCount { get; set; }
public int LogFileOffset{ get; set; }
}
}

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Configuration.External;
namespace BTCPayServer.Models.ServerViewModels
{
@ -10,9 +11,10 @@ namespace BTCPayServer.Models.ServerViewModels
public class LNDServiceViewModel
{
public string Crypto { get; set; }
public string Type { get; set; }
public LndTypes Type { get; set; }
public int Index { get; set; }
}
public List<LNDServiceViewModel> LNDServices { get; set; } = new List<LNDServiceViewModel>();
public bool HasSSH { get; set; }
}

@ -23,11 +23,6 @@ namespace BTCPayServer.Models.StoreViewModels
public string DefaultCryptoCurrency { get; set; }
[Display(Name = "Default language on checkout")]
public string DefaultLang { get; set; }
[Display(Name = "Allow conversion through third party (Shapeshift, Changelly...)")]
public bool AllowCoinConversion
{
get; set;
}
[Display(Name = "Do not propose lightning payment if value of the invoice is above...")]
[MaxLength(20)]
public string LightningMaxValue { get; set; }
@ -63,7 +58,7 @@ namespace BTCPayServer.Models.StoreViewModels
public void SetLanguages(LanguageService langService, string defaultLang)
{
defaultLang = defaultLang ?? "en-US";
defaultLang = langService.GetLanguages().Any(language => language.Code == defaultLang)? defaultLang : "en";
var choices = langService.GetLanguages().Select(o => new Format() { Name = o.DisplayName, Value = o.Code }).ToArray();
var chosen = choices.FirstOrDefault(f => f.Value == defaultLang) ?? choices.FirstOrDefault();
Languages = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);

@ -29,7 +29,6 @@ namespace BTCPayServer.Models.StoreViewModels
public bool Confirmation { get; set; }
public bool Enabled { get; set; } = true;
public string ServerUrl { get; set; }
public string StatusMessage { get; internal set; }
public KeyPath RootKeyPath { get; set; }
}

@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models.StoreViewModels
{
public class PayButtonViewModel
{
public decimal Price { get; set; }
public string InvoiceId { get; set; }
[Required]
public string Currency { get; set; }
public string CheckoutDesc { get; set; }
public string OrderId { get; set; }
public int ButtonSize { get; set; }
[Url]
public string ServerIpn { get; set; }
[Url]
public string BrowserRedirect { get; set; }
[EmailAddress]
public string NotifyEmail { get; set; }
public string StoreId { get; set; }
// Data that influences Pay Button UI, but not invoice creation
public string UrlRoot { get; set; }
public List<string> CurrencyDropdown { get; set; }
public string PayButtonImageUrl { get; set; }
}
}

@ -2,7 +2,6 @@
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Validation;
using BTCPayServer.Validations;
using Microsoft.AspNetCore.Mvc.Rendering;
using System;
using System.Collections.Generic;
@ -21,7 +20,13 @@ namespace BTCPayServer.Models.StoreViewModels
public WalletId WalletId { get; set; }
public bool Enabled { get; set; }
}
public class ThirdPartyPaymentMethod
{
public string Provider { get; set; }
public bool Enabled { get; set; }
public string Action { get; set; }
}
public StoreViewModel()
{
@ -47,8 +52,14 @@ namespace BTCPayServer.Models.StoreViewModels
set;
}
[Display(Name = "Allow anyone to create invoice")]
public bool AnyoneCanCreateInvoice { get; set; }
public List<StoreViewModel.DerivationScheme> DerivationSchemes { get; set; } = new List<StoreViewModel.DerivationScheme>();
public List<ThirdPartyPaymentMethod> ThirdPartyPaymentMethods { get; set; } =
new List<ThirdPartyPaymentMethod>();
[Display(Name = "Invoice expires if the full amount has not been paid after ... minutes")]
[Range(1, 60 * 24 * 24)]
public int InvoiceExpiration

@ -1,4 +1,4 @@
using BTCPayServer.Validations;
using BTCPayServer.Validation;
using Microsoft.AspNetCore.Mvc.Rendering;
using System;
using System.Collections.Generic;

@ -0,0 +1,33 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Runtime.InteropServices;
using BTCPayServer.Payments;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace BTCPayServer.Models.StoreViewModels
{
public class UpdateChangellySettingsViewModel
{
[Required] public string ApiKey { get; set; }
[Required] public string ApiSecret { get; set; }
[Required] public string ApiUrl { get; set; } = "https://api.changelly.com";
[Display(Name = "Optional, Changelly Merchant Id")]
public string ChangellyMerchantId { get; set; }
[Display(Name = "Show Fiat Currencies as option in conversion")]
public bool ShowFiat { get; set; } = true;
[Required]
[Range(0, 100)]
[Display(Name =
"Percentage to multiply amount requested at Changelly to avoid underpaid situations due to Changelly not guaranteeing rates. ")]
public decimal AmountMarkupPercentage { get; set; } = new decimal(2);
public bool Enabled { get; set; }
public string StatusMessage { get; set; }
}
}

@ -10,6 +10,7 @@ namespace BTCPayServer.Models.WalletViewModels
public class TransactionViewModel
{
public DateTimeOffset Timestamp { get; set; }
public bool IsConfirmed { get; set; }
public string Id { get; set; }
public string Link { get; set; }
public bool Positive { get; set; }

@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using NBXplorer.Models;
namespace BTCPayServer.Models.WalletViewModels
{
public class RescanWalletModel
{
public bool IsServerAdmin { get; set; }
public bool IsSupportedByCurrency { get; set; }
public bool IsFullySync { get; set; }
public bool IsSegwit { get; set; }
public bool Ok => IsServerAdmin && IsSupportedByCurrency && IsFullySync && IsSegwit;
[Range(1000, 10_000)]
public int BatchSize { get; set; } = 3000;
[Range(0, 10_000_000)]
public int StartingIndex { get; set; } = 0;
[Range(100, 100000)]
public int GapLimit { get; set; } = 10000;
public int? Progress { get; set; }
public string PreviousError { get; set; }
public ScanUTXOProgress LastSuccess { get; internal set; }
public string TimeOfScan { get; internal set; }
public string RemainingTime { get; internal set; }
}
}

@ -15,6 +15,9 @@ namespace BTCPayServer.Models.WalletViewModels
get;
set;
}
public string DefaultAddress { get; set; }
public string DefaultAmount { get; set; }
public decimal? Rate { get; set; }
public int Divisibility { get; set; }
public string Fiat { get; set; }

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models.WalletViewModels
{
public class WalletSendLedgerModel
{
public int FeeSatoshiPerByte { get; set; }
public bool SubstractFees { get; set; }
public decimal Amount { get; set; }
public string Destination { get; set; }
}
}

@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models.WalletViewModels
{
public class WalletSendModel
{
[Required]
public string Destination { get; set; }
[Range(0.0, double.MaxValue)]
[Required]
public decimal? Amount { get; set; }
public decimal CurrentBalance { get; set; }
public string CryptoCode { get; set; }
public int RecommendedSatoshiPerByte { get; set; }
[Display(Name = "Subtract fees from amount")]
public bool SubstractFees { get; set; }
[Range(1, int.MaxValue)]
[Display(Name = "Fee rate (satoshi per byte)")]
[Required]
public int FeeSatoshiPerByte { get; set; }
public decimal? Rate { get; set; }
public int Divisibility { get; set; }
public string Fiat { get; set; }
public string RateError { get; set; }
}
}

@ -27,16 +27,30 @@ namespace BTCPayServer.Payments.Bitcoin
_WalletProvider = walletProvider;
}
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(DerivationStrategy supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network)
class Prepare
{
public Task<FeeRate> GetFeeRate;
public Task<BitcoinAddress> ReserveAddress;
}
public override object PreparePayment(DerivationStrategy supportedPaymentMethod, StoreData store, BTCPayNetwork network)
{
return new Prepare()
{
GetFeeRate = _FeeRateProviderFactory.CreateFeeProvider(network).GetFeeRateAsync(),
ReserveAddress = _WalletProvider.GetWallet(network).ReserveAddressAsync(supportedPaymentMethod.DerivationStrategyBase)
};
}
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(DerivationStrategy supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network, object preparePaymentObject)
{
if (!_ExplorerProvider.IsAvailable(network))
throw new PaymentMethodUnavailableException($"Full node not available");
var getFeeRate = _FeeRateProviderFactory.CreateFeeProvider(network).GetFeeRateAsync();
var getAddress = _WalletProvider.GetWallet(network).ReserveAddressAsync(supportedPaymentMethod.DerivationStrategyBase);
var prepare = (Prepare)preparePaymentObject;
Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod onchainMethod = new Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod();
onchainMethod.FeeRate = await getFeeRate;
onchainMethod.FeeRate = await prepare.GetFeeRate;
onchainMethod.TxFee = onchainMethod.FeeRate.GetFee(100); // assume price for 100 bytes
onchainMethod.DepositAddress = (await getAddress).ToString();
onchainMethod.DepositAddress = (await prepare.ReserveAddress).ToString();
return onchainMethod;
}
}

@ -51,7 +51,7 @@ namespace BTCPayServer.Payments.Bitcoin
}
CompositeDisposable leases = new CompositeDisposable();
ConcurrentDictionary<string, NotificationSession> _SessionsByCryptoCode = new ConcurrentDictionary<string, NotificationSession>();
ConcurrentDictionary<string, WebsocketNotificationSession> _SessionsByCryptoCode = new ConcurrentDictionary<string, WebsocketNotificationSession>();
private Timer _ListenPoller;
TimeSpan _PollInterval;
@ -114,7 +114,7 @@ namespace BTCPayServer.Payments.Bitcoin
return;
if (_Cts.IsCancellationRequested)
return;
var session = await client.CreateNotificationSessionAsync(_Cts.Token).ConfigureAwait(false);
var session = await client.CreateWebsocketNotificationSessionAsync(_Cts.Token).ConfigureAwait(false);
if (!_SessionsByCryptoCode.TryAdd(network.CryptoCode, session))
{
await session.DisposeAsync();
@ -149,8 +149,7 @@ namespace BTCPayServer.Payments.Bitcoin
foreach (var output in evt.Outputs)
{
foreach (var txCoin in evt.TransactionData.Transaction.Outputs.AsCoins()
.Where(o => o.ScriptPubKey == output.ScriptPubKey)
.Select(o => output.Redeem == null ? o : o.ToScriptCoin(output.Redeem)))
.Where(o => o.ScriptPubKey == output.ScriptPubKey))
{
var invoice = await _InvoiceRepository.GetInvoiceFromScriptPubKey(output.ScriptPubKey, network.CryptoCode);
if (invoice != null)
@ -188,7 +187,7 @@ namespace BTCPayServer.Payments.Bitcoin
if (cleanup)
{
Logs.PayServer.LogInformation($"Disconnected from WebSocket of NBXplorer ({network.CryptoCode})");
_SessionsByCryptoCode.TryRemove(network.CryptoCode, out NotificationSession unused);
_SessionsByCryptoCode.TryRemove(network.CryptoCode, out WebsocketNotificationSession unused);
if (_SessionsByCryptoCode.Count == 0 && _Cts.IsCancellationRequested)
{
_RunningTask.TrySetResult(true);

@ -0,0 +1,109 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Payments.Changelly.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using SshNet.Security.Cryptography;
namespace BTCPayServer.Payments.Changelly
{
public class Changelly
{
private readonly string _apisecret;
private readonly bool _showFiat;
private readonly HttpClient _httpClient;
public Changelly(IHttpClientFactory httpClientFactory, string apiKey, string apiSecret, string apiUrl, bool showFiat = true)
{
_apisecret = apiSecret;
_showFiat = showFiat;
_httpClient = httpClientFactory.CreateClient();
_httpClient.BaseAddress = new Uri(apiUrl);
_httpClient.DefaultRequestHeaders.Add("api-key", apiKey);
}
private static string ToHexString(byte[] array)
{
var hex = new StringBuilder(array.Length * 2);
foreach (var b in array)
{
hex.AppendFormat(CultureInfo.InvariantCulture, "{0:x2}", b);
}
return hex.ToString();
}
private async Task<ChangellyResponse<T>> PostToApi<T>(string message)
{
var hmac = new HMACSHA512(Encoding.UTF8.GetBytes(_apisecret));
var hashMessage = hmac.ComputeHash(Encoding.UTF8.GetBytes(message));
var sign = ToHexString(hashMessage);
var request = new HttpRequestMessage(HttpMethod.Post, "");
request.Headers.Add("sign", sign);
request.Content = new StringContent(message, Encoding.UTF8, "application/json");
var result = await _httpClient.SendAsync(request);
if (!result.IsSuccessStatusCode)
throw new ChangellyException(result.ReasonPhrase);
var content =
await result.Content.ReadAsStringAsync();
return JObject.Parse(content).ToObject<ChangellyResponse<T>>();
}
public virtual async Task<IEnumerable<CurrencyFull>> GetCurrenciesFull()
{
const string message = @"{
""jsonrpc"": ""2.0"",
""id"": 1,
""method"": ""getCurrenciesFull"",
""params"": []
}";
var result = await PostToApi<IEnumerable<CurrencyFull>>(message);
var appendedResult = _showFiat
? result.Result.Concat(new[]
{
new CurrencyFull()
{
Enable = true,
Name = "EUR",
FullName = "Euro",
PayInConfirmations = 0,
ImageLink = "https://changelly.com/api/coins/eur.png"
},
new CurrencyFull()
{
Enable = true,
Name = "USD",
FullName = "US Dollar",
PayInConfirmations = 0,
ImageLink = "https://changelly.com/api/coins/usd.png"
}
})
: result.Result;
return appendedResult;
}
public virtual async Task<decimal> GetExchangeAmount(string fromCurrency,
string toCurrency,
decimal amount)
{
var message =
$"{{\"id\": \"test\",\"jsonrpc\": \"2.0\",\"method\": \"getExchangeAmount\",\"params\":{{\"from\": \"{fromCurrency}\",\"to\": \"{toCurrency}\",\"amount\": \"{amount}\"}}}}";
var result = await PostToApi<string>(message);
return Convert.ToDecimal(result.Result, CultureInfo.InvariantCulture);
}
}
}

@ -0,0 +1,16 @@
namespace BTCPayServer.Payments.Changelly
{
public static class ChangellyCalculationHelper
{
public static decimal ComputeBaseAmount(decimal baseRate, decimal toAmount)
{
return (1m / baseRate) * toAmount;
}
public static decimal ComputeCorrectAmount(decimal currentFromAmount, decimal currentAmount,
decimal expectedAmount)
{
return (currentFromAmount / currentAmount) * expectedAmount;
}
}
}

@ -0,0 +1,70 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Services.Stores;
using NBitcoin;
namespace BTCPayServer.Payments.Changelly
{
public class ChangellyClientProvider
{
private readonly StoreRepository _storeRepository;
private readonly IHttpClientFactory _httpClientFactory;
private readonly ConcurrentDictionary<string, Changelly> _clientCache =
new ConcurrentDictionary<string, Changelly>();
public ChangellyClientProvider(StoreRepository storeRepository, IHttpClientFactory httpClientFactory)
{
_storeRepository = storeRepository;
_httpClientFactory = httpClientFactory;
}
public void InvalidateClient(string storeId)
{
if (_clientCache.ContainsKey(storeId))
{
_clientCache.Remove(storeId, out var value);
}
}
public virtual async Task<Changelly> TryGetChangellyClient(string storeId, StoreData storeData = null)
{
if (_clientCache.ContainsKey(storeId))
{
return _clientCache[storeId];
}
if (storeData == null)
{
storeData = await _storeRepository.FindStore(storeId);
if (storeData == null)
{
throw new ChangellyException("Store not found");
}
}
var blob = storeData.GetStoreBlob();
var changellySettings = blob.ChangellySettings;
if (changellySettings == null || !changellySettings.IsConfigured())
{
throw new ChangellyException("Changelly not configured for this store");
}
if (!changellySettings.Enabled)
{
throw new ChangellyException("Changelly not enabled for this store");
}
var changelly = new Changelly(_httpClientFactory, changellySettings.ApiKey, changellySettings.ApiSecret,
changellySettings.ApiUrl, changellySettings.ShowFiat);
_clientCache.AddOrReplace(storeId, changelly);
return changelly;
}
}
}

@ -0,0 +1,11 @@
using System;
namespace BTCPayServer.Payments.Changelly
{
public class ChangellyException : Exception
{
public ChangellyException(string message) : base(message)
{
}
}
}

@ -0,0 +1,21 @@
namespace BTCPayServer.Payments.Changelly
{
public class ChangellySettings
{
public string ApiKey { get; set; }
public string ApiSecret { get; set; }
public string ApiUrl { get; set; }
public bool Enabled { get; set; }
public string ChangellyMerchantId { get; set; }
public decimal AmountMarkupPercentage { get; set; }
public bool ShowFiat { get; set; }
public bool IsConfigured()
{
return
!string.IsNullOrEmpty(ApiKey) ||
!string.IsNullOrEmpty(ApiSecret) ||
!string.IsNullOrEmpty(ApiUrl);
}
}
}

@ -0,0 +1,17 @@
using Newtonsoft.Json;
namespace BTCPayServer.Payments.Changelly.Models
{
public class ChangellyResponse<T>
{
[JsonProperty("jsonrpc")]
public string JsonRPC { get; set; }
[JsonProperty("id")]
public object Id { get; set; }
[JsonProperty("result")]
public T Result { get; set; }
[JsonProperty("error")]
public Error Error { get; set; }
}
}

@ -0,0 +1,18 @@
using Newtonsoft.Json;
namespace BTCPayServer.Payments.Changelly.Models
{
public class CurrencyFull
{
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("fullName")]
public string FullName { get; set; }
[JsonProperty("enabled")]
public bool Enable { get; set; }
[JsonProperty("payinConfirmations")]
public int PayInConfirmations { get; set; }
[JsonProperty("image")]
public string ImageLink { get; set; }
}
}

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