Compare commits

..

260 Commits

Author SHA1 Message Date
c1fe473cc7 remove from state when unreserved 2021-04-09 14:25:49 +02:00
d3923da8e4 pr changes 2021-04-08 13:41:29 +02:00
eac4d54466 add to docs 2021-04-08 13:41:29 +02:00
717836e9e0 style better 2021-04-08 13:41:29 +02:00
8d1ec4b5c0 show bip21 and additional work 2021-04-08 13:41:29 +02:00
1ee5c82b8b wip 2021-04-08 13:41:29 +02:00
c775c3cc78 Allow Payjoin for wallet receive addresses 2021-04-08 13:41:29 +02:00
6473da7114 Merge pull request from bolatovumar/increase-landing-page-copy-contrast
Increase landing page masthead text contrast
2021-04-08 13:38:44 +02:00
bb78ae59d4 Merge pull request from dennisreimann/fix-hot-wallet-docs-link
Fix hot wallet docs link
2021-04-08 12:21:11 +02:00
459f3c4a93 Document and handle Invoice Metadata better ()
* Document and handle Invoice Metadata better

PosData would crash in certain scenarios when created via API. Invoice metadata known fields were not completely documented

* Fix value setter when null

* fix swagger conformity

* make all swagger invoice metadata optional looking in json
2021-04-08 16:42:18 +09:00
e014b30915 Remove on code warning 2021-04-08 14:33:58 +09:00
cc5a388106 Properly escape script inputs for shopify integration 2021-04-08 13:44:51 +09:00
5de93f8cc4 Abstract Store integrations ()
* Decouple Shopify from Store

* Decouple shopify from store blob

* Update BTCPayServer.Tests.csproj

* Make sure shopify obj is set

* make shopify a system plugin
2021-04-08 13:37:05 +09:00
ad1b708da5 Provide more data through OnChain Wallet API ()
Provides unconf/conf balanaces, keypath + address + timestamp of utxos
2021-04-08 12:43:51 +09:00
c7ff36b314 Increase landing page masthead text contrast 2021-04-07 19:37:35 -07:00
f367480857 GreenField: Add FeeRate To Wallets API ()
* GreenField: Add FeeRate To Wallets API

closes 

* make dedicated endpoint for fee rate

* remove unused call
2021-04-07 15:16:17 +09:00
475809b1a0 Make Invoice Create Faster And Fix Gap Limit Issue ()
* Make Invoice Create Faster And Fix Gap Limit Issue

This make address reserve only when user "activate" paymet method in ui. optional setting in store checkout ui.

* Fix swagger documentation around Lazy payment methods

* fix changed code signature

* Add missing GreenField API for activate feature

* Fix checkout experience styling for activate feature

* Fix issue with Checkout activate button

* Make lightning also work with activation

* Make sure PreparePaymentModel is still called on payment handlers even when unactivated

* Make payment  link return empty if not activated

* Add activate payment method method to client and add test

* remove debugger

* add e2e test

* Rearranging lazy payments position in UI to be near dependent Unified QR code

* fix rebase conflicts

* Make lazy payment method mode activate on UI load.

Co-authored-by: Kukks <evilkukka@gmail.com>
Co-authored-by: rockstardev <rockstardev@users.noreply.github.com>
Co-authored-by: Andrew Camilleri <kukks@btcpayserver.org>
2021-04-07 13:08:42 +09:00
fce7fdb3b7 Update link 2021-04-06 16:32:04 +02:00
b2c72f1d75 Merge pull request from bolatovumar/fix/2434
Set new store hints consistently for API and GUI
2021-04-06 14:50:11 +02:00
f0ac7d2c16 Set new store hints consistently for API and GUI
closes 
2021-04-05 21:24:34 -07:00
7099a3b394 Fix hot wallet docs link
# Conflicts:
#	BTCPayServer/Views/Server/Policies.cshtml
2021-04-05 21:29:41 +02:00
3e985d8554 Fix crash when sat is negative (liquid only) () 2021-04-05 12:39:37 +09:00
f94e8c9719 Fix typo in PoS cart view ()
Noticed by @MaxHillebrand 👀
2021-04-05 12:39:02 +09:00
bbe1442c28 Merge pull request from kristapsk/explorer.bc-2.jp
Fix block explorer links for signet
2021-04-03 12:47:01 +02:00
3a248d7707 Fix block explorer links for signet 2021-04-03 09:19:36 +03:00
0bb1f16d2a Merge pull request from g33kme/patch-1
Update swagger.template.stores-wallet.on-chain.json
2021-04-01 13:59:45 +02:00
0837756152 Update swagger.template.stores-wallet.on-chain.json 2021-04-01 13:48:06 +02:00
23236c96cb Remove internal setters in BTCPayNetwork 2021-04-01 08:56:22 +02:00
6ead5c3800 Plugin FailSafe ()
This introduces the concept of plugins being disabled in the case of an unrecoverable runtime error caused by a plugin.
2021-04-01 12:27:22 +09:00
64db865e1e Changelog 2021-04-01 12:16:23 +09:00
1b2399745d Remove stable and latest branch 2021-04-01 12:06:29 +09:00
94acf60100 Properly show browser date 2021-04-01 11:55:43 +09:00
a4298e8c19 Ensure root app mapping works ()
close 
2021-04-01 11:49:46 +09:00
76985838c4 Improve Lightning setup page ()
* Redesign Lightning setup page

* Improve Lightning setup page

Closes .

* Test fix

* Fix LightningNetworkPaymentMethodAPITests

* Bootstrap customization fix
2021-03-31 20:23:36 +09:00
d24964e900 Merge pull request from dennisreimann/ui-improvements
UI: Header and navigation improvements
2021-03-31 16:08:37 +09:00
f4fde8f5f7 Merge pull request from btcpayserver/feat/lnd-v0.12.1-beta
Bumping LND to v0.12.1
2021-03-31 12:35:39 +09:00
3461dd6464 Changelog for 1.0.7.1 2021-03-30 23:54:43 +09:00
560671b57f Remove autocorrect and autocapitalize for seed input 2021-03-30 19:21:29 +09:00
abf3962d91 Remove old styles 2021-03-30 11:38:04 +02:00
f53a85fcd1 Bootstrap customization fix 2021-03-30 11:38:03 +02:00
73730355b8 Add active indicator for main navigation 2021-03-30 11:38:02 +02:00
8827721605 Layout: Update header and navigation 2021-03-30 11:38:01 +02:00
2497413c60 Fix test 2021-03-30 17:15:03 +09:00
a6537ef282 Update lightning charge 2021-03-30 17:08:58 +09:00
c92adc36c6 Remove useless file 2021-03-30 15:23:36 +09:00
a64f71f186 Add user email search and sort ()
* Add user sort by email

* Add user search by email
2021-03-30 14:32:44 +09:00
0c766a2714 Merge pull request from bumbummen99/patch-3
Fix payment request template
2021-03-30 14:30:56 +09:00
6b7c49431a Merge pull request from AlexGidge/1725-pull-payments-display-date
Changed display date format on View Pull Payments screen
2021-03-30 14:29:46 +09:00
4b37121b75 Updated "required" form input styling () 2021-03-30 14:27:42 +09:00
120c7b9730 Ensure submitting empty currency does not break update PoS page () 2021-03-30 14:26:33 +09:00
1aa233ca47 Order files list by descending timestamp ()
closes 
2021-03-30 14:26:02 +09:00
14e6e492dd Refactor domain mapping () 2021-03-30 11:47:03 +09:00
17bdf55bcc Remove misleading title from hint icon ()
Brought up by @Zaxounette [on Mattermost](https://chat.btcpayserver.org/btcpayserver/pl/yrxqodfswbby5qj69zhpojuefc).
2021-03-30 11:18:24 +09:00
1ae6508a43 Make dates/timespan swagger docs more clear ()
* Make dates/tiemspan swagger docs more clear

* fix schema conformity
2021-03-30 11:18:00 +09:00
b7b6cef880 Fix ratelimiter for forgotpassword 2021-03-28 20:56:46 +09:00
52b5c56da9 Bumping LND to use the latest docker image 2021-03-26 08:52:17 -05:00
85ba9e96a0 Rate limit password forgot 2021-03-26 18:01:59 +09:00
b4e15cb27f Fix Pay-Button url preview ()
* Fix 

Adresses  by creating a new element and setting the url as text rather as html.

* Fix closing tag
2021-03-26 16:37:15 +09:00
068cfe5e3e Decrease command timeout for selenium 2021-03-26 14:31:36 +09:00
c0449a633d Revert "Fix docker-compose for arm64 dev env"
This reverts commit 7dfce5e3061d5e0ada87ede81960f0a50d86615d.
2021-03-26 13:22:54 +09:00
7dfce5e306 Fix docker-compose for arm64 dev env 2021-03-26 12:44:55 +09:00
fd53f7476e Fix flaky test () 2021-03-24 21:33:49 +09:00
ceb541ad8a Upgrade to Bootstrap v4.6 ()
Also upgrades jQuery from v3.2.1 to v3.6.0
2021-03-24 18:47:55 +09:00
d92ced6c6b Make CanUsePayjoin2 more resilient 2021-03-24 14:18:54 +09:00
0847088391 Make CanUsePayjoin2 more resilient 2021-03-24 13:55:46 +09:00
a128685b83 If an input already used in a payjoin is reused in another, we should not attempt to broadcast the original transaction. 2021-03-24 13:48:33 +09:00
749d26fafa Revert "Fix Payjoin test randomly crashing"
This reverts commit 485faf014183321420da8ae7babf5eb9e2668034.
2021-03-23 23:59:26 +09:00
485faf0141 Fix Payjoin test randomly crashing 2021-03-23 18:34:34 +09:00
af9d896510 Do not use Random 2021-03-23 17:53:23 +09:00
1fc114fec7 Check the authentication cookie every 5 min rather than 30min 2021-03-23 17:43:13 +09:00
e6938cef6f Merge pull request from pavlenex/supportercc
Add new supporter to readme
2021-03-22 17:17:42 +01:00
ffafd291ee replace the svg with an improved one 2021-03-22 17:05:46 +01:00
f532759543 add Coincards supporter to readme 2021-03-22 11:28:04 +01:00
db3ba6db3c Merge pull request from AryanJ-NYC/fix-swag-docs
fix swagger docs missing query param
2021-03-21 16:44:06 +01:00
0a333f8476 Merge pull request from bolatovumar/fix/2382
Update Swagger for "/api/v1/stores/{storeId}/payment-methods/LightningNetwork"
2021-03-21 10:50:11 +01:00
738aaeed12 Update Swagger for "/api/v1/stores/{storeId}/payment-methods/LightningNetwork" 2021-03-20 19:53:03 -07:00
ce6c9c91fc Make sure payment method uppercase logic only happens for BTC 2021-03-20 06:52:18 +01:00
7035b71ccd Fix POS item newline break ()
* Fix POS item newline break

fixes 

* Update TemplateEditor.cshtml

* fix template editor with "

* apply sanitize on save
2021-03-19 23:25:04 +09:00
923a567822 Make cookies secure 2021-03-19 20:54:30 +09:00
9b24e9378f Explicitely disallow \ for in filename 2021-03-19 20:22:24 +09:00
779f21a1ca Make sure cookie are HttpOnly 2021-03-19 20:09:55 +09:00
73d70aa5e5 Better validate file names 2021-03-19 18:55:21 +09:00
fc78eacf8f Merge pull request from btcpayserver/addbettermenu
Make main menu show text instead of icons when on small screens
2021-03-18 19:16:21 +01:00
006af636e6 Merge pull request from nosovk/patch-1
turn of autocomplete for PrivKey input
2021-03-16 17:29:25 +01:00
4575fda10a turn of autocomplete for "BIP39 Seed (12/24 word mnemonic phrase) or HD private key" input
autocomplete not pretend to be safe store for your wallet key
2021-03-16 16:05:16 +02:00
6c960628c2 Update pull payment template 2021-03-15 13:27:14 +01:00
00aa7deaae Fix payment request template
Fix payment request template body/page height and footer style.
2021-03-15 13:25:20 +01:00
f722956864 Merge pull request from britttttk/improve-sign-seed-copy
Fix typos on wallet sign with seed page
2021-03-15 09:14:54 +01:00
8f520bff12 Merge pull request from btcpayserver/fix/selenium-tests
Attempting selenium test fix for CanUseLightningSatsFeature
2021-03-14 18:57:41 -05:00
8398534fa0 Attempting selenium test fix for CanUseLightningSatsFeature
Swithcing to using WaitForElement and simplifying finding of alert message
2021-03-14 18:43:16 -05:00
e1fed90b71 bump versions 2021-03-11 22:48:40 +09:00
5ba6e53379 Changelog for 1.0.7.0 2021-03-11 22:45:47 +09:00
d33bdfd50c Make CanUseCoinSelection less flaky 2021-03-11 22:20:25 +09:00
4f5392eb74 Fix test 2021-03-11 22:18:02 +09:00
064087a7c0 fix test 2021-03-11 22:08:36 +09:00
e13821ba49 Fix view-seed 2021-03-11 21:58:20 +09:00
c2b85779c3 Rewrite the CanUseHotWallet, check if the derivationscheme is actually a hotwallet, before retrieving the seed 2021-03-11 21:46:32 +09:00
cdfdad3e3d GreenField API: Wallet API ()
* GreenField: Wallet API

* more work

* wip

* rough fiunish of transaction sending api

* Allow to create tx without broadcasting and small fixes

* Refactor Wallet Receive feature ad add greenfield api for address reserve for wallet

* add wallet api client

* add docs

* fix json converter tags

* fixes and add wallet tests

* fix tests

* fix rebase

* fixes

* just pass the tests already

* ugggh

* small cleanup

* revert int support in numeric string converter and make block id as native number in json

* fix LN endpoint

* try fix flaky test

* Revert "try fix flaky test"

This reverts commit 2e0d256325b892f7741325dcbab01196f74d182a.

* try fix other flaky test

* return proepr error if fee rate could not be fetched

* try fix test again

* reduce fee related logic for wallet api

* try reduce code changes for pr scope

* change auth logic for initial release of wallet api
2021-03-11 21:34:52 +09:00
1f7992e5da Remove some code duplication 2021-03-11 21:29:13 +09:00
4bad7d7c52 Fix issue with store payment methods after having payment method that is no longer supported () 2021-03-10 19:51:51 +09:00
1fcf39d4ab Fix new store incorrectly showing lightning enabled even if not 2021-03-09 17:56:17 +09:00
a64e304d16 Fix new store showing incorrectly being paired to internal node 2021-03-09 17:54:38 +09:00
30c7cbba96 Fix issue around new bech uppercase + vault supported flag () 2021-03-09 12:45:56 +09:00
a7d324901d Reverted "Last Refreshed" back to "Last Updated"
Co-authored-by: Zaxounette <51208677+Zaxounette@users.noreply.github.com>
2021-03-08 08:45:16 +00:00
15c87dc0b6 Fix typos on wallet sign with seed page 2021-03-07 23:30:40 -07:00
0d2de4c421 - Changed to display in user's local date time format 2021-03-07 23:19:39 +00:00
da95bd6127 - Added start date & changed display timezone 2021-03-07 22:51:50 +00:00
e31b5529b0 Fixed a typo in README.md. () 2021-03-07 20:54:36 +09:00
0836df6974 Update invoice log row styling ()
closes 
2021-03-07 20:50:55 +09:00
de4cd55adf Update coingecko exchanges 2021-03-06 13:58:02 +09:00
6b156f2144 Reenabling uppercase BECH32 in QR codes ()
* Reenabling uppercase BECH32 in QR codes

* Fixing the test now that we're uppercasing BECH32 address

* Implementing Nicolas' feedback

Co-authored-by: rockstardev <rockstardev@users.noreply.github.com>
2021-03-06 13:52:25 +09:00
2b1efd9347 Merge pull request from dennisreimann/wallet-setup-finetuning
Wallet setup finetuning
2021-03-06 13:51:01 +09:00
97dd10edc0 Fix some NRE in the FileService 2021-03-06 13:36:36 +09:00
cc4e46d212 Fix NRE if invoice not found 2021-03-06 13:25:40 +09:00
dc0671942d Make main menu show text instead of icons when on small screens 2021-03-05 08:48:15 +01:00
f79c8ab641 Xpub import: Toggle multi-sig examples 2021-03-04 11:07:05 +01:00
314fda7877 Update import wallet cells 2021-03-03 22:27:39 +01:00
aaf77515fc Update icons 2021-03-03 21:29:03 +01:00
b5f7b1aad4 Hover and active states for wizard navigation 2021-03-03 21:17:25 +01:00
d36974a47e Fix test 2021-03-02 12:26:19 +01:00
5bd16f990c Add replace confirmation; distinguish wallet types 2021-03-02 12:26:18 +01:00
28d7924077 Fix AltcoinTests 2021-03-02 12:26:17 +01:00
89ecba961c Remove old AddDerivationSchemes views 2021-03-02 12:26:16 +01:00
3481a5fd19 Fix wording 2021-03-02 12:26:15 +01:00
70a21c5136 Refactoring: Move checking condition up 2021-03-02 12:26:15 +01:00
2e2c9764f3 Remove old Stores.BTCLike controller 2021-03-02 12:26:14 +01:00
bd447b6c79 Fix test 2021-03-02 11:50:01 +09:00
4f6ec3aa32 Merge pull request from btcpayserver/fix/nextnetworkfee-null
Returning 0 for NextNetworkFee if it's null
2021-03-02 11:19:43 +09:00
e37b3179ba Reactivate CanGetRateCryptoCurrenciesByDefault for DSH 2021-03-02 11:14:52 +09:00
808214f973 Fix Rates Test (Dash rate source switched to bitfinex) ()
fixes 
2021-03-02 11:13:05 +09:00
7e714f1ef8 Refactor how we handle and validate LN ConnectionStrings ()
* Refactor how we handle and validate LN ConnectionStrings

* Migrate existing connection string to Internal Node if they are the same. Cleanup some obsolete fields

* Fix typos, remove duplicated method

* Add a InternalNodeRef to LightningSupportedPaymentMethod
2021-03-02 11:11:58 +09:00
e65e46f664 NextNetworkFee is not directly initialized, falling back to null check 2021-03-01 09:56:57 -06:00
5e7eb6635f Initializing NextNetworkFee values if GetFee returns null 2021-03-01 09:33:02 -06:00
49ae62b02e Use library for Payjoin Sender ()
* Use library for Payjoin Sender

* update payjoin sender to use new package and reduce code

* fix using statements
2021-03-01 22:44:53 +09:00
c9cfe5cc6e Fix direct URL for local storage with custom root path ()
* Fix direct URL for local storage with custom root path

* Remove "Context.Request.PathBase" when generating file URL display string
2021-03-01 22:43:57 +09:00
e8df010449 Add redirectAutomatically to GreenField Invoice API () 2021-03-01 22:34:07 +09:00
949136b161 GreenField API: Configure Store Lightning Payment Method v2 ()
* GreenField API: Configure Store Lightning Payment Method

* Remove internal ln node endpoint and use Auth service to check internal node usage

* fix test
2021-02-26 11:58:51 +09:00
1e00c63146 Merge pull request from dennisreimann/policies-ui
Improve policies structure and wordings
2021-02-26 11:49:00 +09:00
6d9b93a407 Merge pull request from dennisreimann/pos-item-button-text
PoS: Custom buy button text per product
2021-02-26 11:45:44 +09:00
37c39ad587 Merge pull request from btcpayserver/invoice-status-marker
Allow invoice to be marked even when new
2021-02-26 11:26:09 +09:00
e3e65878aa Allow invoice to be marked even when new
fixes 
2021-02-26 11:17:03 +09:00
6843b0eaab Merge pull request from btcpayserver/gf/paymenttypeparse
Make payment type parsing more dynamic
2021-02-26 11:00:27 +09:00
e25f76753a Update BTCPayServer.Tests/README.md
Co-authored-by: Max Hillebrand <30683012+MaxHillebrand@users.noreply.github.com>
2021-02-24 17:16:27 +01:00
75c2fabd7f Make Selenium test more robust
Fixes an issue similar to what we fixed in .
2021-02-24 15:10:05 +01:00
35aeb19fcd Controller cleanups 2021-02-24 15:10:04 +01:00
5a00f6a4fc Fix missing view name 2021-02-24 15:10:03 +01:00
3a9fc52b8c Add "vertical-align: middle;" to pay button image CSS ()
closes 
2021-02-24 15:10:02 +01:00
64a8de938b Update BTCPayServer.Tests/README.md
Co-authored-by: Pavlenex <pavle@pavle.org>
2021-02-24 15:09:45 +01:00
605cf407a8 Update README.md
Update README.md 
2021-02-24 15:06:16 +01:00
9ed5297e91 Update README.md
Using Polar to test Lightning payments.
2021-02-24 15:06:15 +01:00
07da404a23 Merge pull request from dennisreimann/webhooks-controller
Webhooks controller fix and cleanup
2021-02-24 22:03:01 +09:00
21467ef65d Add "vertical-align: middle;" to pay button image CSS ()
closes 
2021-02-24 22:01:05 +09:00
4d5b2c4033 Make Selenium test more robust
Fixes an issue similar to what we fixed in .
2021-02-23 16:57:21 +01:00
5e7836b293 Controller cleanups 2021-02-23 16:51:58 +01:00
a6fe61d508 Fix missing view name 2021-02-23 16:51:11 +01:00
c0aa320f0a Improve policies structure and wordings
Closes .
2021-02-23 10:39:26 +01:00
4bcc18fb41 JavaScript formatting fixes 2021-02-23 09:51:25 +01:00
32370545cb Fix variable assignment in yaml parsing loop 2021-02-23 09:50:54 +01:00
2fd8c831c0 Merge pull request from radWorx/dev-with-polar
Update README.md
2021-02-22 10:22:16 +01:00
5b4877c402 PoS: Custom buy button text for custom price 2021-02-22 08:59:59 +01:00
71c11b34f4 Make payment type parsing more dynamic
fixes bug described in 
2021-02-19 08:23:55 +01:00
3123718166 Crowdfund: Add custom buy button text 2021-02-18 12:32:43 +01:00
dfbec71906 PoS: Add test for custom buy button text 2021-02-18 12:20:27 +01:00
757c087afd PoS: Custom buy button text per product
Closes .
2021-02-18 10:54:06 +01:00
b30aa968b0 Merge pull request from dennisreimann/flaky-selenium
Tame flaky Selenium tests
2021-02-18 09:39:28 +01:00
1e902c8dee Tests: Toggle advanced settings via JS instead 2021-02-17 15:54:05 +01:00
db5f64432e Tests: Wait for advanced settings closing animation 2021-02-17 12:34:58 +01:00
6c9c463da9 Fix plugin projects reference in solution ()
Fixes .
2021-02-17 10:56:48 +01:00
c73878ccca Fix missing hot wallet option on seed import ()
* Fix missing hot wallet option on seed import

Thanks @kukks for spotting!

* Tests: Wait for button to be ready for interaction

* Camelcase test selectors

* Tests: Remove general ImplicitWait

* Tests: Add WaitForAndClick helper

* Tests: Refactor SetCheckbox

* Tests: Add WaitForElement helper

* Tests: Refactor and use wait.UntilJsIsReady helper

* Fix missing helper in ethereum tests

* Tests: Some more refactorings
2021-02-17 12:14:29 +09:00
41e453306d Merge pull request from bumbummen99/patch-4
Fix view payment request loading spinner alignment
2021-02-17 12:10:18 +09:00
626c6007fd Merge pull request from btcpayserver/remove-upload-limit
Remove Max body request size
2021-02-16 10:40:43 +09:00
deb88032cb Update README.md
added missing space,
moved below Using the test lightning-cli
2021-02-15 15:15:38 -05:00
eca317b3c4 Fix view payment request loading spinner alignment 2021-02-15 16:37:38 +01:00
9300326483 Remove Max body request size
Upload limit by kestrel restricts plugin upload of 30mb+. this removes that limit
2021-02-15 13:42:08 +01:00
ebc46eb7d2 fix swagger docs missing query param
fixes 
2021-02-15 10:05:54 +01:00
cb83669802 Merge pull request from bumbummen99/patch-2
Fix cart pay button loading spinner vertical alignment
2021-02-15 11:17:57 +09:00
f6f616a21d Fix cart pay button loading spinner vertical alignment 2021-02-14 21:05:26 +01:00
62d50c0189 Profile email change should check email's availability () 2021-02-12 16:48:43 +09:00
dd5b143c13 Update BTCPayServer/Controllers/ManageController.cs
Co-authored-by: d11n <mail@dennisreimann.de>
2021-02-12 16:48:26 +09:00
2e864c32fa Profile email change should check email's availability 2021-02-12 12:48:05 +09:00
f4fa7c927c Wallet setup redesign ()
* Prepare existing layouts and views

* Add icon view component and sprite svg

* Add wallet setup basics

* Add import method view basics

* Use external sprite file instead of inline svg

* Refactor hardware wallet setup flow

* Manually enter an xpub

* Prepare other views

* Update views and models

* Finalize wallet setup flow

* Updat tests, part 1

* Update tests, part 2

* Vaul: Fix missing retry button

* Add better Scan QR subtext

Still tbd.

* Make wallet account an advanced setting

* Prevent empty xpub

* Use textarea for seed input

* Remove redundant error message for missing file upload

* Confirm store updates after generating a new wallet

* Update wording

* Modify existing wallets

* Fix proposed method name

* Suggest using ColdCard Electrum export option only

Advise the user to use the electrum export of the coldcard instead of saying either electrum or wasabi export file … the electurm one contains more info, e.g. the wasabi one doesn't include the account key path.

* More concise WalletSetupMethod setting

* Test fix

* Update wallet removal code

* Fix back navigation quirk in change wallet case

* Fix behaviour on wallet enable/disable

* Fix initial wallet setup

* Improve modify view and messages

* Test fixes

* Seed import fix

Uses the correct form url for confirming addresses

* Quickfixes from design meeting

* Add enable toggle switch on modify page

* Confirm wallet removal

* Update setup view

* Update import view

* Icon finetuning

* Improve import options page

* Refactor QR code scanner

Allow for usage with and without modal

* Update copy and instructions on import pages

* Split generate options: Hot wallet and watch-only

* Implement hot wallet options correctly

* Minor test changes

* Navbar improvements

* Fix tables

* Fix badge color

* Routing related updates

Thanks @kukks for the suggestions!

* Wording updates

Thanks @kukks for the suggestions!

* Extend address types table for xpub import

Thanks @kukks for the suggestions!

* Rename controller

* Unify precondition checks

* Improve removal warning for hot wallets

* Add tooltip on why seed import is not recommended

* Add tooltip icon

* Add Specter import info
2021-02-11 19:48:54 +09:00
3736cbc107 Merge pull request from dennisreimann/specter-wallet-file
Add Specter wallet file import
2021-02-10 23:02:26 +09:00
776825cc66 Merge pull request from dennisreimann/onchain-symbol
Invoices list: Remove icon indicator for onchain
2021-02-10 22:48:34 +09:00
5cb647e57f Merge pull request from bolatovumar/fix-2247
Ensure "No" selection is maintained for custom price in POS app
2021-02-10 22:42:55 +09:00
9c5f826bb4 Merge pull request from bumbummen99/patch-1
Fix current version
2021-02-10 22:35:34 +09:00
6b1803629d Merge pull request from dennisreimann/tabindex
Login: Improve tab navigation for input fields
2021-02-10 22:35:09 +09:00
5cea0571e3 Merge pull request from dennisreimann/checkout-tabs
Checkout: Fix scan/copy tab sizes with varying content
2021-02-10 22:34:24 +09:00
95c8afd5ba Merge pull request from ketominer/fix-mysql-2021
fixed mysql support
2021-02-10 18:26:20 +09:00
ecc9a34359 Checkout: Fix scan/copy tab sizes with varying content
Fixes .
2021-02-08 17:54:40 +01:00
1fed7fb5af fix for sqlite 2021-02-07 21:14:31 +01:00
93d1ded4c2 fixed mysql support 2021-02-07 20:57:48 +01:00
f199775437 Register: Autofocus email field on page load 2021-02-05 13:04:21 +01:00
40271f420d Login: Improve tab navigation for input fields
Improves the tab indexes so that keyboard navigation goes: Email > Password > Sign in > Create account > Forgot password.

Also autofocuses the email field so that you can start typing right away.

Closes .
2021-02-05 13:01:55 +01:00
9e71c02eb9 Merge pull request from pavlenex/readme-improvements
Readme overhaul
2021-02-05 11:29:16 +01:00
c9cbfe630d Fix current version 2021-02-05 03:02:15 +01:00
bf9331b147 Merge pull request from dennisreimann/api-lightning
Fix empty GetLightningClient return value
2021-02-04 11:26:15 +09:00
f223d2e00c Add Specter wallet file import
Closes .
2021-02-03 16:36:45 +01:00
5246e7f035 Fix empty GetLightningClient return value 2021-02-03 11:19:35 +01:00
42de0803c9 Ensure "No" selection is maintained for custom price in POS app 2021-02-02 20:13:39 -08:00
242f9c6197 Merge pull request from btcpayserver/feat/lnd-0.12.0-beta
Updating development LND to v0.12.0-beta
2021-02-02 12:56:29 +09:00
5288198474 Updating connection to merchant_lnd for tests 2021-02-01 01:37:58 -06:00
b21353c4f9 Updating LND in altcoins docker file as well 2021-02-01 01:37:12 -06:00
65a0c6a4b3 Updating development LND to v0.12.0-beta 2021-02-01 00:55:13 -06:00
af5bd89f34 Invoices list: Remove icon indicator for onchain
As [discussed on the chat](https://chat.btcpayserver.org/btcpayserver/pl/tcjy5ornhbd8i8jm4yj9y3maie) the icon feels unnecessary and isn't clear to users. Leaving it off and only indicating Lightning transactions avoids confusion.
2021-01-30 18:53:21 +01:00
73aeffd13c Apply suggestions from code review
Co-authored-by: britttttk <39231115+britttttk@users.noreply.github.com>
2021-01-30 10:58:48 +01:00
03d2f6c017 Merge pull request from btcpayserver/invoice-metadata-test
Improve test on Greenfield Inoice metadata update
2021-01-30 11:17:11 +09:00
7e1481c43f Apply suggestions from code review
Co-authored-by: Zaxounette <51208677+Zaxounette@users.noreply.github.com>
2021-01-29 18:31:41 +01:00
8a1069bf70 Apply suggestions from code review
Co-authored-by: Zaxounette <51208677+Zaxounette@users.noreply.github.com>
2021-01-29 18:30:57 +01:00
6097ab5d12 Update README.md
Co-authored-by: d11n <mail@dennisreimann.de>
2021-01-29 14:04:35 +01:00
d951575f80 Merge branch 'readme-improvements' of https://github.com/pavlenex/btcpayserver into readme-improvements 2021-01-29 12:38:47 +01:00
d887546e58 remove chat 2021-01-29 12:37:59 +01:00
ec3b80cec9 Apply suggestions from code review
Co-authored-by: Andrew Camilleri <evilkukka@gmail.com>
2021-01-29 12:36:12 +01:00
2ef442cf83 Improve readme 2021-01-29 12:19:51 +01:00
6e5a4a7546 Update changelog.md 2021-01-29 18:36:26 +09:00
e0d46002cb Changelog 1.0.6.8 2021-01-29 18:31:40 +09:00
739f13b7a3 Merge pull request from dennisreimann/safe-browsing
Safe browsing quick fixes
2021-01-29 18:24:11 +09:00
34af74b288 Apply URL changes from code review
Co-authored-by: Andrew Camilleri <evilkukka@gmail.com>

Update BTCPayServer/Controllers/AccountController.cs

Co-authored-by: Andrew Camilleri <evilkukka@gmail.com>

Update BTCPayServer/Controllers/AccountController.cs

Co-authored-by: Andrew Camilleri <evilkukka@gmail.com>

Update BTCPayServer/Controllers/AccountController.cs

Co-authored-by: Andrew Camilleri <evilkukka@gmail.com>

Update BTCPayServer/Controllers/AccountController.cs

Co-authored-by: Andrew Camilleri <evilkukka@gmail.com>

Update BTCPayServer/Controllers/AccountController.cs

Co-authored-by: Andrew Camilleri <evilkukka@gmail.com>

Update BTCPayServer/Controllers/AccountController.cs

Co-authored-by: Andrew Camilleri <evilkukka@gmail.com>
2021-01-28 12:33:12 +01:00
4ee0cc8145 Remove clipboard code from main-bundle
See https://github.com/btcpayserver/btcpayserver/issues/2139#issuecomment-768216462
2021-01-28 10:48:06 +01:00
4984674b1d Remove Tor URL from login and register page
Brave and Tor Browser now show availaibility of Tor automatically in the url box of the browser.
2021-01-28 10:47:28 +01:00
f5ee67aafb Remove allowtransparency from checkout overlay 2021-01-28 10:34:34 +01:00
98a1b0da71 Update public account URLs
- /Account/Login -> /login
- /Account/Register -> /register
- /Account/ForgotPassword -> /forgot-password
2021-01-28 10:08:22 +01:00
074ff76d49 Merge pull request from dennisreimann/selenium-tests
Selenium tests: Remove hacks, make them more reliable
2021-01-27 17:50:57 +09:00
b75409a6bf Remove Firefox as option for Selenium tests 2021-01-27 09:35:14 +01:00
94abda6e3e bump lightning libs 2021-01-27 17:21:18 +09:00
68419a9510 Improve password reset email copy ()
* Reset password email copy unified and updated

* Grammar fix for reset email copy

* Email strings added to facilitate html and style and call to action

* First pass html. Saving for later

* Compacted the html and inline styles and removed logo

Co-authored-by: Salie Hendricks <salie@safarinow.com>
2021-01-27 08:42:47 +01:00
db0854f203 Update NBitcoin, NetworkType => ChainName, signet support () 2021-01-27 14:39:38 +09:00
994301ea4c Bump Bitcoin Core and NBXPlorer () 2021-01-26 21:01:32 +09:00
5e344b9be7 Fix missing user secrets code 2021-01-26 10:22:17 +01:00
66d3da8ac9 Remove function checks as they didn't help 2021-01-26 09:40:31 +01:00
71bb876c9e Configure browser settings with user secrets
Default to headless Chrome
2021-01-25 17:23:08 +01:00
1e029f3290 Fix EthereumTests 2021-01-25 16:49:20 +01:00
7926b689fd Default to headless, add Firefox, update Chrome 2021-01-25 15:01:53 +01:00
4638f781f9 Fix JS error 2021-01-25 14:11:11 +01:00
8bea1505dd Cleanups, remove WaitForPageLoad hack 2021-01-25 14:10:40 +01:00
e567f7a80c Refactor tests
Remove the hacky `ScrollTo`, `ForceClick` and `WaitForElement`.
Add the hacky `WaitForPageLoad`.
2021-01-25 13:04:58 +01:00
3774e8dc51 Merge pull request from btcpayserver/feat/new-error-pages
New Error Pages
2021-01-24 23:49:36 -06:00
dc27ffa6ba Http error page 406 for our dear man Jack
Yes, acceptable
2021-01-24 13:41:30 -06:00
ca25eedfbc Extracting common layout for Error Pages 2021-01-24 13:33:34 -06:00
1627c05224 Fixing typos and grammar
Co-authored-by: Umar Bolatov <bolatovumar@gmail.com>
2021-01-24 13:19:26 -06:00
f65ca04507 Http error 502, Miles page 2021-01-24 13:19:26 -06:00
a662b6ef6a Http error 417, page for Pavlenex 2021-01-24 13:19:26 -06:00
bd6d38b3b0 Tests: Move extension method 2021-01-22 12:57:46 +01:00
a890d5300b Update BTCPayServer.Tests/README.md
Co-authored-by: Pavlenex <pavle@pavle.org>
2021-01-19 13:30:45 -05:00
eb8411611a Update README.md
Update README.md 
2021-01-19 13:07:19 -05:00
9360ddf294 Merge branch 'dev-with-polar' of https://github.com/radworx/btcpayserver into dev-with-polar 2021-01-19 13:04:32 -05:00
e3c138fa98 Update README.md
Update README.md 
2021-01-19 13:02:26 -05:00
7147dadb2a Remove dependency to DBriize 2021-01-19 17:19:32 +09:00
8d03738a50 Update BTCPayServer.Tests/README.md
Co-authored-by: britttttk <39231115+britttttk@users.noreply.github.com>
2021-01-18 22:52:21 -05:00
e5540ee79f Update BTCPayServer.Tests/README.md
Co-authored-by: britttttk <39231115+britttttk@users.noreply.github.com>
2021-01-18 22:52:09 -05:00
2ccbb6d6a7 Update BTCPayServer.Tests/README.md
Co-authored-by: britttttk <39231115+britttttk@users.noreply.github.com>
2021-01-18 22:51:59 -05:00
3dce7e7e9f Update README.md
Using Polar to test Lightning payments.
2021-01-18 22:15:52 -05:00
79e8ce9226 Improve test on Greenfield Inoice metadata update 2021-01-18 14:24:38 +01:00
89857ca77c Merge pull request from btcpayserver/fix/error-pages
Improving styling of error pages in preparation for adding new ones
2021-01-17 10:10:02 -06:00
5ce60be78a Update BTCPayServer/Views/Shared/_BTCPaySupporters.cshtml
Co-authored-by: d11n <mail@dennisreimann.de>
2021-01-17 10:09:50 -06:00
600cc20423 changelog 2021-01-17 21:50:02 +09:00
987d09041d bump 2021-01-17 21:45:54 +09:00
df52d01a1d Revert "GreenField API: Configure Store Lightning Payment Method"
This reverts commit b40095f6039dbce20ee92b2da8cd4530c7b8b486.
2021-01-17 21:40:16 +09:00
07de4af581 Revert "Apply suggestions from code review"
This reverts commit 48b2e682bf758ad76b8a50ddcc4473a19f5b0836.
2021-01-17 21:39:20 +09:00
9acb8c2ba2 Restoring project name in partial 2021-01-16 16:26:35 -06:00
307f7b6bd7 Improving styling of error pages in preparation for adding new ones 2021-01-16 14:55:31 -06:00
317 changed files with 26701 additions and 20301 deletions
BTCPayServer.Abstractions/Contracts
BTCPayServer.Client
BTCPayServer.Common
BTCPayServer.Data/Migrations
BTCPayServer.Rating
BTCPayServer.Tests
BTCPayServer
BTCPayServer.csproj
Components/NotificationsDropdown
Configuration
Controllers
Data
DerivationSchemeSettings.csExtensions.cs
Extensions
Filters
HostedServices
Hosting
Models
Payments
Plugins
Program.cs
Security/Bitpay
Services
Storage
Views
Account
Apps
AppsPublic
Error
Home
Invoice
Manage
Notifications
PaymentRequest
PullPayment
Server
Shared
Shopify
Stores
UserStores
ViewsRazor.cs
Wallets
ZoneLimits.csbundleconfig.json
wwwroot
Build
Changelog.mdREADME.mdbtcpayserver.slnpublish-docker.ps1

@ -1,3 +1,4 @@
#nullable enable
using System.Threading;
using System.Threading.Tasks;
@ -5,8 +6,8 @@ namespace BTCPayServer.Abstractions.Contracts
{
public interface ISettingsRepository
{
Task<T> GetSettingAsync<T>(string name = null);
Task UpdateSetting<T>(T obj, string name = null);
Task<T> WaitSettingsChanged<T>(CancellationToken cancellationToken = default);
Task<T?> GetSettingAsync<T>(string? name = null) where T : class;
Task UpdateSetting<T>(T obj, string? name = null) where T : class;
Task<T> WaitSettingsChanged<T>(CancellationToken cancellationToken = default) where T : class;
}
}

@ -13,7 +13,7 @@
<RepositoryType>git</RepositoryType>
</PropertyGroup>
<PropertyGroup>
<Version Condition=" '$(Version)' == '' ">1.2.0</Version>
<Version Condition=" '$(Version)' == '' ">1.3.0</Version>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<PublishRepositoryUrl>true</PublishRepositoryUrl>
@ -27,7 +27,7 @@
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="NBitcoin" Version="5.0.68" />
<PackageReference Include="NBitcoin" Version="5.0.73" />
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.2.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
</ItemGroup>

@ -85,5 +85,13 @@ namespace BTCPayServer.Client
method: HttpMethod.Post), token);
return await HandleResponse<InvoiceData>(response);
}
public virtual async Task ActivateInvoicePaymentMethod(string storeId, string invoiceId, string paymentMethod, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/invoices/{invoiceId}/payment-methods/{paymentMethod}/activate",
method: HttpMethod.Post), token);
await HandleResponse(response);
}
}
}

@ -0,0 +1,122 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
using NBitcoin;
namespace BTCPayServer.Client
{
public partial class BTCPayServerClient
{
public virtual async Task<OnChainWalletOverviewData> ShowOnChainWalletOverview(string storeId, string cryptoCode,
CancellationToken token = default)
{
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/wallet"), token);
return await HandleResponse<OnChainWalletOverviewData>(response);
}
public virtual async Task<OnChainWalletFeeRateData> GetOnChainFeeRate(string storeId, string cryptoCode, int? blockTarget = null,
CancellationToken token = default)
{
Dictionary<string, object> queryParams = new Dictionary<string, object>();
if (blockTarget != null)
{
queryParams.Add("blockTarget",blockTarget);
}
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/wallet/feeRate", queryParams), token);
return await HandleResponse<OnChainWalletFeeRateData>(response);
}
public virtual async Task<OnChainWalletAddressData> GetOnChainWalletReceiveAddress(string storeId, string cryptoCode, bool forceGenerate = false,
CancellationToken token = default)
{
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/wallet/address", new Dictionary<string, object>()
{
{"forceGenerate", forceGenerate}
}), token);
return await HandleResponse<OnChainWalletAddressData>(response);
}
public virtual async Task UnReserveOnChainWalletReceiveAddress(string storeId, string cryptoCode,
CancellationToken token = default)
{
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/wallet/address",method:HttpMethod.Delete), token);
await HandleResponse(response);
}
public virtual async Task<IEnumerable<OnChainWalletTransactionData>> ShowOnChainWalletTransactions(
string storeId, string cryptoCode, TransactionStatus[] statusFilter = null,
CancellationToken token = default)
{
var query = new Dictionary<string, object>();
if (statusFilter?.Any() is true)
{
query.Add(nameof(statusFilter), statusFilter);
}
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/wallet/transactions", query), token);
return await HandleResponse<IEnumerable<OnChainWalletTransactionData>>(response);
}
public virtual async Task<OnChainWalletTransactionData> GetOnChainWalletTransaction(
string storeId, string cryptoCode, string transactionId,
CancellationToken token = default)
{
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/wallet/transactions/{transactionId}"), token);
return await HandleResponse<OnChainWalletTransactionData>(response);
}
public virtual async Task<IEnumerable<OnChainWalletUTXOData>> GetOnChainWalletUTXOs(string storeId,
string cryptoCode,
CancellationToken token = default)
{
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/wallet/utxos"), token);
return await HandleResponse<IEnumerable<OnChainWalletUTXOData>>(response);
}
public virtual async Task<OnChainWalletTransactionData> CreateOnChainTransaction(string storeId,
string cryptoCode, CreateOnChainTransactionRequest request,
CancellationToken token = default)
{
if (!request.ProceedWithBroadcast)
{
throw new ArgumentOutOfRangeException(nameof(request.ProceedWithBroadcast),
"Please use CreateOnChainTransactionButDoNotBroadcast when wanting to only create the transaction");
}
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/wallet/transactions", null, request, HttpMethod.Post), token);
return await HandleResponse<OnChainWalletTransactionData>(response);
}
public virtual async Task<Transaction> CreateOnChainTransactionButDoNotBroadcast(string storeId,
string cryptoCode, CreateOnChainTransactionRequest request, Network network,
CancellationToken token = default)
{
if (request.ProceedWithBroadcast)
{
throw new ArgumentOutOfRangeException(nameof(request.ProceedWithBroadcast),
"Please use CreateOnChainTransaction when wanting to also broadcast the transaction");
}
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/wallet/transactions", null, request, HttpMethod.Post), token);
return Transaction.Parse(await HandleResponse<string>(response), network);
}
}
}

@ -9,7 +9,7 @@ namespace BTCPayServer.Client.Models
{
public class CreateInvoiceRequest
{
[JsonProperty(ItemConverterType = typeof(NumericStringJsonConverter))]
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal Amount { get; set; }
public string Currency { get; set; }
public JObject Metadata { get; set; }
@ -33,6 +33,8 @@ namespace BTCPayServer.Client.Models
public double? PaymentTolerance { get; set; }
[JsonProperty("redirectURL")]
public string RedirectURL { get; set; }
public bool? RedirectAutomatically { get; set; }
public string DefaultLanguage { get; set; }
}
}

@ -17,7 +17,7 @@ namespace BTCPayServer.Client.Models
Description = description;
Expiry = expiry;
}
[JsonConverter(typeof(LightMoneyJsonConverter))]
[JsonConverter(typeof(BTCPayServer.Client.JsonConverters.LightMoneyJsonConverter))]
public LightMoney Amount { get; set; }
public string Description { get; set; }
[JsonConverter(typeof(JsonConverters.TimeSpanJsonConverter.Seconds))]

@ -0,0 +1,30 @@
using System.Collections.Generic;
using BTCPayServer.JsonConverters;
using NBitcoin;
using NBitcoin.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models
{
public class CreateOnChainTransactionRequest
{
public class CreateOnChainTransactionRequestDestination
{
public string Destination { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal? Amount { get; set; }
public bool SubtractFromAmount { get; set; }
}
[JsonConverter(typeof(FeeRateJsonConverter))]
public FeeRate FeeRate { get; set; }
public bool ProceedWithPayjoin { get; set; }= true;
public bool ProceedWithBroadcast { get; set; } = true;
public bool NoChange { get; set; } = false;
[JsonProperty(ItemConverterType = typeof(OutpointJsonConverter))]
public List<OutPoint> SelectedInputs { get; set; } = null;
public List<CreateOnChainTransactionRequestDestination> Destinations { get; set; }
[JsonProperty("rbf")]
public bool? RBF { get; set; } = null;
}
}

@ -8,6 +8,7 @@ namespace BTCPayServer.Client.Models
{
public class InvoicePaymentMethodDataModel
{
public bool Activated { get; set; }
public string Destination { get; set; }
public string PaymentLink { get; set; }

@ -0,0 +1,14 @@
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client.Models
{
public class LabelData
{
public string Type { get; set; }
public string Text { get; set; }
[JsonExtensionData] public Dictionary<string, JToken> AdditionalData { get; set; }
}
}

@ -0,0 +1,15 @@
using NBitcoin;
using NBitcoin.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models
{
public class OnChainWalletAddressData
{
public string Address { get; set; }
[JsonConverter(typeof(KeyPathJsonConverter))]
public KeyPath KeyPath { get; set; }
public string PaymentLink { get; set; }
}
}

@ -0,0 +1,12 @@
using NBitcoin;
using NBitcoin.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models
{
public class OnChainWalletFeeRateData
{
[JsonConverter(typeof(FeeRateJsonConverter))]
public FeeRate FeeRate { get; set; }
}
}

@ -0,0 +1,17 @@
using BTCPayServer.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models
{
public class OnChainWalletOverviewData
{
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal Balance { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal UnconfirmedBalance { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal ConfirmedBalance { get; set; }
public string Label { get; set; }
}
}

@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using BTCPayServer.JsonConverters;
using NBitcoin;
using NBitcoin.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models
{
public class OnChainWalletTransactionData
{
[JsonConverter(typeof(UInt256JsonConverter))]
public uint256 TransactionHash { get; set; }
public string Comment { get; set; }
public Dictionary<string, LabelData> Labels { get; set; } = new Dictionary<string, LabelData>();
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal Amount { get; set; }
[JsonConverter(typeof(UInt256JsonConverter))]
public uint256 BlockHash { get; set; }
public int? BlockHeight { get; set; }
public int Confirmations { get; set; }
[JsonConverter(typeof(DateTimeToUnixTimeConverter))]
public DateTimeOffset Timestamp { get; set; }
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
public TransactionStatus Status { get; set; }
}
}

@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using BTCPayServer.JsonConverters;
using NBitcoin;
using NBitcoin.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models
{
public class OnChainWalletUTXOData
{
public string Comment { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal Amount { get; set; }
[JsonConverter(typeof(OutpointJsonConverter))]
public OutPoint Outpoint { get; set; }
public string Link { get; set; }
public Dictionary<string, LabelData> Labels { get; set; }
[JsonConverter(typeof(DateTimeToUnixTimeConverter))]
public DateTimeOffset Timestamp { get; set; }
[JsonConverter(typeof(KeyPathJsonConverter))]
public KeyPath KeyPath { get; set; }
public string Address { get; set; }
}
}

@ -35,6 +35,7 @@ namespace BTCPayServer.Client.Models
public bool LightningAmountInSatoshi { get; set; }
public bool LightningPrivateRouteHints { get; set; }
public bool OnChainWithLnInvoiceFallback { get; set; }
public bool LazyPaymentMethods { get; set; }
public bool RedirectAutomatically { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
@ -53,8 +54,6 @@ namespace BTCPayServer.Client.Models
public string HtmlTitle { get; set; }
[JsonConverter(typeof(StringEnumConverter))]
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public NetworkFeeMode NetworkFeeMode { get; set; } = NetworkFeeMode.Never;

@ -0,0 +1,9 @@
namespace BTCPayServer.Client.Models
{
public enum TransactionStatus
{
Unconfirmed,
Confirmed,
Replaced
}
}

@ -11,7 +11,7 @@ namespace BTCPayServer
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Argoneum",
BlockExplorerLink = NetworkType == NetworkType.Mainnet
BlockExplorerLink = NetworkType == ChainName.Mainnet
? "https://chainz.cryptoid.info/agm/tx.dws?{0}"
: "https://chainz.cryptoid.info/agm-test/tx.dws?{0}",
NBXplorerNetwork = nbxplorerNetwork,
@ -23,7 +23,7 @@ namespace BTCPayServer
},
CryptoImagePath = "imlegacy/argoneum.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("421'")
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("421'")
: new KeyPath("1'")
});
}

@ -11,7 +11,7 @@ namespace BTCPayServer
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "BGold",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://btgexplorer.com/tx/{0}" : "https://testnet.btgexplorer.com/tx/{0}",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://btgexplorer.com/tx/{0}" : "https://testnet.btgexplorer.com/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "bitcoingold",
DefaultRateRules = new[]
@ -22,7 +22,7 @@ namespace BTCPayServer
CryptoImagePath = "imlegacy/btg.svg",
LightningImagePath = "imlegacy/btg-lightning.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("156'") : new KeyPath("1'")
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("156'") : new KeyPath("1'")
});
}
}

@ -12,7 +12,7 @@ namespace BTCPayServer
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "BPlus",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://chainz.cryptoid.info/xbc/tx.dws?{0}" : "https://chainz.cryptoid.info/xbc/tx.dws?{0}",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://chainz.cryptoid.info/xbc/tx.dws?{0}" : "https://chainz.cryptoid.info/xbc/tx.dws?{0}",
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "bplus-fix-it",
DefaultRateRules = new[]
@ -22,7 +22,7 @@ namespace BTCPayServer
},
CryptoImagePath = "imlegacy/xbc.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("65'") : new KeyPath("1'")
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("65'") : new KeyPath("1'")
});
}
}

@ -12,7 +12,7 @@ namespace BTCPayServer
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Bitcore",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://insight.bitcore.cc/tx/{0}" : "https://insight.bitcore.cc/tx/{0}",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://insight.bitcore.cc/tx/{0}" : "https://insight.bitcore.cc/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "bitcore",
DefaultRateRules = new[]
@ -23,7 +23,7 @@ namespace BTCPayServer
CryptoImagePath = "imlegacy/bitcore.svg",
LightningImagePath = "imlegacy/bitcore-lightning.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("160'") : new KeyPath("1'")
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("160'") : new KeyPath("1'")
});
}
}

@ -11,7 +11,7 @@ namespace BTCPayServer
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Chaincoin",
BlockExplorerLink = NetworkType == NetworkType.Mainnet
BlockExplorerLink = NetworkType == ChainName.Mainnet
? "https://explorer.chaincoin.org/Explorer/Transaction/{0}"
: "https://test.explorer.chaincoin.org/Explorer/Transaction/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
@ -24,7 +24,7 @@ namespace BTCPayServer
CryptoImagePath = "imlegacy/chaincoin.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
//https://github.com/satoshilabs/slips/blob/master/slip-0044.md
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("711'")
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("711'")
: new KeyPath("1'")
});
}

@ -12,7 +12,7 @@ namespace BTCPayServer
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Dash",
BlockExplorerLink = NetworkType == NetworkType.Mainnet
BlockExplorerLink = NetworkType == ChainName.Mainnet
? "https://insight.dash.org/insight/tx/{0}"
: "https://testnet-insight.dashevo.org/insight/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
@ -20,12 +20,12 @@ namespace BTCPayServer
DefaultRateRules = new[]
{
"DASH_X = DASH_BTC * BTC_X",
"DASH_BTC = bittrex(DASH_BTC)"
"DASH_BTC = bitfinex(DSH_BTC)"
},
CryptoImagePath = "imlegacy/dash.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
//https://github.com/satoshilabs/slips/blob/master/slip-0044.md
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("5'")
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("5'")
: new KeyPath("1'")
});
}

@ -12,7 +12,7 @@ namespace BTCPayServer
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Dogecoin",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://dogechain.info/tx/{0}" : "https://dogechain.info/tx/{0}",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://dogechain.info/tx/{0}" : "https://dogechain.info/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "dogecoin",
DefaultRateRules = new[]
@ -22,7 +22,7 @@ namespace BTCPayServer
},
CryptoImagePath = "imlegacy/dogecoin.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("3'") : new KeyPath("1'")
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("3'") : new KeyPath("1'")
});
}
}

@ -12,7 +12,7 @@ namespace BTCPayServer
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Feathercoin",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://explorer.feathercoin.com/tx/{0}" : "https://explorer.feathercoin.com/tx/{0}",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://explorer.feathercoin.com/tx/{0}" : "https://explorer.feathercoin.com/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "feathercoin",
DefaultRateRules = new[]
@ -22,7 +22,7 @@ namespace BTCPayServer
},
CryptoImagePath = "imlegacy/feathercoin.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("8'") : new KeyPath("1'")
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("8'") : new KeyPath("1'")
});
}
}

@ -11,7 +11,7 @@ namespace BTCPayServer
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Groestlcoin",
BlockExplorerLink = NetworkType == NetworkType.Mainnet
BlockExplorerLink = NetworkType == ChainName.Mainnet
? "https://chainz.cryptoid.info/grs/tx.dws?{0}.htm"
: "https://chainz.cryptoid.info/grs-test/tx.dws?{0}.htm",
NBXplorerNetwork = nbxplorerNetwork,
@ -24,9 +24,10 @@ namespace BTCPayServer
CryptoImagePath = "imlegacy/groestlcoin.png",
LightningImagePath = "imlegacy/groestlcoin-lightning.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("17'") : new KeyPath("1'"),
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("17'") : new KeyPath("1'"),
SupportRBF = true,
SupportPayJoin = true
SupportPayJoin = true,
VaultSupported = true
});
}
}

@ -13,7 +13,7 @@ namespace BTCPayServer
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Litecoin",
BlockExplorerLink = NetworkType == NetworkType.Mainnet
BlockExplorerLink = NetworkType == ChainName.Mainnet
? "https://live.blockcypher.com/ltc/tx/{0}/"
: "http://explorer.litecointools.com/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
@ -26,9 +26,9 @@ namespace BTCPayServer
CryptoImagePath = "imlegacy/litecoin.svg",
LightningImagePath = "imlegacy/litecoin-lightning.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("2'") : new KeyPath("1'"),
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("2'") : new KeyPath("1'"),
//https://github.com/pooler/electrum-ltc/blob/0d6989a9d2fb2edbea421c116e49d1015c7c5a91/electrum_ltc/constants.py
ElectrumMapping = NetworkType == NetworkType.Mainnet
ElectrumMapping = NetworkType == ChainName.Mainnet
? new Dictionary<uint, DerivationType>()
{
{0x0488b21eU, DerivationType.Legacy },

@ -12,7 +12,7 @@ namespace BTCPayServer
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Monacoin",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://mona.insight.monaco-ex.org/insight/tx/{0}" : "https://testnet-mona.insight.monaco-ex.org/insight/tx/{0}",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://mona.insight.monaco-ex.org/insight/tx/{0}" : "https://testnet-mona.insight.monaco-ex.org/insight/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "monacoin",
DefaultRateRules = new[]
@ -23,7 +23,7 @@ namespace BTCPayServer
CryptoImagePath = "imlegacy/monacoin.png",
LightningImagePath = "imlegacy/mona-lightning.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("22'") : new KeyPath("1'")
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("22'") : new KeyPath("1'")
});
}
}

@ -12,7 +12,7 @@ namespace BTCPayServer
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "MonetaryUnit",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://explorer.monetaryunit.org/#/MUE/mainnet/tx/{0}" : "https://explorer.monetaryunit.org/#/MUE/mainnet/tx/{0}",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://explorer.monetaryunit.org/#/MUE/mainnet/tx/{0}" : "https://explorer.monetaryunit.org/#/MUE/mainnet/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "monetaryunit",
DefaultRateRules = new[]
@ -22,7 +22,7 @@ namespace BTCPayServer
},
CryptoImagePath = "imlegacy/monetaryunit.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("31'") : new KeyPath("1'")
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("31'") : new KeyPath("1'")
});
}
}

@ -12,7 +12,7 @@ namespace BTCPayServer
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Polis",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://blockbook.polispay.org/tx/{0}" : "https://blockbook.polispay.org/tx/{0}",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://blockbook.polispay.org/tx/{0}" : "https://blockbook.polispay.org/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "polis",
DefaultRateRules = new[]
@ -22,7 +22,7 @@ namespace BTCPayServer
},
CryptoImagePath = "imlegacy/polis.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("1997'") : new KeyPath("1'")
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("1997'") : new KeyPath("1'")
});
}
}

@ -12,7 +12,7 @@ namespace BTCPayServer
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Ufo",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://chainz.cryptoid.info/ufo/tx.dws?{0}" : "https://chainz.cryptoid.info/ufo/tx.dws?{0}",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://chainz.cryptoid.info/ufo/tx.dws?{0}" : "https://chainz.cryptoid.info/ufo/tx.dws?{0}",
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "ufo",
DefaultRateRules = new[]
@ -22,7 +22,7 @@ namespace BTCPayServer
},
CryptoImagePath = "imlegacy/ufo.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("202'") : new KeyPath("1'")
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("202'") : new KeyPath("1'")
});
}
}

@ -12,7 +12,7 @@ namespace BTCPayServer
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Viacoin",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://explorer.viacoin.org/tx/{0}" : "https://explorer.viacoin.org/tx/{0}",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://explorer.viacoin.org/tx/{0}" : "https://explorer.viacoin.org/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "viacoin",
DefaultRateRules = new[]
@ -22,7 +22,7 @@ namespace BTCPayServer
},
CryptoImagePath = "imlegacy/viacoin.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("14'") : new KeyPath("1'")
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("14'") : new KeyPath("1'")
});
}
}

@ -13,20 +13,20 @@ namespace BTCPayServer
DisplayName = "Ethereum",
DefaultRateRules = new[] {"ETH_X = ETH_BTC * BTC_X", "ETH_BTC = kraken(ETH_BTC)"},
BlockExplorerLink =
NetworkType == NetworkType.Mainnet
NetworkType == ChainName.Mainnet
? "https://etherscan.io/address/{0}"
: "https://ropsten.etherscan.io/address/{0}",
CryptoImagePath = "/imlegacy/eth.png",
ShowSyncSummary = true,
CoinType = NetworkType == NetworkType.Mainnet? 60 : 1,
ChainId = NetworkType == NetworkType.Mainnet ? 1 : 3,
CoinType = NetworkType == ChainName.Mainnet? 60 : 1,
ChainId = NetworkType == ChainName.Mainnet ? 1 : 3,
Divisibility = 18,
});
}
public void InitERC20()
{
if (NetworkType != NetworkType.Mainnet)
if (NetworkType != ChainName.Mainnet)
{
Add(new ERC20BTCPayNetwork()
{
@ -60,13 +60,13 @@ namespace BTCPayServer
"USDT20_BTC = bitfinex(UST_BTC)",
},
BlockExplorerLink =
NetworkType == NetworkType.Mainnet
NetworkType == ChainName.Mainnet
? "https://etherscan.io/address/{0}#tokentxns"
: "https://ropsten.etherscan.io/address/{0}#tokentxns",
CryptoImagePath = "/imlegacy/liquid-tether.svg",
ShowSyncSummary = false,
CoinType = NetworkType == NetworkType.Mainnet? 60 : 1,
ChainId = NetworkType == NetworkType.Mainnet ? 1 : 3,
CoinType = NetworkType == ChainName.Mainnet? 60 : 1,
ChainId = NetworkType == ChainName.Mainnet ? 1 : 3,
SmartContractAddress = "0xdAC17F958D2ee523a2206206994597C13D831ec7",
Divisibility = 6
});

@ -13,7 +13,7 @@ namespace BTCPayServer
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("LBTC");
Add(new ElementsBTCPayNetwork()
{
AssetId = NetworkType == NetworkType.Mainnet ? ElementsParams<Liquid>.PeggedAssetId : ElementsParams<Liquid.LiquidRegtest>.PeggedAssetId,
AssetId = NetworkType == ChainName.Mainnet ? ElementsParams<Liquid>.PeggedAssetId : ElementsParams<Liquid.LiquidRegtest>.PeggedAssetId,
CryptoCode = "LBTC",
NetworkCryptoCode = "LBTC",
DisplayName = "Liquid Bitcoin",
@ -22,12 +22,12 @@ namespace BTCPayServer
"LBTC_X = LBTC_BTC * BTC_X",
"LBTC_BTC = 1",
},
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://blockstream.info/liquid/tx/{0}" : "https://blockstream.info/testnet/liquid/tx/{0}",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://blockstream.info/liquid/tx/{0}" : "https://blockstream.info/testnet/liquid/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "liquidnetwork",
CryptoImagePath = "imlegacy/liquid.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("1776'") : new KeyPath("1'"),
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("1776'") : new KeyPath("1'"),
SupportRBF = true
});
}

@ -21,12 +21,12 @@ namespace BTCPayServer
},
AssetId = new uint256("ce091c998b83c78bb71a632313ba3760f1763d9cfcffae02258ffa9865a37bd2"),
DisplayName = "Liquid Tether",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://blockstream.info/liquid/tx/{0}" : "https://blockstream.info/testnet/liquid/tx/{0}",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://blockstream.info/liquid/tx/{0}" : "https://blockstream.info/testnet/liquid/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "liquidnetwork",
CryptoImagePath = "imlegacy/liquid-tether.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("1776'") : new KeyPath("1'"),
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("1776'") : new KeyPath("1'"),
SupportRBF = true,
SupportLightning = false
});
@ -45,12 +45,12 @@ namespace BTCPayServer
Divisibility = 2,
AssetId = new uint256("aa775044c32a7df391902b3659f46dfe004ccb2644ce2ddc7dba31e889391caf"),
DisplayName = "Ethiopian Birr",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://blockstream.info/liquid/tx/{0}" : "https://blockstream.info/testnet/liquid/tx/{0}",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://blockstream.info/liquid/tx/{0}" : "https://blockstream.info/testnet/liquid/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "liquidnetwork",
CryptoImagePath = "imlegacy/etb.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("1776'") : new KeyPath("1'"),
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("1776'") : new KeyPath("1'"),
SupportRBF = true,
SupportLightning = false
});
@ -68,12 +68,12 @@ namespace BTCPayServer
},
AssetId = new uint256("0e99c1a6da379d1f4151fb9df90449d40d0608f6cb33a5bcbfc8c265f42bab0a"),
DisplayName = "Liquid CAD",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://blockstream.info/liquid/tx/{0}" : "https://blockstream.info/testnet/liquid/tx/{0}",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://blockstream.info/liquid/tx/{0}" : "https://blockstream.info/testnet/liquid/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "liquidnetwork",
CryptoImagePath = "imlegacy/lcad.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("1776'") : new KeyPath("1'"),
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("1776'") : new KeyPath("1'"),
SupportRBF = true,
SupportLightning = false
});

@ -24,38 +24,22 @@ namespace BTCPayServer
});
}
public override GetTransactionsResponse FilterValidTransactions(GetTransactionsResponse response)
public override List<TransactionInformation> FilterValidTransactions(List<TransactionInformation> transactionInformationSet)
{
TransactionInformationSet Filter(TransactionInformationSet transactionInformationSet)
{
return new TransactionInformationSet()
{
Transactions =
transactionInformationSet.Transactions.FindAll(information =>
information.Outputs.Any(output =>
output.Value is AssetMoney assetMoney && assetMoney.AssetId == AssetId) ||
information.Inputs.Any(output =>
output.Value is AssetMoney assetMoney && assetMoney.AssetId == AssetId))
};
}
return new GetTransactionsResponse()
{
Height = response.Height,
ConfirmedTransactions = Filter(response.ConfirmedTransactions),
ReplacedTransactions = Filter(response.ReplacedTransactions),
UnconfirmedTransactions = Filter(response.UnconfirmedTransactions)
};
return transactionInformationSet.FindAll(information =>
information.Outputs.Any(output =>
output.Value is AssetMoney assetMoney && assetMoney.AssetId == AssetId) ||
information.Inputs.Any(output =>
output.Value is AssetMoney assetMoney && assetMoney.AssetId == AssetId));
}
public override string GenerateBIP21(string cryptoInfoAddress, Money cryptoInfoDue)
{
//precision 0: 10 = 0.00000010
//precision 2: 10 = 0.00001000
//precision 8: 10 = 10
var money = new Money(cryptoInfoDue.ToDecimal(MoneyUnit.BTC) / decimal.Parse("1".PadRight(1 + 8 - Divisibility, '0')), MoneyUnit.BTC);
return $"{base.GenerateBIP21(cryptoInfoAddress, money)}&assetid={AssetId}";
var money = cryptoInfoDue is null? null: new Money(cryptoInfoDue.ToDecimal(MoneyUnit.BTC) / decimal.Parse("1".PadRight(1 + 8 - Divisibility, '0')), MoneyUnit.BTC);
return $"{base.GenerateBIP21(cryptoInfoAddress, money)}{(money is null? "?": "&")}assetid={AssetId}";
}
}
}

@ -12,7 +12,7 @@ namespace BTCPayServer
DisplayName = "Monero",
Divisibility = 12,
BlockExplorerLink =
NetworkType == NetworkType.Mainnet
NetworkType == ChainName.Mainnet
? "https://www.exploremonero.com/transaction/{0}"
: "https://testnet.xmrchain.net/tx/{0}",
DefaultRateRules = new[]

@ -18,25 +18,29 @@ namespace BTCPayServer
{
static BTCPayDefaultSettings()
{
_Settings = new Dictionary<NetworkType, BTCPayDefaultSettings>();
foreach (var chainType in new[] { NetworkType.Mainnet, NetworkType.Testnet, NetworkType.Regtest })
_Settings = new Dictionary<ChainName, BTCPayDefaultSettings>();
}
static readonly Dictionary<ChainName, BTCPayDefaultSettings> _Settings;
public static BTCPayDefaultSettings GetDefaultSettings(ChainName chainType)
{
if (_Settings.TryGetValue(chainType, out var v))
return v;
lock (_Settings)
{
if (_Settings.TryGetValue(chainType, out v))
return v;
var settings = new BTCPayDefaultSettings();
_Settings.Add(chainType, settings);
settings.DefaultDataDirectory = StandardConfiguration.DefaultDataDirectory.GetDirectory("BTCPayServer", NBXplorerDefaultSettings.GetFolderName(chainType));
settings.DefaultPluginDirectory =
StandardConfiguration.DefaultDataDirectory.GetDirectory("BTCPayServer", "Plugins");
settings.DefaultConfigurationFile = Path.Combine(settings.DefaultDataDirectory, "settings.config");
settings.DefaultPort = (chainType == NetworkType.Mainnet ? 23000 :
chainType == NetworkType.Regtest ? 23002 :
chainType == NetworkType.Testnet ? 23001 : throw new NotSupportedException(chainType.ToString()));
settings.DefaultPort = (chainType == ChainName.Mainnet ? 23000 :
chainType == ChainName.Regtest ? 23002
: 23001);
}
}
static readonly Dictionary<NetworkType, BTCPayDefaultSettings> _Settings;
public static BTCPayDefaultSettings GetDefaultSettings(NetworkType chainType)
{
return _Settings[chainType];
}
@ -50,7 +54,7 @@ namespace BTCPayServer
{
public Network NBitcoinNetwork { get { return NBXplorerNetwork?.NBitcoinNetwork; } }
public NBXplorer.NBXplorerNetwork NBXplorerNetwork { get; set; }
public bool SupportRBF { get; internal set; }
public bool SupportRBF { get; set; }
public string LightningImagePath { get; set; }
public BTCPayDefaultSettings DefaultSettings { get; set; }
public KeyPath CoinType { get; set; }
@ -59,9 +63,9 @@ namespace BTCPayServer
public virtual bool WalletSupported { get; set; } = true;
public virtual bool ReadonlyWallet { get; set; } = false;
public int MaxTrackedConfirmation { get; internal set; } = 6;
public string UriScheme { get; internal set; }
public virtual bool VaultSupported { get; set; } = false;
public int MaxTrackedConfirmation { get; set; } = 6;
public string UriScheme { get; set; }
public bool SupportPayJoin { get; set; } = false;
public bool SupportLightning { get; set; } = true;
@ -119,12 +123,12 @@ namespace BTCPayServer
public virtual string GenerateBIP21(string cryptoInfoAddress, Money cryptoInfoDue)
{
return $"{UriScheme}:{cryptoInfoAddress}?amount={cryptoInfoDue.ToString(false, true)}";
return $"{UriScheme}:{cryptoInfoAddress}{(cryptoInfoDue is null? string.Empty: $"?amount={cryptoInfoDue.ToString(false, true)}")}";
}
public virtual GetTransactionsResponse FilterValidTransactions(GetTransactionsResponse response)
public virtual List<TransactionInformation> FilterValidTransactions(List<TransactionInformation> transactionInformationSet)
{
return response;
return transactionInformationSet;
}
}
@ -148,7 +152,7 @@ namespace BTCPayServer
}
}
public string BlockExplorerLinkDefault { get; internal set; }
public string BlockExplorerLinkDefault { get; set; }
public string DisplayName { get; set; }
public int Divisibility { get; set; } = 8;
[Obsolete("Should not be needed")]

@ -13,17 +13,20 @@ namespace BTCPayServer
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Bitcoin",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://blockstream.info/tx/{0}" : "https://blockstream.info/testnet/tx/{0}",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://blockstream.info/tx/{0}" :
NetworkType == Bitcoin.Instance.Signet.ChainName ? "https://explorer.bc-2.jp/tx/{0}"
: "https://blockstream.info/testnet/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "bitcoin",
CryptoImagePath = "imlegacy/bitcoin.svg",
LightningImagePath = "imlegacy/bitcoin-lightning.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("0'") : new KeyPath("1'"),
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("0'") : new KeyPath("1'"),
SupportRBF = true,
SupportPayJoin = true,
VaultSupported = true,
//https://github.com/spesmilo/electrum/blob/11733d6bc271646a00b69ff07657119598874da4/electrum/constants.py
ElectrumMapping = NetworkType == NetworkType.Mainnet
ElectrumMapping = NetworkType == ChainName.Mainnet
? new Dictionary<uint, DerivationType>()
{
{0x0488b21eU, DerivationType.Legacy }, // xpub

@ -35,8 +35,8 @@ namespace BTCPayServer
}
public NetworkType NetworkType { get; private set; }
public BTCPayNetworkProvider(NetworkType networkType)
public ChainName NetworkType { get; private set; }
public BTCPayNetworkProvider(ChainName networkType)
{
_NBXplorerNetworkProvider = new NBXplorerNetworkProvider(networkType);
NetworkType = networkType;

@ -1,10 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="../Build/Version.csproj" Condition="Exists('../Build/Version.csproj')" />
<Import Project="../Build/Common.csproj" />
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="NBXplorer.Client" Version="3.0.19" />
<PackageReference Include="NBXplorer.Client" Version="3.0.20" />
</ItemGroup>
<ItemGroup Condition="'$(Altcoins)' != 'true'">
<Compile Remove="Altcoins\**\*.cs"></Compile>

@ -11,13 +11,16 @@ namespace BTCPayServer.Migrations
{
protected override void Up(MigrationBuilder migrationBuilder)
{
int? maxLength = this.IsMySql(migrationBuilder.ActiveProvider) ? (int?)255 : null;
migrationBuilder.DropTable(
name: "RefundAddresses");
migrationBuilder.AddColumn<string>(
name: "CurrentRefundId",
table: "Invoices",
nullable: true);
nullable: true,
maxLength: maxLength);
migrationBuilder.CreateTable(
name: "Notifications",
@ -73,7 +76,7 @@ namespace BTCPayServer.Migrations
PullPaymentDataId = table.Column<string>(maxLength: 30, nullable: true),
State = table.Column<string>(maxLength: 20, nullable: false),
PaymentMethodId = table.Column<string>(maxLength: 20, nullable: false),
Destination = table.Column<string>(nullable: true),
Destination = table.Column<string>(maxLength: maxLength, nullable: true),
Blob = table.Column<byte[]>(nullable: true),
Proof = table.Column<byte[]>(nullable: true)
},
@ -92,8 +95,8 @@ namespace BTCPayServer.Migrations
name: "Refunds",
columns: table => new
{
InvoiceDataId = table.Column<string>(nullable: false),
PullPaymentDataId = table.Column<string>(nullable: false)
InvoiceDataId = table.Column<string>(maxLength: maxLength, nullable: false),
PullPaymentDataId = table.Column<string>(maxLength: maxLength, nullable: false)
},
constraints: table =>
{

@ -27,8 +27,8 @@ namespace BTCPayServer.Migrations
name: "StoreWebhooks",
columns: table => new
{
StoreId = table.Column<string>(nullable: false),
WebhookId = table.Column<string>(nullable: false)
StoreId = table.Column<string>(maxLength: 50, nullable: false),
WebhookId = table.Column<string>(maxLength: 25, nullable: false)
},
constraints: table =>
{
@ -71,8 +71,8 @@ namespace BTCPayServer.Migrations
name: "InvoiceWebhookDeliveries",
columns: table => new
{
InvoiceId = table.Column<string>(nullable: false),
DeliveryId = table.Column<string>(nullable: false)
InvoiceId = table.Column<string>(maxLength: 255, nullable: false),
DeliveryId = table.Column<string>(maxLength: 100, nullable: false)
},
constraints: table =>
{

@ -10,7 +10,17 @@ namespace BTCPayServer.Migrations
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
if (!migrationBuilder.IsSqlite())
{
migrationBuilder.AlterColumn<string>(
name: "OrderId",
table: "Invoices",
maxLength: 100,
nullable: true,
oldClrType: typeof(string));
}
migrationBuilder.CreateIndex(
name: "IX_Invoices_OrderId",
table: "Invoices",
column: "OrderId");

@ -21,8 +21,8 @@ namespace BTCPayServer.Migrations
.Annotation("MySql:ValueGeneratedOnAdd", true)
.Annotation("Sqlite:Autoincrement", true),
// eof manually added
InvoiceDataId = table.Column<string>(nullable: true),
Value = table.Column<string>(nullable: true)
InvoiceDataId = table.Column<string>(maxLength: 255, nullable: true),
Value = table.Column<string>(maxLength: 512, nullable: true)
},
constraints: table =>
{

@ -6,7 +6,7 @@
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.6.0" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.7" />
<PackageReference Include="NBitcoin" Version="5.0.68" />
<PackageReference Include="NBitcoin" Version="5.0.73" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.6.3" />
</ItemGroup>

File diff suppressed because one or more lines are too long

@ -1,61 +1,31 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Security;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Configuration;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.HostedServices;
using BTCPayServer.Lightning;
using BTCPayServer.Models;
using BTCPayServer.Models.AccountViewModels;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Models.ServerViewModels;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Models.WalletViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Rating;
using BTCPayServer.Security.Bitpay;
using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Tests.Logging;
using BTCPayServer.U2F.Models;
using BTCPayServer.Validation;
using ExchangeSharp;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBitcoin.Payment;
using NBitcoin.Scripting.Parser;
using NBitpayClient;
using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Schema;
using OpenQA.Selenium;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;
using RatesViewModel = BTCPayServer.Models.StoreViewModels.RatesViewModel;
namespace BTCPayServer.Tests
{
@ -72,7 +42,7 @@ namespace BTCPayServer.Tests
[Trait("Integration", "Integration")]
[Trait("Altcoins", "Altcoins")]
[Trait("Lightning", "Lightning")]
public async Task CanAddDerivationSchemes()
public async Task CanSetupWallet()
{
using (var tester = ServerTester.Create())
{
@ -80,7 +50,7 @@ namespace BTCPayServer.Tests
tester.ActivateLightning();
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess();
user.GrantAccess(true);
user.RegisterDerivationScheme("BTC");
user.RegisterDerivationScheme("LTC");
user.RegisterLightningNode("BTC", LightningConnectionType.CLightning);
@ -99,37 +69,37 @@ namespace BTCPayServer.Tests
Assert.Equal(3, invoice.CryptoInfo.Length);
var controller = user.GetController<StoresController>();
var lightningVM =
(LightningNodeViewModel)Assert.IsType<ViewResult>(controller.AddLightningNode(user.StoreId, "BTC"))
.Model;
Assert.True(lightningVM.Enabled);
lightningVM.Enabled = false;
controller.AddLightningNode(user.StoreId, lightningVM, "save", "BTC").GetAwaiter().GetResult();
lightningVM =
(LightningNodeViewModel)Assert.IsType<ViewResult>(controller.AddLightningNode(user.StoreId, "BTC"))
.Model;
Assert.False(lightningVM.Enabled);
var lightningVm = (LightningNodeViewModel)Assert.IsType<ViewResult>(controller.AddLightningNode(user.StoreId, "BTC")).Model;
Assert.True(lightningVm.Enabled);
lightningVm.Enabled = false;
controller.AddLightningNode(user.StoreId, lightningVm, "save", "BTC").GetAwaiter().GetResult();
lightningVm = (LightningNodeViewModel)Assert.IsType<ViewResult>(controller.AddLightningNode(user.StoreId, "BTC")).Model;
Assert.False(lightningVm.Enabled);
WalletSetupViewModel setupVm;
var storeId = user.StoreId;
var cryptoCode = "BTC";
var response = await controller.GenerateWallet(storeId, cryptoCode, WalletSetupMethod.GenerateOptions, new GenerateWalletRequest());
Assert.IsType<ViewResult>(response);
// Get setup view model from modify action
response = await controller.ModifyWallet(new WalletSetupViewModel { StoreId = storeId, CryptoCode = cryptoCode });
setupVm = (WalletSetupViewModel)Assert.IsType<ViewResult>(response).Model;
Assert.True(setupVm.Enabled);
// Only Enabling/Disabling the payment method must redirect to store page
var derivationVM = (DerivationSchemeViewModel)Assert
.IsType<ViewResult>(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
Assert.True(derivationVM.Enabled);
derivationVM.Enabled = false;
Assert.IsType<RedirectToActionResult>(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC")
.GetAwaiter().GetResult());
derivationVM = (DerivationSchemeViewModel)Assert
.IsType<ViewResult>(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
Assert.False(derivationVM.Enabled);
setupVm.Enabled = false;
response = controller.UpdateWallet(setupVm).GetAwaiter().GetResult();
Assert.IsType<RedirectToActionResult>(response);
// Clicking next without changing anything should send to the confirmation screen
derivationVM = (DerivationSchemeViewModel)Assert
.IsType<ViewResult>(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller
.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model;
Assert.True(derivationVM.Confirmation);
response = await controller.ModifyWallet(new WalletSetupViewModel { StoreId = storeId, CryptoCode = cryptoCode });
setupVm = (WalletSetupViewModel)Assert.IsType<ViewResult>(response).Model;
Assert.False(setupVm.Enabled);
var oldScheme = setupVm.DerivationScheme;
invoice = user.BitPay.CreateInvoice(
new Invoice()
new Invoice
{
Price = 1.5m,
Currency = "USD",
@ -143,76 +113,57 @@ namespace BTCPayServer.Tests
Assert.Equal("LTC", invoice.CryptoInfo[0].CryptoCode);
// Removing the derivation scheme, should redirect to store page
var oldScheme = derivationVM.DerivationScheme;
derivationVM = (DerivationSchemeViewModel)Assert
.IsType<ViewResult>(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
derivationVM.DerivationScheme = null;
Assert.IsType<RedirectToActionResult>(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC")
.GetAwaiter().GetResult());
response = controller.ConfirmDeleteWallet(user.StoreId, "BTC").GetAwaiter().GetResult();
Assert.IsType<RedirectToActionResult>(response);
// Setting it again should redirect to the confirmation page
derivationVM = (DerivationSchemeViewModel)Assert
.IsType<ViewResult>(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
derivationVM.DerivationScheme = oldScheme;
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller
.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model;
Assert.True(derivationVM.Confirmation);
// Setting it again should show the confirmation page
response = await controller.UpdateWallet(new WalletSetupViewModel {StoreId = storeId, CryptoCode = cryptoCode, DerivationScheme = oldScheme });
setupVm = (WalletSetupViewModel)Assert.IsType<ViewResult>(response).Model;
Assert.True(setupVm.Confirmation);
// The following part posts a wallet update, confirms it and checks the result
//cobo vault file
// cobo vault file
var content = "{\"ExtPubKey\":\"xpub6CEqRFZ7yZxCFXuEWZBAdnC8bdvu9SRHevaoU2SsW9ZmKhrCShmbpGZWwaR15hdLURf8hg47g4TpPGaqEU8hw5LEJCE35AUhne67XNyFGBk\",\"MasterFingerprint\":\"7a7563b5\",\"DerivationPath\":\"M\\/84'\\/0'\\/0'\",\"CoboVaultFirmwareVersion\":\"1.2.0(BTC-Only)\"}";
derivationVM = (DerivationSchemeViewModel)Assert
.IsType<ViewResult>(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
derivationVM.WalletFile = TestUtils.GetFormFile("wallet3.json", content);
derivationVM.Enabled = true;
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller
.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model;
Assert.True(derivationVM.Confirmation);
Assert.IsType<RedirectToActionResult>(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC")
.GetAwaiter().GetResult());
//wasabi wallet file
content =
"{\r\n \"EncryptedSecret\": \"6PYWBQ1zsukowsnTNA57UUx791aBuJusm7E4egXUmF5WGw3tcdG3cmTL57\",\r\n \"ChainCode\": \"waSIVbn8HaoovoQg/0t8IS1+ZCxGsJRGFT21i06nWnc=\",\r\n \"MasterFingerprint\": \"7a7563b5\",\r\n \"ExtPubKey\": \"xpub6CEqRFZ7yZxCFXuEWZBAdnC8bdvu9SRHevaoU2SsW9ZmKhrCShmbpGZWwaR15hdLURf8hg47g4TpPGaqEU8hw5LEJCE35AUhne67XNyFGBk\",\r\n \"PasswordVerified\": false,\r\n \"MinGapLimit\": 21,\r\n \"AccountKeyPath\": \"84'/0'/0'\",\r\n \"BlockchainState\": {\r\n \"Network\": \"RegTest\",\r\n \"Height\": \"0\"\r\n },\r\n \"HdPubKeys\": []\r\n}";
derivationVM = (DerivationSchemeViewModel)Assert
.IsType<ViewResult>(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
derivationVM.WalletFile = TestUtils.GetFormFile("wallet4.json", content);
derivationVM.Enabled = true;
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller
.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model;
Assert.True(derivationVM.Confirmation);
Assert.IsType<RedirectToActionResult>(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC")
.GetAwaiter().GetResult());
response = await controller.UpdateWallet(new WalletSetupViewModel {StoreId = storeId, CryptoCode = cryptoCode, WalletFile = TestUtils.GetFormFile("cobovault.json", content)});
setupVm = (WalletSetupViewModel)Assert.IsType<ViewResult>(response).Model;
Assert.True(setupVm.Confirmation);
response = await controller.UpdateWallet(setupVm);
Assert.IsType<RedirectToActionResult>(response);
response = await controller.ModifyWallet(new WalletSetupViewModel { StoreId = storeId, CryptoCode = cryptoCode });
setupVm = (WalletSetupViewModel)Assert.IsType<ViewResult>(response).Model;
Assert.Equal("CoboVault", setupVm.Source);
// wasabi wallet file
content = "{\r\n \"EncryptedSecret\": \"6PYWBQ1zsukowsnTNA57UUx791aBuJusm7E4egXUmF5WGw3tcdG3cmTL57\",\r\n \"ChainCode\": \"waSIVbn8HaoovoQg/0t8IS1+ZCxGsJRGFT21i06nWnc=\",\r\n \"MasterFingerprint\": \"7a7563b5\",\r\n \"ExtPubKey\": \"xpub6CEqRFZ7yZxCFXuEWZBAdnC8bdvu9SRHevaoU2SsW9ZmKhrCShmbpGZWwaR15hdLURf8hg47g4TpPGaqEU8hw5LEJCE35AUhne67XNyFGBk\",\r\n \"PasswordVerified\": false,\r\n \"MinGapLimit\": 21,\r\n \"AccountKeyPath\": \"84'/0'/0'\",\r\n \"BlockchainState\": {\r\n \"Network\": \"RegTest\",\r\n \"Height\": \"0\"\r\n },\r\n \"HdPubKeys\": []\r\n}";
response = await controller.UpdateWallet(new WalletSetupViewModel {StoreId = storeId, CryptoCode = cryptoCode, WalletFile = TestUtils.GetFormFile("wasabi.json", content)});
setupVm = (WalletSetupViewModel)Assert.IsType<ViewResult>(response).Model;
Assert.True(setupVm.Confirmation);
response = await controller.UpdateWallet(setupVm);
Assert.IsType<RedirectToActionResult>(response);
response = await controller.ModifyWallet(new WalletSetupViewModel { StoreId = storeId, CryptoCode = cryptoCode });
setupVm = (WalletSetupViewModel)Assert.IsType<ViewResult>(response).Model;
Assert.Equal("WasabiFile", setupVm.Source);
// Can we upload coldcard settings? (Should fail, we are giving a mainnet file to a testnet network)
derivationVM = (DerivationSchemeViewModel)Assert
.IsType<ViewResult>(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
content =
"{\"keystore\": {\"ckcc_xpub\": \"xpub661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw\", \"xpub\": \"ypub6WWc2gWwHbdnAAyJDnR4SPL1phRh7REqrPBfZeizaQ1EmTshieRXJC3Z5YoU4wkcdKHEjQGkh6AYEzCQC1Kz3DNaWSwdc1pc8416hAjzqyD\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/0'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}";
derivationVM.WalletFile = TestUtils.GetFormFile("wallet.json", content);
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller
.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model;
Assert.False(derivationVM
.Confirmation); // Should fail, we are giving a mainnet file to a testnet network
content = "{\"keystore\": {\"ckcc_xpub\": \"xpub661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw\", \"xpub\": \"ypub6WWc2gWwHbdnAAyJDnR4SPL1phRh7REqrPBfZeizaQ1EmTshieRXJC3Z5YoU4wkcdKHEjQGkh6AYEzCQC1Kz3DNaWSwdc1pc8416hAjzqyD\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/0'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}";
response = await controller.UpdateWallet(new WalletSetupViewModel {StoreId = storeId, CryptoCode = cryptoCode, WalletFile = TestUtils.GetFormFile("coldcard-ypub.json", content)});
setupVm = (WalletSetupViewModel)Assert.IsType<ViewResult>(response).Model;
Assert.False(setupVm.Confirmation); // Should fail, we are giving a mainnet file to a testnet network
// And with a good file? (upub)
content =
"{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"upub5DBYp1qGgsTrkzCptMGZc2x18pquLwGrBw6nS59T4NViZ4cni1mGowQzziy85K8vzkp1jVtWrSkLhqk9KDfvrGeB369wGNYf39kX8rQfiLn\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/0'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}";
derivationVM = (DerivationSchemeViewModel)Assert
.IsType<ViewResult>(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
derivationVM.WalletFile = TestUtils.GetFormFile("wallet2.json", content);
derivationVM.Enabled = true;
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller
.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model;
Assert.True(derivationVM.Confirmation);
Assert.IsType<RedirectToActionResult>(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC")
.GetAwaiter().GetResult());
content = "{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"upub5DBYp1qGgsTrkzCptMGZc2x18pquLwGrBw6nS59T4NViZ4cni1mGowQzziy85K8vzkp1jVtWrSkLhqk9KDfvrGeB369wGNYf39kX8rQfiLn\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/0'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}";
response = await controller.UpdateWallet(new WalletSetupViewModel {StoreId = storeId, CryptoCode = cryptoCode, WalletFile = TestUtils.GetFormFile("coldcard-upub.json", content)});
setupVm = (WalletSetupViewModel)Assert.IsType<ViewResult>(response).Model;
Assert.True(setupVm.Confirmation);
response = await controller.UpdateWallet(setupVm);
Assert.IsType<RedirectToActionResult>(response);
response = await controller.ModifyWallet(new WalletSetupViewModel { StoreId = storeId, CryptoCode = cryptoCode });
setupVm = (WalletSetupViewModel)Assert.IsType<ViewResult>(response).Model;
Assert.Equal("ElectrumFile", setupVm.Source);
// Now let's check that no data has been lost in the process
var store = tester.PayTester.StoreRepository.FindStore(user.StoreId).GetAwaiter().GetResult();
var store = tester.PayTester.StoreRepository.FindStore(storeId).GetAwaiter().GetResult();
var onchainBTC = store.GetSupportedPaymentMethods(tester.PayTester.Networks)
#pragma warning disable CS0618 // Type or member is obsolete
.OfType<DerivationSchemeSettings>().First(o => o.PaymentId.IsBTCOnChain);
@ -287,7 +238,7 @@ namespace BTCPayServer.Tests
await tester.StartAsync();
await tester.EnsureChannelsSetup();
var user = tester.NewAccount();
user.GrantAccess();
user.GrantAccess(true);
user.RegisterLightningNode("BTC", LightningConnectionType.Charge);
user.RegisterDerivationScheme("BTC");
user.RegisterDerivationScheme("LTC");
@ -295,7 +246,6 @@ namespace BTCPayServer.Tests
var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(100, "BTC"));
Assert.Equal(2, invoice.SupportedTransactionCurrencies.Count);
invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(100, "BTC")
{
SupportedTransactionCurrencies = new Dictionary<string, InvoiceSupportedTransactionCurrency>()
@ -479,21 +429,21 @@ namespace BTCPayServer.Tests
//there should be three now
invoiceId = s.CreateInvoice(store.storeName, 10, "USD", "a@g.com");
s.GoToInvoiceCheckout(invoiceId);
var currencyDropdownButton = s.Driver.WaitForElement(By.ClassName("payment__currencies"));
var currencyDropdownButton = s.Driver.FindElement(By.ClassName("payment__currencies"));
Assert.Contains("BTC", currencyDropdownButton.Text);
currencyDropdownButton.Click();
var elements = s.Driver.FindElement(By.ClassName("vex-content")).FindElements(By.ClassName("vexmenuitem"));
Assert.Equal(3, elements.Count);
elements.Single(element => element.Text.Contains("LTC")).Click();
currencyDropdownButton = s.Driver.WaitForElement(By.ClassName("payment__currencies"));
currencyDropdownButton = s.Driver.FindElement(By.ClassName("payment__currencies"));
Assert.Contains("LTC", currencyDropdownButton.Text);
currencyDropdownButton.Click();
elements = s.Driver.FindElement(By.ClassName("vex-content")).FindElements(By.ClassName("vexmenuitem"));
elements.Single(element => element.Text.Contains("Lightning")).Click();
currencyDropdownButton = s.Driver.WaitForElement(By.ClassName("payment__currencies"));
currencyDropdownButton = s.Driver.FindElement(By.ClassName("payment__currencies"));
Assert.Contains("Lightning", currencyDropdownButton.Text);
s.Driver.Quit();
@ -872,11 +822,11 @@ normal:
{
#pragma warning disable CS0618
var dummy = new Key().PubKey.GetAddress(ScriptPubKeyType.Legacy, Network.RegTest).ToString();
var networkProvider = new BTCPayNetworkProvider(NetworkType.Regtest);
var networkProvider = new BTCPayNetworkProvider(ChainName.Regtest);
var paymentMethodHandlerDictionary = new PaymentMethodHandlerDictionary(new IPaymentMethodHandler[]
{
new BitcoinLikePaymentHandler(null, networkProvider, null, null, null),
new LightningLikePaymentHandler(null, null, networkProvider, null, null),
new LightningLikePaymentHandler(null, null, networkProvider, null, null, null),
});
var networkBTC = networkProvider.GetNetwork("BTC");
var networkLTC = networkProvider.GetNetwork("LTC");
@ -952,9 +902,9 @@ normal:
[Trait("Altcoins", "Altcoins")]
public void CanParseDerivationScheme()
{
var testnetNetworkProvider = new BTCPayNetworkProvider(NetworkType.Testnet);
var regtestNetworkProvider = new BTCPayNetworkProvider(NetworkType.Regtest);
var mainnetNetworkProvider = new BTCPayNetworkProvider(NetworkType.Mainnet);
var testnetNetworkProvider = new BTCPayNetworkProvider(ChainName.Testnet);
var regtestNetworkProvider = new BTCPayNetworkProvider(ChainName.Regtest);
var mainnetNetworkProvider = new BTCPayNetworkProvider(ChainName.Mainnet);
var testnetParser = new DerivationSchemeParser(testnetNetworkProvider.GetNetwork<BTCPayNetwork>("BTC"));
var mainnetParser = new DerivationSchemeParser(mainnetNetworkProvider.GetNetwork<BTCPayNetwork>("BTC"));
NBXplorer.DerivationStrategy.DerivationStrategyBase result;

@ -35,7 +35,7 @@ namespace BTCPayServer.Tests
InitialData = new[] {new KeyValuePair<string, string>("chains", "usdt20"),}
})
});
var networkProvider = config.ConfigureNetworkProvider();
Assert.NotNull(networkProvider.GetNetwork("ETH"));
Assert.NotNull(networkProvider.GetNetwork("USDT20"));
@ -60,7 +60,7 @@ namespace BTCPayServer.Tests
web3Link.Click();
s.Driver.FindElement(By.Id("Web3ProviderUrl")).SendKeys("https://ropsten-rpc.linkpool.io");
s.Driver.FindElement(By.Id("saveButton")).Click();
s.AssertHappyMessage();
s.FindAlertMessage();
TestUtils.Eventually(() =>
{
s.Driver.Navigate().Refresh();
@ -73,20 +73,20 @@ namespace BTCPayServer.Tests
var seed = new Mnemonic(Wordlist.English);
s.Driver.FindElement(By.Id("ModifyETH")).Click();
s.Driver.FindElement(By.Id("Seed")).SendKeys(seed.ToString());
s.SetCheckbox(s.Driver.FindElement(By.Id("StoreSeed")), true);
s.SetCheckbox(s.Driver.FindElement(By.Id("Enabled")), true);
s.Driver.SetCheckbox(By.Id("StoreSeed"), true);
s.Driver.SetCheckbox(By.Id("Enabled"), true);
s.Driver.FindElement(By.Id("SaveButton")).Click();
s.AssertHappyMessage();
s.FindAlertMessage();
s.Driver.FindElement(By.Id("ModifyUSDT20")).Click();
s.Driver.FindElement(By.Id("Seed")).SendKeys(seed.ToString());
s.SetCheckbox(s.Driver.FindElement(By.Id("StoreSeed")), true);
s.SetCheckbox(s.Driver.FindElement(By.Id("Enabled")), true);
s.Driver.SetCheckbox(By.Id("StoreSeed"), true);
s.Driver.SetCheckbox(By.Id("Enabled"), true);
s.Driver.FindElement(By.Id("SaveButton")).Click();
s.AssertHappyMessage();
s.FindAlertMessage();
var invoiceId = s.CreateInvoice(store.storeName, 10);
s.GoToInvoiceCheckout(invoiceId);
var currencyDropdownButton = s.Driver.WaitForElement(By.ClassName("payment__currencies"));
var currencyDropdownButton = s.Driver.FindElement(By.ClassName("payment__currencies"));
Assert.Contains("ETH", currencyDropdownButton.Text);
s.Driver.FindElement(By.Id("copy-tab")).Click();

@ -61,28 +61,28 @@ namespace BTCPayServer.Tests
Assert.Contains("btcpay.server.canmodifyserversettings", s.Driver.PageSource);
//server management should show now
s.SetCheckbox(s, "btcpay.server.canmodifyserversettings", true);
s.SetCheckbox(s, "btcpay.store.canmodifystoresettings", true);
s.SetCheckbox(s, "btcpay.user.canviewprofile", true);
s.Driver.SetCheckbox(By.Id("btcpay.server.canmodifyserversettings"), true);
s.Driver.SetCheckbox(By.Id("btcpay.store.canmodifystoresettings"), true);
s.Driver.SetCheckbox(By.Id("btcpay.user.canviewprofile"), true);
s.Driver.FindElement(By.Id("Generate")).Click();
var superApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text;
var superApiKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text;
//this api key has access to everything
await TestApiAgainstAccessToken(superApiKey, tester, user, Policies.CanModifyServerSettings, Policies.CanModifyStoreSettings, Policies.CanViewProfile);
s.Driver.FindElement(By.Id("AddApiKey")).Click();
s.SetCheckbox(s, "btcpay.server.canmodifyserversettings", true);
s.Driver.SetCheckbox(By.Id("btcpay.server.canmodifyserversettings"), true);
s.Driver.FindElement(By.Id("Generate")).Click();
var serverOnlyApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text;
var serverOnlyApiKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text;
await TestApiAgainstAccessToken(serverOnlyApiKey, tester, user,
Policies.CanModifyServerSettings);
s.Driver.FindElement(By.Id("AddApiKey")).Click();
s.SetCheckbox(s, "btcpay.store.canmodifystoresettings", true);
s.Driver.SetCheckbox(By.Id("btcpay.store.canmodifystoresettings"), true);
s.Driver.FindElement(By.Id("Generate")).Click();
var allStoreOnlyApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text;
var allStoreOnlyApiKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text;
await TestApiAgainstAccessToken(allStoreOnlyApiKey, tester, user,
Policies.CanModifyStoreSettings);
@ -94,13 +94,13 @@ namespace BTCPayServer.Tests
var storeId = option.GetAttribute("value");
option.Click();
s.Driver.FindElement(By.Id("Generate")).Click();
var selectiveStoreApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text;
var selectiveStoreApiKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text;
await TestApiAgainstAccessToken(selectiveStoreApiKey, tester, user,
Permission.Create(Policies.CanModifyStoreSettings, storeId).ToString());
s.Driver.FindElement(By.Id("AddApiKey")).Click();
s.Driver.FindElement(By.Id("Generate")).Click();
var noPermissionsApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text;
var noPermissionsApiKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text;
await TestApiAgainstAccessToken(noPermissionsApiKey, tester, user);
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
@ -149,7 +149,7 @@ namespace BTCPayServer.Tests
Assert.Equal("checkbox", s.Driver.FindElement(By.Id("btcpay.server.canmodifyserversettings")).GetAttribute("type").ToLowerInvariant());
Assert.Equal("true", s.Driver.FindElement(By.Id("btcpay.server.canmodifyserversettings")).GetAttribute("value").ToLowerInvariant());
s.SetCheckbox(s, "btcpay.server.canmodifyserversettings", false);
s.Driver.SetCheckbox(By.Id("btcpay.server.canmodifyserversettings"), false);
Assert.Contains("change-store-mode", s.Driver.PageSource);
s.Driver.FindElement(By.Id("consent-yes")).Click();
Assert.Equal(callbackUrl, s.Driver.Url);
@ -188,7 +188,7 @@ namespace BTCPayServer.Tests
checkbox.Click();
}
s.Driver.FindElement(By.Id("Generate")).Click();
var allAPIKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text;
var allAPIKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text;
var apikeydata = await TestApiAgainstAccessToken<ApiKeyData>(allAPIKey, $"api/v1/api-keys/current", tester.PayTester.HttpClient);
Assert.Equal(checkedPermissionCount, apikeydata.Permissions.Length);
}

@ -23,7 +23,7 @@
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.13" />
<PackageReference Include="Selenium.Support" Version="3.141.0" />
<PackageReference Include="Selenium.WebDriver" Version="3.141.0" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="87.0.4280.8800" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="88.0.4324.9600" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.2">
<PrivateAssets>all</PrivateAssets>

@ -91,7 +91,7 @@ namespace BTCPayServer.Tests
{
if (!Directory.Exists(_Directory))
Directory.CreateDirectory(_Directory);
string chain = NBXplorerDefaultSettings.GetFolderName(NetworkType.Regtest);
string chain = NBXplorerDefaultSettings.GetFolderName(ChainName.Regtest);
string chainDirectory = Path.Combine(_Directory, chain);
if (!Directory.Exists(chainDirectory))
Directory.CreateDirectory(chainDirectory);
@ -112,7 +112,7 @@ namespace BTCPayServer.Tests
if (UseLightning)
{
config.AppendLine($"btc.lightning={IntegratedLightning.AbsoluteUri}");
config.AppendLine($"btc.lightning={IntegratedLightning}");
var localLndBackupFile = Path.Combine(_Directory, "walletunlock.json");
File.Copy(TestUtils.GetTestDataFullPath("LndSeedBackup/walletunlock.json"), localLndBackupFile, true);
config.AppendLine($"btc.external.lndseedbackup={localLndBackupFile}");
@ -269,7 +269,7 @@ namespace BTCPayServer.Tests
public InvoiceRepository InvoiceRepository { get; private set; }
public StoreRepository StoreRepository { get; private set; }
public BTCPayNetworkProvider Networks { get; private set; }
public Uri IntegratedLightning { get; internal set; }
public string IntegratedLightning { get; internal set; }
public bool InContainer { get; internal set; }
public T GetService<T>()

@ -1,13 +1,10 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Lightning;
using BTCPayServer.Payments;
using BTCPayServer.Tests.Logging;
using BTCPayServer.Views.Stores;
using NBitcoin;
using OpenQA.Selenium;
using OpenQA.Selenium.Support.UI;
using Xunit;
using Xunit.Abstractions;
@ -36,7 +33,7 @@ namespace BTCPayServer.Tests
s.AddDerivationScheme("BTC");
s.GoToStore(store.storeId, StoreNavPages.Checkout);
s.Driver.FindElement(By.Id("RequiresRefundEmail")).Click();
s.Driver.FindElement(By.Name("command")).ForceClick();
s.Driver.FindElement(By.Name("command")).Click();
var emailAlreadyThereInvoiceId = s.CreateInvoice(store.storeName, 100, "USD", "a@g.com");
s.GoToInvoiceCheckout(emailAlreadyThereInvoiceId);
@ -112,14 +109,14 @@ namespace BTCPayServer.Tests
s.Server.ActivateLightning();
await s.StartAsync();
s.GoToRegister();
s.RegisterNewUser();
s.RegisterNewUser(true);
var store = s.CreateNewStore();
s.AddInternalLightningNode("BTC");
s.AddLightningNode();
s.GoToStore(store.storeId, StoreNavPages.Checkout);
s.SetCheckbox(s, "LightningAmountInSatoshi", true);
s.Driver.SetCheckbox(By.Id("LightningAmountInSatoshi"), true);
var command = s.Driver.FindElement(By.Name("command"));
command.ForceClick();
command.Click();
var invoiceId = s.CreateInvoice(store.storeName, 10, "USD", "a@g.com");
s.GoToInvoiceCheckout(invoiceId);
Assert.Contains("Sats", s.Driver.FindElement(By.ClassName("payment__currencies_noborder")).Text);
@ -166,30 +163,4 @@ namespace BTCPayServer.Tests
}
}
}
public static class SeleniumExtensions
{
/// <summary>
/// Utility method to wait until timeout for element to be present (optionally displayed)
/// </summary>
/// <param name="context">Wait context</param>
/// <param name="by">How we search for element</param>
/// <param name="displayed">Flag to wait for element to be displayed or just present</param>
/// <param name="timeout">How long to wait for element to be present/displayed</param>
/// <returns>Element we were waiting for</returns>
public static IWebElement WaitForElement(this IWebDriver context, By by, bool displayed = true, uint timeout = 3)
{
var wait = new DefaultWait<IWebDriver>(context);
wait.Timeout = TimeSpan.FromSeconds(timeout);
wait.IgnoreExceptionTypes(typeof(NoSuchElementException));
return wait.Until(ctx =>
{
var elem = ctx.FindElement(by);
if (displayed && !elem.Displayed)
return null;
return elem;
});
}
}
}

@ -7,30 +7,24 @@ using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using OpenQA.Selenium;
using OpenQA.Selenium.Support.UI;
using Xunit;
namespace BTCPayServer.Tests
{
public static class Extensions
{
private static readonly JsonSerializerSettings jsonSettings = new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() };
public static string ToJson(this object o)
private static readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() };
public static string ToJson(this object o) => JsonConvert.SerializeObject(o, Formatting.None, JsonSettings);
public static void LogIn(this SeleniumTester s, string email)
{
var res = JsonConvert.SerializeObject(o, Formatting.None, jsonSettings);
return res;
}
public static void ScrollTo(this IWebDriver driver, By by)
{
var element = driver.FindElement(by);
}
/// <summary>
/// Sometimes the chrome driver is fucked up and we need some magic to click on the element.
/// </summary>
/// <param name="element"></param>
public static void ForceClick(this IWebElement element)
{
element.SendKeys(Keys.Return);
s.Driver.FindElement(By.Id("Email")).SendKeys(email);
s.Driver.FindElement(By.Id("Password")).SendKeys("123456");
s.Driver.FindElement(By.Id("LoginButton")).Click();
s.Driver.AssertNoError();
}
public static void AssertNoError(this IWebDriver driver)
{
try
@ -57,14 +51,18 @@ namespace BTCPayServer.Tests
builder.AppendLine($"[{entry.Level}]: {entry.Message}");
}
}
catch { }
builder.AppendLine($"---------");
catch
{
// ignored
}
builder.AppendLine("---------");
}
Logs.Tester.LogInformation(builder.ToString());
builder = new StringBuilder();
builder.AppendLine($"Selenium [Sources]:");
builder.AppendLine("Selenium [Sources]:");
builder.AppendLine(driver.PageSource);
builder.AppendLine($"---------");
builder.AppendLine("---------");
Logs.Tester.LogInformation(builder.ToString());
throw;
}
@ -108,5 +106,49 @@ namespace BTCPayServer.Tests
}
Assert.False(true, "Elements was found");
}
public static void UntilJsIsReady(this WebDriverWait wait)
{
wait.Until(d=>((IJavaScriptExecutor)d).ExecuteScript("return document.readyState").Equals("complete"));
wait.Until(d=>((IJavaScriptExecutor)d).ExecuteScript("return typeof(jQuery) === 'undefined' || jQuery.active === 0").Equals(true));
}
public static IWebElement WaitForElement(this IWebDriver driver, By selector)
{
var wait = new WebDriverWait(driver, SeleniumTester.ImplicitWait);
wait.UntilJsIsReady();
var el = driver.FindElement(selector);
wait.Until(d => el.Displayed);
return el;
}
public static void WaitForAndClick(this IWebDriver driver, By selector)
{
var wait = new WebDriverWait(driver, SeleniumTester.ImplicitWait);
wait.UntilJsIsReady();
var el = driver.FindElement(selector);
wait.Until(d => el.Displayed && el.Enabled);
el.Click();
wait.UntilJsIsReady();
}
public static void SetCheckbox(this IWebDriver driver, By selector, bool value)
{
var element = driver.FindElement(selector);
if ((value && !element.Selected) || (!value && element.Selected))
{
driver.WaitForAndClick(selector);
}
if (value != element.Selected)
{
Logs.Tester.LogInformation("SetCheckbox recursion, trying to click again");
driver.SetCheckbox(selector, value);
}
}
}
}

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
@ -19,6 +20,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using NBitcoin;
using NBitcoin.OpenAsset;
using NBitcoin.Payment;
using NBitpayClient;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
@ -540,7 +542,7 @@ namespace BTCPayServer.Tests
}
}
private async Task AssertValidationError(string[] fields, Func<Task> act)
private async Task<GreenFieldValidationException> AssertValidationError(string[] fields, Func<Task> act)
{
var remainingFields = fields.ToHashSet();
var ex = await Assert.ThrowsAsync<GreenFieldValidationException>(act);
@ -550,6 +552,7 @@ namespace BTCPayServer.Tests
remainingFields.Remove(field);
}
Assert.Empty(remainingFields);
return ex;
}
private async Task AssertHttpError(int code, Func<Task> act)
@ -948,8 +951,12 @@ namespace BTCPayServer.Tests
});
await user.RegisterDerivationSchemeAsync("BTC");
var newInvoice = await client.CreateInvoice(user.StoreId,
new CreateInvoiceRequest() { Currency = "USD", Amount = 1, Metadata = JObject.Parse("{\"itemCode\": \"testitem\"}") });
new CreateInvoiceRequest() { Currency = "USD", Amount = 1, Metadata = JObject.Parse("{\"itemCode\": \"testitem\"}"), Checkout = new CreateInvoiceRequest.CheckoutOptions()
{
RedirectAutomatically = true
}});
Assert.True(newInvoice.Checkout.RedirectAutomatically);
//list
var invoices = await viewOnly.GetInvoices(user.StoreId);
@ -968,25 +975,28 @@ namespace BTCPayServer.Tests
//update
invoice = await viewOnly.GetInvoice(user.StoreId, newInvoice.Id);
await AssertValidationError(new[] { nameof(MarkInvoiceStatusRequest.Status) }, async () =>
newInvoice = await client.CreateInvoice(user.StoreId,
new CreateInvoiceRequest() { Currency = "USD", Amount = 1 });
await client.MarkInvoiceStatus(user.StoreId, newInvoice.Id, new MarkInvoiceStatusRequest()
{
await client.MarkInvoiceStatus(user.StoreId, invoice.Id, new MarkInvoiceStatusRequest()
{
Status = InvoiceStatus.Settled
});
Status = InvoiceStatus.Settled
});
newInvoice = await client.CreateInvoice(user.StoreId,
new CreateInvoiceRequest() { Currency = "USD", Amount = 1 });
await client.MarkInvoiceStatus(user.StoreId, newInvoice.Id, new MarkInvoiceStatusRequest()
{
Status = InvoiceStatus.Invalid
});
await AssertHttpError(403, async () =>
{
await viewOnly.UpdateInvoice(user.StoreId, newInvoice.Id,
await viewOnly.UpdateInvoice(user.StoreId, invoice.Id,
new UpdateInvoiceRequest()
{
Metadata = JObject.Parse("{\"itemCode\": \"updated\", newstuff: [1,2,3,4,5]}")
});
});
invoice = await client.UpdateInvoice(user.StoreId, newInvoice.Id,
invoice = await client.UpdateInvoice(user.StoreId, invoice.Id,
new UpdateInvoiceRequest()
{
Metadata = JObject.Parse("{\"itemCode\": \"updated\", newstuff: [1,2,3,4,5]}")
@ -994,6 +1004,12 @@ namespace BTCPayServer.Tests
Assert.Equal("updated",invoice.Metadata["itemCode"].Value<string>());
Assert.Equal(15,((JArray) invoice.Metadata["newstuff"]).Values<int>().Sum());
//also test the the metadata actually got saved
invoice = await client.GetInvoice(user.StoreId, invoice.Id);
Assert.Equal("updated",invoice.Metadata["itemCode"].Value<string>());
Assert.Equal(15,((JArray) invoice.Metadata["newstuff"]).Values<int>().Sum());
//archive
await AssertHttpError(403, async () =>
{
@ -1077,10 +1093,28 @@ namespace BTCPayServer.Tests
{
Assert.Equal("pt-PT", langs.FindBestMatch(match).Code);
}
//payment method activation tests
var store = await client.GetStore(user.StoreId);
Assert.False(store.LazyPaymentMethods);
store.LazyPaymentMethods = true;
store = await client.UpdateStore(store.Id,
JObject.FromObject(store).ToObject<UpdateStoreRequest>());
Assert.True(store.LazyPaymentMethods);
invoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest() {Amount = 1, Currency = "USD"});
paymentMethods = await client.GetInvoicePaymentMethods(store.Id, invoice.Id);
Assert.Single(paymentMethods);
Assert.False(paymentMethods.First().Activated);
await client.ActivateInvoicePaymentMethod(user.StoreId, invoice.Id,
paymentMethods.First().PaymentMethod);
paymentMethods = await client.GetInvoicePaymentMethods(store.Id, invoice.Id);
Assert.Single(paymentMethods);
Assert.True(paymentMethods.First().Activated);
}
}
[Fact(Timeout = 60 * 2 * 1000)]
[Fact(Timeout = 60 * 20 * 1000)]
[Trait("Integration", "Integration")]
[Trait("Lightning", "Lightning")]
public async Task CanUseLightningAPI()
@ -1098,8 +1132,7 @@ namespace BTCPayServer.Tests
merchant.GrantAccess(true);
merchant.RegisterLightningNode("BTC", LightningConnectionType.LndREST);
var merchantClient = await merchant.CreateClient($"{Policies.CanUseLightningNodeInStore}:{merchant.StoreId}");
var merchantInvoice = await merchantClient.CreateLightningInvoice(merchant.StoreId, "BTC", new CreateLightningInvoiceRequest(new LightMoney(1_000), "hey", TimeSpan.FromSeconds(60)));
tester.PayTester.GetService<BTCPayServerEnvironment>().DevelopmentOverride = false;
var merchantInvoice = await merchantClient.CreateLightningInvoice(merchant.StoreId, "BTC", new CreateLightningInvoiceRequest(LightMoney.Satoshis(1_000), "hey", TimeSpan.FromSeconds(60)));
// The default client is using charge, so we should not be able to query channels
var client = await user.CreateClient(Policies.CanUseInternalLightningNode);
@ -1170,7 +1203,6 @@ namespace BTCPayServer.Tests
}
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task NotificationAPITests()
@ -1267,8 +1299,6 @@ namespace BTCPayServer.Tests
});
}
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Lightning", "Lightning")]
[Trait("Integration", "Integration")]
@ -1278,33 +1308,86 @@ namespace BTCPayServer.Tests
tester.ActivateLightning();
await tester.StartAsync();
await tester.EnsureChannelsSetup();
var user = tester.NewAccount();
await user.GrantAccessAsync(true);
var client = await user.CreateClient(Policies.CanModifyStoreSettings);
var viewOnlyClient = await user.CreateClient(Policies.CanViewStoreSettings);
tester.PayTester.GetService<BTCPayServerEnvironment>().DevelopmentOverride = false;
var store = await client.GetStore(user.StoreId);
var admin = tester.NewAccount();
await admin.GrantAccessAsync(true);
var admin2 = tester.NewAccount();
await admin2.GrantAccessAsync(true);
var adminClient = await admin.CreateClient(Policies.CanModifyStoreSettings);
var admin2Client = await admin2.CreateClient(Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings);
var viewOnlyClient = await admin.CreateClient(Policies.CanViewStoreSettings);
var store = await adminClient.GetStore(admin.StoreId);
Assert.Empty(await client.GetStoreLightningNetworkPaymentMethods(store.Id));
Assert.Empty(await adminClient.GetStoreLightningNetworkPaymentMethods(store.Id));
await AssertHttpError(403, async () =>
{
await viewOnlyClient.UpdateStoreLightningNetworkPaymentMethod(store.Id, "BTC", new LightningNetworkPaymentMethodData() { });
});
await AssertHttpError(404, async () =>
{
await client.GetStoreLightningNetworkPaymentMethod(store.Id, "BTC");
await adminClient.GetStoreLightningNetworkPaymentMethod(store.Id, "BTC");
});
await user.RegisterLightningNodeAsync("BTC", LightningConnectionType.CLightning, false);
await admin.RegisterLightningNodeAsync("BTC", false);
var method = await client.GetStoreLightningNetworkPaymentMethod(store.Id, "BTC");
var method = await adminClient.GetStoreLightningNetworkPaymentMethod(store.Id, "BTC");
await AssertHttpError(403, async () =>
{
await viewOnlyClient.RemoveStoreOnChainPaymentMethod(store.Id, "BTC");
});
await client.RemoveStoreOnChainPaymentMethod(store.Id, "BTC");
await adminClient.RemoveStoreOnChainPaymentMethod(store.Id, "BTC");
await AssertHttpError(404, async () =>
{
await client.GetStoreOnChainPaymentMethod(store.Id, "BTC");
await adminClient.GetStoreOnChainPaymentMethod(store.Id, "BTC");
});
// Let's verify that the admin client can't change LN to unsafe connection strings without modify server settings rights
foreach (var forbidden in new string[]
{
"type=clightning;server=tcp://127.0.0.1",
"type=clightning;server=tcp://test",
"type=clightning;server=tcp://test.lan",
"type=clightning;server=tcp://test.local",
"type=clightning;server=tcp://192.168.1.2",
"type=clightning;server=unix://8.8.8.8",
"type=clightning;server=unix://[::1]",
"type=clightning;server=unix://[0:0:0:0:0:0:0:1]",
})
{
var ex = await AssertValidationError(new[] { "ConnectionString" }, async () =>
{
await adminClient.UpdateStoreLightningNetworkPaymentMethod(store.Id, "BTC", new LightningNetworkPaymentMethodData()
{
ConnectionString = forbidden,
CryptoCode = "BTC",
Enabled = true
});
});
Assert.Contains("btcpay.server.canmodifyserversettings", ex.Message);
// However, the other client should work because he has `btcpay.server.canmodifyserversettings`
await admin2Client.UpdateStoreLightningNetworkPaymentMethod(admin2.StoreId, "BTC", new LightningNetworkPaymentMethodData()
{
ConnectionString = forbidden,
CryptoCode = "BTC",
Enabled = true
});
}
// Allowed ip should be ok
await adminClient.UpdateStoreLightningNetworkPaymentMethod(store.Id, "BTC", new LightningNetworkPaymentMethodData()
{
ConnectionString = "type=clightning;server=tcp://8.8.8.8",
CryptoCode = "BTC",
Enabled = true
});
// If we strip the admin's right, he should not be able to set unsafe anymore, even if the API key is still valid
await admin2.MakeAdmin(false);
await AssertValidationError(new[] { "ConnectionString" }, async () =>
{
await admin2Client.UpdateStoreLightningNetworkPaymentMethod(admin2.StoreId, "BTC", new LightningNetworkPaymentMethodData()
{
ConnectionString = "type=clightning;server=tcp://127.0.0.1",
CryptoCode = "BTC",
Enabled = true
});
});
var settings = (await tester.PayTester.GetService<SettingsRepository>().GetSettingAsync<PoliciesSettings>())?? new PoliciesSettings();
@ -1329,7 +1412,227 @@ namespace BTCPayServer.Tests
await nonAdminUserClient.UpdateStoreLightningNetworkPaymentMethod(nonAdminUser.StoreId, "BTC", method);
}
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")]
public async Task WalletAPITests()
{
using var tester = ServerTester.Create();
await tester.StartAsync();
var user = tester.NewAccount();
await user.GrantAccessAsync(true);
var client = await user.CreateClient(Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings);
var viewOnlyClient = await user.CreateClient(Policies.CanViewStoreSettings);
var walletId = await user.RegisterDerivationSchemeAsync("BTC", ScriptPubKeyType.Segwit, true);
//view only clients can't do jack shit with this API
await AssertHttpError(403, async () =>
{
await viewOnlyClient.ShowOnChainWalletOverview(walletId.StoreId, walletId.CryptoCode );
});
var overview = await client.ShowOnChainWalletOverview(walletId.StoreId, walletId.CryptoCode );
Assert.Equal(0m, overview.Balance);
var fee = await client.GetOnChainFeeRate(walletId.StoreId, walletId.CryptoCode );
Assert.NotNull( fee.FeeRate);
await AssertHttpError(403, async () =>
{
await viewOnlyClient.GetOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode );
});
var address = await client.GetOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode );
var address2 = await client.GetOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode );
var address3 = await client.GetOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode, true );
Assert.Equal(address.Address, address2.Address);
Assert.NotEqual(address.Address, address3.Address);
await AssertHttpError(403, async () =>
{
await viewOnlyClient.GetOnChainWalletUTXOs(walletId.StoreId, walletId.CryptoCode);
});
Assert.Empty(await client.GetOnChainWalletUTXOs(walletId.StoreId, walletId.CryptoCode));
uint256 txhash = null;
await tester.WaitForEvent<NewOnChainTransactionEvent>(async () =>
{
txhash = await tester.ExplorerNode.SendToAddressAsync(
BitcoinAddress.Create(address3.Address, tester.ExplorerClient.Network.NBitcoinNetwork),
new Money(0.01m, MoneyUnit.BTC));
});
await tester.ExplorerNode.GenerateAsync(1);
var address4 = await client.GetOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode, false );
Assert.NotEqual(address3.Address, address4.Address);
await client.UnReserveOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode);
var address5 = await client.GetOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode, true );
Assert.Equal(address5.Address, address4.Address);
var utxo = Assert.Single(await client.GetOnChainWalletUTXOs(walletId.StoreId, walletId.CryptoCode));
Assert.Equal(0.01m, utxo.Amount);
Assert.Equal(txhash, utxo.Outpoint.Hash);
overview = await client.ShowOnChainWalletOverview(walletId.StoreId, walletId.CryptoCode );
Assert.Equal(0.01m, overview.Balance);
//the simplest request:
var nodeAddress = await tester.ExplorerNode.GetNewAddressAsync();
var createTxRequest = new CreateOnChainTransactionRequest()
{
Destinations =
new List<CreateOnChainTransactionRequest.CreateOnChainTransactionRequestDestination>()
{
new CreateOnChainTransactionRequest.CreateOnChainTransactionRequestDestination()
{
Destination = nodeAddress.ToString(), Amount = 0.001m
}
},
FeeRate = new FeeRate(5m) //only because regtest may fail but not required
};
await AssertHttpError(403, async () =>
{
await viewOnlyClient.CreateOnChainTransaction(walletId.StoreId, walletId.CryptoCode, createTxRequest );
});
await Assert.ThrowsAsync<ArgumentOutOfRangeException>(async () =>
{
await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode,
createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork);
});
await Assert.ThrowsAsync<ArgumentOutOfRangeException>(async () =>
{
createTxRequest.ProceedWithBroadcast = false;
await client.CreateOnChainTransaction(walletId.StoreId, walletId.CryptoCode,
createTxRequest);
});
Transaction tx;
tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode,
createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork);
Assert.NotNull(tx);
Assert.Contains(tx.Outputs, txout => txout.IsTo(nodeAddress) && txout.Value.ToDecimal(MoneyUnit.BTC) == 0.001m);
Assert.True((await tester.ExplorerNode.TestMempoolAcceptAsync(tx)).IsAllowed);
// no change test
createTxRequest.NoChange = true;
tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode,
createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork);
Assert.NotNull(tx);
Assert.True(Assert.Single(tx.Outputs).IsTo(nodeAddress) );
Assert.True((await tester.ExplorerNode.TestMempoolAcceptAsync(tx)).IsAllowed);
createTxRequest.NoChange = false;
//coin selection
await AssertValidationError(new []{nameof(createTxRequest.SelectedInputs)}, async () =>
{
createTxRequest.SelectedInputs = new List<OutPoint>();
tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode,
createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork);
});
createTxRequest.SelectedInputs = new List<OutPoint>()
{
utxo.Outpoint
};
tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode,
createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork);
createTxRequest.SelectedInputs = null;
//destination testing
await AssertValidationError(new []{ "Destinations"}, async () =>
{
createTxRequest.Destinations[0].Amount = utxo.Amount;
tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode,
createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork);
});
createTxRequest.Destinations[0].SubtractFromAmount = true;
tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode,
createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork);
await AssertValidationError(new []{ "Destinations[0]"}, async () =>
{
createTxRequest.Destinations[0].Amount = 0m;
tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode,
createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork);
});
//dest can be a bip21
//cant use bip with subtractfromamount
createTxRequest.Destinations[0].Amount = null;
createTxRequest.Destinations[0].Destination = $"bitcoin:{nodeAddress}?amount=0.001";
await AssertValidationError(new []{ "Destinations[0]"}, async () =>
{
tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode,
createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork);
});
//if amt specified, it overrides bip21 amount
createTxRequest.Destinations[0].Amount = 0.0001m;
createTxRequest.Destinations[0].SubtractFromAmount = false;
tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode,
createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork);
Assert.Contains(tx.Outputs, txout => txout.Value.GetValue(tester.NetworkProvider.GetNetwork<BTCPayNetwork>("BTC")) ==0.0001m );
//fee rate test
createTxRequest.FeeRate = FeeRate.Zero;
await AssertValidationError(new []{ "FeeRate"}, async () =>
{
tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode,
createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork);
});
createTxRequest.FeeRate = new FeeRate(5.0m);
createTxRequest.Destinations[0].Amount = 0.001m;
createTxRequest.Destinations[0].Destination = nodeAddress.ToString();
createTxRequest.Destinations[0].SubtractFromAmount = false;
await AssertHttpError(403, async () =>
{
await viewOnlyClient.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode,
createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork);
});
createTxRequest.ProceedWithBroadcast = true;
var txdata=
await client.CreateOnChainTransaction(walletId.StoreId, walletId.CryptoCode,
createTxRequest);
Assert.Equal(TransactionStatus.Unconfirmed, txdata.Status);
Assert.Null(txdata.BlockHeight);
Assert.Null(txdata.BlockHash);
Assert.NotNull(await tester.ExplorerClient.GetTransactionAsync(txdata.TransactionHash));
await AssertHttpError(403, async () =>
{
await viewOnlyClient.GetOnChainWalletTransaction(walletId.StoreId, walletId.CryptoCode, txdata.TransactionHash.ToString());
});
await client.GetOnChainWalletTransaction(walletId.StoreId, walletId.CryptoCode, txdata.TransactionHash.ToString());
await AssertHttpError(403, async () =>
{
await viewOnlyClient.ShowOnChainWalletTransactions(walletId.StoreId, walletId.CryptoCode);
});
Assert.True(Assert.Single(
await client.ShowOnChainWalletTransactions(walletId.StoreId, walletId.CryptoCode,
new[] {TransactionStatus.Confirmed})).TransactionHash == utxo.Outpoint.Hash);
Assert.Contains(
await client.ShowOnChainWalletTransactions(walletId.StoreId, walletId.CryptoCode,
new[] {TransactionStatus.Unconfirmed}), data => data.TransactionHash == txdata.TransactionHash);
Assert.Contains(
await client.ShowOnChainWalletTransactions(walletId.StoreId, walletId.CryptoCode), data => data.TransactionHash == txdata.TransactionHash);
await tester.WaitForEvent<NewBlockEvent>(async () =>
{
await tester.ExplorerNode.GenerateAsync(1);
}, bevent => bevent.CryptoCode.Equals("BTC", StringComparison.Ordinal));
Assert.Contains(
await client.ShowOnChainWalletTransactions(walletId.StoreId, walletId.CryptoCode,
new[] {TransactionStatus.Confirmed}), data => data.TransactionHash == txdata.TransactionHash);
}
[Fact(Timeout = TestTimeout)]
[Trait("Fast", "Fast")]
public void NumericJsonConverterTests()
@ -1345,7 +1648,6 @@ namespace BTCPayServer.Tests
Assert.True(jsonConverter.CanConvert(typeof(double)));
Assert.True(jsonConverter.CanConvert(typeof(double?)));
Assert.False(jsonConverter.CanConvert(typeof(float)));
Assert.False(jsonConverter.CanConvert(typeof(int)));
Assert.False(jsonConverter.CanConvert(typeof(string)));
var numberJson = "1";

@ -8,10 +8,10 @@ using BTCPayServer.Client.Models;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Models;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payments.PayJoin;
using BTCPayServer.Payments.PayJoin.Sender;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Wallets;
@ -19,12 +19,14 @@ using BTCPayServer.Tests.Logging;
using BTCPayServer.Views.Wallets;
using Microsoft.AspNetCore.Http;
using NBitcoin;
using BTCPayServer.BIP78.Sender;
using NBitcoin.Payment;
using NBitpayClient;
using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
using Newtonsoft.Json.Linq;
using OpenQA.Selenium;
using OpenQA.Selenium.Support.UI;
using Xunit;
using Xunit.Abstractions;
@ -85,6 +87,19 @@ namespace BTCPayServer.Tests
Assert.True(await repo.TryLock(outpoint));
Assert.True(await repo.TryUnlock(outpoint));
Assert.False(await repo.TryUnlock(outpoint));
// Make sure that if any can't be locked, all are not locked
var outpoint1 = RandomOutpoint();
var outpoint2 = RandomOutpoint();
Assert.True(await repo.TryLockInputs(new[] { outpoint1 }));
Assert.False(await repo.TryLockInputs(new[] { outpoint1, outpoint2 }));
Assert.True(await repo.TryLockInputs(new[] { outpoint2 }));
outpoint1 = RandomOutpoint();
outpoint2 = RandomOutpoint();
Assert.True(await repo.TryLockInputs(new[] { outpoint1 }));
Assert.False(await repo.TryLockInputs(new[] { outpoint2, outpoint1 }));
Assert.True(await repo.TryLockInputs(new[] { outpoint2 }));
}
}
@ -164,9 +179,7 @@ namespace BTCPayServer.Tests
var cashCow = tester.ExplorerNode;
cashCow.Generate(2); // get some money in case
var unsupportedFormats = Enum.GetValues(typeof(ScriptPubKeyType))
.AssertType<ScriptPubKeyType[]>()
.Where(type => !PayjoinClient.SupportedFormats.Contains(type));
var unsupportedFormats = new[] {ScriptPubKeyType.Legacy};
foreach (ScriptPubKeyType senderAddressType in Enum.GetValues(typeof(ScriptPubKeyType)))
@ -219,7 +232,7 @@ namespace BTCPayServer.Tests
var invoiceRepository = s.Server.PayTester.GetService<InvoiceRepository>();
s.RegisterNewUser(true);
foreach (var format in PayjoinClient.SupportedFormats)
foreach (var format in new []{ScriptPubKeyType.Segwit, ScriptPubKeyType.SegwitP2SH})
{
var receiver = s.CreateNewStore();
var receiverSeed = s.GenerateWallet("BTC", "", true, true, format);
@ -236,9 +249,10 @@ namespace BTCPayServer.Tests
s.GoToStore(receiver.storeId);
//payjoin is not enabled by default.
Assert.False(s.Driver.FindElement(By.Id("PayJoinEnabled")).Selected);
s.SetCheckbox(s, "PayJoinEnabled", true);
s.Driver.SetCheckbox(By.Id("PayJoinEnabled"), true);
s.Driver.FindElement(By.Id("Save")).Click();
Assert.True(s.Driver.FindElement(By.Id("PayJoinEnabled")).Selected);
var sender = s.CreateNewStore();
var senderSeed = s.GenerateWallet("BTC", "", true, true, format);
var senderWalletId = new WalletId(sender.storeId, "BTC");
@ -257,16 +271,17 @@ namespace BTCPayServer.Tests
s.Driver.SwitchTo().Alert().Accept();
Assert.False(string.IsNullOrEmpty(s.Driver.FindElement(By.Id("PayJoinBIP21"))
.GetAttribute("value")));
s.Driver.ScrollTo(By.Id("SendMenu"));
s.Driver.FindElement(By.Id("SendMenu")).ForceClick();
s.Driver.FindElement(By.CssSelector("button[value=nbx-seed]")).Click();
s.Driver.FindElement(By.Id("SendMenu")).Click();
var nbxSeedButton = s.Driver.FindElement(By.CssSelector("button[value=nbx-seed]"));
new WebDriverWait(s.Driver, SeleniumTester.ImplicitWait).Until(d=> nbxSeedButton.Enabled);
nbxSeedButton.Click();
await s.Server.WaitForEvent<NewOnChainTransactionEvent>(() =>
{
s.Driver.FindElement(By.CssSelector("button[value=payjoin]")).ForceClick();
s.Driver.FindElement(By.CssSelector("button[value=payjoin]")).Click();
return Task.CompletedTask;
});
//no funds in receiver wallet to do payjoin
s.AssertHappyMessage(StatusMessageModel.StatusSeverity.Warning);
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Warning);
await TestUtils.EventuallyAsync(async () =>
{
var invoice = await s.Server.PayTester.GetService<InvoiceRepository>().GetInvoice(invoiceId);
@ -294,15 +309,14 @@ namespace BTCPayServer.Tests
.GetAttribute("value")));
s.Driver.FindElement(By.Id("FeeSatoshiPerByte")).Clear();
s.Driver.FindElement(By.Id("FeeSatoshiPerByte")).SendKeys("2");
s.Driver.ScrollTo(By.Id("SendMenu"));
s.Driver.FindElement(By.Id("SendMenu")).ForceClick();
s.Driver.FindElement(By.Id("SendMenu")).Click();
s.Driver.FindElement(By.CssSelector("button[value=nbx-seed]")).Click();
var txId = await s.Server.WaitForEvent<NewOnChainTransactionEvent>(() =>
{
s.Driver.FindElement(By.CssSelector("button[value=payjoin]")).ForceClick();
s.Driver.FindElement(By.CssSelector("button[value=payjoin]")).Click();
return Task.CompletedTask;
});
s.AssertHappyMessage(StatusMessageModel.StatusSeverity.Success);
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success);
await TestUtils.EventuallyAsync(async () =>
{
var invoice = await invoiceRepository.GetInvoice(invoiceId);
@ -363,10 +377,15 @@ namespace BTCPayServer.Tests
var alice = tester.NewAccount();
await alice.RegisterDerivationSchemeAsync("BTC", ScriptPubKeyType.Segwit, true);
await notifications.ListenDerivationSchemesAsync(new[] { alice.DerivationScheme });
var address = (await nbx.GetUnusedAsync(alice.DerivationScheme, DerivationFeature.Deposit)).Address;
await tester.ExplorerNode.GenerateAsync(1);
tester.ExplorerNode.SendToAddress(address, Money.Coins(1.0m));
await notifications.NextEventAsync();
BitcoinAddress address = null;
for (int i = 0; i < 5; i++)
{
address = (await nbx.GetUnusedAsync(alice.DerivationScheme, DerivationFeature.Deposit)).Address;
await tester.ExplorerNode.GenerateAsync(1);
tester.ExplorerNode.SendToAddress(address, Money.Coins(1.0m));
await notifications.NextEventAsync();
}
var paymentAddress = new Key().PubKey.GetAddress(ScriptPubKeyType.Legacy, Network.RegTest);
var otherAddress = new Key().PubKey.GetAddress(ScriptPubKeyType.Legacy, Network.RegTest);
var psbt = (await nbx.CreatePSBTAsync(alice.DerivationScheme, new CreatePSBTRequest()
@ -408,7 +427,7 @@ namespace BTCPayServer.Tests
using var fakeServer = new FakeServer();
await fakeServer.Start();
var bip21 = new BitcoinUrlBuilder($"bitcoin:{paymentAddress}?pj={fakeServer.ServerUri}", Network.RegTest);
var requesting = pjClient.RequestPayjoin(bip21, derivationSchemeSettings, psbt, default);
var requesting = pjClient.RequestPayjoin(bip21, new PayjoinWallet(derivationSchemeSettings), psbt, default);
var request = await fakeServer.GetNextRequest();
Assert.Equal("1", request.Request.Query["v"][0]);
Assert.Equal(changeIndex.ToString(), request.Request.Query["additionalfeeoutputindex"][0]);
@ -424,7 +443,7 @@ namespace BTCPayServer.Tests
Assert.Contains("contribution is more than maxadditionalfeecontribution", ex.Message);
Logs.Tester.LogInformation("The payjoin receiver tries to change one of our output");
requesting = pjClient.RequestPayjoin(bip21, derivationSchemeSettings, psbt, default);
requesting = pjClient.RequestPayjoin(bip21, new PayjoinWallet(derivationSchemeSettings), psbt, default);
request = await fakeServer.GetNextRequest();
originalPSBT = await ParsePSBT(request);
proposalTx = originalPSBT.GetGlobalTransaction();
@ -433,9 +452,8 @@ namespace BTCPayServer.Tests
fakeServer.Done();
ex = await Assert.ThrowsAsync<PayjoinSenderException>(async () => await requesting);
Assert.Contains("The receiver decreased the value of one", ex.Message);
Logs.Tester.LogInformation("The payjoin receiver tries to pocket the fee");
requesting = pjClient.RequestPayjoin(bip21, derivationSchemeSettings, psbt, default);
requesting = pjClient.RequestPayjoin(bip21, new PayjoinWallet(derivationSchemeSettings), psbt, default);
request = await fakeServer.GetNextRequest();
originalPSBT = await ParsePSBT(request);
proposalTx = originalPSBT.GetGlobalTransaction();
@ -446,7 +464,7 @@ namespace BTCPayServer.Tests
Assert.Contains("The receiver decreased absolute fee", ex.Message);
Logs.Tester.LogInformation("The payjoin receiver tries to remove one of our output");
requesting = pjClient.RequestPayjoin(bip21, derivationSchemeSettings, psbt, default);
requesting = pjClient.RequestPayjoin(bip21, new PayjoinWallet(derivationSchemeSettings), psbt, default);
request = await fakeServer.GetNextRequest();
originalPSBT = await ParsePSBT(request);
proposalTx = originalPSBT.GetGlobalTransaction();
@ -458,7 +476,7 @@ namespace BTCPayServer.Tests
Assert.Contains("Some of our outputs are not included in the proposal", ex.Message);
Logs.Tester.LogInformation("The payjoin receiver tries to change their own output");
requesting = pjClient.RequestPayjoin(bip21, derivationSchemeSettings, psbt, default);
requesting = pjClient.RequestPayjoin(bip21, new PayjoinWallet(derivationSchemeSettings), psbt, default);
request = await fakeServer.GetNextRequest();
originalPSBT = await ParsePSBT(request);
proposalTx = originalPSBT.GetGlobalTransaction();
@ -470,7 +488,7 @@ namespace BTCPayServer.Tests
Logs.Tester.LogInformation("The payjoin receiver tries to send money to himself");
pjClient.MaxFeeBumpContribution = Money.Satoshis(1);
requesting = pjClient.RequestPayjoin(bip21, derivationSchemeSettings, psbt, default);
requesting = pjClient.RequestPayjoin(bip21, new PayjoinWallet(derivationSchemeSettings), psbt, default);
request = await fakeServer.GetNextRequest();
originalPSBT = await ParsePSBT(request);
proposalTx = originalPSBT.GetGlobalTransaction();
@ -481,10 +499,9 @@ namespace BTCPayServer.Tests
ex = await Assert.ThrowsAsync<PayjoinSenderException>(async () => await requesting);
Assert.Contains("is not only paying fee", ex.Message);
pjClient.MaxFeeBumpContribution = null;
Logs.Tester.LogInformation("The payjoin receiver can't use additional fee without adding inputs");
pjClient.MinimumFeeRate = new FeeRate(50m);
requesting = pjClient.RequestPayjoin(bip21, derivationSchemeSettings, psbt, default);
requesting = pjClient.RequestPayjoin(bip21, new PayjoinWallet(derivationSchemeSettings), psbt, default);
request = await fakeServer.GetNextRequest();
originalPSBT = await ParsePSBT(request);
proposalTx = originalPSBT.GetGlobalTransaction();
@ -499,6 +516,9 @@ namespace BTCPayServer.Tests
var bob = tester.NewAccount();
await bob.GrantAccessAsync();
await bob.RegisterDerivationSchemeAsync("BTC", ScriptPubKeyType.Segwit, true);
await notifications.DisposeAsync();
notifications = await nbx.CreateWebsocketNotificationSessionAsync();
await notifications.ListenDerivationSchemesAsync(new[] { bob.DerivationScheme });
address = (await nbx.GetUnusedAsync(bob.DerivationScheme, DerivationFeature.Deposit)).Address;
tester.ExplorerNode.SendToAddress(address, Money.Coins(1.1m));
@ -508,6 +528,7 @@ namespace BTCPayServer.Tests
new Invoice() { Price = 0.1m, Currency = "BTC", FullNotifications = true });
var invoiceBIP21 = new BitcoinUrlBuilder(invoice.CryptoInfo.First().PaymentUrls.BIP21,
tester.ExplorerClient.Network.NBitcoinNetwork);
psbt = (await nbx.CreatePSBTAsync(alice.DerivationScheme, new CreatePSBTRequest()
{
Destinations =
@ -526,7 +547,7 @@ namespace BTCPayServer.Tests
psbt.SignAll(derivationSchemeSettings.AccountDerivation, alice.GenerateWalletResponseV.AccountHDKey, signingAccount.GetRootedKeyPath());
var endpoint = TestAccount.GetPayjoinBitcoinUrl(invoice, Network.RegTest);
pjClient.MaxFeeBumpContribution = Money.Satoshis(50);
var proposal = await pjClient.RequestPayjoin(endpoint, derivationSchemeSettings, psbt, default);
var proposal = await pjClient.RequestPayjoin(endpoint, new PayjoinWallet(derivationSchemeSettings), psbt, default);
Assert.True(proposal.TryGetFee(out var newFee));
Assert.Equal(Money.Satoshis(3001 + 50), newFee);
proposal = proposal.SignAll(derivationSchemeSettings.AccountDerivation, alice.GenerateWalletResponseV.AccountHDKey, signingAccount.GetRootedKeyPath());
@ -557,7 +578,7 @@ namespace BTCPayServer.Tests
psbt.SignAll(derivationSchemeSettings.AccountDerivation, alice.GenerateWalletResponseV.AccountHDKey, signingAccount.GetRootedKeyPath());
endpoint = TestAccount.GetPayjoinBitcoinUrl(invoice, Network.RegTest);
pjClient.MinimumFeeRate = new FeeRate(100_000_000.2m);
var ex2 = await Assert.ThrowsAsync<PayjoinReceiverException>(async () => await pjClient.RequestPayjoin(endpoint, derivationSchemeSettings, psbt, default));
var ex2 = await Assert.ThrowsAsync<PayjoinReceiverException>(async () => await pjClient.RequestPayjoin(endpoint, new PayjoinWallet(derivationSchemeSettings), psbt, default));
Assert.Equal(PayjoinReceiverWellknownErrors.NotEnoughMoney, ex2.WellknownError);
}
}
@ -799,16 +820,32 @@ retry:
//give the cow some cash
await cashCow.GenerateAsync(1);
//let's get some more utxos first
await receiverUser.ReceiveUTXO(Money.Coins(0.011m), btcPayNetwork);
await receiverUser.ReceiveUTXO(Money.Coins(0.012m), btcPayNetwork);
await receiverUser.ReceiveUTXO(Money.Coins(0.013m), btcPayNetwork);
await receiverUser.ReceiveUTXO(Money.Coins(0.014m), btcPayNetwork);
await senderUser.ReceiveUTXO(Money.Coins(0.021m), btcPayNetwork);
await senderUser.ReceiveUTXO(Money.Coins(0.022m), btcPayNetwork);
await senderUser.ReceiveUTXO(Money.Coins(0.023m), btcPayNetwork);
await senderUser.ReceiveUTXO(Money.Coins(0.024m), btcPayNetwork);
await senderUser.ReceiveUTXO(Money.Coins(0.025m), btcPayNetwork);
await senderUser.ReceiveUTXO(Money.Coins(0.026m), btcPayNetwork);
foreach (var m in new []
{
Money.Coins(0.011m),
Money.Coins(0.012m),
Money.Coins(0.013m),
Money.Coins(0.014m),
Money.Coins(0.015m),
Money.Coins(0.016m)
})
{
await receiverUser.ReceiveUTXO(m, btcPayNetwork);
}
foreach (var m in new[]
{
Money.Coins(0.021m),
Money.Coins(0.022m),
Money.Coins(0.023m),
Money.Coins(0.024m),
Money.Coins(0.025m),
Money.Coins(0.026m)
})
{
await senderUser.ReceiveUTXO(m, btcPayNetwork);
}
var senderChange = await senderUser.GetNewAddress(btcPayNetwork);
//Let's start the harassment
@ -839,17 +876,24 @@ retry:
settings.PaymentId == paymentMethodId);
ReceivedCoin[] senderCoins = null;
ReceivedCoin coin = null;
ReceivedCoin coin2 = null;
ReceivedCoin coin3 = null;
ReceivedCoin coin4 = null;
ReceivedCoin coin5 = null;
ReceivedCoin coin6 = null;
await TestUtils.EventuallyAsync(async () =>
{
senderCoins = await btcPayWallet.GetUnspentCoins(senderUser.DerivationScheme);
Assert.Contains(senderCoins, coin => coin.Value.GetValue(btcPayNetwork) == 0.026m);
coin = Assert.Single(senderCoins, coin => coin.Value.GetValue(btcPayNetwork) == 0.021m);
coin2 = Assert.Single(senderCoins, coin => coin.Value.GetValue(btcPayNetwork) == 0.022m);
coin3 = Assert.Single(senderCoins, coin => coin.Value.GetValue(btcPayNetwork) == 0.023m);
coin4 = Assert.Single(senderCoins, coin => coin.Value.GetValue(btcPayNetwork) == 0.024m);
coin5 = Assert.Single(senderCoins, coin => coin.Value.GetValue(btcPayNetwork) == 0.025m);
coin6 = Assert.Single(senderCoins, coin => coin.Value.GetValue(btcPayNetwork) == 0.026m);
});
var coin = senderCoins.Single(coin => coin.Value.GetValue(btcPayNetwork) == 0.021m);
var coin2 = senderCoins.Single(coin => coin.Value.GetValue(btcPayNetwork) == 0.022m);
var coin3 = senderCoins.Single(coin => coin.Value.GetValue(btcPayNetwork) == 0.023m);
var coin4 = senderCoins.Single(coin => coin.Value.GetValue(btcPayNetwork) == 0.024m);
var coin5 = senderCoins.Single(coin => coin.Value.GetValue(btcPayNetwork) == 0.025m);
var coin6 = senderCoins.Single(coin => coin.Value.GetValue(btcPayNetwork) == 0.026m);
var signingKeySettings = derivationSchemeSettings.GetSigningAccountKeySettings();
signingKeySettings.RootFingerprint =
@ -893,10 +937,6 @@ retry:
.SendEstimatedFees(new FeeRate(100m))
.BuildTransaction(true);
//Attempt 1: Send a signed tx to invoice 1 that does not pay the invoice at all
//Result: reject
// Assert.False((await tester.PayTester.HttpClient.PostAsync(endpoint,
// new StringContent(Invoice2Coin1.ToHex(), Encoding.UTF8, "text/plain"))).IsSuccessStatusCode);
//Attempt 2: Create two transactions using different inputs and send them to the same invoice.
//Result: Second Tx should be rejected.

@ -26,6 +26,21 @@ You can also generate blocks:
.\docker-bitcoin-generate.ps1 3
```
### Using Polar to test Lightning payments
- Install and run [Polar](https://lightningpolar.com/). Setup a small network of nodes.
- Go to your store's General Settings and enable Lightning.
- Build your connection string using the Connect infomation in the Polar app.
LND Connection string example:
type=lnd-rest;server=https://127.0.0.1:8084/;macaroonfilepath="local path to admin.macaroon on your computer, without these quotes";allowinsecure=true
Now you can create a Lightning invoice on BTCPay Server regtest and make a payment through Polar.
PLEASE NOTE: You may get an exception break in Visual Studio. You must quickly click "Continue" in VS so the invoice is generated.
Or, uncheck the box that says, "Break when this exceptiontype is thrown".
### Using the test litecoin-cli
Same as bitcoin-cli, but with `.\docker-litecoin-cli.ps1` and `.\docker-litecoin-cli.sh` instead.
@ -55,16 +70,45 @@ Please, run the test `CanSetLightningServer`, this will establish a channel betw
Alternatively you can run the `./docker-lightning-channel-setup.sh` script to establish the channel connection.
The `./docker-lightning-channel-teardown.sh` script closes any existing lightning channels.
### Alternative Lightning testing: Using Polar to test Lightning payments
- Install and run [Polar](https://lightningpolar.com/). Setup a small network of nodes.
- Go to your store's General Settings and enable Lightning.
- Build your connection string using the Connect information in the Polar app.
LND Connection string example:
type=lnd-rest;server=https://127.0.0.1:8084/;macaroonfilepath="local path to admin.macaroon on your computer, without these quotes";allowinsecure=true
Now you can create a lightning invoice on BTCPay Server regtest and make a payment through Polar.
PLEASE NOTE: You may get an exception break in Visual Studio. You must quickly click "Continue" in VS so the invoice is generated.
Or, uncheck the box that says, "Break when this exception type is thrown".
## FAQ
`docker-compose up dev` failed or tests are not passing, what should I do?
### `docker-compose up dev` failed or tests are not passing, what should I do?
1. Run `docker-compose down --v` (this will reset your test environment)
2. Run `docker-compose pull` (this will ensure you have the lastest images)
3. Run again with `docker-compose up dev`
How to run the Altcoin environment?
### How to run the Altcoin environment?
`docker-compose -f docker-compose.altcoins.yml up dev`
If you still have issues, try to restart docker.
### How to run the Selenium test with a browser?
Run `dotnet user-secrets set RunSeleniumInBrowser true` to run tests in browser.
To switch back to headless mode (recommended) you can run `dotnet user-secrets remove RunSeleniumInBrowser`.
### Session not created: This version of ChromeDriver only supports Chrome version 88
When you run tests for selenium, you may end up with this error.
This happen when we update the selenium packages on BTCPay Server while you did not update your chrome version.
If you want to use a older chrome driver on [this page](https://chromedriver.chromium.org/downloads) then point to it with
`dotnet user-secrets set ChromeDriverDirectory "path/to/the/driver/directory"`

@ -3,23 +3,22 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Lightning;
using BTCPayServer.Lightning.CLightning;
using BTCPayServer.Models;
using BTCPayServer.Services;
using BTCPayServer.Tests.Logging;
using BTCPayServer.Views.Manage;
using BTCPayServer.Views.Server;
using BTCPayServer.Views.Stores;
using BTCPayServer.Views.Wallets;
using Microsoft.Extensions.Configuration;
using NBitcoin;
using BTCPayServer.BIP78.Sender;
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Interactions;
using OpenQA.Selenium.Support.Extensions;
using Xunit;
namespace BTCPayServer.Tests
@ -28,64 +27,69 @@ namespace BTCPayServer.Tests
{
public IWebDriver Driver { get; set; }
public ServerTester Server { get; set; }
public WalletId WalletId { get; set; }
public static SeleniumTester Create([CallerMemberNameAttribute] string scope = null, bool newDb = false)
{
var server = ServerTester.Create(scope, newDb);
return new SeleniumTester()
{
Server = server
};
}
public string StoreId { get; set; }
public static SeleniumTester Create([CallerMemberNameAttribute] string scope = null, bool newDb = false) =>
new SeleniumTester { Server = ServerTester.Create(scope, newDb) };
public static readonly TimeSpan ImplicitWait = TimeSpan.FromSeconds(5);
public async Task StartAsync()
{
await Server.StartAsync();
ChromeOptions options = new ChromeOptions();
var windowSize = (Width: 1200, Height: 1000);
var builder = new ConfigurationBuilder();
builder.AddUserSecrets("AB0AC1DD-9D26-485B-9416-56A33F268117");
var config = builder.Build();
// Run `dotnet user-secrets set RunSeleniumInBrowser true` to run tests in browser
var runInBrowser = config["RunSeleniumInBrowser"] == "true";
// Reset this using `dotnet user-secrets remove RunSeleniumInBrowser`
var chromeDriverPath = config["ChromeDriverDirectory"] ?? (Server.PayTester.InContainer ? "/usr/bin" : Directory.GetCurrentDirectory());
var options = new ChromeOptions();
if (Server.PayTester.InContainer)
{
// this must be first option https://stackoverflow.com/questions/53073411/selenium-webdriverexceptionchrome-failed-to-start-crashed-as-google-chrome-is#comment102570662_53073789
options.AddArgument("no-sandbox");
}
var isDebug = !Server.PayTester.InContainer;
if (!isDebug)
if (!runInBrowser)
{
options.AddArguments("headless"); // Comment to view browser
options.AddArguments("window-size=1200x1000"); // Comment to view browser
options.AddArguments("headless");
}
options.AddArguments($"window-size={windowSize.Width}x{windowSize.Height}");
options.AddArgument("shm-size=2g");
Driver = new ChromeDriver(Server.PayTester.InContainer ? "/usr/bin" : Directory.GetCurrentDirectory(), options);
if (isDebug)
Driver = new ChromeDriver(chromeDriverPath, options,
// A bit less than test timeout
TimeSpan.FromSeconds(50));
if (runInBrowser)
{
//when running locally, depending on your resolution, the website may go into mobile responsive mode and screw with navigation of tests
// ensure maximized window size
// otherwise TESTS WILL FAIL because of different hierarchy in navigation menu
Driver.Manage().Window.Maximize();
}
Logs.Tester.LogInformation("Selenium: Using chrome driver");
Logs.Tester.LogInformation("Selenium: Browsing to " + Server.PayTester.ServerUri);
Logs.Tester.LogInformation($"Selenium: Using {Driver.GetType()}");
Logs.Tester.LogInformation($"Selenium: Browsing to {Server.PayTester.ServerUri}");
Logs.Tester.LogInformation($"Selenium: Resolution {Driver.Manage().Window.Size}");
Driver.Manage().Timeouts().ImplicitWait = ImplicitWait;
GoToRegister();
Driver.AssertNoError();
}
internal IWebElement AssertHappyMessage(StatusMessageModel.StatusSeverity severity = StatusMessageModel.StatusSeverity.Success)
internal IWebElement FindAlertMessage(StatusMessageModel.StatusSeverity severity = StatusMessageModel.StatusSeverity.Success)
{
using var cts = new CancellationTokenSource(20_000);
while (!cts.IsCancellationRequested)
{
var result = Driver.FindElements(By.ClassName($"alert-{StatusMessageModel.ToString(severity)}")).Where(el => el.Displayed);
if (result.Any())
return result.First();
Thread.Sleep(100);
}
Logs.Tester.LogInformation(this.Driver.PageSource);
Assert.True(false, $"Should have shown {severity} message");
return null;
var className = $"alert-{StatusMessageModel.ToString(severity)}";
var el = Driver.FindElement(By.ClassName(className)) ?? Driver.WaitForElement(By.ClassName(className));
if (el is null)
throw new NoSuchElementException($"Unable to find {className}");
return el;
}
public static readonly TimeSpan ImplicitWait = TimeSpan.FromSeconds(10);
public string Link(string relativeLink)
{
return Server.PayTester.ServerUri.AbsoluteUri.WithoutEndingSlash() + relativeLink.WithStartingSlash();
@ -93,8 +97,9 @@ namespace BTCPayServer.Tests
public void GoToRegister()
{
Driver.Navigate().GoToUrl(this.Link("/Account/Register"));
Driver.Navigate().GoToUrl(Link("/register"));
}
public string RegisterNewUser(bool isAdmin = false)
{
var usr = RandomUtils.GetUInt256().ToString().Substring(64 - 20) + "@a.com";
@ -111,34 +116,59 @@ namespace BTCPayServer.Tests
public (string storeName, string storeId) CreateNewStore()
{
var usr = "Store" + RandomUtils.GetUInt64().ToString();
Driver.FindElement(By.Id("Stores")).Click();
Driver.FindElement(By.Id("CreateStore")).Click();
Driver.FindElement(By.Id("Name")).SendKeys(usr);
Driver.FindElement(By.Id("Create")).Click();
StoreId = Driver.FindElement(By.Id("Id")).GetAttribute("value");
return (usr, StoreId);
Driver.WaitForElement(By.Id("Stores")).Click();
Driver.WaitForElement(By.Id("CreateStore")).Click();
var name = "Store" + RandomUtils.GetUInt64();
Driver.WaitForElement(By.Id("Name")).SendKeys(name);
Driver.WaitForElement(By.Id("Create")).Click();
StoreId = Driver.WaitForElement(By.Id("Id")).GetAttribute("value");
return (name, StoreId);
}
public string StoreId { get; set; }
public Mnemonic GenerateWallet(string cryptoCode = "BTC", string seed = "", bool importkeys = false, bool privkeys = false, ScriptPubKeyType format = ScriptPubKeyType.Segwit)
{
Driver.FindElement(By.Id($"Modify{cryptoCode}")).ForceClick();
Driver.FindElement(By.Id("import-from-btn")).ForceClick();
Driver.FindElement(By.Id("nbxplorergeneratewalletbtn")).ForceClick();
Driver.WaitForElement(By.Id("ExistingMnemonic")).SendKeys(seed);
SetCheckbox(Driver.WaitForElement(By.Id("SavePrivateKeys")), privkeys);
SetCheckbox(Driver.WaitForElement(By.Id("ImportKeysToRPC")), importkeys);
Driver.WaitForElement(By.Id("ScriptPubKeyType")).Click();
Driver.WaitForElement(By.CssSelector($"#ScriptPubKeyType option[value={format}]")).Click();
Logs.Tester.LogInformation("Trying to click btn-generate");
Driver.WaitForElement(By.Id("btn-generate")).ForceClick();
// Seed backup page
AssertHappyMessage();
Driver.FindElement(By.Id($"Modify{cryptoCode}")).Click();
// Replace previous wallet case
if (Driver.PageSource.Contains("id=\"ChangeWalletLink\""))
{
Driver.FindElement(By.Id("ChangeWalletLink")).Click();
Driver.FindElement(By.Id("continue")).Click();
}
if (string.IsNullOrEmpty(seed))
{
seed = Driver.FindElements(By.Id("recovery-phrase")).First().GetAttribute("data-mnemonic");
var option = privkeys ? "Hotwallet" : "Watchonly";
Logs.Tester.LogInformation($"Generating new seed ({option})");
Driver.FindElement(By.Id("GenerateWalletLink")).Click();
Driver.FindElement(By.Id($"Generate{option}Link")).Click();
}
else
{
Logs.Tester.LogInformation("Progressing with existing seed");
Driver.FindElement(By.Id("ImportWalletOptionsLink")).Click();
Driver.FindElement(By.Id("ImportSeedLink")).Click();
Driver.FindElement(By.Id("ExistingMnemonic")).SendKeys(seed);
Driver.SetCheckbox(By.Id("SavePrivateKeys"), privkeys);
}
Driver.FindElement(By.Id("ScriptPubKeyType")).Click();
Driver.FindElement(By.CssSelector($"#ScriptPubKeyType option[value={format}]")).Click();
// Open advanced settings via JS, because if we click the link it triggers the toggle animation.
// This leads to Selenium trying to click the button while it is moving resulting in an error.
Driver.ExecuteJavaScript("document.getElementById('AdvancedSettings').classList.add('show')");
Driver.SetCheckbox(By.Id("ImportKeysToRPC"), importkeys);
Driver.FindElement(By.Id("Continue")).Click();
// Seed backup page
FindAlertMessage();
if (string.IsNullOrEmpty(seed))
{
seed = Driver.FindElements(By.Id("RecoveryPhrase")).First().GetAttribute("data-mnemonic");
}
// Confirm seed backup
Driver.FindElement(By.Id("confirm")).Click();
Driver.FindElement(By.Id("submit")).Click();
@ -146,38 +176,51 @@ namespace BTCPayServer.Tests
WalletId = new WalletId(StoreId, cryptoCode);
return new Mnemonic(seed);
}
public WalletId WalletId { get; set; }
public void AddDerivationScheme(string cryptoCode = "BTC", string derivationScheme = "xpub661MyMwAqRbcGABgHMUXDzPzH1tU7eZaAaJQXhDXsSxsqyQzQeU6kznNfSuAyqAK9UaWSaZaMFdNiY5BCF4zBPAzSnwfUAwUhwttuAKwfRX-[legacy]")
{
Driver.FindElement(By.Id($"Modify{cryptoCode}")).ForceClick();
Driver.FindElement(By.ClassName("store-derivation-scheme")).SendKeys(derivationScheme);
Driver.FindElement(By.Id("Continue")).ForceClick();
Driver.FindElement(By.Id("Confirm")).ForceClick();
AssertHappyMessage();
Driver.FindElement(By.Id($"Modify{cryptoCode}")).Click();
Driver.FindElement(By.Id("ImportWalletOptionsLink")).Click();
Driver.FindElement(By.Id("ImportXpubLink")).Click();
Driver.FindElement(By.Id("DerivationScheme")).SendKeys(derivationScheme);
Driver.FindElement(By.Id("Continue")).Click();
Driver.FindElement(By.Id("Confirm")).Click();
FindAlertMessage();
}
public void AddLightningNode(string cryptoCode, LightningConnectionType connectionType)
public void AddLightningNode(string cryptoCode = "BTC", LightningConnectionType? connectionType = null)
{
string connectionString = null;
if (connectionType == LightningConnectionType.Charge)
connectionString = $"type=charge;server={Server.MerchantCharge.Client.Uri.AbsoluteUri};allowinsecure=true";
else if (connectionType == LightningConnectionType.CLightning)
connectionString = "type=clightning;server=" + ((CLightningClient)Server.MerchantLightningD).Address.AbsoluteUri;
else if (connectionType == LightningConnectionType.LndREST)
connectionString = $"type=lnd-rest;server={Server.MerchantLnd.Swagger.BaseUrl};allowinsecure=true";
Driver.FindElement(By.Id($"Modify-Lightning{cryptoCode}")).Click();
var connectionString = connectionType switch
{
LightningConnectionType.Charge =>
$"type=charge;server={Server.MerchantCharge.Client.Uri.AbsoluteUri};allowinsecure=true",
LightningConnectionType.CLightning =>
$"type=clightning;server={((CLightningClient) Server.MerchantLightningD).Address.AbsoluteUri}",
LightningConnectionType.LndREST =>
$"type=lnd-rest;server={Server.MerchantLnd.Swagger.BaseUrl};allowinsecure=true",
_ => null
};
if (connectionString == null)
{
Assert.True(Driver.FindElement(By.Id("LightningNodeType-Internal")).Enabled, "Usage of the internal Lightning node is disabled.");
Driver.FindElement(By.CssSelector("label[for=\"LightningNodeType-Internal\"]")).Click();
}
else
throw new NotSupportedException(connectionType.ToString());
{
Driver.FindElement(By.CssSelector("label[for=\"LightningNodeType-Custom\"]")).Click();
Driver.FindElement(By.Id("ConnectionString")).SendKeys(connectionString);
}
Driver.FindElement(By.Id($"Modify-Lightning{cryptoCode}")).ForceClick();
Driver.FindElement(By.Name($"ConnectionString")).SendKeys(connectionString);
Driver.FindElement(By.Id($"save")).ForceClick();
}
var enabled = Driver.FindElement(By.Id("Enabled"));
if (!enabled.Selected) enabled.Click();
public void AddInternalLightningNode(string cryptoCode)
{
Driver.FindElement(By.Id($"Modify-Lightning{cryptoCode}")).ForceClick();
Driver.FindElement(By.Id($"internal-ln-node-setter")).ForceClick();
Driver.FindElement(By.Id($"save")).ForceClick();
Driver.FindElement(By.Id("test")).Click();
Assert.Contains("Connection to the Lightning node succeeded.", FindAlertMessage().Text);
Driver.FindElement(By.Id("save")).Click();
}
public void ClickOnAllSideMenus()
@ -193,21 +236,23 @@ namespace BTCPayServer.Tests
}
}
public void Dispose()
{
if (Driver != null)
{
try
{
Driver.Close();
Driver.Quit();
}
catch { }
catch
{
// ignored
}
Driver.Dispose();
}
if (Server != null)
Server.Dispose();
Server?.Dispose();
}
internal void AssertNotFound()
@ -241,6 +286,7 @@ namespace BTCPayServer.Tests
{
Driver.FindElement(By.Id("Stores")).Click();
Driver.FindElement(By.Id($"update-store-{storeId}")).Click();
if (storeNavPage != StoreNavPages.Index)
{
Driver.FindElement(By.Id(storeNavPage.ToString())).Click();
@ -254,33 +300,6 @@ namespace BTCPayServer.Tests
CheckForJSErrors();
}
public void SetCheckbox(IWebElement element, bool value)
{
if ((value && !element.Selected) || (!value && element.Selected))
{
element.Click();
}
if (value != element.Selected)
{
Logs.Tester.LogInformation("SetCheckbox recursion, trying to click again");
SetCheckbox(element, value);
}
}
public void SetCheckbox(SeleniumTester s, string checkboxId, bool value)
{
SetCheckbox(s.Driver.WaitForElement(By.Id(checkboxId)), value);
}
public void ScrollToElement(IWebElement element)
{
Actions actions = new Actions(Driver);
actions.MoveToElement(element);
actions.Perform();
}
public void GoToInvoices()
{
Driver.FindElement(By.Id("Invoices")).Click();
@ -297,19 +316,13 @@ namespace BTCPayServer.Tests
public void GoToLogin()
{
Driver.Navigate().GoToUrl(new Uri(Server.PayTester.ServerUri, "Account/Login"));
}
public void GoToCreateInvoicePage()
{
GoToInvoices();
Driver.FindElement(By.Id("CreateNewInvoice")).Click();
Driver.Navigate().GoToUrl(new Uri(Server.PayTester.ServerUri, "/login"));
}
public string CreateInvoice(string storeName, decimal amount = 100, string currency = "USD", string refundEmail = "")
{
GoToInvoices();
Driver.FindElement(By.Id("CreateNewInvoice")).Click(); // ocassionally gets stuck for some reason, tried force click and wait for element
Driver.FindElement(By.Id("CreateNewInvoice")).Click();
Driver.FindElement(By.Id("Amount")).SendKeys(amount.ToString(CultureInfo.InvariantCulture));
var currencyEl = Driver.FindElement(By.Id("Currency"));
currencyEl.Clear();
@ -318,8 +331,7 @@ namespace BTCPayServer.Tests
Driver.FindElement(By.Name("StoreId")).SendKeys(storeName);
Driver.FindElement(By.Id("Create")).Click();
AssertHappyMessage();
var statusElement = Driver.FindElement(By.ClassName("alert-success"));
var statusElement = FindAlertMessage();
var id = statusElement.Text.Split(" ")[1];
return id;
}
@ -331,7 +343,7 @@ namespace BTCPayServer.Tests
Driver.FindElement(By.Id("generateButton")).Click();
var addressStr = Driver.FindElement(By.Id("address")).GetProperty("value");
var address = BitcoinAddress.Create(addressStr, ((BTCPayNetwork)Server.NetworkProvider.GetNetwork(walletId.CryptoCode)).NBitcoinNetwork);
for (int i = 0; i < coins; i++)
for (var i = 0; i < coins; i++)
{
await Server.ExplorerNode.SendToAddressAsync(address, Money.Coins(denomination));
}
@ -344,19 +356,15 @@ namespace BTCPayServer.Tests
.GetAttribute("href");
Assert.Contains($"{PayjoinClient.BIP21EndpointKey}", bip21);
GoToWallet(walletId, WalletsNavPages.Send);
GoToWallet(walletId);
Driver.FindElement(By.Id("bip21parse")).Click();
Driver.SwitchTo().Alert().SendKeys(bip21);
Driver.SwitchTo().Alert().Accept();
Driver.ScrollTo(By.Id("SendMenu"));
Driver.FindElement(By.Id("SendMenu")).ForceClick();
Driver.FindElement(By.Id("SendMenu")).Click();
Driver.FindElement(By.CssSelector("button[value=nbx-seed]")).Click();
Driver.FindElement(By.CssSelector("button[value=broadcast]")).ForceClick();
Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click();
}
private void CheckForJSErrors()
{
//wait for seleniun update: https://stackoverflow.com/questions/57520296/selenium-webdriver-3-141-0-driver-manage-logs-availablelogtypes-throwing-syste
@ -402,7 +410,6 @@ namespace BTCPayServer.Tests
{
Driver.FindElement(By.Id($"Server-{navPages}")).Click();
}
}
public void GoToInvoice(string id)

@ -3,37 +3,36 @@ using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Models;
using BTCPayServer.Services.Wallets;
using BTCPayServer.Tests.Logging;
using BTCPayServer.Views.Manage;
using BTCPayServer.Views.Server;
using BTCPayServer.Views.Stores;
using BTCPayServer.Views.Wallets;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBitcoin.Payment;
using NBitpayClient;
using Newtonsoft.Json.Linq;
using OpenQA.Selenium;
using OpenQA.Selenium.Support.Extensions;
using OpenQA.Selenium.Support.UI;
using Org.BouncyCastle.Ocsp;
using Renci.SshNet.Security.Cryptography;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace BTCPayServer.Tests
{
[Trait("Selenium", "Selenium")]
public class ChromeTests
{
public const int TestTimeout = TestUtils.TestTimeout;
private const int TestTimeout = TestUtils.TestTimeout;
public ChromeTests(ITestOutputHelper helper)
{
Logs.Tester = new XUnitLog(helper) { Name = "Tests" };
@ -85,12 +84,51 @@ namespace BTCPayServer.Tests
Assert.Contains(passEl.Text, "hellorockstar", StringComparison.OrdinalIgnoreCase);
s.Driver.FindElement(By.Id("delete")).Click();
s.Driver.FindElement(By.Id("continue")).Click();
s.AssertHappyMessage();
s.FindAlertMessage();
seedEl = s.Driver.FindElement(By.Id("SeedTextArea"));
Assert.Contains("Seed removed", seedEl.Text, StringComparison.OrdinalIgnoreCase);
}
}
[Fact(Timeout = TestTimeout)]
[Trait("Selenium", "Selenium")]
public async Task CanChangeUserMail()
{
using (var s = SeleniumTester.Create())
{
await s.StartAsync();
var tester = s.Server;
var u1 = tester.NewAccount();
u1.GrantAccess();
await u1.MakeAdmin(false);
var u2 = tester.NewAccount();
u2.GrantAccess();
await u2.MakeAdmin(false);
s.GoToLogin();
s.Login(u1.RegisterDetails.Email, u1.RegisterDetails.Password);
s.GoToProfile(ManageNavPages.Index);
s.Driver.FindElement(By.Id("Email")).Clear();
s.Driver.FindElement(By.Id("Email")).SendKeys(u2.RegisterDetails.Email);
s.Driver.FindElement(By.Id("save")).Click();
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Error);
s.GoToProfile(ManageNavPages.Index);
s.Driver.FindElement(By.Id("Email")).Clear();
var changedEmail = Guid.NewGuid() + "@lol.com";
s.Driver.FindElement(By.Id("Email")).SendKeys(changedEmail);
s.Driver.FindElement(By.Id("save")).Click();
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success);
var manager = tester.PayTester.GetService<UserManager<ApplicationUser>>();
Assert.NotNull(await manager.FindByNameAsync(changedEmail));
Assert.NotNull(await manager.FindByEmailAsync(changedEmail));
}
}
[Fact(Timeout = TestTimeout)]
public async Task NewUserLogin()
{
@ -101,9 +139,7 @@ namespace BTCPayServer.Tests
var email = s.RegisterNewUser();
s.Logout();
s.Driver.AssertNoError();
Assert.Contains("Account/Login", s.Driver.Url);
// Should show the Tor address
Assert.Contains("wsaxew3qa5ljfuenfebmaf3m5ykgatct3p6zjrqwoouj3foererde3id.onion", s.Driver.PageSource);
Assert.Contains("/login", s.Driver.Url);
s.Driver.Navigate().GoToUrl(s.Link("/invoices"));
Assert.Contains("ReturnUrl=%2Finvoices", s.Driver.Url);
@ -143,15 +179,15 @@ namespace BTCPayServer.Tests
//let's test invite link
s.Logout();
s.GoToRegister();
var newAdminUser = s.RegisterNewUser(true);
s.RegisterNewUser(true);
s.GoToServer(ServerNavPages.Users);
s.Driver.FindElement(By.Id("CreateUser")).Click();
var usr = RandomUtils.GetUInt256().ToString().Substring(64 - 20) + "@a.com";
s.Driver.FindElement(By.Id("Email")).SendKeys(usr);
s.Driver.FindElement(By.Id("Save")).Click();
var url = s.AssertHappyMessage().FindElement(By.TagName("a")).Text;
;
var url = s.FindAlertMessage().FindElement(By.TagName("a")).Text;
s.Logout();
s.Driver.Navigate().GoToUrl(url);
Assert.Equal("hidden", s.Driver.FindElement(By.Id("Email")).GetAttribute("type"));
@ -160,7 +196,7 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("Password")).SendKeys("123456");
s.Driver.FindElement(By.Id("ConfirmPassword")).SendKeys("123456");
s.Driver.FindElement(By.Id("SetPassword")).Click();
s.AssertHappyMessage();
s.FindAlertMessage();
s.Driver.FindElement(By.Id("Email")).SendKeys(usr);
s.Driver.FindElement(By.Id("Password")).SendKeys("123456");
s.Driver.FindElement(By.Id("LoginButton")).Click();
@ -170,23 +206,16 @@ namespace BTCPayServer.Tests
}
}
static void LogIn(SeleniumTester s, string email)
{
s.Driver.FindElement(By.Id("Email")).SendKeys(email);
s.Driver.FindElement(By.Id("Password")).SendKeys("123456");
s.Driver.FindElement(By.Id("LoginButton")).Click();
s.Driver.AssertNoError();
}
[Fact(Timeout = TestTimeout)]
public async Task CanUseSSHService()
{
using (var s = SeleniumTester.Create())
{
await s.StartAsync();
var alice = s.RegisterNewUser(isAdmin: true);
s.RegisterNewUser(isAdmin: true);
s.Driver.Navigate().GoToUrl(s.Link("/server/services"));
Assert.Contains("server/services/ssh", s.Driver.PageSource);
using (var client = await s.Server.PayTester.GetService<BTCPayServer.Configuration.BTCPayServerOptions>().SSHSettings.ConnectAsync())
using (var client = await s.Server.PayTester.GetService<Configuration.BTCPayServerOptions>().SSHSettings.ConnectAsync())
{
var result = await client.RunBash("echo hello");
Assert.Equal(string.Empty, result.Error);
@ -197,7 +226,7 @@ namespace BTCPayServer.Tests
s.Driver.AssertNoError();
s.Driver.FindElement(By.Id("SSHKeyFileContent")).Clear();
s.Driver.FindElement(By.Id("SSHKeyFileContent")).SendKeys("tes't\r\ntest2");
s.Driver.FindElement(By.Id("submit")).ForceClick();
s.Driver.FindElement(By.Id("submit")).Click();
s.Driver.AssertNoError();
var text = s.Driver.FindElement(By.Id("SSHKeyFileContent")).Text;
@ -207,7 +236,7 @@ namespace BTCPayServer.Tests
Assert.True(s.Driver.PageSource.Contains("authorized_keys has been updated", StringComparison.OrdinalIgnoreCase));
s.Driver.FindElement(By.Id("SSHKeyFileContent")).Clear();
s.Driver.FindElement(By.Id("submit")).ForceClick();
s.Driver.FindElement(By.Id("submit")).Click();
text = s.Driver.FindElement(By.Id("SSHKeyFileContent")).Text;
Assert.DoesNotContain("test2", text);
@ -220,12 +249,12 @@ namespace BTCPayServer.Tests
using (var s = SeleniumTester.Create())
{
await s.StartAsync();
var alice = s.RegisterNewUser(isAdmin: true);
s.RegisterNewUser(isAdmin: true);
s.Driver.Navigate().GoToUrl(s.Link("/server/emails"));
if (s.Driver.PageSource.Contains("Configured"))
{
s.Driver.FindElement(By.CssSelector("button[value=\"ResetPassword\"]")).Submit();
s.AssertHappyMessage();
s.FindAlertMessage();
}
CanSetupEmailCore(s);
s.CreateNewStore();
@ -234,33 +263,13 @@ namespace BTCPayServer.Tests
}
}
private static void CanSetupEmailCore(SeleniumTester s)
{
s.Driver.FindElement(By.ClassName("dropdown-toggle")).Click();
s.Driver.FindElement(By.ClassName("dropdown-item")).Click();
s.Driver.FindElement(By.Id("Settings_Login")).SendKeys("test@gmail.com");
s.Driver.FindElement(By.CssSelector("button[value=\"Save\"]")).Submit();
s.AssertHappyMessage();
s.Driver.FindElement(By.Id("Settings_Password")).SendKeys("mypassword");
s.Driver.FindElement(By.CssSelector("button[value=\"Save\"]")).Submit();
Assert.Contains("Configured", s.Driver.PageSource);
s.Driver.FindElement(By.Id("Settings_Login")).SendKeys("test_fix@gmail.com");
s.Driver.FindElement(By.CssSelector("button[value=\"Save\"]")).Submit();
Assert.Contains("Configured", s.Driver.PageSource);
Assert.Contains("test_fix", s.Driver.PageSource);
s.Driver.FindElement(By.CssSelector("button[value=\"ResetPassword\"]")).Submit();
s.AssertHappyMessage();
Assert.DoesNotContain("Configured", s.Driver.PageSource);
Assert.Contains("test_fix", s.Driver.PageSource);
}
[Fact(Timeout = TestTimeout)]
public async Task CanUseDynamicDns()
{
using (var s = SeleniumTester.Create())
{
await s.StartAsync();
var alice = s.RegisterNewUser(isAdmin: true);
s.RegisterNewUser(isAdmin: true);
s.Driver.Navigate().GoToUrl(s.Link("/server/services"));
Assert.Contains("Dynamic DNS", s.Driver.PageSource);
@ -305,13 +314,15 @@ namespace BTCPayServer.Tests
}
[Fact(Timeout = TestTimeout)]
[Trait("Lightning", "Lightning")]
public async Task CanCreateStores()
{
using (var s = SeleniumTester.Create())
{
s.Server.ActivateLightning();
await s.StartAsync();
var alice = s.RegisterNewUser();
var storeData = s.CreateNewStore();
var alice = s.RegisterNewUser(true);
var (storeName, storeId) = s.CreateNewStore();
var onchainHint = "Set up your wallet to receive payments at your store.";
var offchainHint = "A connection to a Lightning node is required to receive Lightning payments.";
@ -320,39 +331,47 @@ namespace BTCPayServer.Tests
Assert.True(s.Driver.PageSource.Contains(offchainHint), "Lightning hint not present");
s.GoToStores();
Assert.True(s.Driver.PageSource.Contains("warninghint_" + storeData.storeId),
"Warning hint on list not present");
Assert.True(s.Driver.PageSource.Contains($"warninghint_{storeId}"), "Warning hint on list not present");
s.GoToStore(storeData.storeId);
s.GoToStore(storeId);
Assert.Contains(storeName, s.Driver.PageSource);
Assert.True(s.Driver.PageSource.Contains(onchainHint), "Wallet hint should be present at this point");
Assert.True(s.Driver.PageSource.Contains(offchainHint), "Lightning hint should be present at this point");
s.AddDerivationScheme(); // wallet hint should be dismissed
// setup onchain wallet
s.GoToStore(storeId);
s.AddDerivationScheme();
s.Driver.AssertNoError();
Assert.False(s.Driver.PageSource.Contains(onchainHint),
"Wallet hint not dismissed on derivation scheme add");// dismiss lightning hint
Assert.False(s.Driver.PageSource.Contains(onchainHint), "Wallet hint not dismissed on derivation scheme add");
// setup offchain wallet
s.GoToStore(storeId);
s.AddLightningNode();
s.Driver.AssertNoError();
var successAlert = s.FindAlertMessage();
Assert.Contains("BTC Lightning node modified.", successAlert.Text);
Assert.False(s.Driver.PageSource.Contains(offchainHint), "Lightning hint should be dismissed at this point");
Assert.Contains(storeData.storeName, s.Driver.PageSource);
var storeUrl = s.Driver.Url;
s.ClickOnAllSideMenus();
s.GoToInvoices();
var invoiceId = s.CreateInvoice(storeData.storeName);
s.AssertHappyMessage();
var invoiceId = s.CreateInvoice(storeName);
s.FindAlertMessage();
s.Driver.FindElement(By.ClassName("invoice-details-link")).Click();
var invoiceUrl = s.Driver.Url;
//let's test archiving an invoice
Assert.DoesNotContain("Archived", s.Driver.FindElement(By.Id("btn-archive-toggle")).Text);
s.Driver.FindElement(By.Id("btn-archive-toggle")).Click();
s.AssertHappyMessage();
Assert.Contains("Archived", s.Driver.FindElement(By.Id("btn-archive-toggle")).Text);
//check that it no longer appears in list
s.GoToInvoices();
Assert.DoesNotContain(invoiceId, s.Driver.PageSource);
//ok, let's unarchive and see that it shows again
s.Driver.Navigate().GoToUrl(invoiceUrl);
s.Driver.FindElement(By.Id("btn-archive-toggle")).Click();
s.AssertHappyMessage();
s.FindAlertMessage();
Assert.DoesNotContain("Archived", s.Driver.FindElement(By.Id("btn-archive-toggle")).Text);
s.GoToInvoices();
Assert.Contains(invoiceId, s.Driver.PageSource);
@ -374,14 +393,14 @@ namespace BTCPayServer.Tests
s.Logout();
// Let's add Bob as a guest to alice's store
LogIn(s, alice);
s.LogIn(alice);
s.Driver.Navigate().GoToUrl(storeUrl + "/users");
s.Driver.FindElement(By.Id("Email")).SendKeys(bob + Keys.Enter);
Assert.Contains("User added successfully", s.Driver.PageSource);
s.Logout();
// Bob should not have access to store, but should have access to invoice
LogIn(s, bob);
s.LogIn(bob);
s.Driver.Navigate().GoToUrl(storeUrl);
Assert.Contains("ReturnUrl", s.Driver.Url);
s.Driver.Navigate().GoToUrl(invoiceUrl);
@ -389,12 +408,8 @@ namespace BTCPayServer.Tests
// Alice should be able to delete the store
s.Logout();
LogIn(s, alice);
s.LogIn(alice);
s.Driver.FindElement(By.Id("Stores")).Click();
// there shouldn't be any hints now
Assert.False(s.Driver.PageSource.Contains(offchainHint), "Lightning hint should be dismissed at this point");
s.Driver.FindElement(By.LinkText("Remove")).Click();
s.Driver.FindElement(By.Id("continue")).Click();
s.Driver.FindElement(By.Id("Stores")).Click();
@ -412,17 +427,17 @@ namespace BTCPayServer.Tests
s.Driver.Navigate().GoToUrl(s.Link("/api-access-request"));
Assert.Contains("ReturnUrl", s.Driver.Url);
s.GoToRegister();
var alice = s.RegisterNewUser();
var store = s.CreateNewStore().storeName;
s.RegisterNewUser();
s.CreateNewStore();
s.AddDerivationScheme();
s.Driver.FindElement(By.Id("Tokens")).Click();
s.Driver.FindElement(By.Id("CreateNewToken")).Click();
s.Driver.FindElement(By.Id("RequestPairing")).Click();
string pairingCode = AssertUrlHasPairingCode(s);
var pairingCode = AssertUrlHasPairingCode(s);
s.Driver.FindElement(By.Id("ApprovePairing")).Click();
s.AssertHappyMessage();
s.FindAlertMessage();
Assert.Contains(pairingCode, s.Driver.PageSource);
var client = new NBitpayClient.Bitpay(new Key(), s.Server.PayTester.ServerUri);
@ -454,15 +469,6 @@ namespace BTCPayServer.Tests
}
}
private static string AssertUrlHasPairingCode(SeleniumTester s)
{
var regex = Regex.Match(new Uri(s.Driver.Url, UriKind.Absolute).Query, "pairingCode=([^&]*)");
Assert.True(regex.Success, $"{s.Driver.Url} does not match expected regex");
var pairingCode = regex.Groups[1].Value;
return pairingCode;
}
[Fact(Timeout = TestTimeout)]
public async Task CanCreateAppPoS()
{
@ -470,57 +476,62 @@ namespace BTCPayServer.Tests
{
await s.StartAsync();
s.RegisterNewUser();
var store = s.CreateNewStore();
var (storeName, _) = s.CreateNewStore();
s.Driver.FindElement(By.Id("Apps")).Click();
s.Driver.FindElement(By.Id("CreateNewApp")).Click();
s.Driver.FindElement(By.Name("Name")).SendKeys("PoS" + Guid.NewGuid());
s.Driver.FindElement(By.Id("SelectedAppType")).SendKeys("PointOfSale" + Keys.Enter);
s.Driver.FindElement(By.Id("SelectedStore")).SendKeys(store + Keys.Enter);
s.Driver.FindElement(By.Id("SelectedAppType")).SendKeys("PointOfSale");
s.Driver.FindElement(By.Id("SelectedStore")).SendKeys(storeName);
s.Driver.FindElement(By.Id("Create")).Click();
s.Driver.FindElement(By.Id("DefaultView")).SendKeys("Cart" + Keys.Enter);
s.Driver.FindElement(By.Id("SaveSettings")).ForceClick();
s.Driver.FindElement(By.Id("ViewApp")).ForceClick();
s.Driver.FindElement(By.Id("DefaultView")).SendKeys("Cart");
s.Driver.FindElement(By.CssSelector(".template-item:nth-of-type(1) .btn-primary")).Click();
s.Driver.FindElement(By.Id("BuyButtonText")).SendKeys("Take my money");
s.Driver.FindElement(By.Id("SaveItemChanges")).Click();
s.Driver.FindElement(By.Id("ToggleRawEditor")).Click();
var template = s.Driver.FindElement(By.Id("Template")).GetAttribute("value");
Assert.Contains("buyButtonText: Take my money", template);
s.Driver.FindElement(By.Id("SaveSettings")).Click();
s.Driver.FindElement(By.Id("ViewApp")).Click();
var posBaseUrl = s.Driver.Url.Replace("/Cart", "");
Assert.True(s.Driver.PageSource.Contains("Tea shop"), "Unable to create PoS");
Assert.True(s.Driver.PageSource.Contains("Cart"), "PoS not showing correct default view");
Assert.True(s.Driver.PageSource.Contains("Take my money"), "PoS not showing correct default view");
s.Driver.Url = posBaseUrl + "/static";
Assert.False(s.Driver.PageSource.Contains("Cart"), "Static PoS not showing correct view");
s.Driver.Url = posBaseUrl + "/cart";
Assert.True(s.Driver.PageSource.Contains("Cart"), "Cart PoS not showing correct view");
s.Driver.Quit();
}
}
[Fact(Timeout = TestTimeout)]
public async Task CanCreateAppCF()
public async Task CanCreateCrowdfundingApp()
{
using (var s = SeleniumTester.Create())
{
await s.StartAsync();
s.RegisterNewUser();
var store = s.CreateNewStore();
var (storeName, _) = s.CreateNewStore();
s.AddDerivationScheme();
s.Driver.FindElement(By.Id("Apps")).Click();
s.Driver.FindElement(By.Id("CreateNewApp")).Click();
s.Driver.FindElement(By.Name("Name")).SendKeys("CF" + Guid.NewGuid());
s.Driver.FindElement(By.Id("SelectedAppType")).SendKeys("Crowdfund" + Keys.Enter);
s.Driver.FindElement(By.Id("SelectedStore")).SendKeys(store + Keys.Enter);
s.Driver.FindElement(By.Id("SelectedAppType")).SendKeys("Crowdfund");
s.Driver.FindElement(By.Id("SelectedStore")).SendKeys(storeName);
s.Driver.FindElement(By.Id("Create")).Click();
s.Driver.FindElement(By.Id("Title")).SendKeys("Kukkstarter");
s.Driver.FindElement(By.CssSelector("div.note-editable.card-block")).SendKeys("1BTC = 1BTC");
s.Driver.FindElement(By.Id("TargetCurrency")).SendKeys("JPY");
s.Driver.FindElement(By.Id("TargetAmount")).SendKeys("700");
s.Driver.FindElement(By.Id("SaveSettings")).ForceClick();
s.Driver.FindElement(By.Id("ViewApp")).ForceClick();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
Assert.True(s.Driver.PageSource.Contains("Currently Active!"), "Unable to create CF");
s.Driver.Quit();
s.Driver.FindElement(By.Id("SaveSettings")).Click();
s.Driver.FindElement(By.Id("ViewApp")).Click();
Assert.Equal("Currently Active!", s.Driver.FindElement(By.CssSelector(".h6.text-muted")).Text);
}
}
@ -539,11 +550,10 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("Title")).SendKeys("Pay123");
s.Driver.FindElement(By.Id("Amount")).SendKeys("700");
s.Driver.FindElement(By.Id("Currency")).SendKeys("BTC");
s.Driver.FindElement(By.Id("SaveButton")).ForceClick();
s.Driver.FindElement(By.Name("ViewAppButton")).SendKeys(Keys.Return);
s.Driver.FindElement(By.Id("SaveButton")).Click();
s.Driver.FindElement(By.Name("ViewAppButton")).Click();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
Assert.True(s.Driver.PageSource.Contains("Amount due"), "Unable to create Payment Request");
s.Driver.Quit();
Assert.Equal("Amount due", s.Driver.FindElement(By.CssSelector("[data-test='amount-due-title']")).Text);
}
}
@ -554,8 +564,8 @@ namespace BTCPayServer.Tests
using (var s = SeleniumTester.Create())
{
await s.StartAsync();
var userId = s.RegisterNewUser(true);
var storeId = s.CreateNewStore().storeId;
s.RegisterNewUser(true);
var (_, storeId) = s.CreateNewStore();
s.GenerateWallet("BTC", "", false, true);
var walletId = new WalletId(storeId, "BTC");
s.GoToWallet(walletId, WalletsNavPages.Receive);
@ -576,16 +586,17 @@ namespace BTCPayServer.Tests
var x = store.GetSupportedPaymentMethods(s.Server.NetworkProvider)
.OfType<DerivationSchemeSettings>()
.Single(settings => settings.PaymentId.CryptoCode == walletId.CryptoCode);
var wallet = s.Server.PayTester.GetService<BTCPayWalletProvider>().GetWallet(walletId.CryptoCode);
wallet.InvalidateCache(x.AccountDerivation);
Assert.Contains(
await s.Server.PayTester.GetService<BTCPayWalletProvider>().GetWallet(walletId.CryptoCode)
.GetUnspentCoins(x.AccountDerivation),
await wallet.GetUnspentCoins(x.AccountDerivation),
coin => coin.OutPoint == spentOutpoint);
});
await s.Server.ExplorerNode.GenerateAsync(1);
s.GoToWallet(walletId, WalletsNavPages.Send);
s.GoToWallet(walletId);
s.Driver.FindElement(By.Id("advancedSettings")).Click();
s.Driver.FindElement(By.Id("toggleInputSelection")).Click();
s.Driver.WaitForElement(By.Id(spentOutpoint.ToString()));
s.Driver.WaitForAndClick(By.Id("toggleInputSelection"));
s.Driver.FindElement(By.Id(spentOutpoint.ToString()));
Assert.Equal("true", s.Driver.FindElement(By.Name("InputSelection")).GetAttribute("value").ToLowerInvariant());
var el = s.Driver.FindElement(By.Id(spentOutpoint.ToString()));
s.Driver.FindElement(By.Id(spentOutpoint.ToString())).Click();
@ -596,8 +607,8 @@ namespace BTCPayServer.Tests
SetTransactionOutput(s, 0, bob, 0.3m);
s.Driver.FindElement(By.Id("SendMenu")).Click();
s.Driver.FindElement(By.Id("spendWithNBxplorer")).Click();
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).ForceClick();
var happyElement = s.AssertHappyMessage();
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click();
var happyElement = s.FindAlertMessage();
var happyText = happyElement.Text;
var txid = Regex.Match(happyText, @"\((.*)\)").Groups[1].Value;
@ -614,11 +625,11 @@ namespace BTCPayServer.Tests
{
await s.StartAsync();
s.RegisterNewUser(true);
var store = s.CreateNewStore();
s.GoToStore(store.storeId, Views.Stores.StoreNavPages.Webhooks);
var (storeName, storeId) = s.CreateNewStore();
s.GoToStore(storeId, Views.Stores.StoreNavPages.Webhooks);
Logs.Tester.LogInformation("Let's create two webhooks");
for (int i = 0; i < 2; i++)
for (var i = 0; i < 2; i++)
{
s.Driver.FindElement(By.Id("CreateWebhook")).Click();
s.Driver.FindElement(By.Name("PayloadUrl")).SendKeys($"http://127.0.0.1/callback{i}");
@ -636,7 +647,7 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("continue")).Click();
deletes = s.Driver.FindElements(By.LinkText("Delete"));
Assert.Single(deletes);
s.AssertHappyMessage();
s.FindAlertMessage();
Logs.Tester.LogInformation("Let's try to update one of them");
s.Driver.FindElement(By.LinkText("Modify")).Click();
@ -648,7 +659,7 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Name("Secret")).Clear();
s.Driver.FindElement(By.Name("Secret")).SendKeys("HelloWorld");
s.Driver.FindElement(By.Name("update")).Click();
s.AssertHappyMessage();
s.FindAlertMessage();
s.Driver.FindElement(By.LinkText("Modify")).Click();
foreach (var value in Enum.GetValues(typeof(WebhookEventType)))
{
@ -664,13 +675,13 @@ namespace BTCPayServer.Tests
Assert.DoesNotContain($"value=\"InvoiceReceivedPayment\" checked", s.Driver.PageSource);
s.Driver.FindElement(By.Name("update")).Click();
s.AssertHappyMessage();
s.FindAlertMessage();
Assert.Contains(server.ServerUri.AbsoluteUri, s.Driver.PageSource);
Logs.Tester.LogInformation("Let's see if we can generate an event");
s.GoToStore(store.storeId);
s.GoToStore(storeId);
s.AddDerivationScheme();
s.CreateInvoice(store.storeName);
s.CreateInvoice(storeName);
var request = await server.GetNextRequest();
var headers = request.Request.Headers;
var actualSig = headers["BTCPay-Sig"].First();
@ -681,21 +692,22 @@ namespace BTCPayServer.Tests
server.Done();
Logs.Tester.LogInformation("Let's make a failed event");
s.CreateInvoice(store.storeName);
s.CreateInvoice(storeName);
request = await server.GetNextRequest();
request.Response.StatusCode = 404;
server.Done();
// The delivery is done asynchronously, so small wait here
await Task.Delay(500);
s.GoToStore(store.storeId, Views.Stores.StoreNavPages.Webhooks);
s.GoToStore(storeId, Views.Stores.StoreNavPages.Webhooks);
s.Driver.FindElement(By.LinkText("Modify")).Click();
var elements = s.Driver.FindElements(By.ClassName("redeliver"));
// One worked, one failed
s.Driver.FindElement(By.ClassName("fa-times"));
s.Driver.FindElement(By.ClassName("fa-check"));
elements[0].Click();
s.AssertHappyMessage();
s.FindAlertMessage();
request = await server.GetNextRequest();
request.Response.StatusCode = 404;
server.Done();
@ -708,32 +720,23 @@ namespace BTCPayServer.Tests
CanBrowseContent(s);
var element = s.Driver.FindElement(By.ClassName("redeliver"));
element.Click();
s.AssertHappyMessage();
s.FindAlertMessage();
request = await server.GetNextRequest();
request.Response.StatusCode = 404;
server.Done();
Logs.Tester.LogInformation("Let's see if we can delete store with some webhooks inside");
s.GoToStore(store.storeId);
s.Driver.ExecuteJavaScript("window.scrollBy(0,1000);");
s.Driver.FindElement(By.Id("danger-zone-expander")).Click();
s.GoToStore(storeId);
// Open danger zone via JS, because if we click the link it triggers the toggle animation.
// This leads to Selenium trying to click the button while it is moving resulting in an error.
s.Driver.ExecuteJavaScript("document.getElementById('danger-zone').classList.add('show')");
s.Driver.FindElement(By.Id("delete-store")).Click();
s.Driver.FindElement(By.Id("continue")).Click();
s.AssertHappyMessage();
s.FindAlertMessage();
}
}
private static void CanBrowseContent(SeleniumTester s)
{
s.Driver.FindElement(By.ClassName("delivery-content")).Click();
var windows = s.Driver.WindowHandles;
Assert.Equal(2, windows.Count);
s.Driver.SwitchTo().Window(windows[1]);
JObject.Parse(s.Driver.FindElement(By.TagName("body")).Text);
s.Driver.Close();
s.Driver.SwitchTo().Window(windows[0]);
}
[Fact(Timeout = TestTimeout)]
public async Task CanManageWallet()
{
@ -741,20 +744,19 @@ namespace BTCPayServer.Tests
{
await s.StartAsync();
s.RegisterNewUser(true);
var storeId = s.CreateNewStore();
var (storeName, storeId) = s.CreateNewStore();
// In this test, we try to spend from a manual seed. We import the xpub 49'/0'/0', then try to use the seed
// to sign the transaction
s.GenerateWallet("BTC", "", true, false);
// In this test, we try to spend from a manual seed. We import the xpub 49'/0'/0',
// then try to use the seed to sign the transaction
s.GenerateWallet("BTC", "", true);
//let's test quickly the receive wallet page
s.Driver.FindElement(By.Id("Wallets")).Click();
s.Driver.FindElement(By.LinkText("Manage")).Click();
s.Driver.FindElement(By.Id("WalletSend")).Click();
s.Driver.ScrollTo(By.Id("SendMenu"));
s.Driver.FindElement(By.Id("SendMenu")).ForceClick();
//you cant use the Sign with NBX option without saving private keys when generating the wallet.
s.Driver.FindElement(By.Id("SendMenu")).Click();
//you cannot use the Sign with NBX option without saving private keys when generating the wallet.
Assert.DoesNotContain("nbx-seed", s.Driver.PageSource);
s.Driver.FindElement(By.Id("WalletReceive")).Click();
@ -764,17 +766,16 @@ namespace BTCPayServer.Tests
var receiveAddr = s.Driver.FindElement(By.Id("address")).GetAttribute("value");
//unreserve
s.Driver.FindElement(By.CssSelector("button[value=unreserve-current-address]")).Click();
//generate it again, should be the same one as before as nothign got used in the meantime
//generate it again, should be the same one as before as nothing got used in the meantime
s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click();
Assert.True(s.Driver.FindElement(By.ClassName("qr-container")).Displayed);
Assert.Equal(receiveAddr, s.Driver.FindElement(By.Id("address")).GetAttribute("value"));
//send money to addr and ensure it changed
var sess = await s.Server.ExplorerClient.CreateWebsocketNotificationSessionAsync();
sess.ListenAllTrackedSource();
await sess.ListenAllTrackedSourceAsync();
var nextEvent = sess.NextEventAsync();
s.Server.ExplorerNode.SendToAddress(BitcoinAddress.Create(receiveAddr, Network.RegTest),
Money.Parse("0.1"));
await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(receiveAddr, Network.RegTest), Money.Parse("0.1"));
await nextEvent;
await Task.Delay(200);
s.Driver.Navigate().Refresh();
@ -783,34 +784,35 @@ namespace BTCPayServer.Tests
receiveAddr = s.Driver.FindElement(By.Id("address")).GetAttribute("value");
//change the wallet and ensure old address is not there and generating a new one does not result in the prev one
s.GoToStore(storeId.storeId);
s.GenerateWallet("BTC", "", true, false);
s.GoToStore(storeId);
s.GenerateWallet("BTC", "", true);
s.Driver.FindElement(By.Id("Wallets")).Click();
s.Driver.FindElement(By.LinkText("Manage")).Click();
s.Driver.FindElement(By.Id("WalletReceive")).Click();
s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click();
Assert.NotEqual(receiveAddr, s.Driver.FindElement(By.Id("address")).GetAttribute("value"));
var invoiceId = s.CreateInvoice(storeId.storeName);
var invoiceId = s.CreateInvoice(storeName);
var invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice(invoiceId);
var address = invoice.EntityToDTO().Addresses["BTC"];
//wallet should have been imported to bitcoin core wallet in watch only mode.
var result = await s.Server.ExplorerNode.GetAddressInfoAsync(BitcoinAddress.Create(address, Network.RegTest));
Assert.True(result.IsWatchOnly);
s.GoToStore(storeId.storeId);
s.GoToStore(storeId);
var mnemonic = s.GenerateWallet("BTC", "", true, true);
//lets import and save private keys
var root = mnemonic.DeriveExtKey();
invoiceId = s.CreateInvoice(storeId.storeName);
invoiceId = s.CreateInvoice(storeName);
invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice(invoiceId);
address = invoice.EntityToDTO().Addresses["BTC"];
result = await s.Server.ExplorerNode.GetAddressInfoAsync(BitcoinAddress.Create(address, Network.RegTest));
//spendable from bitcoin core wallet!
Assert.False(result.IsWatchOnly);
var tx = s.Server.ExplorerNode.SendToAddress(BitcoinAddress.Create(address, Network.RegTest), Money.Coins(3.0m));
s.Server.ExplorerNode.Generate(1);
await s.Server.ExplorerNode.GenerateAsync(1);
s.Driver.FindElement(By.Id("Wallets")).Click();
s.Driver.FindElement(By.LinkText("Manage")).Click();
@ -818,16 +820,17 @@ namespace BTCPayServer.Tests
s.ClickOnAllSideMenus();
// Make sure we can rescan, because we are admin!
s.Driver.FindElement(By.Id("WalletRescan")).ForceClick();
s.Driver.FindElement(By.Id("WalletRescan")).Click();
Assert.Contains("The batch size make sure", s.Driver.PageSource);
// We setup the fingerprint and the account key path
s.Driver.FindElement(By.Id("WalletSettings")).ForceClick();
s.Driver.FindElement(By.Id("WalletSettings")).Click();
// s.Driver.FindElement(By.Id("AccountKeys_0__MasterFingerprint")).SendKeys("8bafd160");
// s.Driver.FindElement(By.Id("AccountKeys_0__AccountKeyPath")).SendKeys("m/49'/0'/0'" + Keys.Enter);
// Check the tx sent earlier arrived
s.Driver.FindElement(By.Id("WalletTransactions")).ForceClick();
s.Driver.FindElement(By.Id("WalletTransactions")).Click();
var walletTransactionLink = s.Driver.Url;
Assert.Contains(tx.ToString(), s.Driver.PageSource);
@ -838,17 +841,16 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("WalletSend")).Click();
var bob = new Key().PubKey.Hash.GetAddress(Network.RegTest);
SetTransactionOutput(s, 0, bob, 1);
s.Driver.ScrollTo(By.Id("SendMenu"));
s.Driver.FindElement(By.Id("SendMenu")).ForceClick();
s.Driver.FindElement(By.Id("SendMenu")).Click();
s.Driver.FindElement(By.CssSelector("button[value=seed]")).Click();
// Input the seed
s.Driver.FindElement(By.Id("SeedOrKey")).SendKeys(signingSource.ToString() + Keys.Enter);
s.Driver.FindElement(By.Id("SeedOrKey")).SendKeys(signingSource + Keys.Enter);
// Broadcast
Assert.Contains(bob.ToString(), s.Driver.PageSource);
Assert.Contains("1.00000000", s.Driver.PageSource);
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).ForceClick();
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click();
Assert.Equal(walletTransactionLink, s.Driver.Url);
}
@ -860,18 +862,17 @@ namespace BTCPayServer.Tests
var jack = new Key().PubKey.Hash.GetAddress(Network.RegTest);
SetTransactionOutput(s, 0, jack, 0.01m);
s.Driver.ScrollTo(By.Id("SendMenu"));
s.Driver.FindElement(By.Id("SendMenu")).ForceClick();
s.Driver.FindElement(By.Id("SendMenu")).Click();
s.Driver.FindElement(By.CssSelector("button[value=nbx-seed]")).Click();
Assert.Contains(jack.ToString(), s.Driver.PageSource);
Assert.Contains("0.01000000", s.Driver.PageSource);
s.Driver.FindElement(By.CssSelector("button[value=analyze-psbt]")).ForceClick();
s.Driver.FindElement(By.CssSelector("button[value=analyze-psbt]")).Click();
Assert.EndsWith("psbt", s.Driver.Url);
s.Driver.FindElement(By.CssSelector("#OtherActions")).ForceClick();
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).ForceClick();
s.Driver.FindElement(By.CssSelector("#OtherActions")).Click();
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click();
Assert.EndsWith("psbt/ready", s.Driver.Url);
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).ForceClick();
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click();
Assert.Equal(walletTransactionLink, s.Driver.Url);
var bip21 = invoice.EntityToDTO().CryptoInfo.First().PaymentUrls.BIP21;
@ -884,18 +885,18 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("bip21parse")).Click();
s.Driver.SwitchTo().Alert().SendKeys(bip21);
s.Driver.SwitchTo().Alert().Accept();
s.AssertHappyMessage(StatusMessageModel.StatusSeverity.Info);
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Info);
Assert.Equal(parsedBip21.Amount.ToString(false), s.Driver.FindElement(By.Id($"Outputs_0__Amount")).GetAttribute("value"));
Assert.Equal(parsedBip21.Address.ToString(), s.Driver.FindElement(By.Id($"Outputs_0__DestinationAddress")).GetAttribute("value"));
s.GoToWallet(new WalletId(storeId.storeId, "BTC"), WalletsNavPages.Settings);
s.GoToWallet(new WalletId(storeId, "BTC"), WalletsNavPages.Settings);
var walletUrl = s.Driver.Url;
s.Driver.FindElement(By.Id("SettingsMenu")).ForceClick();
s.Driver.FindElement(By.Id("SettingsMenu")).Click();
s.Driver.FindElement(By.CssSelector("button[value=view-seed]")).Click();
// Seed backup page
var recoveryPhrase = s.Driver.FindElements(By.Id("recovery-phrase")).First().GetAttribute("data-mnemonic");
var recoveryPhrase = s.Driver.FindElements(By.Id("RecoveryPhrase")).First().GetAttribute("data-mnemonic");
Assert.Equal(mnemonic.ToString(), recoveryPhrase);
Assert.Contains("The recovery phrase will also be stored on the server as a hot wallet.", s.Driver.PageSource);
@ -905,18 +906,6 @@ namespace BTCPayServer.Tests
Assert.Equal(walletUrl, s.Driver.Url);
}
}
void SetTransactionOutput(SeleniumTester s, int index, BitcoinAddress dest, decimal amount, bool subtract = false)
{
s.Driver.FindElement(By.Id($"Outputs_{index}__DestinationAddress")).SendKeys(dest.ToString());
var amountElement = s.Driver.FindElement(By.Id($"Outputs_{index}__Amount"));
amountElement.Clear();
amountElement.SendKeys(amount.ToString());
var checkboxElement = s.Driver.FindElement(By.Id($"Outputs_{index}__SubtractFeesFromOutput"));
if (checkboxElement.Selected != subtract)
{
checkboxElement.Click();
}
}
[Fact]
[Trait("Selenium", "Selenium")]
@ -926,45 +915,47 @@ namespace BTCPayServer.Tests
{
await s.StartAsync();
s.RegisterNewUser(true);
var receiver = s.CreateNewStore();
var receiverSeed = s.GenerateWallet("BTC", "", true, true, ScriptPubKeyType.Segwit);
s.CreateNewStore();
s.GenerateWallet("BTC", "", true, true);
await s.Server.ExplorerNode.GenerateAsync(1);
await s.FundStoreWallet(denomination: 50.0m);
s.GoToWallet(navPages: WalletsNavPages.PullPayments);
s.Driver.FindElement(By.Id("NewPullPayment")).Click();
s.Driver.FindElement(By.Id("Name")).SendKeys("PP1");
s.Driver.FindElement(By.Id("Amount")).Clear();
s.Driver.FindElement(By.Id("Amount")).SendKeys("99.0" + Keys.Enter);
s.Driver.FindElement(By.Id("Amount")).SendKeys("99.0");;
s.Driver.FindElement(By.Id("Create")).Click();
s.Driver.FindElement(By.LinkText("View")).Click();
Thread.Sleep(1000);
s.GoToWallet(navPages: WalletsNavPages.PullPayments);
s.Driver.FindElement(By.Id("NewPullPayment")).Click();
s.Driver.FindElement(By.Id("Name")).SendKeys("PP2");
s.Driver.FindElement(By.Id("Amount")).Clear();
s.Driver.FindElement(By.Id("Amount")).SendKeys("100.0" + Keys.Enter);
s.Driver.FindElement(By.Id("Amount")).SendKeys("100.0");
s.Driver.FindElement(By.Id("Create")).Click();
// This should select the first View, ie, the last one PP2
s.Driver.FindElement(By.LinkText("View")).Click();
Thread.Sleep(1000);
var address = await s.Server.ExplorerNode.GetNewAddressAsync();
s.Driver.FindElement(By.Id("Destination")).SendKeys(address.ToString());
s.Driver.FindElement(By.Id("ClaimedAmount")).Clear();
s.Driver.FindElement(By.Id("ClaimedAmount")).SendKeys("15" + Keys.Enter);
s.AssertHappyMessage();
s.FindAlertMessage();
// We should not be able to use an address already used
s.Driver.FindElement(By.Id("Destination")).SendKeys(address.ToString());
s.Driver.FindElement(By.Id("ClaimedAmount")).Clear();
s.Driver.FindElement(By.Id("ClaimedAmount")).SendKeys("20" + Keys.Enter);
s.AssertHappyMessage(StatusMessageModel.StatusSeverity.Error);
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Error);
address = await s.Server.ExplorerNode.GetNewAddressAsync();
s.Driver.FindElement(By.Id("Destination")).Clear();
s.Driver.FindElement(By.Id("Destination")).SendKeys(address.ToString());
s.Driver.FindElement(By.Id("ClaimedAmount")).Clear();
s.Driver.FindElement(By.Id("ClaimedAmount")).SendKeys("20" + Keys.Enter);
s.AssertHappyMessage();
s.FindAlertMessage();
Assert.Contains("Awaiting Approval", s.Driver.PageSource);
var viewPullPaymentUrl = s.Driver.Url;
@ -982,11 +973,11 @@ namespace BTCPayServer.Tests
Assert.DoesNotContain("No payout waiting for approval", s.Driver.PageSource);
s.Driver.FindElement(By.Id("selectAllCheckbox")).Click();
s.Driver.FindElement(By.Id("payCommand")).Click();
s.Driver.ScrollTo(By.Id("SendMenu"));
s.Driver.FindElement(By.Id("SendMenu")).ForceClick();
s.Driver.FindElement(By.Id("SendMenu")).Click();
s.Driver.FindElement(By.CssSelector("button[value=nbx-seed]")).Click();
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).ForceClick();
s.AssertHappyMessage();
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click();
s.FindAlertMessage();
TestUtils.Eventually(() =>
{
@ -1027,5 +1018,57 @@ namespace BTCPayServer.Tests
});
}
}
private static void CanBrowseContent(SeleniumTester s)
{
s.Driver.FindElement(By.ClassName("delivery-content")).Click();
var windows = s.Driver.WindowHandles;
Assert.Equal(2, windows.Count);
s.Driver.SwitchTo().Window(windows[1]);
JObject.Parse(s.Driver.FindElement(By.TagName("body")).Text);
s.Driver.Close();
s.Driver.SwitchTo().Window(windows[0]);
}
private static void CanSetupEmailCore(SeleniumTester s)
{
s.Driver.FindElement(By.ClassName("dropdown-toggle")).Click();
s.Driver.FindElement(By.ClassName("dropdown-item")).Click();
s.Driver.FindElement(By.Id("Settings_Login")).SendKeys("test@gmail.com");
s.Driver.FindElement(By.CssSelector("button[value=\"Save\"]")).Submit();
s.FindAlertMessage();
s.Driver.FindElement(By.Id("Settings_Password")).SendKeys("mypassword");
s.Driver.FindElement(By.CssSelector("button[value=\"Save\"]")).Submit();
Assert.Contains("Configured", s.Driver.PageSource);
s.Driver.FindElement(By.Id("Settings_Login")).SendKeys("test_fix@gmail.com");
s.Driver.FindElement(By.CssSelector("button[value=\"Save\"]")).Submit();
Assert.Contains("Configured", s.Driver.PageSource);
Assert.Contains("test_fix", s.Driver.PageSource);
s.Driver.FindElement(By.CssSelector("button[value=\"ResetPassword\"]")).Submit();
s.FindAlertMessage();
Assert.DoesNotContain("Configured", s.Driver.PageSource);
Assert.Contains("test_fix", s.Driver.PageSource);
}
private static string AssertUrlHasPairingCode(SeleniumTester s)
{
var regex = Regex.Match(new Uri(s.Driver.Url, UriKind.Absolute).Query, "pairingCode=([^&]*)");
Assert.True(regex.Success, $"{s.Driver.Url} does not match expected regex");
var pairingCode = regex.Groups[1].Value;
return pairingCode;
}
private void SetTransactionOutput(SeleniumTester s, int index, BitcoinAddress dest, decimal amount, bool subtract = false)
{
s.Driver.FindElement(By.Id($"Outputs_{index}__DestinationAddress")).SendKeys(dest.ToString());
var amountElement = s.Driver.FindElement(By.Id($"Outputs_{index}__Amount"));
amountElement.Clear();
amountElement.SendKeys(amount.ToString(CultureInfo.InvariantCulture));
var checkboxElement = s.Driver.FindElement(By.Id($"Outputs_{index}__SubtractFeesFromOutput"));
if (checkboxElement.Selected != subtract)
{
checkboxElement.Click();
}
}
}
}

@ -7,6 +7,8 @@ using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using BTCPayServer.Lightning;
using BTCPayServer.Lightning.CLightning;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Tests.Lnd;
using BTCPayServer.Tests.Logging;
using NBitcoin;
@ -33,7 +35,7 @@ namespace BTCPayServer.Tests
if (!Directory.Exists(_Directory))
Directory.CreateDirectory(_Directory);
NetworkProvider = new BTCPayNetworkProvider(NetworkType.Regtest);
NetworkProvider = new BTCPayNetworkProvider(ChainName.Regtest);
ExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_BTCRPCCONNECTION", "server=http://127.0.0.1:43782;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork<BTCPayNetwork>("BTC").NBitcoinNetwork);
ExplorerNode.ScanRPCCapabilities();
@ -86,14 +88,50 @@ namespace BTCPayServer.Tests
#endif
public void ActivateLightning()
{
ActivateLightning(LightningConnectionType.Charge);
}
public void ActivateLightning(LightningConnectionType internalNode)
{
var btc = NetworkProvider.GetNetwork<BTCPayNetwork>("BTC").NBitcoinNetwork;
CustomerLightningD = LightningClientFactory.CreateClient(GetEnvironment("TEST_CUSTOMERLIGHTNINGD", "type=clightning;server=tcp://127.0.0.1:30992/"), btc);
MerchantLightningD = LightningClientFactory.CreateClient(GetEnvironment("TEST_MERCHANTLIGHTNINGD", "type=clightning;server=tcp://127.0.0.1:30993/"), btc);
MerchantCharge = new ChargeTester(this, "TEST_MERCHANTCHARGE", "type=charge;server=http://127.0.0.1:54938/;api-token=foiewnccewuify;allowinsecure=true", "merchant_lightningd", btc);
MerchantLnd = new LndMockTester(this, "TEST_MERCHANTLND", "https://lnd:lnd@127.0.0.1:35531/", "merchant_lnd", btc);
MerchantLnd = new LndMockTester(this, "TEST_MERCHANTLND", "http://lnd:lnd@127.0.0.1:35531/", "merchant_lnd", btc);
PayTester.UseLightning = true;
PayTester.IntegratedLightning = MerchantCharge.Client.Uri;
PayTester.IntegratedLightning = GetLightningConnectionString(internalNode, true);
}
public string GetLightningConnectionString(LightningConnectionType? connectionType, bool isMerchant)
{
string connectionString = null;
if (connectionType is null)
return LightningSupportedPaymentMethod.InternalNode;
if (connectionType == LightningConnectionType.Charge)
{
if (isMerchant)
connectionString = $"type=charge;server={MerchantCharge.Client.Uri.AbsoluteUri};allowinsecure=true";
else
throw new NotSupportedException();
}
else if (connectionType == LightningConnectionType.CLightning)
{
if (isMerchant)
connectionString = "type=clightning;server=" +
((CLightningClient)MerchantLightningD).Address.AbsoluteUri;
else
connectionString = "type=clightning;server=" +
((CLightningClient)CustomerLightningD).Address.AbsoluteUri;
}
else if (connectionType == LightningConnectionType.LndREST)
{
if (isMerchant)
connectionString = $"type=lnd-rest;server={MerchantLnd.Swagger.BaseUrl};allowinsecure=true";
else
throw new NotSupportedException();
}
else
throw new NotSupportedException(connectionType.ToString());
return connectionString;
}
public bool Dockerized

@ -10,7 +10,6 @@ using BTCPayServer.Storage.Services.Providers.AzureBlobStorage.Configuration;
using BTCPayServer.Storage.Services.Providers.FileSystemStorage.Configuration;
using BTCPayServer.Storage.ViewModels;
using BTCPayServer.Tests.Logging;
using DBriize.Utils;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Xunit;
@ -212,10 +211,9 @@ namespace BTCPayServer.Tests
Assert.NotNull(statusMessageModel);
Assert.Equal(StatusMessageModel.StatusSeverity.Success, statusMessageModel.Severity);
var index = statusMessageModel.Html.IndexOf("target='_blank'>");
var url = statusMessageModel.Html.Substring(index).ReplaceMultiple(new Dictionary<string, string>()
{
{"</a>", string.Empty}, {"target='_blank'>", string.Empty}
});
var url = statusMessageModel.Html.Substring(index)
.Replace("</a>", string.Empty)
.Replace("target='_blank'>", string.Empty);
//verify tmpfile is available and the same
data = await net.DownloadStringTaskAsync(new Uri(url));
Assert.Equal(fileContent, data);

@ -14,6 +14,7 @@ using BTCPayServer.Lightning;
using BTCPayServer.Lightning.CLightning;
using BTCPayServer.Models.AccountViewModels;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments.PayJoin.Sender;
using BTCPayServer.Services;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
@ -22,6 +23,8 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.CodeAnalysis.Operations;
using NBitcoin;
using BTCPayServer.BIP78.Sender;
using BTCPayServer.Payments.Lightning;
using NBitcoin.Payment;
using NBitpayClient;
using NBXplorer.DerivationStrategy;
@ -171,7 +174,7 @@ namespace BTCPayServer.Tests
}
public async Task<WalletId> RegisterDerivationSchemeAsync(string cryptoCode, ScriptPubKeyType segwit = ScriptPubKeyType.Legacy,
bool importKeysToNBX = false)
bool importKeysToNBX = false, bool importsKeysToBitcoinCore = false)
{
if (StoreId is null)
await CreateStoreAsync();
@ -181,10 +184,13 @@ namespace BTCPayServer.Tests
{
ScriptPubKeyType = segwit,
SavePrivateKeys = importKeysToNBX,
ImportKeysToRPC = importsKeysToBitcoinCore
});
await store.AddDerivationScheme(StoreId,
new DerivationSchemeViewModel()
await store.UpdateWallet(
new WalletSetupViewModel
{
StoreId = StoreId,
Method = importKeysToNBX ? WalletSetupMethod.HotWallet : WalletSetupMethod.WatchOnly,
Enabled = true,
CryptoCode = cryptoCode,
Network = SupportedNetwork,
@ -196,7 +202,7 @@ namespace BTCPayServer.Tests
KeyPath = GenerateWalletResponseV.AccountKeyPath.KeyPath.ToString(),
DerivationScheme = DerivationScheme.ToString(),
Confirmation = true
}, cryptoCode);
});
return new WalletId(StoreId, cryptoCode);
}
@ -256,40 +262,19 @@ namespace BTCPayServer.Tests
{
RegisterLightningNodeAsync(cryptoCode, connectionType, isMerchant).GetAwaiter().GetResult();
}
public async Task RegisterLightningNodeAsync(string cryptoCode, LightningConnectionType connectionType, bool isMerchant = true, string storeId = null)
public Task RegisterLightningNodeAsync(string cryptoCode, bool isMerchant = true, string storeId = null)
{
var storeController = this.GetController<StoresController>();
return RegisterLightningNodeAsync(cryptoCode, null, isMerchant, storeId);
}
public async Task RegisterLightningNodeAsync(string cryptoCode, LightningConnectionType? connectionType, bool isMerchant = true, string storeId = null)
{
var storeController = GetController<StoresController>();
string connectionString = null;
if (connectionType == LightningConnectionType.Charge)
{
if (isMerchant)
connectionString = $"type=charge;server={parent.MerchantCharge.Client.Uri.AbsoluteUri};allowinsecure=true";
else
throw new NotSupportedException();
}
else if (connectionType == LightningConnectionType.CLightning)
{
if (isMerchant)
connectionString = "type=clightning;server=" +
((CLightningClient)parent.MerchantLightningD).Address.AbsoluteUri;
else
connectionString = "type=clightning;server=" +
((CLightningClient)parent.CustomerLightningD).Address.AbsoluteUri;
}
else if (connectionType == LightningConnectionType.LndREST)
{
if (isMerchant)
connectionString = $"type=lnd-rest;server={parent.MerchantLnd.Swagger.BaseUrl};allowinsecure=true";
else
throw new NotSupportedException();
}
else
throw new NotSupportedException(connectionType.ToString());
var connectionString = parent.GetLightningConnectionString(connectionType, isMerchant);
var nodeType = connectionString == LightningSupportedPaymentMethod.InternalNode ? LightningNodeType.Internal : LightningNodeType.Custom;
await storeController.AddLightningNode(storeId ?? StoreId,
new LightningNodeViewModel() {ConnectionString = connectionString, SkipPortTest = true}, "save", "BTC");
new LightningNodeViewModel { ConnectionString = connectionString, LightningNodeType = nodeType, SkipPortTest = true }, "save", "BTC");
if (storeController.ModelState.ErrorCount != 0)
Assert.False(true, storeController.ModelState.FirstOrDefault().Value.Errors[0].ErrorMessage);
}
@ -358,7 +343,7 @@ namespace BTCPayServer.Tests
Logs.Tester.LogInformation($"Proposing {psbt.GetGlobalTransaction().GetHash()}");
if (expectedError is null && !senderError)
{
var proposed = await pjClient.RequestPayjoin(endpoint, settings, psbt, default);
var proposed = await pjClient.RequestPayjoin(endpoint, new PayjoinWallet(settings), psbt, default);
Logs.Tester.LogInformation($"Proposed payjoin is {proposed.GetGlobalTransaction().GetHash()}");
Assert.NotNull(proposed);
return proposed;
@ -367,11 +352,11 @@ namespace BTCPayServer.Tests
{
if (senderError)
{
await Assert.ThrowsAsync<PayjoinSenderException>(async () => await pjClient.RequestPayjoin(endpoint, settings, psbt, default));
await Assert.ThrowsAsync<PayjoinSenderException>(async () => await pjClient.RequestPayjoin(endpoint, new PayjoinWallet(settings), psbt, default));
}
else
{
var ex = await Assert.ThrowsAsync<PayjoinReceiverException>(async () => await pjClient.RequestPayjoin(endpoint, settings, psbt, default));
var ex = await Assert.ThrowsAsync<PayjoinReceiverException>(async () => await pjClient.RequestPayjoin(endpoint, new PayjoinWallet(settings), psbt, default));
var split = expectedError.Split('|');
Assert.Equal(split[0], ex.ErrorCode);
if (split.Length > 1)

@ -32,6 +32,7 @@ using BTCPayServer.Models.WalletViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Payments.PayJoin.Sender;
using BTCPayServer.Rating;
using BTCPayServer.Security.Bitpay;
using BTCPayServer.Services;
@ -43,7 +44,6 @@ using BTCPayServer.Services.Rates;
using BTCPayServer.Tests.Logging;
using BTCPayServer.U2F.Models;
using BTCPayServer.Validation;
using DBriize.Utils;
using ExchangeSharp;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
@ -263,21 +263,12 @@ namespace BTCPayServer.Tests
ManageController.AddApiKeyViewModel.PermissionValueItem.PermissionDescriptions.Where(pair =>
!Policies.IsStorePolicy(pair.Key) && !Policies.IsServerPolicy(pair.Key));
description = description.ReplaceMultiple(new Dictionary<string, string>()
{
{
"#OTHERPERMISSIONS#",
string.Join("\n", otherPolicies.Select(pair => $"* `{pair.Key}`: {pair.Value.Title}"))
},
{
"#SERVERPERMISSIONS#",
string.Join("\n", serverPolicies.Select(pair => $"* `{pair.Key}`: {pair.Value.Title}"))
},
{
"#STOREPERMISSIONS#",
string.Join("\n", storePolicies.Select(pair => $"* `{pair.Key}`: {pair.Value.Title}"))
}
});
description = description.Replace("#OTHERPERMISSIONS#",
string.Join("\n", otherPolicies.Select(pair => $"* `{pair.Key}`: {pair.Value.Title}")))
.Replace("#SERVERPERMISSIONS#",
string.Join("\n", serverPolicies.Select(pair => $"* `{pair.Key}`: {pair.Value.Title}")))
.Replace("#STOREPERMISSIONS#",
string.Join("\n", storePolicies.Select(pair => $"* `{pair.Key}`: {pair.Value.Title}")));
Logs.Tester.LogInformation(description);
var sresp = Assert
@ -417,11 +408,11 @@ namespace BTCPayServer.Tests
[Trait("Fast", "Fast")]
public void CanCalculateCryptoDue()
{
var networkProvider = new BTCPayNetworkProvider(NetworkType.Regtest);
var networkProvider = new BTCPayNetworkProvider(ChainName.Regtest);
var paymentMethodHandlerDictionary = new PaymentMethodHandlerDictionary(new IPaymentMethodHandler[]
{
new BitcoinLikePaymentHandler(null, networkProvider, null, null, null),
new LightningLikePaymentHandler(null, null, networkProvider, null, null),
new LightningLikePaymentHandler(null, null, networkProvider, null, null, null),
});
var entity = new InvoiceEntity();
entity.Networks = networkProvider;
@ -705,11 +696,11 @@ namespace BTCPayServer.Tests
[Trait("Fast", "Fast")]
public void CanAcceptInvoiceWithTolerance()
{
var networkProvider = new BTCPayNetworkProvider(NetworkType.Regtest);
var networkProvider = new BTCPayNetworkProvider(ChainName.Regtest);
var paymentMethodHandlerDictionary = new PaymentMethodHandlerDictionary(new IPaymentMethodHandler[]
{
new BitcoinLikePaymentHandler(null, networkProvider, null, null, null),
new LightningLikePaymentHandler(null, null, networkProvider, null, null),
new LightningLikePaymentHandler(null, null, networkProvider, null, null, null),
});
var entity = new InvoiceEntity();
entity.Networks = networkProvider;
@ -883,7 +874,7 @@ namespace BTCPayServer.Tests
[Trait("Fast", "Fast")]
public async Task CanEnumerateTorServices()
{
var tor = new TorServices(new BTCPayNetworkProvider(NetworkType.Regtest),
var tor = new TorServices(new BTCPayNetworkProvider(ChainName.Regtest),
new BTCPayServerOptions() { TorrcFile = TestUtils.GetTestDataFullPath("Tor/torrc") });
await tor.Refresh();
@ -904,7 +895,7 @@ namespace BTCPayServer.Tests
await tester.StartAsync();
await tester.EnsureChannelsSetup();
var user = tester.NewAccount();
await user.GrantAccessAsync();
await user.GrantAccessAsync(true);
await user.RegisterDerivationSchemeAsync("BTC");
await user.RegisterLightningNodeAsync("BTC", LightningConnectionType.CLightning);
user.SetNetworkFeeMode(NetworkFeeMode.Never);
@ -954,7 +945,7 @@ namespace BTCPayServer.Tests
await tester.StartAsync();
await tester.EnsureChannelsSetup();
var user = tester.NewAccount();
user.GrantAccess();
user.GrantAccess(true);
var storeController = user.GetController<StoresController>();
Assert.IsType<ViewResult>(storeController.UpdateStore());
Assert.IsType<ViewResult>(storeController.AddLightningNode(user.StoreId, "BTC"));
@ -1021,7 +1012,7 @@ namespace BTCPayServer.Tests
await tester.StartAsync();
await tester.EnsureChannelsSetup();
var user = tester.NewAccount();
user.GrantAccess();
user.GrantAccess(true);
user.RegisterLightningNode("BTC", type);
user.RegisterDerivationScheme("BTC");
@ -1180,7 +1171,7 @@ namespace BTCPayServer.Tests
[Trait("Integration", "Integration")]
public void CanSolveTheDogesRatesOnKraken()
{
var provider = new BTCPayNetworkProvider(NetworkType.Mainnet);
var provider = new BTCPayNetworkProvider(ChainName.Mainnet);
var factory = CreateBTCPayRateFactory();
var fetcher = new RateFetcher(factory);
@ -1211,7 +1202,7 @@ namespace BTCPayServer.Tests
});
}
var httpFactory = tester.PayTester.GetService<IHttpClientFactory>();
var client = httpFactory.CreateClient(PayjoinClient.PayjoinOnionNamedClient);
var client = httpFactory.CreateClient(PayjoinServerCommunicator.PayjoinOnionNamedClient);
Assert.NotNull(client);
var response = await client.GetAsync("https://check.torproject.org/");
response.EnsureSuccessStatusCode();
@ -1230,7 +1221,7 @@ namespace BTCPayServer.Tests
AssertConnectionDropped();
client.Dispose();
AssertConnectionDropped();
client = httpFactory.CreateClient(PayjoinClient.PayjoinOnionNamedClient);
client = httpFactory.CreateClient(PayjoinServerCommunicator.PayjoinOnionNamedClient);
response = await client.GetAsync("http://explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion/");
response.EnsureSuccessStatusCode();
AssertConnectionDropped();
@ -2058,7 +2049,7 @@ namespace BTCPayServer.Tests
[Trait("Fast", "Fast")]
public void HasCurrencyDataForNetworks()
{
var btcPayNetworkProvider = new BTCPayNetworkProvider(NetworkType.Regtest);
var btcPayNetworkProvider = new BTCPayNetworkProvider(ChainName.Regtest);
foreach (var network in btcPayNetworkProvider.GetAll())
{
var cd = CurrencyNameTable.Instance.GetCurrencyData(network.CryptoCode, false);
@ -2135,7 +2126,7 @@ namespace BTCPayServer.Tests
await tester.StartAsync();
await tester.EnsureChannelsSetup();
var user = tester.NewAccount();
user.GrantAccess();
user.GrantAccess(true);
user.RegisterDerivationScheme("BTC", ScriptPubKeyType.Segwit);
user.RegisterLightningNode("BTC", LightningConnectionType.CLightning);
@ -2175,10 +2166,9 @@ namespace BTCPayServer.Tests
Assert.StartsWith("bitcoin:", paymentMethodSecond.InvoiceBitcoinUrlQR);
var split = paymentMethodSecond.InvoiceBitcoinUrlQR.Split('?')[0];
// Standard for uppercase Bech32 addresses in QR codes is still not implemented in all wallets
// When it is widely propagated consider uncommenting these lines
//Assert.True($"BITCOIN:{paymentMethodSecond.BtcAddress.ToUpperInvariant()}" == split);
Assert.True($"bitcoin:{paymentMethodSecond.BtcAddress}" == split);
// Standard for all uppercase characters in QR codes is still not implemented in all wallets
// But we're proceeding with BECH32 being uppercase
Assert.True($"bitcoin:{paymentMethodSecond.BtcAddress.ToUpperInvariant()}" == split);
}
}
@ -2193,7 +2183,7 @@ namespace BTCPayServer.Tests
await tester.StartAsync();
await tester.EnsureChannelsSetup();
var user = tester.NewAccount();
user.GrantAccess();
user.GrantAccess(true);
user.RegisterLightningNode("BTC", LightningConnectionType.Charge);
var vm = Assert.IsType<CheckoutExperienceViewModel>(Assert
.IsType<ViewResult>(user.GetController<StoresController>().CheckoutExperience()).Model);
@ -2996,7 +2986,7 @@ namespace BTCPayServer.Tests
[Trait("Integration", "Integration")]
public void CanGetRateCryptoCurrenciesByDefault()
{
var provider = new BTCPayNetworkProvider(NetworkType.Mainnet);
var provider = new BTCPayNetworkProvider(ChainName.Mainnet);
var factory = CreateBTCPayRateFactory();
var fetcher = new RateFetcher(factory);
var pairs =
@ -3068,35 +3058,35 @@ namespace BTCPayServer.Tests
var unusedUri = new Uri("https://toto.com");
Assert.True(ExternalConnectionString.TryParse("server=/test", out var connStr, out var error));
var expanded = await connStr.Expand(new Uri("https://toto.com"), ExternalServiceTypes.Charge,
NetworkType.Mainnet);
ChainName.Mainnet);
Assert.Equal(new Uri("https://toto.com/test"), expanded.Server);
expanded = await connStr.Expand(new Uri("http://toto.onion"), ExternalServiceTypes.Charge,
NetworkType.Mainnet);
ChainName.Mainnet);
Assert.Equal(new Uri("http://toto.onion/test"), expanded.Server);
await Assert.ThrowsAsync<SecurityException>(() =>
connStr.Expand(new Uri("http://toto.com"), ExternalServiceTypes.Charge, NetworkType.Mainnet));
await connStr.Expand(new Uri("http://toto.com"), ExternalServiceTypes.Charge, NetworkType.Testnet);
connStr.Expand(new Uri("http://toto.com"), ExternalServiceTypes.Charge, ChainName.Mainnet));
await connStr.Expand(new Uri("http://toto.com"), ExternalServiceTypes.Charge, ChainName.Testnet);
// Make sure absolute paths are not expanded
Assert.True(ExternalConnectionString.TryParse("server=https://tow/test", out connStr, out error));
expanded = await connStr.Expand(new Uri("https://toto.com"), ExternalServiceTypes.Charge,
NetworkType.Mainnet);
ChainName.Mainnet);
Assert.Equal(new Uri("https://tow/test"), expanded.Server);
// Error if directory not exists
Assert.True(ExternalConnectionString.TryParse($"server={unusedUri};macaroondirectorypath=pouet",
out connStr, out error));
await Assert.ThrowsAsync<DirectoryNotFoundException>(() =>
connStr.Expand(unusedUri, ExternalServiceTypes.LNDGRPC, NetworkType.Mainnet));
connStr.Expand(unusedUri, ExternalServiceTypes.LNDGRPC, ChainName.Mainnet));
await Assert.ThrowsAsync<DirectoryNotFoundException>(() =>
connStr.Expand(unusedUri, ExternalServiceTypes.LNDRest, NetworkType.Mainnet));
await connStr.Expand(unusedUri, ExternalServiceTypes.Charge, NetworkType.Mainnet);
connStr.Expand(unusedUri, ExternalServiceTypes.LNDRest, ChainName.Mainnet));
await connStr.Expand(unusedUri, ExternalServiceTypes.Charge, ChainName.Mainnet);
var macaroonDirectory = CreateDirectory();
Assert.True(ExternalConnectionString.TryParse(
$"server={unusedUri};macaroondirectorypath={macaroonDirectory}", out connStr, out error));
await connStr.Expand(unusedUri, ExternalServiceTypes.LNDGRPC, NetworkType.Mainnet);
expanded = await connStr.Expand(unusedUri, ExternalServiceTypes.LNDRest, NetworkType.Mainnet);
await connStr.Expand(unusedUri, ExternalServiceTypes.LNDGRPC, ChainName.Mainnet);
expanded = await connStr.Expand(unusedUri, ExternalServiceTypes.LNDRest, ChainName.Mainnet);
Assert.NotNull(expanded.Macaroons);
Assert.Null(expanded.MacaroonFilePath);
Assert.Null(expanded.Macaroons.AdminMacaroon);
@ -3106,7 +3096,7 @@ namespace BTCPayServer.Tests
File.WriteAllBytes($"{macaroonDirectory}/admin.macaroon", new byte[] { 0xaa });
File.WriteAllBytes($"{macaroonDirectory}/invoice.macaroon", new byte[] { 0xab });
File.WriteAllBytes($"{macaroonDirectory}/readonly.macaroon", new byte[] { 0xac });
expanded = await connStr.Expand(unusedUri, ExternalServiceTypes.LNDRest, NetworkType.Mainnet);
expanded = await connStr.Expand(unusedUri, ExternalServiceTypes.LNDRest, ChainName.Mainnet);
Assert.NotNull(expanded.Macaroons.AdminMacaroon);
Assert.NotNull(expanded.Macaroons.InvoiceMacaroon);
Assert.Equal("ab", expanded.Macaroons.InvoiceMacaroon.Hex);
@ -3116,7 +3106,7 @@ namespace BTCPayServer.Tests
Assert.True(ExternalConnectionString.TryParse(
$"server={unusedUri};cookiefilepath={macaroonDirectory}/charge.cookie", out connStr, out error));
File.WriteAllText($"{macaroonDirectory}/charge.cookie", "apitoken");
expanded = await connStr.Expand(unusedUri, ExternalServiceTypes.Charge, NetworkType.Mainnet);
expanded = await connStr.Expand(unusedUri, ExternalServiceTypes.Charge, ChainName.Mainnet);
Assert.Equal("apitoken", expanded.APIToken);
}
@ -3127,6 +3117,24 @@ namespace BTCPayServer.Tests
return name;
}
[Fact(Timeout = TestTimeout)]
[Trait("Fast", "Fast")]
public void CanCheckFileNameValid()
{
var tests = new[]
{
("test.com", true),
("/test.com", false),
("te/st.com", false),
("\\test.com", false),
("te\\st.com", false)
};
foreach(var t in tests)
{
Assert.Equal(t.Item2, t.Item1.IsValidFileName());
}
}
[Fact(Timeout = TestTimeout)]
[Trait("Fast", "Fast")]
public async Task CanCreateSqlitedb()
@ -3201,10 +3209,12 @@ namespace BTCPayServer.Tests
[Trait("Fast", "Fast")]
public void ParseDerivationSchemeSettings()
{
var mainnet = new BTCPayNetworkProvider(NetworkType.Mainnet).GetNetwork<BTCPayNetwork>("BTC");
var mainnet = new BTCPayNetworkProvider(ChainName.Mainnet).GetNetwork<BTCPayNetwork>("BTC");
var root = new Mnemonic(
"usage fever hen zero slide mammal silent heavy donate budget pulse say brain thank sausage brand craft about save attract muffin advance illegal cabbage")
.DeriveExtKey();
// ColdCard
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
"{\"keystore\": {\"ckcc_xpub\": \"xpub661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw\", \"xpub\": \"ypub6WWc2gWwHbdnAAyJDnR4SPL1phRh7REqrPBfZeizaQ1EmTshieRXJC3Z5YoU4wkcdKHEjQGkh6AYEzCQC1Kz3DNaWSwdc1pc8416hAjzqyD\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/0'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}",
mainnet, out var settings));
@ -3218,8 +3228,7 @@ namespace BTCPayServer.Tests
settings.AccountOriginal);
Assert.Equal(root.Derive(new KeyPath("m/49'/0'/0'")).Neuter().PubKey.WitHash.ScriptPubKey.Hash.ScriptPubKey,
settings.AccountDerivation.GetDerivation().ScriptPubKey);
var testnet = new BTCPayNetworkProvider(NetworkType.Testnet).GetNetwork<BTCPayNetwork>("BTC");
var testnet = new BTCPayNetworkProvider(ChainName.Testnet).GetNetwork<BTCPayNetwork>("BTC");
// Should be legacy
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
@ -3239,6 +3248,15 @@ namespace BTCPayServer.Tests
"{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"vpub5YjYxTemJ39tFRnuAhwduyxG2tKGjoEpmvqVQRPqdYrqa6YGoeSzBtHXaJUYB19zDbXs3JjbEcVWERjQBPf9bEfUUMZNMv1QnMyHV8JPqyf\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/84'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}",
testnet, out settings));
Assert.True(settings.AccountDerivation is DirectDerivationStrategy s3 && s3.Segwit);
// Specter
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
"{\"label\": \"Specter\", \"blockheight\": 123456, \"descriptor\": \"wpkh([8bafd160/49h/0h/0h]xpub661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw/0/*)#9x4vkw48\"}",
mainnet, out var specter));
Assert.Equal(root.GetPublicKey().GetHDFingerPrint(), specter.AccountKeySettings[0].RootFingerprint);
Assert.Equal(specter.AccountKeySettings[0].RootFingerprint, hd);
Assert.Equal("49'/0'/0'", specter.AccountKeySettings[0].AccountKeyPath.ToString());
Assert.Equal("Specter", specter.Label);
}
@ -3409,7 +3427,86 @@ namespace BTCPayServer.Tests
Assert.False(fn.Seen);
}
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanDoLightningInternalNodeMigration()
{
using (var tester = ServerTester.Create(newDb: true))
{
tester.ActivateLightning(LightningConnectionType.CLightning);
await tester.StartAsync();
var acc = tester.NewAccount();
await acc.GrantAccessAsync(true);
await acc.CreateStoreAsync();
// Test if legacy DerivationStrategy column is converted to DerivationStrategies
var store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId);
var xpub = "tpubDDmH1briYfZcTDMEc7uMEA5hinzjUTzR9yMC1drxTMeiWyw1VyCqTuzBke6df2sqbfw9QG6wbgTLF5yLjcXsZNaXvJMZLwNEwyvmiFWcLav";
var derivation = $"{xpub}-[legacy]";
store.DerivationStrategy = derivation;
await tester.PayTester.StoreRepository.UpdateStore(store);
await RestartMigration(tester);
store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId);
Assert.True(string.IsNullOrEmpty(store.DerivationStrategy));
var v = (DerivationSchemeSettings)store.GetSupportedPaymentMethods(tester.NetworkProvider).First();
Assert.Equal(derivation, v.AccountDerivation.ToString());
Assert.Equal(derivation, v.AccountOriginal.ToString());
Assert.Equal(xpub, v.SigningKey.ToString());
Assert.Equal(xpub, v.GetSigningAccountKeySettings().AccountKey.ToString());
await acc.RegisterLightningNodeAsync("BTC", LightningConnectionType.CLightning, true);
store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId);
var lnMethod = store.GetSupportedPaymentMethods(tester.NetworkProvider).OfType<LightningSupportedPaymentMethod>().First();
Assert.NotNull(lnMethod.GetExternalLightningUrl());
await RestartMigration(tester);
store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId);
lnMethod = store.GetSupportedPaymentMethods(tester.NetworkProvider).OfType<LightningSupportedPaymentMethod>().First();
Assert.Null(lnMethod.GetExternalLightningUrl());
// Test if legacy lightning charge settings are converted to LightningConnectionString
store.DerivationStrategies = new JObject()
{
new JProperty("BTC_LightningLike", new JObject()
{
new JProperty("LightningChargeUrl", "http://mycharge.com/"),
new JProperty("Username", "usr"),
new JProperty("Password", "pass"),
new JProperty("CryptoCode", "BTC"),
new JProperty("PaymentId", "someshit"),
})
}.ToString();
await tester.PayTester.StoreRepository.UpdateStore(store);
await RestartMigration(tester);
store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId);
lnMethod = store.GetSupportedPaymentMethods(tester.NetworkProvider).OfType<LightningSupportedPaymentMethod>().First();
Assert.NotNull(lnMethod.GetExternalLightningUrl());
var url = lnMethod.GetExternalLightningUrl();
Assert.Equal(LightningConnectionType.Charge, url.ConnectionType);
Assert.Equal("pass", url.Password);
Assert.Equal("usr", url.Username);
// Test if lightning connection strings get migrated to internal
store.DerivationStrategies = new JObject()
{
new JProperty("BTC_LightningLike", new JObject()
{
new JProperty("CryptoCode", "BTC"),
new JProperty("LightningConnectionString", tester.PayTester.IntegratedLightning),
})
}.ToString();
await tester.PayTester.StoreRepository.UpdateStore(store);
await RestartMigration(tester);
store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId);
lnMethod = store.GetSupportedPaymentMethods(tester.NetworkProvider).OfType<LightningSupportedPaymentMethod>().First();
Assert.True(lnMethod.IsInternalNode);
}
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanDoInvoiceMigrations()
@ -3423,7 +3520,7 @@ namespace BTCPayServer.Tests
await acc.CreateStoreAsync();
await acc.RegisterDerivationSchemeAsync("BTC");
var store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId);
var blob = store.GetStoreBlob();
var serializer = new Serializer(null);
@ -3444,10 +3541,10 @@ namespace BTCPayServer.Tests
new KeyPath("44'/0'/0'").ToString()
}
})));
blob.AdditionalData.Add("networkFeeDisabled", JToken.Parse(
serializer.ToString((bool?)true)));
blob.AdditionalData.Add("onChainMinValue", JToken.Parse(
serializer.ToString(new CurrencyValue()
{
@ -3460,18 +3557,13 @@ namespace BTCPayServer.Tests
Currency = "USD",
Value = 5m
}.ToString())));
store.SetStoreBlob(blob);
await tester.PayTester.StoreRepository.UpdateStore(store);
var settings = tester.PayTester.GetService<SettingsRepository>();
await settings.UpdateSetting<MigrationSettings>(new MigrationSettings());
var migrationStartupTask = tester.PayTester.GetService<IServiceProvider>().GetServices<IStartupTask>()
.Single(task => task is MigrationStartupTask);
await migrationStartupTask.ExecuteAsync();
await RestartMigration(tester);
store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId);
blob = store.GetStoreBlob();
Assert.Empty(blob.AdditionalData);
Assert.Single(blob.PaymentMethodCriteria);
@ -3486,7 +3578,16 @@ namespace BTCPayServer.Tests
}
}
private static async Task RestartMigration(ServerTester tester)
{
var settings = tester.PayTester.GetService<SettingsRepository>();
await settings.UpdateSetting<MigrationSettings>(new MigrationSettings());
var migrationStartupTask = tester.PayTester.GetService<IServiceProvider>().GetServices<IStartupTask>()
.Single(task => task is MigrationStartupTask);
await migrationStartupTask.ExecuteAsync();
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task EmailSenderTests()

@ -1,5 +1,6 @@
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading;

@ -24,7 +24,7 @@ services:
TEST_MERCHANTLIGHTNINGD: "type=clightning;server=unix://etc/merchant_lightningd_datadir/lightning-rpc"
TEST_CUSTOMERLIGHTNINGD: "type=clightning;server=unix://etc/customer_lightningd_datadir/lightning-rpc"
TEST_MERCHANTCHARGE: "type=charge;server=http://lightning-charged:9112/;api-token=foiewnccewuify;allowinsecure=true"
TEST_MERCHANTLND: "https://lnd:lnd@merchant_lnd:8080/"
TEST_MERCHANTLND: "http://lnd:lnd@merchant_lnd:8080/"
TESTS_INCONTAINER: "true"
TESTS_SSHCONNECTION: "root@sshd:22"
TESTS_SSHPASSWORD: ""
@ -69,20 +69,22 @@ services:
- "sshd_datadir:/root/.ssh"
devlnd:
image: btcpayserver/bitcoin:0.20.1
image: btcpayserver/bitcoin:0.21.0
environment:
BITCOIN_NETWORK: regtest
BITCOIN_WALLETDIR: "/data/wallets"
BITCOIN_EXTRA_ARGS: |
deprecatedrpc=signrawtransaction
connect=bitcoind:39388
fallbackfee=0.0002
rpcallowip=0.0.0.0/0
links:
- nbxplorer
- postgres
- customer_lnd
- merchant_lnd
nbxplorer:
image: nicolasdorier/nbxplorer:2.1.45
image: nicolasdorier/nbxplorer:2.1.49
restart: unless-stopped
ports:
- "32838:32838"
@ -116,14 +118,16 @@ services:
bitcoind:
restart: unless-stopped
image: btcpayserver/bitcoin:0.20.1
image: btcpayserver/bitcoin:0.21.0
environment:
BITCOIN_NETWORK: regtest
BITCOIN_WALLETDIR: "/data/wallets"
BITCOIN_EXTRA_ARGS: |-
rpcuser=ceiwHEbqWI83
rpcpassword=DwubwWsoo3
rpcport=43782
rpcbind=0.0.0.0:43782
rpcallowip=0.0.0.0/0
port=39388
whitelist=0.0.0.0/0
zmqpubrawblock=tcp://0.0.0.0:28332
@ -142,7 +146,7 @@ services:
- "bitcoin_datadir:/data"
customer_lightningd:
image: btcpayserver/lightning:v0.9.0-1-dev
image: btcpayserver/lightning:v0.9.3-1-dev
stop_signal: SIGKILL
restart: unless-stopped
environment:
@ -169,12 +173,14 @@ services:
- bitcoind
lightning-charged:
image: shesek/lightning-charge:0.4.19-standalone
image: shesek/lightning-charge:0.4.23-1-standalone
restart: unless-stopped
environment:
NETWORK: regtest
API_TOKEN: foiewnccewuify
BITCOIND_RPCCONNECT: bitcoind
LN_NET_PATH: /etc/lightning
LN_NET: /etc/lightning
volumes:
- "bitcoin_datadir:/etc/bitcoin"
- "lightning_charge_datadir:/data"
@ -189,7 +195,7 @@ services:
- merchant_lightningd
merchant_lightningd:
image: btcpayserver/lightning:v0.9.0-1-dev
image: btcpayserver/lightning:v0.9.3-1-dev
stop_signal: SIGKILL
environment:
EXPOSE_TCP: "true"
@ -221,18 +227,21 @@ services:
- "5432"
merchant_lnd:
image: btcpayserver/lnd:v0.11.0-beta
image: btcpayserver/lnd:v0.12.1-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
LND_ENVIRONMENT: "regtest"
LND_EXPLORERURL: "http://nbxplorer:32838/"
LND_REST_LISTEN_HOST: http://merchant_lnd:8080
LND_EXTRA_ARGS: |
restlisten=0.0.0.0:8080
restlisten=merchant_lnd:8080
rpclisten=127.0.0.1:10008
rpclisten=0.0.0.0:10009
rpclisten=merchant_lnd:10009
bitcoin.node=bitcoind
bitcoind.rpchost=bitcoind:43782
bitcoind.rpcuser=ceiwHEbqWI83
bitcoind.rpcpass=DwubwWsoo3
bitcoind.zmqpubrawblock=tcp://bitcoind:28332
bitcoind.zmqpubrawtx=tcp://bitcoind:28333
externalip=merchant_lnd:9735
@ -240,6 +249,7 @@ services:
no-macaroons=1
debuglevel=debug
trickledelay=1000
no-rest-tls=1
ports:
- "35531:8080"
expose:
@ -251,18 +261,21 @@ services:
- bitcoind
customer_lnd:
image: btcpayserver/lnd:v0.11.0-beta
image: btcpayserver/lnd:v0.12.1-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
LND_ENVIRONMENT: "regtest"
LND_EXPLORERURL: "http://nbxplorer:32838/"
LND_REST_LISTEN_HOST: http://customer_lnd:8080
LND_EXTRA_ARGS: |
restlisten=0.0.0.0:8080
restlisten=customer_lnd:8080
rpclisten=127.0.0.1:10008
rpclisten=0.0.0.0:10009
rpclisten=customer_lnd:10009
bitcoin.node=bitcoind
bitcoind.rpchost=bitcoind:43782
bitcoind.rpcuser=ceiwHEbqWI83
bitcoind.rpcpass=DwubwWsoo3
bitcoind.zmqpubrawblock=tcp://bitcoind:28332
bitcoind.zmqpubrawtx=tcp://bitcoind:28333
externalip=customer_lnd:10009
@ -270,6 +283,7 @@ services:
no-macaroons=1
debuglevel=debug
trickledelay=1000
no-rest-tls=1
ports:
- "35532:8080"
expose:

@ -22,7 +22,7 @@ services:
TEST_MERCHANTLIGHTNINGD: "type=clightning;server=unix://etc/merchant_lightningd_datadir/lightning-rpc"
TEST_CUSTOMERLIGHTNINGD: "type=clightning;server=unix://etc/customer_lightningd_datadir/lightning-rpc"
TEST_MERCHANTCHARGE: "type=charge;server=http://lightning-charged:9112/;api-token=foiewnccewuify;allowinsecure=true"
TEST_MERCHANTLND: "https://lnd:lnd@merchant_lnd:8080/"
TEST_MERCHANTLND: "http://lnd:lnd@merchant_lnd:8080/"
TESTS_INCONTAINER: "true"
TESTS_SSHCONNECTION: "root@sshd:22"
TESTS_SSHPASSWORD: ""
@ -66,12 +66,14 @@ services:
- "sshd_datadir:/root/.ssh"
devlnd:
image: btcpayserver/bitcoin:0.20.1
image: btcpayserver/bitcoin:0.21.0
environment:
BITCOIN_NETWORK: regtest
BITCOIN_WALLETDIR: "/data/wallets"
BITCOIN_EXTRA_ARGS: |
deprecatedrpc=signrawtransaction
connect=bitcoind:39388
rpcallowip=0.0.0.0/0
fallbackfee=0.0002
links:
- nbxplorer
@ -79,7 +81,7 @@ services:
- customer_lnd
- merchant_lnd
nbxplorer:
image: nicolasdorier/nbxplorer:2.1.45
image: nicolasdorier/nbxplorer:2.1.49
restart: unless-stopped
ports:
- "32838:32838"
@ -103,14 +105,16 @@ services:
bitcoind:
restart: unless-stopped
image: btcpayserver/bitcoin:0.20.1
image: btcpayserver/bitcoin:0.21.0
environment:
BITCOIN_NETWORK: regtest
BITCOIN_WALLETDIR: "/data/wallets"
BITCOIN_EXTRA_ARGS: |-
rpcuser=ceiwHEbqWI83
rpcpassword=DwubwWsoo3
rpcport=43782
rpcbind=0.0.0.0:43782
rpcallowip=0.0.0.0/0
port=39388
whitelist=0.0.0.0/0
zmqpubrawblock=tcp://0.0.0.0:28332
@ -129,7 +133,7 @@ services:
- "bitcoin_datadir:/data"
customer_lightningd:
image: btcpayserver/lightning:v0.9.0-1-dev
image: btcpayserver/lightning:v0.9.3-1-dev
stop_signal: SIGKILL
restart: unless-stopped
environment:
@ -156,12 +160,14 @@ services:
- bitcoind
lightning-charged:
image: shesek/lightning-charge:0.4.19-standalone
image: shesek/lightning-charge:0.4.23-1-standalone
restart: unless-stopped
environment:
NETWORK: regtest
API_TOKEN: foiewnccewuify
BITCOIND_RPCCONNECT: bitcoind
LN_NET_PATH: /etc/lightning
LN_NET: /etc/lightning
volumes:
- "bitcoin_datadir:/etc/bitcoin"
- "lightning_charge_datadir:/data"
@ -176,7 +182,7 @@ services:
- merchant_lightningd
merchant_lightningd:
image: btcpayserver/lightning:v0.9.0-1-dev
image: btcpayserver/lightning:v0.9.3-1-dev
stop_signal: SIGKILL
environment:
EXPOSE_TCP: "true"
@ -209,18 +215,21 @@ services:
- "5432"
merchant_lnd:
image: btcpayserver/lnd:v0.11.0-beta
image: btcpayserver/lnd:v0.12.1-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
LND_ENVIRONMENT: "regtest"
LND_EXPLORERURL: "http://nbxplorer:32838/"
LND_REST_LISTEN_HOST: http://merchant_lnd:8080
LND_EXTRA_ARGS: |
restlisten=0.0.0.0:8080
restlisten=merchant_lnd:8080
rpclisten=127.0.0.1:10008
rpclisten=0.0.0.0:10009
rpclisten=merchant_lnd:10009
bitcoin.node=bitcoind
bitcoind.rpchost=bitcoind:43782
bitcoind.rpcuser=ceiwHEbqWI83
bitcoind.rpcpass=DwubwWsoo3
bitcoind.zmqpubrawblock=tcp://bitcoind:28332
bitcoind.zmqpubrawtx=tcp://bitcoind:28333
externalip=merchant_lnd:9735
@ -228,6 +237,7 @@ services:
no-macaroons=1
debuglevel=debug
trickledelay=1000
no-rest-tls=1
ports:
- "35531:8080"
expose:
@ -239,18 +249,21 @@ services:
- bitcoind
customer_lnd:
image: btcpayserver/lnd:v0.11.0-beta
image: btcpayserver/lnd:v0.12.1-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
LND_ENVIRONMENT: "regtest"
LND_EXPLORERURL: "http://nbxplorer:32838/"
LND_REST_LISTEN_HOST: http://customer_lnd:8080
LND_EXTRA_ARGS: |
restlisten=0.0.0.0:8080
restlisten=customer_lnd:8080
rpclisten=127.0.0.1:10008
rpclisten=0.0.0.0:10009
rpclisten=customer_lnd:10009
bitcoin.node=bitcoind
bitcoind.rpchost=bitcoind:43782
bitcoind.rpcuser=ceiwHEbqWI83
bitcoind.rpcpass=DwubwWsoo3
bitcoind.zmqpubrawblock=tcp://bitcoind:28332
bitcoind.zmqpubrawtx=tcp://bitcoind:28333
externalip=customer_lnd:10009
@ -258,6 +271,7 @@ services:
no-macaroons=1
debuglevel=debug
trickledelay=1000
no-rest-tls=1
ports:
- "35532:8080"
expose:

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
 <Project Sdk="Microsoft.NET.Sdk.Web">
<Import Project="../Build/Version.csproj" Condition="Exists('../Build/Version.csproj')" />
<Import Project="../Build/Common.csproj" />
@ -44,8 +44,9 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="BIP78.Sender" Version="0.2.0" />
<PackageReference Include="BTCPayServer.Hwi" Version="1.1.3" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.2.4" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.2.7" />
<PackageReference Include="BuildBundlerMinifier" Version="3.2.435" />
<PackageReference Include="BundlerMinifier.Core" Version="3.2.435" />
<PackageReference Include="BundlerMinifier.TagHelpers" Version="3.2.435" />
@ -60,7 +61,6 @@
<PackageReference Include="QRCoder" Version="1.4.1" />
<PackageReference Include="System.IO.Pipelines" Version="4.7.2" />
<PackageReference Include="NBitpayClient" Version="1.0.0.39" />
<PackageReference Include="DBriize" Version="1.0.1.3" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.2" />
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.3" />
@ -134,6 +134,7 @@
</ItemGroup>
<ItemGroup>
<Folder Include="Views\Stores\Integrations" />
<Folder Include="wwwroot\vendor\clipboard.js\" />
<Folder Include="wwwroot\vendor\highlightjs\" />
<Folder Include="wwwroot\vendor\summernote" />

@ -2,6 +2,7 @@
@inject UserManager<ApplicationUser> UserManager
@inject CssThemeManager CssThemeManager
@using BTCPayServer.HostedServices
@using BTCPayServer.Views.Notifications
@using Microsoft.AspNetCore.Http.Extensions
@model BTCPayServer.Components.NotificationsDropdown.NotificationSummaryViewModel
@addTagHelper *, BundlerMinifier.TagHelpers
@ -9,15 +10,15 @@
@if (Model.UnseenCount > 0)
{
<li class="nav-item dropdown" id="notifications-nav-item">
<a class="nav-link js-scroll-trigger border-bottom-0" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" id="Notifications">
<i class="fa fa-bell"></i>
<a class="nav-link js-scroll-trigger @ViewData.IsActiveCategory(typeof(NotificationsNavPages))" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" id="Notifications">
<span class="d-lg-none d-sm-block">Notifications</span><i class="fa fa-bell d-lg-inline-block d-none"></i>
</a>
<span class="alerts-badge badge badge-pill badge-danger">@Model.UnseenCount</span>
<div class="dropdown-menu dropdown-menu-right text-center notification-dropdown" aria-labelledby="navbarDropdown">
<div class="d-flex align-items-center justify-content-between py-3 px-4 border-bottom border-light">
<h5 class="m-0">Notifications</h5>
<form asp-controller="Notifications" asp-action="MarkAllAsSeen" asp-route-returnUrl="@Context.Request.GetCurrentPathWithQueryString()" method="post">
<button class="btn btn-link p-0 font-weight-semibold" type="submit">Mark all as seen</button>
<button class="btn btn-link p-0" type="submit">Mark all as seen</button>
</form>
</div>
@foreach (var notif in Model.Last5)
@ -28,7 +29,7 @@
</div>
<div class="notification-item__content">
<div class="text-left text-wrap font-weight-semibold">
<div class="text-left text-wrap">
@notif.Body
</div>
<div class="text-left d-flex">
@ -38,7 +39,7 @@
</a>
}
<div class="p-3">
<a class="font-weight-semibold" asp-controller="Notifications" asp-action="Index">View all</a>
<a asp-controller="Notifications" asp-action="Index">View all</a>
</div>
</div>
</li>
@ -46,8 +47,8 @@
else
{
<li class="nav-item" id="notifications-nav-item">
<a asp-controller="Notifications" asp-action="Index" title="Notifications" class="nav-link js-scroll-trigger" id="Notifications">
<i class="fa fa-bell"></i>
<a asp-controller="Notifications" asp-action="Index" title="Notifications" class="nav-link js-scroll-trigger @ViewData.IsActiveCategory(typeof(NotificationsNavPages))" id="Notifications">
<span class="d-lg-none d-sm-block">Notifications</span><i class="fa fa-bell d-lg-inline-block d-none"></i>
</a>
</li>
}

@ -13,7 +13,7 @@ namespace BTCPayServer.Configuration
{
public class BTCPayServerOptions
{
public NetworkType NetworkType
public ChainName NetworkType
{
get; set;
}
@ -61,7 +61,7 @@ namespace BTCPayServer.Configuration
Logs.Configuration.LogInformation("Network: " + NetworkType.ToString());
if (conf.GetOrDefault<bool>("launchsettings", false) && NetworkType != NetworkType.Regtest)
if (conf.GetOrDefault<bool>("launchsettings", false) && NetworkType != ChainName.Regtest)
throw new ConfigException($"You need to run BTCPayServer with the run.sh or run.ps1 script");

@ -12,7 +12,7 @@ namespace BTCPayServer.Configuration
{
protected override CommandLineApplication CreateCommandLineApplicationCore()
{
var provider = new BTCPayNetworkProvider(NetworkType.Mainnet);
var provider = new BTCPayNetworkProvider(ChainName.Mainnet);
var chains = string.Join(",", provider.GetAll().Select(n => n.CryptoCode.ToLowerInvariant()).ToArray());
CommandLineApplication app = new CommandLineApplication(true)
{
@ -23,6 +23,7 @@ namespace BTCPayServer.Configuration
app.Option("-n | --network", $"Set the network among (mainnet,testnet,regtest) (default: mainnet)", CommandOptionType.SingleValue);
app.Option("--testnet | -testnet", $"Use testnet (deprecated, use --network instead)", CommandOptionType.BoolValue);
app.Option("--regtest | -regtest", $"Use regtest (deprecated, use --network instead)", CommandOptionType.BoolValue);
app.Option("--signet | -signet", $"Use signet (deprecated, use --network instead)", CommandOptionType.BoolValue);
app.Option("--allow-admin-registration", $"For debug only, will show a checkbox when a new user register to add himself as admin. (default: false)", CommandOptionType.BoolValue);
app.Option("--chains | -c", $"Chains to support as a comma separated (default: btc; available: {chains})", CommandOptionType.SingleValue);
app.Option("--postgres", $"Connection string to a PostgreSQL database", CommandOptionType.SingleValue);
@ -89,7 +90,7 @@ namespace BTCPayServer.Configuration
return Path.Combine(chainDir, fileName);
}
public static NetworkType GetNetworkType(IConfiguration conf)
public static ChainName GetNetworkType(IConfiguration conf)
{
var network = conf.GetOrDefault<string>("network", null);
if (network != null)
@ -99,10 +100,12 @@ namespace BTCPayServer.Configuration
{
throw new ConfigException($"Invalid network parameter '{network}'");
}
return n.NetworkType;
return n.ChainName;
}
var net = conf.GetOrDefault<bool>("regtest", false) ? NetworkType.Regtest :
conf.GetOrDefault<bool>("testnet", false) ? NetworkType.Testnet : NetworkType.Mainnet;
var net = conf.GetOrDefault<bool>("regtest", false) ? ChainName.Regtest :
conf.GetOrDefault<bool>("testnet", false) ? ChainName.Testnet :
conf.GetOrDefault<bool>("signet", false) ? Bitcoin.Instance.Signet.ChainName :
ChainName.Mainnet;
return net;
}

@ -30,12 +30,12 @@ namespace BTCPayServer.Configuration
/// Return a connectionString which does not depends on external resources or information like relative path or file path
/// </summary>
/// <returns></returns>
public async Task<ExternalConnectionString> Expand(Uri absoluteUrlBase, ExternalServiceTypes serviceType, NetworkType network)
public async Task<ExternalConnectionString> Expand(Uri absoluteUrlBase, ExternalServiceTypes serviceType, ChainName network)
{
var connectionString = this.Clone();
// Transform relative URI into absolute URI
var serviceUri = connectionString.Server.IsAbsoluteUri ? connectionString.Server : ToRelative(absoluteUrlBase, connectionString.Server.ToString());
var isSecure = network != NetworkType.Mainnet ||
var isSecure = network != ChainName.Mainnet ||
serviceUri.Scheme == "https" ||
serviceUri.DnsSafeHost.EndsWith(".onion", StringComparison.OrdinalIgnoreCase) ||
Extensions.IsLocalNetwork(serviceUri.DnsSafeHost);

@ -1,5 +1,6 @@
using System;
using System.Globalization;
using System.Security.Policy;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
@ -34,6 +35,7 @@ namespace BTCPayServer.Controllers
readonly Configuration.BTCPayServerOptions _Options;
private readonly BTCPayServerEnvironment _btcPayServerEnvironment;
public U2FService _u2FService;
private readonly RateLimitService _rateLimitService;
private readonly EventAggregator _eventAggregator;
readonly ILogger _logger;
@ -45,6 +47,7 @@ namespace BTCPayServer.Controllers
Configuration.BTCPayServerOptions options,
BTCPayServerEnvironment btcPayServerEnvironment,
U2FService u2FService,
RateLimitService rateLimitService,
EventAggregator eventAggregator)
{
_userManager = userManager;
@ -54,6 +57,7 @@ namespace BTCPayServer.Controllers
_Options = options;
_btcPayServerEnvironment = btcPayServerEnvironment;
_u2FService = u2FService;
_rateLimitService = rateLimitService;
_eventAggregator = eventAggregator;
_logger = Logs.PayServer;
}
@ -66,6 +70,8 @@ namespace BTCPayServer.Controllers
[HttpGet]
[AllowAnonymous]
[Route("~/login", Order = 1)]
[Route("~/Account/Login", Order = 2)]
public async Task<IActionResult> Login(string returnUrl = null, string email = null)
{
@ -89,6 +95,8 @@ namespace BTCPayServer.Controllers
[HttpPost]
[AllowAnonymous]
[Route("~/login", Order = 1)]
[Route("~/Account/Login", Order = 2)]
[ValidateAntiForgeryToken]
[RateLimitsFilter(ZoneLimits.Login, Scope = RateLimitsScope.RemoteAddress)]
public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
@ -396,6 +404,8 @@ namespace BTCPayServer.Controllers
[HttpGet]
[AllowAnonymous]
[Route("~/register", Order = 1)]
[Route("~/Account/Register", Order = 2)]
[RateLimitsFilter(ZoneLimits.Register, Scope = RateLimitsScope.RemoteAddress)]
public async Task<IActionResult> Register(string returnUrl = null, bool logon = true)
{
@ -413,6 +423,8 @@ namespace BTCPayServer.Controllers
[HttpPost]
[AllowAnonymous]
[Route("~/register", Order = 1)]
[Route("~/Account/Register", Order = 2)]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Register(RegisterViewModel model, string returnUrl = null, bool logon = true)
{
@ -535,6 +547,7 @@ namespace BTCPayServer.Controllers
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
[RateLimitsFilter(ZoneLimits.ForgotPassword, Scope = RateLimitsScope.RemoteAddress)]
public async Task<IActionResult> ForgotPassword(ForgotPasswordViewModel model)
{
if (ModelState.IsValid)

@ -71,7 +71,7 @@ namespace BTCPayServer.Controllers
try
{
_AppService.Parse(vm.PerksTemplate, vm.TargetCurrency).ToString();
vm.PerksTemplate = _AppService.SerializeTemplate(_AppService.Parse(vm.PerksTemplate, vm.TargetCurrency));
}
catch
{

@ -161,11 +161,16 @@ namespace BTCPayServer.Controllers
[Route("{appId}/settings/pos")]
public async Task<IActionResult> UpdatePointOfSale(string appId, UpdatePointOfSaleViewModel vm)
{
if (!ModelState.IsValid)
{
return View(vm);
}
if (_currencies.GetCurrencyData(vm.Currency, false) == null)
ModelState.AddModelError(nameof(vm.Currency), "Invalid currency");
try
{
_AppService.Parse(vm.Template, vm.Currency);
vm.Template = _AppService.SerializeTemplate(_AppService.Parse(vm.Template, vm.Currency));
}
catch
{

@ -57,8 +57,10 @@ namespace BTCPayServer.Controllers
}
[HttpGet]
[Route("/")]
[Route("/apps/{appId}/pos/{viewType?}")]
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
[DomainMappingConstraint(AppType.PointOfSale)]
public async Task<IActionResult> ViewPointOfSale(string appId, PosViewType? viewType = null)
{
var app = await _AppService.GetApp(appId, AppType.PointOfSale);
@ -104,10 +106,12 @@ namespace BTCPayServer.Controllers
}
[HttpPost]
[Route("/")]
[Route("/apps/{appId}/pos/{viewType?}")]
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
[IgnoreAntiforgeryToken]
[EnableCors(CorsPolicies.All)]
[DomainMappingConstraint(AppType.PointOfSale)]
public async Task<IActionResult> ViewPointOfSale(string appId,
PosViewType viewType,
[ModelBinder(typeof(InvariantDecimalModelBinder))] decimal amount,
@ -232,8 +236,10 @@ namespace BTCPayServer.Controllers
}
[HttpGet]
[Route("/")]
[Route("/apps/{appId}/crowdfund")]
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
[DomainMappingConstraintAttribute(AppType.Crowdfund)]
public async Task<IActionResult> ViewCrowdfund(string appId, string statusMessage)
{
var app = await _AppService.GetApp(appId, AppType.Crowdfund, true);
@ -263,10 +269,12 @@ namespace BTCPayServer.Controllers
}
[HttpPost]
[Route("/")]
[Route("/apps/{appId}/crowdfund")]
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
[IgnoreAntiforgeryToken]
[EnableCors(CorsPolicies.All)]
[DomainMappingConstraintAttribute(AppType.Crowdfund)]
public async Task<IActionResult> ContributeToCrowdfund(string appId, ContributeToCrowdfund request, CancellationToken cancellationToken)
{
if (request.Amount <= 0)

@ -13,7 +13,7 @@ namespace BTCPayServer.Controllers
{
if (statusCode.HasValue)
{
var specialPages = new[] { 404, 429, 500 };
var specialPages = new[] { 404, 406, 417, 429, 500, 502 };
if (specialPages.Any(a => a == statusCode.Value))
{
var viewName = statusCode.ToString();

@ -5,14 +5,12 @@ using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Payments;
using BTCPayServer.Security;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.EntityFrameworkCore.Metadata.Conventions;
using NBitcoin;
using CreateInvoiceRequest = BTCPayServer.Client.Models.CreateInvoiceRequest;
using InvoiceData = BTCPayServer.Client.Models.InvoiceData;
@ -27,14 +25,22 @@ namespace BTCPayServer.Controllers.GreenField
private readonly InvoiceController _invoiceController;
private readonly InvoiceRepository _invoiceRepository;
private readonly LinkGenerator _linkGenerator;
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly EventAggregator _eventAggregator;
private readonly PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary;
public LanguageService LanguageService { get; }
public GreenFieldInvoiceController(InvoiceController invoiceController, InvoiceRepository invoiceRepository, LinkGenerator linkGenerator, LanguageService languageService)
public GreenFieldInvoiceController(InvoiceController invoiceController, InvoiceRepository invoiceRepository,
LinkGenerator linkGenerator, LanguageService languageService, BTCPayNetworkProvider btcPayNetworkProvider,
EventAggregator eventAggregator, PaymentMethodHandlerDictionary paymentMethodHandlerDictionary)
{
_invoiceController = invoiceController;
_invoiceRepository = invoiceRepository;
_linkGenerator = linkGenerator;
_btcPayNetworkProvider = btcPayNetworkProvider;
_eventAggregator = eventAggregator;
_paymentMethodHandlerDictionary = paymentMethodHandlerDictionary;
LanguageService = languageService;
}
@ -72,7 +78,7 @@ namespace BTCPayServer.Controllers.GreenField
}
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
if (invoice.StoreId != store.Id)
if (invoice?.StoreId != store.Id)
{
return NotFound();
}
@ -212,7 +218,7 @@ namespace BTCPayServer.Controllers.GreenField
if (!await _invoiceRepository.MarkInvoiceStatus(invoice.Id, request.Status))
{
ModelState.AddModelError(nameof(request.Status),
"Status can only be marked to invalid or complete within certain conditions.");
"Status can only be marked to invalid or settled within certain conditions.");
}
if (!ModelState.IsValid)
@ -270,6 +276,32 @@ namespace BTCPayServer.Controllers.GreenField
return Ok(ToPaymentMethodModels(invoice));
}
[Authorize(Policy = Policies.CanViewInvoices,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/stores/{storeId}/invoices/{invoiceId}/payment-methods/{paymentMethod}/activate")]
public async Task<IActionResult> ActivateInvoicePaymentMethod(string storeId, string invoiceId, string paymentMethod)
{
var store = HttpContext.GetStoreData();
if (store == null)
{
return NotFound();
}
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
if (invoice?.StoreId != store.Id)
{
return NotFound();
}
if (PaymentMethodId.TryParse(paymentMethod, out var paymentMethodId))
{
await _invoiceRepository.ActivateInvoicePaymentMethod(_eventAggregator, _btcPayNetworkProvider,
_paymentMethodHandlerDictionary, store, invoice, paymentMethodId);
return Ok();
}
return BadRequest();
}
private InvoicePaymentMethodDataModel[] ToPaymentMethodModels(InvoiceEntity entity)
{
@ -283,6 +315,7 @@ namespace BTCPayServer.Controllers.GreenField
return new InvoicePaymentMethodDataModel()
{
Activated = details.Activated,
PaymentMethod = method.GetId().ToStringNormalized(),
Destination = details.GetPaymentDestination(),
Rate = method.Rate,
@ -337,7 +370,9 @@ namespace BTCPayServer.Controllers.GreenField
PaymentMethods =
entity.GetPaymentMethods().Select(method => method.GetId().ToStringNormalized()).ToArray(),
SpeedPolicy = entity.SpeedPolicy,
DefaultLanguage = entity.DefaultLanguage
DefaultLanguage = entity.DefaultLanguage,
RedirectAutomatically = entity.RedirectAutomatically,
RedirectURL = entity.RedirectURLTemplate
}
};
}

@ -28,8 +28,9 @@ namespace BTCPayServer.Controllers.GreenField
public InternalLightningNodeApiController(
BTCPayNetworkProvider btcPayNetworkProvider, BTCPayServerEnvironment btcPayServerEnvironment,
CssThemeManager cssThemeManager, LightningClientFactoryService lightningClientFactory,
IOptions<LightningNetworkOptions> lightningNetworkOptions ) : base(
btcPayNetworkProvider, btcPayServerEnvironment, cssThemeManager)
IOptions<LightningNetworkOptions> lightningNetworkOptions,
IAuthorizationService authorizationService) : base(
btcPayNetworkProvider, btcPayServerEnvironment, cssThemeManager, authorizationService)
{
_btcPayNetworkProvider = btcPayNetworkProvider;
_lightningClientFactory = lightningClientFactory;
@ -100,17 +101,17 @@ namespace BTCPayServer.Controllers.GreenField
return base.CreateInvoice(cryptoCode, request);
}
protected override Task<ILightningClient> GetLightningClient(string cryptoCode, bool doingAdminThings)
protected override async Task<ILightningClient> GetLightningClient(string cryptoCode, bool doingAdminThings)
{
_lightningNetworkOptions.Value.InternalLightningByCryptoCode.TryGetValue(cryptoCode,
out var internalLightningNode);
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
if (network == null || !CanUseInternalLightning(doingAdminThings) || internalLightningNode == null)
if (network == null ||
!_lightningNetworkOptions.Value.InternalLightningByCryptoCode.TryGetValue(network.CryptoCode,
out var internalLightningNode) ||
!await CanUseInternalLightning(doingAdminThings))
{
return null;
}
return Task.FromResult(_lightningClientFactory.Create(internalLightningNode, network));
return _lightningClientFactory.Create(internalLightningNode, network);
}
}
}

@ -31,8 +31,9 @@ namespace BTCPayServer.Controllers.GreenField
public StoreLightningNodeApiController(
IOptions<LightningNetworkOptions> lightningNetworkOptions,
LightningClientFactoryService lightningClientFactory, BTCPayNetworkProvider btcPayNetworkProvider,
BTCPayServerEnvironment btcPayServerEnvironment, CssThemeManager cssThemeManager) : base(
btcPayNetworkProvider, btcPayServerEnvironment, cssThemeManager)
BTCPayServerEnvironment btcPayServerEnvironment, CssThemeManager cssThemeManager,
IAuthorizationService authorizationService) : base(
btcPayNetworkProvider, btcPayServerEnvironment, cssThemeManager, authorizationService)
{
_lightningNetworkOptions = lightningNetworkOptions;
_lightningClientFactory = lightningClientFactory;
@ -100,11 +101,10 @@ namespace BTCPayServer.Controllers.GreenField
return base.CreateInvoice(cryptoCode, request);
}
protected override Task<ILightningClient> GetLightningClient(string cryptoCode,
protected override async Task<ILightningClient> GetLightningClient(string cryptoCode,
bool doingAdminThings)
{
_lightningNetworkOptions.Value.InternalLightningByCryptoCode.TryGetValue(cryptoCode,
out var internalLightningNode);
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
var store = HttpContext.GetStoreData();
@ -117,13 +117,20 @@ namespace BTCPayServer.Controllers.GreenField
var existing = store.GetSupportedPaymentMethods(_btcPayNetworkProvider)
.OfType<LightningSupportedPaymentMethod>()
.FirstOrDefault(d => d.PaymentId == id);
if (existing == null || (existing.GetLightningUrl().IsInternalNode(internalLightningNode) &&
!CanUseInternalLightning(doingAdminThings)))
{
if (existing == null)
return null;
if (existing.GetExternalLightningUrl() is LightningConnectionString connectionString)
{
return _lightningClientFactory.Create(connectionString, network);
}
return Task.FromResult(_lightningClientFactory.Create(existing.GetLightningUrl(), network));
else if (
await CanUseInternalLightning(doingAdminThings) &&
_lightningNetworkOptions.Value.InternalLightningByCryptoCode.TryGetValue(network.CryptoCode,
out var internalLightningNode))
{
return _lightningClientFactory.Create(internalLightningNode, network);
}
return null;
}
}
}

@ -2,10 +2,13 @@ using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.HostedServices;
using BTCPayServer.Lightning;
using BTCPayServer.Security;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Newtonsoft.Json.Linq;
@ -32,13 +35,16 @@ namespace BTCPayServer.Controllers.GreenField
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly BTCPayServerEnvironment _btcPayServerEnvironment;
private readonly CssThemeManager _cssThemeManager;
private readonly IAuthorizationService _authorizationService;
protected LightningNodeApiController(BTCPayNetworkProvider btcPayNetworkProvider,
BTCPayServerEnvironment btcPayServerEnvironment, CssThemeManager cssThemeManager)
BTCPayServerEnvironment btcPayServerEnvironment, CssThemeManager cssThemeManager,
IAuthorizationService authorizationService)
{
_btcPayNetworkProvider = btcPayNetworkProvider;
_btcPayServerEnvironment = btcPayServerEnvironment;
_cssThemeManager = cssThemeManager;
_authorizationService = authorizationService;
}
public virtual async Task<IActionResult> GetInfo(string cryptoCode)
@ -294,10 +300,11 @@ namespace BTCPayServer.Controllers.GreenField
};
}
protected bool CanUseInternalLightning(bool doingAdminThings)
protected async Task<bool> CanUseInternalLightning(bool doingAdminThings)
{
return (_btcPayServerEnvironment.IsDeveloping || User.IsInRole(Roles.ServerAdmin) ||
(_cssThemeManager.AllowLightningInternalNodeForAll && !doingAdminThings));
return (!doingAdminThings && _cssThemeManager.AllowLightningInternalNodeForAll) ||
(await _authorizationService.AuthorizeAsync(User, null,
new PolicyRequirement(Policies.CanUseInternalLightningNode))).Succeeded;
}
protected abstract Task<ILightningClient> GetLightningClient(string cryptoCode, bool doingAdminThings);

@ -10,6 +10,7 @@ using BTCPayServer.HostedServices;
using BTCPayServer.Lightning;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Security;
using BTCPayServer.Services;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
@ -27,24 +28,24 @@ namespace BTCPayServer.Controllers.GreenField
private readonly StoreRepository _storeRepository;
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly IOptions<LightningNetworkOptions> _lightningNetworkOptions;
private readonly IAuthorizationService _authorizationService;
private readonly CssThemeManager _cssThemeManager;
private readonly BTCPayServerEnvironment _btcPayServerEnvironment;
public StoreLightningNetworkPaymentMethodsController(
StoreRepository storeRepository,
BTCPayNetworkProvider btcPayNetworkProvider,
IOptions<LightningNetworkOptions> lightningNetworkOptions,
CssThemeManager cssThemeManager,
BTCPayServerEnvironment btcPayServerEnvironment)
IAuthorizationService authorizationService,
CssThemeManager cssThemeManager)
{
_storeRepository = storeRepository;
_btcPayNetworkProvider = btcPayNetworkProvider;
_lightningNetworkOptions = lightningNetworkOptions;
_authorizationService = authorizationService;
_cssThemeManager = cssThemeManager;
_btcPayServerEnvironment = btcPayServerEnvironment;
}
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/LightningNetwork")]
public ActionResult<IEnumerable<LightningNetworkPaymentMethodData>> GetLightningPaymentMethods(
[FromQuery] bool enabledOnly = false)
@ -56,13 +57,13 @@ namespace BTCPayServer.Controllers.GreenField
.OfType<LightningSupportedPaymentMethod>()
.Select(paymentMethod =>
new LightningNetworkPaymentMethodData(paymentMethod.PaymentId.CryptoCode,
paymentMethod.GetLightningUrl().ToString(), !excludedPaymentMethods.Match(paymentMethod.PaymentId)))
paymentMethod.GetExternalLightningUrl().ToString(), !excludedPaymentMethods.Match(paymentMethod.PaymentId)))
.Where((result) => !enabledOnly || result.Enabled)
.ToList()
);
}
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/LightningNetwork/{cryptoCode}")]
public ActionResult<LightningNetworkPaymentMethodData> GetLightningNetworkPaymentMethod(string cryptoCode)
{
@ -102,15 +103,13 @@ namespace BTCPayServer.Controllers.GreenField
public async Task<IActionResult> UpdateLightningNetworkPaymentMethod(string cryptoCode,
[FromBody] LightningNetworkPaymentMethodData paymentMethodData)
{
var id = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
var paymentMethodId = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
if (!GetNetwork(cryptoCode, out var network))
{
return NotFound();
}
var internalLightning = GetInternalLightningNode(network.CryptoCode);
if (string.IsNullOrEmpty(paymentMethodData?.ConnectionString))
{
ModelState.AddModelError(nameof(LightningNetworkPaymentMethodData.ConnectionString),
@ -120,91 +119,58 @@ namespace BTCPayServer.Controllers.GreenField
if (!ModelState.IsValid)
return this.CreateValidationError(ModelState);
PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.LightningLike);
Payments.Lightning.LightningSupportedPaymentMethod paymentMethod = null;
LightningSupportedPaymentMethod paymentMethod = null;
if (!string.IsNullOrEmpty(paymentMethodData.ConnectionString))
{
if (!LightningConnectionString.TryParse(paymentMethodData.ConnectionString, false,
out var connectionString, out var error))
if (paymentMethodData.ConnectionString == LightningSupportedPaymentMethod.InternalNode)
{
ModelState.AddModelError(nameof(paymentMethodData.ConnectionString), $"Invalid URL ({error})");
return this.CreateValidationError(ModelState);
}
if (connectionString.ConnectionType == LightningConnectionType.LndGRPC)
{
ModelState.AddModelError(nameof(paymentMethodData.ConnectionString),
$"BTCPay does not support gRPC connections");
return this.CreateValidationError(ModelState);
}
bool isInternalNode = connectionString.IsInternalNode(internalLightning);
if (connectionString.BaseUri.Scheme == "http")
{
if (!isInternalNode && !connectionString.AllowInsecure)
if (!await CanUseInternalLightning())
{
ModelState.AddModelError(nameof(paymentMethodData.ConnectionString), "The url must be HTTPS");
ModelState.AddModelError(nameof(paymentMethodData.ConnectionString), $"You are not authorized to use the internal lightning node");
return this.CreateValidationError(ModelState);
}
paymentMethod = new Payments.Lightning.LightningSupportedPaymentMethod()
{
CryptoCode = paymentMethodId.CryptoCode
};
paymentMethod.SetInternalNode();
}
if (connectionString.MacaroonFilePath != null)
else
{
if (!CanUseInternalLightning())
if (!LightningConnectionString.TryParse(paymentMethodData.ConnectionString, false,
out var connectionString, out var error))
{
ModelState.AddModelError(nameof(paymentMethodData.ConnectionString), $"Invalid URL ({error})");
return this.CreateValidationError(ModelState);
}
if (connectionString.ConnectionType == LightningConnectionType.LndGRPC)
{
ModelState.AddModelError(nameof(paymentMethodData.ConnectionString),
"You are not authorized to use macaroonfilepath");
$"BTCPay does not support gRPC connections");
return this.CreateValidationError(ModelState);
}
if (!System.IO.File.Exists(connectionString.MacaroonFilePath))
if (!await CanManageServer() && !connectionString.IsSafe())
{
ModelState.AddModelError(nameof(paymentMethodData.ConnectionString),
"The macaroonfilepath file does not exist");
ModelState.AddModelError(nameof(paymentMethodData.ConnectionString), $"You do not have 'btcpay.server.canmodifyserversettings' rights, so the connection string should not contain 'cookiefilepath', 'macaroondirectorypath', 'macaroonfilepath', and should not point to a local ip or to a dns name ending with '.internal', '.local', '.lan' or '.'.");
return this.CreateValidationError(ModelState);
}
if (!System.IO.Path.IsPathRooted(connectionString.MacaroonFilePath))
paymentMethod = new Payments.Lightning.LightningSupportedPaymentMethod()
{
ModelState.AddModelError(nameof(paymentMethodData.ConnectionString),
"The macaroonfilepath should be fully rooted");
return this.CreateValidationError(ModelState);
}
CryptoCode = paymentMethodId.CryptoCode
};
paymentMethod.SetLightningUrl(connectionString);
}
if (isInternalNode && !CanUseInternalLightning())
{
ModelState.AddModelError(nameof(paymentMethodData.ConnectionString), "Unauthorized url");
return this.CreateValidationError(ModelState);
}
paymentMethod = new Payments.Lightning.LightningSupportedPaymentMethod()
{
CryptoCode = paymentMethodId.CryptoCode
};
paymentMethod.SetLightningUrl(connectionString);
}
var store = Store;
var storeBlob = store.GetStoreBlob();
store.SetSupportedPaymentMethod(id, paymentMethod);
storeBlob.SetExcluded(id, !paymentMethodData.Enabled);
store.SetSupportedPaymentMethod(paymentMethodId, paymentMethod);
storeBlob.SetExcluded(paymentMethodId, !paymentMethodData.Enabled);
store.SetStoreBlob(storeBlob);
await _storeRepository.UpdateStore(store);
return Ok(GetExistingLightningLikePaymentMethod(cryptoCode, store));
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPut("~/api/v1/stores/{storeId}/payment-methods/LightningNetwork/{cryptoCode}/internal")]
public Task<IActionResult> UpdateLightningNetworkPaymentMethodToInternalNode(string cryptoCode)
{
return UpdateLightningNetworkPaymentMethod(cryptoCode,
new LightningNetworkPaymentMethodData(cryptoCode, GetInternalLightningNode(cryptoCode).ToString(), true));
}
private LightningNetworkPaymentMethodData GetExistingLightningLikePaymentMethod(string cryptoCode, StoreData store = null)
{
store ??= Store;
@ -219,7 +185,7 @@ namespace BTCPayServer.Controllers.GreenField
return paymentMethod == null
? null
: new LightningNetworkPaymentMethodData(paymentMethod.PaymentId.CryptoCode,
paymentMethod.GetLightningUrl().ToString(), !excluded);
paymentMethod.GetDisplayableConnectionString(), !excluded);
}
private bool GetNetwork(string cryptoCode, out BTCPayNetwork network)
@ -229,18 +195,17 @@ namespace BTCPayServer.Controllers.GreenField
return network != null;
}
private LightningConnectionString GetInternalLightningNode(string cryptoCode)
private async Task<bool> CanUseInternalLightning()
{
if (_lightningNetworkOptions.Value.InternalLightningByCryptoCode.TryGetValue(cryptoCode, out var connectionString))
{
return CanUseInternalLightning() ? connectionString : null;
}
return null;
return _cssThemeManager.AllowLightningInternalNodeForAll ||
(await _authorizationService.AuthorizeAsync(User, null,
new PolicyRequirement(Policies.CanUseInternalLightningNode))).Succeeded;
}
private bool CanUseInternalLightning()
private async Task<bool> CanManageServer()
{
return (_btcPayServerEnvironment.IsDeveloping || User.IsInRole(Roles.ServerAdmin) || _cssThemeManager.AllowLightningInternalNodeForAll);
return
(await _authorizationService.AuthorizeAsync(User, null,
new PolicyRequirement(Policies.CanModifyServerSettings))).Succeeded;
}
}
}

@ -0,0 +1,563 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.BIP78.Sender;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.Models.WalletViewModels;
using BTCPayServer.Payments.PayJoin;
using BTCPayServer.Payments.PayJoin.Sender;
using BTCPayServer.Services;
using BTCPayServer.Services.Wallets;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBitcoin.Payment;
using NBXplorer;
using NBXplorer.Models;
using Newtonsoft.Json.Linq;
using StoreData = BTCPayServer.Data.StoreData;
namespace BTCPayServer.Controllers.GreenField
{
[ApiController]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[EnableCors(CorsPolicies.All)]
public class StoreOnChainWalletsController : Controller
{
private StoreData Store => HttpContext.GetStoreData();
private readonly IAuthorizationService _authorizationService;
private readonly BTCPayWalletProvider _btcPayWalletProvider;
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly WalletRepository _walletRepository;
private readonly ExplorerClientProvider _explorerClientProvider;
private readonly CssThemeManager _cssThemeManager;
private readonly NBXplorerDashboard _nbXplorerDashboard;
private readonly WalletsController _walletsController;
private readonly PayjoinClient _payjoinClient;
private readonly DelayedTransactionBroadcaster _delayedTransactionBroadcaster;
private readonly EventAggregator _eventAggregator;
private readonly WalletReceiveService _walletReceiveService;
private readonly IFeeProviderFactory _feeProviderFactory;
public StoreOnChainWalletsController(
IAuthorizationService authorizationService,
BTCPayWalletProvider btcPayWalletProvider,
BTCPayNetworkProvider btcPayNetworkProvider,
WalletRepository walletRepository,
ExplorerClientProvider explorerClientProvider,
CssThemeManager cssThemeManager,
NBXplorerDashboard nbXplorerDashboard,
WalletsController walletsController,
PayjoinClient payjoinClient,
DelayedTransactionBroadcaster delayedTransactionBroadcaster,
EventAggregator eventAggregator,
WalletReceiveService walletReceiveService,
IFeeProviderFactory feeProviderFactory)
{
_authorizationService = authorizationService;
_btcPayWalletProvider = btcPayWalletProvider;
_btcPayNetworkProvider = btcPayNetworkProvider;
_walletRepository = walletRepository;
_explorerClientProvider = explorerClientProvider;
_cssThemeManager = cssThemeManager;
_nbXplorerDashboard = nbXplorerDashboard;
_walletsController = walletsController;
_payjoinClient = payjoinClient;
_delayedTransactionBroadcaster = delayedTransactionBroadcaster;
_eventAggregator = eventAggregator;
_walletReceiveService = walletReceiveService;
_feeProviderFactory = feeProviderFactory;
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet")]
public async Task<IActionResult> ShowOnChainWalletOverview(string storeId, string cryptoCode)
{
if (IsInvalidWalletRequest(cryptoCode, out BTCPayNetwork network,
out DerivationSchemeSettings derivationScheme, out IActionResult actionResult)) return actionResult;
var wallet = _btcPayWalletProvider.GetWallet(network);
var balance = await wallet.GetBalance(derivationScheme.AccountDerivation);
return Ok(new OnChainWalletOverviewData()
{
Label = derivationScheme.ToPrettyString(),
Balance = balance.Total.GetValue(network),
UnconfirmedBalance= balance.Unconfirmed.GetValue(network),
ConfirmedBalance= balance.Confirmed.GetValue(network),
});
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/feerate")]
public async Task<IActionResult> GetOnChainFeeRate(string storeId, string cryptoCode, int? blockTarget = null)
{
if (IsInvalidWalletRequest(cryptoCode, out BTCPayNetwork network,
out DerivationSchemeSettings derivationScheme, out IActionResult actionResult)) return actionResult;
var feeRateTarget = blockTarget?? Store.GetStoreBlob().RecommendedFeeBlockTarget;
return Ok(new OnChainWalletFeeRateData()
{
FeeRate = await _feeProviderFactory.CreateFeeProvider(network)
.GetFeeRateAsync(feeRateTarget),
});
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/address")]
public async Task<IActionResult> GetOnChainWalletReceiveAddress(string storeId, string cryptoCode, bool forceGenerate = false)
{
if (IsInvalidWalletRequest(cryptoCode, out BTCPayNetwork network,
out DerivationSchemeSettings derivationScheme, out IActionResult actionResult)) return actionResult;
var kpi = await _walletReceiveService.GetOrGenerate(new WalletId(storeId, cryptoCode), forceGenerate);
if (kpi is null)
{
return BadRequest();
}
var bip21 = network.GenerateBIP21(kpi.Address.ToString(), null);
var allowedPayjoin = derivationScheme.IsHotWallet && Store.GetStoreBlob().PayJoinEnabled;
if (allowedPayjoin)
{
bip21 +=
$"?{PayjoinClient.BIP21EndpointKey}={Request.GetAbsoluteUri(Url.Action(nameof(PayJoinEndpointController.Submit), "PayJoinEndpoint", new {cryptoCode}))}";
}
return Ok(new OnChainWalletAddressData()
{
Address = kpi.Address.ToString(),
PaymentLink = bip21,
KeyPath = kpi.KeyPath
});
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpDelete("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/address")]
public async Task<IActionResult> UnReserveOnChainWalletReceiveAddress(string storeId, string cryptoCode)
{
if (IsInvalidWalletRequest(cryptoCode, out BTCPayNetwork network,
out DerivationSchemeSettings derivationScheme, out IActionResult actionResult)) return actionResult;
var addr = await _walletReceiveService.UnReserveAddress(new WalletId(storeId, cryptoCode));
if (addr is null)
{
return NotFound();
}
return Ok();
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/transactions")]
public async Task<IActionResult> ShowOnChainWalletTransactions(string storeId, string cryptoCode,
[FromQuery]TransactionStatus[] statusFilter = null)
{
if (IsInvalidWalletRequest(cryptoCode, out BTCPayNetwork network,
out DerivationSchemeSettings derivationScheme, out IActionResult actionResult)) return actionResult;
var wallet = _btcPayWalletProvider.GetWallet(network);
var walletId = new WalletId(storeId, cryptoCode);
var walletTransactionsInfoAsync = await _walletRepository.GetWalletTransactionsInfo(walletId);
var txs = await wallet.FetchTransactions(derivationScheme.AccountDerivation);
var filteredFlatList = new List<TransactionInformation>();
if (statusFilter is null || !statusFilter.Any() || statusFilter.Contains(TransactionStatus.Confirmed))
{
filteredFlatList.AddRange(txs.ConfirmedTransactions.Transactions);
}
if (statusFilter is null || !statusFilter.Any() || statusFilter.Contains(TransactionStatus.Unconfirmed))
{
filteredFlatList.AddRange(txs.UnconfirmedTransactions.Transactions);
}
if (statusFilter is null || !statusFilter.Any() ||statusFilter.Contains(TransactionStatus.Replaced))
{
filteredFlatList.AddRange(txs.ReplacedTransactions.Transactions);
}
var result = filteredFlatList.Select(information =>
{
walletTransactionsInfoAsync.TryGetValue(information.TransactionId.ToString(), out var transactionInfo);
return ToModel(transactionInfo, information, wallet);
}).ToList();
return Ok(result);
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/transactions/{transactionId}")]
public async Task<IActionResult> GetOnChainWalletTransaction(string storeId, string cryptoCode,
string transactionId)
{
if (IsInvalidWalletRequest(cryptoCode, out BTCPayNetwork network,
out DerivationSchemeSettings derivationScheme, out IActionResult actionResult)) return actionResult;
var wallet = _btcPayWalletProvider.GetWallet(network);
var tx = await wallet.FetchTransaction(derivationScheme.AccountDerivation, uint256.Parse(transactionId));
if (tx is null)
{
return NotFound();
}
var walletId = new WalletId(storeId, cryptoCode);
var walletTransactionsInfoAsync =
(await _walletRepository.GetWalletTransactionsInfo(walletId, new[] {transactionId})).Values
.FirstOrDefault();
return Ok(ToModel(walletTransactionsInfoAsync, tx, wallet));
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/utxos")]
public async Task<IActionResult> GetOnChainWalletUTXOs(string storeId, string cryptoCode)
{
if (IsInvalidWalletRequest(cryptoCode, out BTCPayNetwork network,
out DerivationSchemeSettings derivationScheme, out IActionResult actionResult)) return actionResult;
var wallet = _btcPayWalletProvider.GetWallet(network);
var walletId = new WalletId(storeId, cryptoCode);
var walletTransactionsInfoAsync = await _walletRepository.GetWalletTransactionsInfo(walletId);
var utxos = await wallet.GetUnspentCoins(derivationScheme.AccountDerivation);
return Ok(utxos.Select(coin =>
{
walletTransactionsInfoAsync.TryGetValue(coin.OutPoint.Hash.ToString(), out var info);
return new OnChainWalletUTXOData()
{
Outpoint = coin.OutPoint,
Amount = coin.Value.GetValue(network),
Comment = info?.Comment,
Labels = info?.Labels,
Link = string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink,
coin.OutPoint.Hash.ToString()),
Timestamp = coin.Timestamp,
KeyPath = coin.KeyPath,
Address = network.NBXplorerNetwork.CreateAddress(derivationScheme.AccountDerivation, coin.KeyPath, coin.ScriptPubKey).ToString()
};
}).ToList()
);
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/transactions")]
public async Task<IActionResult> CreateOnChainTransaction(string storeId, string cryptoCode,
[FromBody] CreateOnChainTransactionRequest request)
{
if (IsInvalidWalletRequest(cryptoCode, out BTCPayNetwork network,
out DerivationSchemeSettings derivationScheme, out IActionResult actionResult)) return actionResult;
if (network.ReadonlyWallet)
{
return this.CreateAPIError("not-available",
$"{cryptoCode} sending services are not currently available");
}
//This API is only meant for hot wallet usage for now. We can expand later when we allow PSBT manipulation.
if (!(await CanUseHotWallet()).HotWallet)
{
return Unauthorized();
}
var explorerClient = _explorerClientProvider.GetExplorerClient(cryptoCode);
var wallet = _btcPayWalletProvider.GetWallet(network);
var utxos = await wallet.GetUnspentCoins(derivationScheme.AccountDerivation);
if (request.SelectedInputs != null || !utxos.Any())
{
utxos = utxos.Where(coin => request.SelectedInputs?.Contains(coin.OutPoint) ?? true)
.ToArray();
if (utxos.Any() is false)
{
//no valid utxos selected
request.AddModelError(transactionRequest => transactionRequest.SelectedInputs,
"There are no available utxos based on your request", this);
}
}
var balanceAvailable = utxos.Sum(coin => coin.Value.GetValue(network));
var subtractFeesOutputsCount = new List<int>();
var subtractFees = request.Destinations.Any(o => o.SubtractFromAmount);
int? payjoinOutputIndex = null;
var sum = 0m;
var outputs = new List<WalletSendModel.TransactionOutput>();
for (var index = 0; index < request.Destinations.Count; index++)
{
var destination = request.Destinations[index];
if (destination.SubtractFromAmount)
{
subtractFeesOutputsCount.Add(index);
}
BitcoinUrlBuilder bip21 = null;
var amount = destination.Amount;
if (amount.GetValueOrDefault(0) <= 0)
{
amount = null;
}
var address = string.Empty;
try
{
destination.Destination = destination.Destination.Replace(network.UriScheme+":", "bitcoin:", StringComparison.InvariantCultureIgnoreCase);
bip21 = new BitcoinUrlBuilder(destination.Destination, network.NBitcoinNetwork);
amount ??= bip21.Amount.GetValue(network);
address = bip21.Address.ToString();
if (destination.SubtractFromAmount)
{
request.AddModelError(transactionRequest => transactionRequest.Destinations[index],
"You cannot use a BIP21 destination along with SubtractFromAmount", this);
}
}
catch (FormatException)
{
try
{
address = BitcoinAddress.Create(destination.Destination, network.NBitcoinNetwork).ToString();
}
catch (Exception)
{
request.AddModelError(transactionRequest => transactionRequest.Destinations[index],
"Destination must be a BIP21 payment link or an address", this);
}
}
if (amount is null || amount <= 0)
{
request.AddModelError(transactionRequest => transactionRequest.Destinations[index],
"Amount must be specified or destination must be a BIP21 payment link, and greater than 0", this);
}
if (request.ProceedWithPayjoin && bip21?.UnknowParameters?.ContainsKey(PayjoinClient.BIP21EndpointKey) is true)
{
payjoinOutputIndex = index;
}
outputs.Add(new WalletSendModel.TransactionOutput()
{
DestinationAddress = address,
Amount = amount,
SubtractFeesFromOutput = destination.SubtractFromAmount
});
sum += destination.Amount ?? 0;
}
if (subtractFeesOutputsCount.Count > 1)
{
foreach (var subtractFeesOutput in subtractFeesOutputsCount)
{
request.AddModelError(model => model.Destinations[subtractFeesOutput].SubtractFromAmount,
"You can only subtract fees from one destination", this);
}
}
if (balanceAvailable < sum)
{
request.AddModelError(transactionRequest => transactionRequest.Destinations,
"You are attempting to send more than is available", this);
}
else if (balanceAvailable == sum && !subtractFees)
{
request.AddModelError(transactionRequest => transactionRequest.Destinations,
"You are sending your entire balance, you should subtract the fees from a destination", this);
}
var minRelayFee = _nbXplorerDashboard.Get(network.CryptoCode).Status.BitcoinStatus?.MinRelayTxFee ??
new FeeRate(1.0m);
if (request.FeeRate != null && request.FeeRate < minRelayFee)
{
ModelState.AddModelError(nameof(request.FeeRate),
"The fee rate specified is lower than the current minimum relay fee");
}
if (!ModelState.IsValid)
{
return this.CreateValidationError(ModelState);
}
CreatePSBTResponse psbt;
try
{
psbt = await _walletsController.CreatePSBT(network, derivationScheme,
new WalletSendModel()
{
SelectedInputs = request.SelectedInputs?.Select(point => point.ToString()),
Outputs = outputs,
AlwaysIncludeNonWitnessUTXO = true,
InputSelection = request.SelectedInputs?.Any() is true,
AllowFeeBump =
!request.RBF.HasValue ? WalletSendModel.ThreeStateBool.Maybe :
request.RBF.Value ? WalletSendModel.ThreeStateBool.Yes :
WalletSendModel.ThreeStateBool.No,
FeeSatoshiPerByte = request.FeeRate?.SatoshiPerByte,
NoChange = request.NoChange
},
CancellationToken.None);
}
catch (NBXplorerException ex)
{
return this.CreateAPIError(ex.Error.Code, ex.Error.Message);
}
catch (NotSupportedException)
{
return this.CreateAPIError("not-available", "You need to update your version of NBXplorer");
}
derivationScheme.RebaseKeyPaths(psbt.PSBT);
var signingContext = new SigningContextModel()
{
PayJoinBIP21 =
payjoinOutputIndex is null
? null
: request.Destinations.ElementAt(payjoinOutputIndex.Value).Destination,
EnforceLowR = psbt.Suggestions?.ShouldEnforceLowR,
ChangeAddress = psbt.ChangeAddress?.ToString()
};
var signingKeyStr = await explorerClient
.GetMetadataAsync<string>(derivationScheme.AccountDerivation,
WellknownMetadataKeys.MasterHDKey);
if (!derivationScheme.IsHotWallet || signingKeyStr is null)
{
return this.CreateAPIError("not-available",
$"{cryptoCode} sending services are not currently available");
}
var signingKey = ExtKey.Parse(signingKeyStr, network.NBitcoinNetwork);
var signingKeySettings = derivationScheme.GetSigningAccountKeySettings();
signingKeySettings.RootFingerprint ??= signingKey.GetPublicKey().GetHDFingerPrint();
RootedKeyPath rootedKeyPath = signingKeySettings.GetRootedKeyPath();
psbt.PSBT.RebaseKeyPaths(signingKeySettings.AccountKey, rootedKeyPath);
var accountKey = signingKey.Derive(rootedKeyPath.KeyPath);
var changed = psbt.PSBT.PSBTChanged(() => psbt.PSBT.SignAll(derivationScheme.AccountDerivation, accountKey,
rootedKeyPath, new SigningOptions() {EnforceLowR = signingContext?.EnforceLowR is bool v ? v : psbt.Suggestions.ShouldEnforceLowR }));
if (!changed)
{
return this.CreateAPIError("psbt-signing-error",
"Impossible to sign the transaction. Probable cause: Incorrect account key path in wallet settings, PSBT already signed.");
}
psbt.PSBT.Finalize();
var transaction = psbt.PSBT.ExtractTransaction();
var transactionHash = transaction.GetHash();
BroadcastResult broadcastResult;
if (!string.IsNullOrEmpty(signingContext.PayJoinBIP21))
{
signingContext.OriginalPSBT = psbt.PSBT.ToBase64();
try
{
await _delayedTransactionBroadcaster.Schedule(DateTimeOffset.UtcNow + TimeSpan.FromMinutes(2.0),
transaction, network);
var payjoinPSBT = await _payjoinClient.RequestPayjoin(
new BitcoinUrlBuilder(signingContext.PayJoinBIP21, network.NBitcoinNetwork), new PayjoinWallet(derivationScheme),
psbt.PSBT, CancellationToken.None);
payjoinPSBT = psbt.PSBT.SignAll(derivationScheme.AccountDerivation, accountKey, rootedKeyPath,
new SigningOptions() {EnforceLowR = !(signingContext?.EnforceLowR is false)});
payjoinPSBT.Finalize();
var payjoinTransaction = payjoinPSBT.ExtractTransaction();
var hash = payjoinTransaction.GetHash();
_eventAggregator.Publish(new UpdateTransactionLabel(new WalletId(Store.Id, cryptoCode), hash,
UpdateTransactionLabel.PayjoinLabelTemplate()));
broadcastResult = await explorerClient.BroadcastAsync(payjoinTransaction);
if (broadcastResult.Success)
{
return await GetOnChainWalletTransaction(storeId, cryptoCode, hash.ToString());
}
}
catch (PayjoinException)
{
//not a critical thing, payjoin is great if possible, fine if not
}
}
if (!request.ProceedWithBroadcast)
{
return Ok(new JValue(transaction.ToHex()));
}
broadcastResult = await explorerClient.BroadcastAsync(transaction);
if (broadcastResult.Success)
{
return await GetOnChainWalletTransaction(storeId, cryptoCode, transactionHash.ToString());
}
else
{
return this.CreateAPIError("broadcast-error", broadcastResult.RPCMessage);
}
}
private async Task<(bool HotWallet, bool RPCImport)> CanUseHotWallet()
{
return await _authorizationService.CanUseHotWallet(_cssThemeManager.Policies, User);
}
private bool IsInvalidWalletRequest(string cryptoCode, out BTCPayNetwork network,
out DerivationSchemeSettings derivationScheme, out IActionResult actionResult)
{
derivationScheme = null;
actionResult = null;
network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
if (network is null)
{
actionResult = NotFound();
return true;
}
if (!network.WalletSupported || !_btcPayWalletProvider.IsAvailable(network))
{
actionResult = this.CreateAPIError("not-available",
$"{cryptoCode} services are not currently available");
return true;
}
derivationScheme = GetDerivationSchemeSettings(cryptoCode);
if (derivationScheme?.AccountDerivation is null)
{
actionResult = NotFound();
return true;
}
return false;
}
private DerivationSchemeSettings GetDerivationSchemeSettings(string cryptoCode)
{
var paymentMethod = Store
.GetSupportedPaymentMethods(_btcPayNetworkProvider)
.OfType<DerivationSchemeSettings>()
.FirstOrDefault(p =>
p.PaymentId.PaymentType == Payments.PaymentTypes.BTCLike &&
p.PaymentId.CryptoCode.Equals(cryptoCode, StringComparison.InvariantCultureIgnoreCase));
return paymentMethod;
}
private OnChainWalletTransactionData ToModel(WalletTransactionInfo walletTransactionsInfoAsync,
TransactionInformation tx,
BTCPayWallet wallet)
{
return new OnChainWalletTransactionData()
{
TransactionHash = tx.TransactionId,
Comment = walletTransactionsInfoAsync?.Comment?? string.Empty,
Labels = walletTransactionsInfoAsync?.Labels?? new Dictionary<string, LabelData>(),
Amount = tx.BalanceChange.GetValue(wallet.Network),
BlockHash = tx.BlockHash,
BlockHeight = tx.Height,
Confirmations = tx.Confirmations,
Timestamp = tx.Timestamp,
Status = tx.Confirmations > 0 ? TransactionStatus.Confirmed :
tx.ReplacedBy != null ? TransactionStatus.Replaced : TransactionStatus.Unconfirmed
};
}
}
}

@ -131,6 +131,7 @@ namespace BTCPayServer.Controllers.GreenField
LightningPrivateRouteHints = storeBlob.LightningPrivateRouteHints,
OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback,
RedirectAutomatically = storeBlob.RedirectAutomatically,
LazyPaymentMethods = storeBlob.LazyPaymentMethods,
ShowRecommendedFee = storeBlob.ShowRecommendedFee,
RecommendedFeeBlockTarget = storeBlob.RecommendedFeeBlockTarget,
DefaultLang = storeBlob.DefaultLang,
@ -167,6 +168,7 @@ namespace BTCPayServer.Controllers.GreenField
blob.LightningAmountInSatoshi = restModel.LightningAmountInSatoshi;
blob.LightningPrivateRouteHints = restModel.LightningPrivateRouteHints;
blob.OnChainWithLnInvoiceFallback = restModel.OnChainWithLnInvoiceFallback;
blob.LazyPaymentMethods = restModel.LazyPaymentMethods;
blob.RedirectAutomatically = restModel.RedirectAutomatically;
blob.ShowRecommendedFee = restModel.ShowRecommendedFee;
blob.RecommendedFeeBlockTarget = restModel.RecommendedFeeBlockTarget;

@ -7,6 +7,7 @@ using System.Net.Http;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Data;
using BTCPayServer.Filters;
using BTCPayServer.HostedServices;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
@ -48,71 +49,18 @@ namespace BTCPayServer.Controllers
SignInManager = signInManager;
}
private async Task<ViewResult> GoToApp(string appId, AppType? appType)
{
if (appType.HasValue && !string.IsNullOrEmpty(appId))
{
this.HttpContext.Response.Headers.Remove("Onion-Location");
switch (appType.Value)
{
case AppType.Crowdfund:
{
var serviceProvider = HttpContext.RequestServices;
var controller = (AppsPublicController)serviceProvider.GetService(typeof(AppsPublicController));
controller.Url = Url;
controller.ControllerContext = ControllerContext;
var res = await controller.ViewCrowdfund(appId, null) as ViewResult;
if (res != null)
{
res.ViewName = $"/Views/AppsPublic/ViewCrowdfund.cshtml";
return res; // return
}
break;
}
case AppType.PointOfSale:
{
var serviceProvider = HttpContext.RequestServices;
var controller = (AppsPublicController)serviceProvider.GetService(typeof(AppsPublicController));
controller.Url = Url;
controller.ControllerContext = ControllerContext;
var res = await controller.ViewPointOfSale(appId) as ViewResult;
if (res != null)
{
res.ViewName = $"/Views/AppsPublic/{res.ViewName}.cshtml";
return res; // return
}
break;
}
}
}
return null;
}
public async Task<IActionResult> Index()
[Route("")]
[DomainMappingConstraint()]
public IActionResult Index()
{
if (_cachedServerSettings.FirstRun)
{
return RedirectToAction(nameof(AccountController.Register), "Account");
}
var matchedDomainMapping = _cachedServerSettings.DomainToAppMapping.FirstOrDefault(item =>
item.Domain.Equals(Request.Host.Host, StringComparison.InvariantCultureIgnoreCase));
if (matchedDomainMapping != null)
{
return await GoToApp(matchedDomainMapping.AppId, matchedDomainMapping.AppType) ?? GoToHome();
}
return await GoToApp(_cachedServerSettings.RootAppId, _cachedServerSettings.RootAppType) ?? GoToHome();
}
private IActionResult GoToHome()
{
if (SignInManager.IsSignedIn(User))
return View("Home");
else
return RedirectToAction(nameof(AccountController.Login), "Account");
return Challenge();
}
[Route("misc/lang")]

@ -14,6 +14,7 @@ using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Filters;
using BTCPayServer.Logging;
using BTCPayServer.HostedServices;
using BTCPayServer.Models;
using BTCPayServer.Models.InvoicingModels;
@ -25,7 +26,6 @@ using BTCPayServer.Security;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Invoices.Export;
using BTCPayServer.Services.Rates;
using DBriize.Utils;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
@ -504,9 +504,9 @@ namespace BTCPayServer.Controllers
{
if (!isDefaultPaymentId)
return null;
var paymentMethodTemp = invoice.GetPaymentMethods()
.Where(c => paymentMethodId.CryptoCode == c.GetId().CryptoCode)
.FirstOrDefault();
var paymentMethodTemp = invoice
.GetPaymentMethods()
.FirstOrDefault(c => paymentMethodId.CryptoCode == c.GetId().CryptoCode);
if (paymentMethodTemp == null)
paymentMethodTemp = invoice.GetPaymentMethods().First();
network = paymentMethodTemp.Network;
@ -515,6 +515,12 @@ namespace BTCPayServer.Controllers
var paymentMethod = invoice.GetPaymentMethod(paymentMethodId);
var paymentMethodDetails = paymentMethod.GetPaymentMethodDetails();
if (!paymentMethodDetails.Activated)
{
await _InvoiceRepository.ActivateInvoicePaymentMethod(_EventAggregator, _NetworkProvider,
_paymentMethodHandlerDictionary, store, invoice, paymentMethod.GetId());
return await GetInvoiceModel(invoiceId, paymentMethodId, lang);
}
var dto = invoice.EntityToDTO();
var storeBlob = store.GetStoreBlob();
var accounting = paymentMethod.Calculate();
@ -530,6 +536,7 @@ namespace BTCPayServer.Controllers
var divisibility = _CurrencyNameTable.GetNumberFormatInfo(paymentMethod.GetId().CryptoCode, false)?.CurrencyDecimalDigits;
var model = new PaymentModel()
{
Activated = paymentMethodDetails.Activated,
CryptoCode = network.CryptoCode,
RootPath = this.Request.PathBase.Value.WithTrailingSlash(),
OrderId = invoice.Metadata.OrderId,

@ -44,12 +44,10 @@ namespace BTCPayServer.Controllers
private readonly PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary;
private readonly ApplicationDbContextFactory _dbContextFactory;
private readonly PullPaymentHostedService _paymentHostedService;
readonly IServiceProvider _ServiceProvider;
public WebhookNotificationManager WebhookNotificationManager { get; }
public InvoiceController(
IServiceProvider serviceProvider,
InvoiceRepository invoiceRepository,
CurrencyNameTable currencyNameTable,
UserManager<ApplicationUser> userManager,
@ -63,7 +61,6 @@ namespace BTCPayServer.Controllers
PullPaymentHostedService paymentHostedService,
WebhookNotificationManager webhookNotificationManager)
{
_ServiceProvider = serviceProvider;
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
_StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository));
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
@ -173,6 +170,7 @@ namespace BTCPayServer.Controllers
entity.Price = invoice.Amount;
entity.SpeedPolicy = invoice.Checkout.SpeedPolicy ?? store.SpeedPolicy;
entity.DefaultLanguage = invoice.Checkout.DefaultLanguage;
entity.RedirectAutomatically = invoice.Checkout.RedirectAutomatically ?? storeBlob.RedirectAutomatically;
IPaymentFilter excludeFilter = null;
if (invoice.Checkout.PaymentMethods != null)
{
@ -320,7 +318,16 @@ namespace BTCPayServer.Controllers
{
var logPrefix = $"{supportedPaymentMethod.PaymentId.ToPrettyString()}:";
var storeBlob = store.GetStoreBlob();
var preparePayment = handler.PreparePayment(supportedPaymentMethod, store, network);
object preparePayment;
if (storeBlob.LazyPaymentMethods)
{
preparePayment = null;
}
else
{
preparePayment = handler.PreparePayment(supportedPaymentMethod, store, network);
}
var rate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, entity.Currency)];
if (rate.BidAsk == null)
{

@ -82,7 +82,6 @@ namespace BTCPayServer.Controllers
{
Username = user.UserName,
Email = user.Email,
PhoneNumber = user.PhoneNumber,
IsEmailConfirmed = user.EmailConfirmed
};
return View(model);
@ -97,8 +96,6 @@ namespace BTCPayServer.Controllers
return View(model);
}
bool needUpdate = false;
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
@ -108,33 +105,22 @@ namespace BTCPayServer.Controllers
var email = user.Email;
if (model.Email != email)
{
if (!(await _userManager.FindByEmailAsync(model.Email) is null))
{
TempData[WellKnownTempData.ErrorMessage] = "The email address is already in use with an other account.";
return RedirectToAction(nameof(Index));
}
var setUserResult = await _userManager.SetUserNameAsync(user, model.Email);
if (!setUserResult.Succeeded)
{
throw new ApplicationException($"Unexpected error occurred setting email for user with ID '{user.Id}'.");
}
var setEmailResult = await _userManager.SetEmailAsync(user, model.Email);
if (!setEmailResult.Succeeded)
{
throw new ApplicationException($"Unexpected error occurred setting email for user with ID '{user.Id}'.");
}
await _userManager.SetUserNameAsync(user, model.Username);
}
var phoneNumber = user.PhoneNumber;
if (model.PhoneNumber != phoneNumber)
{
var setPhoneResult = await _userManager.SetPhoneNumberAsync(user, model.PhoneNumber);
if (!setPhoneResult.Succeeded)
{
throw new ApplicationException($"Unexpected error occurred setting phone number for user with ID '{user.Id}'.");
}
}
if (needUpdate)
{
var result = await _userManager.UpdateAsync(user);
if (!result.Succeeded)
{
throw new ApplicationException($"Unexpected error occurred updating user with ID '{user.Id}'.");
}
}
TempData[WellKnownTempData.SuccessMessage] = "Your profile has been updated";
return RedirectToAction(nameof(Index));
}

@ -119,7 +119,7 @@ namespace BTCPayServer.Controllers
[HttpGet]
public async Task<IActionResult> Generate(string version)
{
if (_env.NetworkType != NBitcoin.NetworkType.Regtest)
if (_env.NetworkType != NBitcoin.ChainName.Regtest)
return NotFound();
await _notificationSender.SendNotification(new AdminScope(), new NewVersionNotification(version));
return RedirectToAction(nameof(Index));

@ -70,7 +70,8 @@ namespace BTCPayServer.Controllers
ClaimedAmount = amountDue,
AmountDueFormatted = _currencyNameTable.FormatCurrency(amountDue, blob.Currency),
CurrencyData = cd,
LastUpdated = DateTime.Now,
StartDate = pp.StartDate,
LastRefreshed = DateTime.Now,
Payouts = payouts
.Select(entity => new ViewPullPaymentModel.PayoutLine
{

@ -39,6 +39,7 @@ namespace BTCPayServer.Controllers
Installed = pluginService.LoadedPlugins,
Available = availablePlugins,
Commands = pluginService.GetPendingCommands(),
Disabled = pluginService.GetDisabledPlugins(),
CanShowRestart = btcPayServerOptions.DockerDeployment
};
return View(res);
@ -50,6 +51,7 @@ namespace BTCPayServer.Controllers
public IEnumerable<PluginService.AvailablePlugin> Available { get; set; }
public (string command, string plugin)[] Commands { get; set; }
public bool CanShowRestart { get; set; }
public string[] Disabled { get; set; }
}
[HttpPost("server/plugins/uninstall")]
@ -117,7 +119,7 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> UploadPlugin([FromServices] PluginService pluginService,
List<IFormFile> files)
{
foreach (var formFile in files.Where(file => file.Length > 0))
foreach (var formFile in files.Where(file => file.Length > 0).Where(file => file.FileName.IsValidFileName()))
{
await pluginService.UploadPlugin(formFile);
pluginService.InstallPlugin(formFile.FileName.TrimEnd(PluginManager.BTCPayPluginSuffix,

@ -146,6 +146,15 @@ namespace BTCPayServer.Controllers
[HttpPost("server/files/upload")]
public async Task<IActionResult> CreateFile(IFormFile file)
{
if (!file.FileName.IsValidFileName())
{
this.TempData.SetStatusMessageModel(new StatusMessageModel()
{
Message = "Invalid file name",
Severity = StatusMessageModel.StatusSeverity.Error
});
return RedirectToAction(nameof(Files));
}
var newFile = await _FileService.AddFile(file, GetUserId());
return RedirectToAction(nameof(Files), new
{

@ -19,13 +19,37 @@ namespace BTCPayServer.Controllers
public partial class ServerController
{
[Route("server/users")]
public async Task<IActionResult> ListUsers(UsersViewModel model)
public async Task<IActionResult> ListUsers(
UsersViewModel model,
string sortOrder = null
)
{
model = this.ParseListQuery(model ?? new UsersViewModel());
var users = _UserManager.Users;
model.Total = await users.CountAsync();
model.Users = await users
.Skip(model.Skip).Take(model.Count)
var usersQuery = _UserManager.Users;
if (!string.IsNullOrWhiteSpace(model.SearchTerm))
{
usersQuery = usersQuery.Where(u => u.Email.Contains(model.SearchTerm));
}
if (sortOrder != null)
{
switch (sortOrder)
{
case "desc":
ViewData["NextUserEmailSortOrder"] = "asc";
usersQuery = usersQuery.OrderByDescending(user => user.Email);
break;
case "asc":
usersQuery = usersQuery.OrderBy(user => user.Email);
ViewData["NextUserEmailSortOrder"] = "desc";
break;
}
}
model.Users = await usersQuery
.Skip(model.Skip)
.Take(model.Count)
.Select(u => new UsersViewModel.UserViewModel
{
Name = u.UserName,
@ -33,7 +57,9 @@ namespace BTCPayServer.Controllers
Id = u.Id,
Verified = u.EmailConfirmed || !u.RequiresEmailConfirmation,
Created = u.Created
}).ToListAsync();
})
.ToListAsync();
model.Total = await usersQuery.CountAsync();
return View(model);
}

@ -1,400 +0,0 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
namespace BTCPayServer.Controllers
{
public partial class StoresController
{
[HttpGet]
[Route("{storeId}/derivations/{cryptoCode}")]
public async Task<IActionResult> AddDerivationScheme(string storeId, string cryptoCode)
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
var network = cryptoCode == null ? null : _ExplorerProvider.GetNetwork(cryptoCode);
if (network == null)
{
return NotFound();
}
DerivationSchemeViewModel vm = new DerivationSchemeViewModel();
vm.CryptoCode = cryptoCode;
vm.RootKeyPath = network.GetRootKeyPath();
vm.Network = network;
var derivation = GetExistingDerivationStrategy(vm.CryptoCode, store);
if (derivation != null)
{
vm.DerivationScheme = derivation.AccountDerivation.ToString();
vm.Config = derivation.ToJson();
}
vm.Enabled = !store.GetStoreBlob().IsExcluded(new PaymentMethodId(vm.CryptoCode, PaymentTypes.BTCLike));
var hotWallet = await CanUseHotWallet();
vm.CanUseHotWallet = hotWallet.HotWallet;
vm.CanUseRPCImport = hotWallet.RPCImport;
return View(vm);
}
private DerivationSchemeSettings GetExistingDerivationStrategy(string cryptoCode, StoreData store)
{
var id = new PaymentMethodId(cryptoCode, PaymentTypes.BTCLike);
var existing = store.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<DerivationSchemeSettings>()
.FirstOrDefault(d => d.PaymentId == id);
return existing;
}
[HttpPost]
[Route("{storeId}/derivations/{cryptoCode}")]
[ApiExplorerSettings(IgnoreApi = true)]
public async Task<IActionResult> AddDerivationScheme(string storeId, [FromForm] DerivationSchemeViewModel vm,
string cryptoCode)
{
vm.CryptoCode = cryptoCode;
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
var network = cryptoCode == null ? null : _ExplorerProvider.GetNetwork(cryptoCode);
if (network == null)
{
return NotFound();
}
vm.Network = network;
vm.RootKeyPath = network.GetRootKeyPath();
DerivationSchemeSettings strategy = null;
var wallet = _WalletProvider.GetWallet(network);
if (wallet == null)
{
return NotFound();
}
if (!string.IsNullOrEmpty(vm.Config))
{
if (!DerivationSchemeSettings.TryParseFromJson(vm.Config, network, out strategy))
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Error,
Message = "Config file was not in the correct format"
});
vm.Confirmation = false;
return View(nameof(AddDerivationScheme), vm);
}
}
if (vm.WalletFile != null)
{
if (!DerivationSchemeSettings.TryParseFromWalletFile(await ReadAllText(vm.WalletFile), network,
out strategy))
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Error,
Message = "Wallet file was not in the correct format"
});
vm.Confirmation = false;
return View(nameof(AddDerivationScheme), vm);
}
}
else if (!string.IsNullOrEmpty(vm.WalletFileContent))
{
if (!DerivationSchemeSettings.TryParseFromWalletFile(vm.WalletFileContent, network, out strategy))
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Error,
Message = "QR import was not in the correct format"
});
vm.Confirmation = false;
return View(nameof(AddDerivationScheme), vm);
}
}
else
{
try
{
if (!string.IsNullOrEmpty(vm.DerivationScheme))
{
var newStrategy = ParseDerivationStrategy(vm.DerivationScheme, null, network);
if (newStrategy.AccountDerivation != strategy?.AccountDerivation)
{
var accountKey = string.IsNullOrEmpty(vm.AccountKey)
? null
: new BitcoinExtPubKey(vm.AccountKey, network.NBitcoinNetwork);
if (accountKey != null)
{
var accountSettings =
newStrategy.AccountKeySettings.FirstOrDefault(a => a.AccountKey == accountKey);
if (accountSettings != null)
{
accountSettings.AccountKeyPath =
vm.KeyPath == null ? null : KeyPath.Parse(vm.KeyPath);
accountSettings.RootFingerprint = string.IsNullOrEmpty(vm.RootFingerprint)
? (HDFingerprint?)null
: new HDFingerprint(
NBitcoin.DataEncoders.Encoders.Hex.DecodeData(vm.RootFingerprint));
}
}
strategy = newStrategy;
strategy.Source = vm.Source;
vm.DerivationScheme = strategy.AccountDerivation.ToString();
}
}
else
{
strategy = null;
}
}
catch
{
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme");
vm.Confirmation = false;
return View(nameof(AddDerivationScheme), vm);
}
}
var oldConfig = vm.Config;
vm.Config = strategy == null ? null : strategy.ToJson();
PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
var exisingStrategy = store.GetSupportedPaymentMethods(_NetworkProvider)
.Where(c => c.PaymentId == paymentMethodId)
.OfType<DerivationSchemeSettings>()
.FirstOrDefault();
var storeBlob = store.GetStoreBlob();
var wasExcluded = storeBlob.GetExcludedPaymentMethods().Match(paymentMethodId);
var willBeExcluded = !vm.Enabled;
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 clicking on continue after changing the config
(!vm.Confirmation && oldConfig != vm.Config) ||
// - The user is clicking on continue without changing config nor enabling/disabling
(!vm.Confirmation && oldConfig == vm.Config && willBeExcluded == wasExcluded);
showAddress = showAddress && strategy != null;
if (!showAddress)
{
try
{
if (strategy != null)
await wallet.TrackAsync(strategy.AccountDerivation);
store.SetSupportedPaymentMethod(paymentMethodId, strategy);
storeBlob.SetExcluded(paymentMethodId, willBeExcluded);
storeBlob.Hints.Wallet = false;
store.SetStoreBlob(storeBlob);
}
catch
{
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme");
return View(vm);
}
await _Repo.UpdateStore(store);
_EventAggregator.Publish(new WalletChangedEvent() {WalletId = new WalletId(storeId, cryptoCode)});
if (willBeExcluded != wasExcluded)
{
var label = willBeExcluded ? "disabled" : "enabled";
TempData[WellKnownTempData.SuccessMessage] =
$"On-Chain payments for {network.CryptoCode} has been {label}.";
}
else
{
TempData[WellKnownTempData.SuccessMessage] =
$"Derivation settings for {network.CryptoCode} has been modified.";
}
// This is success case when derivation scheme is added to the store
return RedirectToAction(nameof(UpdateStore), new {storeId = storeId});
}
else if (!string.IsNullOrEmpty(vm.HintAddress))
{
BitcoinAddress address = null;
try
{
address = BitcoinAddress.Create(vm.HintAddress, network.NBitcoinNetwork);
}
catch
{
ModelState.AddModelError(nameof(vm.HintAddress), "Invalid hint address");
return ShowAddresses(vm, strategy);
}
try
{
var newStrategy = ParseDerivationStrategy(vm.DerivationScheme, address.ScriptPubKey, network);
if (newStrategy.AccountDerivation != strategy.AccountDerivation)
{
strategy.AccountDerivation = newStrategy.AccountDerivation;
strategy.AccountOriginal = null;
}
}
catch
{
ModelState.AddModelError(nameof(vm.HintAddress), "Impossible to find a match with this address");
return ShowAddresses(vm, strategy);
}
vm.HintAddress = "";
TempData[WellKnownTempData.SuccessMessage] =
"Address successfully found, please verify that the rest is correct and click on \"Confirm\"";
ModelState.Remove(nameof(vm.HintAddress));
ModelState.Remove(nameof(vm.DerivationScheme));
}
return ShowAddresses(vm, strategy);
}
[HttpPost]
[Route("{storeId}/derivations/{cryptoCode}/generatenbxwallet")]
public async Task<IActionResult> GenerateNBXWallet(string storeId, string cryptoCode,
GenerateWalletRequest request)
{
var hotWallet = await CanUseHotWallet();
if (!hotWallet.HotWallet || (!hotWallet.RPCImport && request.ImportKeysToRPC))
{
return NotFound();
}
var network = _NetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
var client = _ExplorerProvider.GetExplorerClient(cryptoCode);
GenerateWalletResponse response;
try
{
response = await client.GenerateWalletAsync(request);
}
catch (Exception e)
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Error,
Html = $"There was an error generating your wallet: {e.Message}"
});
return RedirectToAction(nameof(AddDerivationScheme), new {storeId, cryptoCode});
}
if (response == null)
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Error,
Html = "There was an error generating your wallet. Is your node available?"
});
return RedirectToAction(nameof(AddDerivationScheme), new {storeId, cryptoCode});
}
var store = HttpContext.GetStoreData();
var result = await AddDerivationScheme(storeId,
new DerivationSchemeViewModel()
{
Confirmation = string.IsNullOrEmpty(request.ExistingMnemonic),
Network = network,
RootFingerprint = response.AccountKeyPath.MasterFingerprint.ToString(),
RootKeyPath = network.GetRootKeyPath(),
CryptoCode = cryptoCode,
DerivationScheme = response.DerivationScheme.ToString(),
Source = "NBXplorer",
AccountKey = response.AccountHDKey.Neuter().ToWif(),
DerivationSchemeFormat = "BTCPay",
KeyPath = response.AccountKeyPath.KeyPath.ToString(),
Enabled = !store.GetStoreBlob()
.IsExcluded(new PaymentMethodId(cryptoCode, PaymentTypes.BTCLike))
}, cryptoCode);
if (!ModelState.IsValid || !(result is RedirectToActionResult))
return result;
TempData.Clear();
if (string.IsNullOrEmpty(request.ExistingMnemonic))
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Success,
Html = $"<span class='text-centered'>Your wallet has been generated.</span>"
});
var vm = new RecoverySeedBackupViewModel()
{
CryptoCode = cryptoCode,
Mnemonic = response.Mnemonic,
Passphrase = response.Passphrase,
IsStored = request.SavePrivateKeys,
ReturnUrl = Url.Action(nameof(UpdateStore), new {storeId})
};
return this.RedirectToRecoverySeedBackup(vm);
}
else
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Warning,
Html = "Please check your addresses and confirm"
});
}
return result;
}
private async Task<(bool HotWallet, bool RPCImport)> CanUseHotWallet()
{
var isAdmin = (await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings))
.Succeeded;
if (isAdmin)
return (true, true);
var policies = await _settingsRepository.GetSettingAsync<PoliciesSettings>();
var hotWallet = policies?.AllowHotWalletForAll is true;
return (hotWallet, hotWallet && policies?.AllowHotWalletRPCImportForAll is true);
}
private async Task<string> ReadAllText(IFormFile file)
{
using (var stream = new StreamReader(file.OpenReadStream()))
{
return await stream.ReadToEndAsync();
}
}
private IActionResult
ShowAddresses(DerivationSchemeViewModel vm, DerivationSchemeSettings strategy)
{
vm.DerivationScheme = strategy.AccountDerivation.ToString();
var deposit = new NBXplorer.KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit);
if (!string.IsNullOrEmpty(vm.DerivationScheme))
{
var line = strategy.AccountDerivation.GetLineFor(deposit);
for (int i = 0; i < 10; i++)
{
var keyPath = deposit.GetKeyPath((uint)i);
var rootedKeyPath = vm.GetAccountKeypath()?.Derive(keyPath);
var derivation = line.Derive((uint)i);
var address = strategy.Network.NBXplorerNetwork.CreateAddress(strategy.AccountDerivation,
line.KeyPathTemplate.GetKeyPath((uint)i),
derivation.ScriptPubKey).ToString();
vm.AddressSamples.Add((keyPath.ToString(), address, rootedKeyPath));
}
}
vm.Confirmation = true;
ModelState.Remove(nameof(vm.Config)); // Remove the cached value
return View(nameof(AddDerivationScheme), vm);
}
}
}

@ -1,189 +1,26 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Shopify;
using BTCPayServer.Services.Shopify.ApiModels;
using BTCPayServer.Services.Shopify.Models;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using NBitcoin;
using NBitcoin.DataEncoders;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NicolasDorier.RateLimits;
namespace BTCPayServer.Controllers
{
public partial class StoresController
{
private static string _cachedShopifyJavascript;
private async Task<string> GetJavascript()
{
if (!string.IsNullOrEmpty(_cachedShopifyJavascript) && !_BTCPayEnv.IsDeveloping)
{
return _cachedShopifyJavascript;
}
string[] fileList = _BtcpayServerOptions.BundleJsCss
? new[] {"bundles/shopify-bundle.min.js"}
: new[] {"modal/btcpay.js", "shopify/btcpay-shopify.js"};
foreach (var file in fileList)
{
await using var stream = _webHostEnvironment.WebRootFileProvider
.GetFileInfo(file).CreateReadStream();
using var reader = new StreamReader(stream);
_cachedShopifyJavascript += Environment.NewLine + await reader.ReadToEndAsync();
}
return _cachedShopifyJavascript;
}
[AllowAnonymous]
[HttpGet("{storeId}/integrations/shopify/shopify.js")]
public async Task<IActionResult> ShopifyJavascript(string storeId)
{
var jsFile =
$"var BTCPAYSERVER_URL = \"{Request.GetAbsoluteRoot()}\"; var STORE_ID = \"{storeId}\"; {await GetJavascript()}";
return Content(jsFile, "text/javascript");
}
[RateLimitsFilter(ZoneLimits.Shopify, Scope = RateLimitsScope.RemoteAddress)]
[AllowAnonymous]
[EnableCors(CorsPolicies.All)]
[HttpGet("{storeId}/integrations/shopify/{orderId}")]
public async Task<IActionResult> ShopifyInvoiceEndpoint(
[FromServices] InvoiceRepository invoiceRepository,
[FromServices] InvoiceController invoiceController,
[FromServices] IHttpClientFactory httpClientFactory,
string storeId, string orderId, decimal amount, bool checkOnly = false)
{
var invoiceOrderId = $"{ShopifyOrderMarkerHostedService.SHOPIFY_ORDER_ID_PREFIX}{orderId}";
var matchedExistingInvoices = await invoiceRepository.GetInvoices(new InvoiceQuery()
{
OrderId = new[] {invoiceOrderId}, StoreId = new[] {storeId}
});
matchedExistingInvoices = matchedExistingInvoices.Where(entity =>
entity.GetInternalTags(ShopifyOrderMarkerHostedService.SHOPIFY_ORDER_ID_PREFIX)
.Any(s => s == orderId))
.ToArray();
var firstInvoiceStillPending =
matchedExistingInvoices.FirstOrDefault(entity => entity.GetInvoiceState().Status == InvoiceStatusLegacy.New);
if (firstInvoiceStillPending != null)
{
return Ok(new
{
invoiceId = firstInvoiceStillPending.Id,
status = firstInvoiceStillPending.Status.ToString().ToLowerInvariant()
});
}
var firstInvoiceSettled =
matchedExistingInvoices.LastOrDefault(entity =>
new[] {InvoiceStatusLegacy.Paid, InvoiceStatusLegacy.Complete, InvoiceStatusLegacy.Confirmed}.Contains(
entity.GetInvoiceState().Status));
var store = await _Repo.FindStore(storeId);
var shopify = store?.GetStoreBlob()?.Shopify;
ShopifyApiClient client = null;
ShopifyOrder order = null;
if (shopify?.IntegratedAt.HasValue is true)
{
client = new ShopifyApiClient(httpClientFactory, shopify.CreateShopifyApiCredentials());
order = await client.GetOrder(orderId);
if (string.IsNullOrEmpty(order?.Id))
{
return NotFound();
}
}
if (firstInvoiceSettled != null)
{
//if BTCPay was shut down before the tx managed to get registered on shopify, this will fix it on the next UI load in shopify
if (client != null && order?.FinancialStatus == "pending" &&
firstInvoiceSettled.Status != InvoiceStatusLegacy.Paid)
{
await new OrderTransactionRegisterLogic(client).Process(orderId, firstInvoiceSettled.Id,
firstInvoiceSettled.Currency,
firstInvoiceSettled.Price.ToString(CultureInfo.InvariantCulture), true);
order = await client.GetOrder(orderId);
}
if (order?.FinancialStatus != "pending" && order?.FinancialStatus != "partially_paid")
{
return Ok(new
{
invoiceId = firstInvoiceSettled.Id,
status = firstInvoiceSettled.Status.ToString().ToLowerInvariant()
});
}
}
if (checkOnly)
{
return Ok();
}
if (shopify?.IntegratedAt.HasValue is true)
{
if (string.IsNullOrEmpty(order?.Id) ||
!new[] {"pending", "partially_paid"}.Contains(order.FinancialStatus))
{
return NotFound();
}
//we create the invoice at due amount provided from order page or full amount if due amount is bigger than order amount
var invoice = await invoiceController.CreateInvoiceCoreRaw(
new CreateInvoiceRequest()
{
Amount = amount < order.TotalPrice ? amount : order.TotalPrice,
Currency = order.Currency,
Metadata = new JObject {["orderId"] = invoiceOrderId}
}, store,
Request.GetAbsoluteUri(""), new List<string>() {invoiceOrderId});
return Ok(new {invoiceId = invoice.Id, status = invoice.Status.ToString().ToLowerInvariant()});
}
return NotFound();
}
[HttpGet]
[Route("{storeId}/integrations")]
[Route("{storeId}/integrations/shopify")]
[HttpGet("{storeId}/integrations")]
public IActionResult Integrations()
{
var blob = CurrentStore.GetStoreBlob();
var vm = new IntegrationsViewModel {Shopify = blob.Shopify};
return View("Integrations", vm);
{
return View("Integrations",new IntegrationsViewModel());
}
[HttpGet]
[Route("{storeId}/webhooks")]
[HttpGet("{storeId}/webhooks")]
public async Task<IActionResult> Webhooks()
{
var webhooks = await this._Repo.GetWebhooks(CurrentStore.Id);
var webhooks = await _Repo.GetWebhooks(CurrentStore.Id);
return View(nameof(Webhooks), new WebhooksViewModel()
{
Webhooks = webhooks.Select(w => new WebhooksViewModel.WebhookViewModel()
@ -193,11 +30,11 @@ namespace BTCPayServer.Controllers
}).ToArray()
});
}
[HttpGet]
[Route("{storeId}/webhooks/new")]
[HttpGet("{storeId}/webhooks/new")]
public IActionResult NewWebhook()
{
return View(nameof(ModifyWebhook), new EditWebhookViewModel()
return View(nameof(ModifyWebhook), new EditWebhookViewModel
{
Active = true,
Everything = true,
@ -206,14 +43,14 @@ namespace BTCPayServer.Controllers
});
}
[HttpGet]
[Route("{storeId}/webhooks/{webhookId}/remove")]
[HttpGet("{storeId}/webhooks/{webhookId}/remove")]
public async Task<IActionResult> DeleteWebhook(string webhookId)
{
var webhook = await _Repo.GetWebhook(CurrentStore.Id, webhookId);
if (webhook is null)
return NotFound();
return View("Confirm", new ConfirmModel()
return View("Confirm", new ConfirmModel
{
Title = $"Delete a webhook",
Description = "This webhook will be removed from this store, do you wish to continue?",
@ -221,36 +58,36 @@ namespace BTCPayServer.Controllers
});
}
[HttpPost]
[Route("{storeId}/webhooks/{webhookId}/remove")]
[HttpPost("{storeId}/webhooks/{webhookId}/remove")]
public async Task<IActionResult> DeleteWebhookPost(string webhookId)
{
var webhook = await _Repo.GetWebhook(CurrentStore.Id, webhookId);
if (webhook is null)
return NotFound();
await _Repo.DeleteWebhook(CurrentStore.Id, webhookId);
TempData[WellKnownTempData.SuccessMessage] = "Webhook successfully deleted";
return RedirectToAction(nameof(Webhooks), new { storeId = CurrentStore.Id });
}
[HttpPost]
[Route("{storeId}/webhooks/new")]
[HttpPost("{storeId}/webhooks/new")]
public async Task<IActionResult> NewWebhook(string storeId, EditWebhookViewModel viewModel)
{
if (!ModelState.IsValid)
return View(viewModel);
return View(nameof(ModifyWebhook), viewModel);
var webhookId = await _Repo.CreateWebhook(CurrentStore.Id, viewModel.CreateBlob());
await _Repo.CreateWebhook(CurrentStore.Id, viewModel.CreateBlob());
TempData[WellKnownTempData.SuccessMessage] = "The webhook has been created";
return RedirectToAction(nameof(Webhooks), new { storeId });
}
[HttpGet]
[Route("{storeId}/webhooks/{webhookId}")]
[HttpGet("{storeId}/webhooks/{webhookId}")]
public async Task<IActionResult> ModifyWebhook(string webhookId)
{
var webhook = await _Repo.GetWebhook(CurrentStore.Id, webhookId);
if (webhook is null)
return NotFound();
var blob = webhook.GetBlob();
var deliveries = await _Repo.GetWebhookDeliveries(CurrentStore.Id, webhookId, 20);
return View(nameof(ModifyWebhook), new EditWebhookViewModel(blob)
@ -259,8 +96,8 @@ namespace BTCPayServer.Controllers
.Select(s => new DeliveryViewModel(s)).ToList()
});
}
[HttpPost]
[Route("{storeId}/webhooks/{webhookId}")]
[HttpPost("{storeId}/webhooks/{webhookId}")]
public async Task<IActionResult> ModifyWebhook(string webhookId, EditWebhookViewModel viewModel)
{
var webhook = await _Repo.GetWebhook(CurrentStore.Id, webhookId);
@ -272,16 +109,17 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(Webhooks), new { storeId = CurrentStore.Id });
}
[HttpPost]
[Route("{storeId}/webhooks/{webhookId}/deliveries/{deliveryId}/redeliver")]
[HttpPost("{storeId}/webhooks/{webhookId}/deliveries/{deliveryId}/redeliver")]
public async Task<IActionResult> RedeliverWebhook(string webhookId, string deliveryId)
{
var delivery = await _Repo.GetWebhookDelivery(CurrentStore.Id, webhookId, deliveryId);
if (delivery is null)
return NotFound();
var newDeliveryId = await WebhookNotificationManager.Redeliver(deliveryId);
if (newDeliveryId is null)
return NotFound();
TempData[WellKnownTempData.SuccessMessage] = "Successfully planned a redelivery";
return RedirectToAction(nameof(ModifyWebhook),
new
@ -290,104 +128,15 @@ namespace BTCPayServer.Controllers
webhookId
});
}
[HttpGet]
[Route("{storeId}/webhooks/{webhookId}/deliveries/{deliveryId}/request")]
[HttpGet("{storeId}/webhooks/{webhookId}/deliveries/{deliveryId}/request")]
public async Task<IActionResult> WebhookDelivery(string webhookId, string deliveryId)
{
var delivery = await _Repo.GetWebhookDelivery(CurrentStore.Id, webhookId, deliveryId);
if (delivery is null)
return NotFound();
return this.File(delivery.GetBlob().Request, "application/json");
}
[HttpPost]
[Route("{storeId}/integrations/shopify")]
public async Task<IActionResult> Integrations([FromServices] IHttpClientFactory clientFactory,
IntegrationsViewModel vm, string command = "", string exampleUrl = "")
{
if (!string.IsNullOrEmpty(exampleUrl))
{
try
{
//https://{apikey}:{password}@{hostname}/admin/api/{version}/{resource}.json
var parsedUrl = new Uri(exampleUrl);
var userInfo = parsedUrl.UserInfo.Split(":");
vm.Shopify = new ShopifySettings()
{
ApiKey = userInfo[0],
Password = userInfo[1],
ShopName = parsedUrl.Host.Replace(".myshopify.com", "",
StringComparison.InvariantCultureIgnoreCase)
};
command = "ShopifySaveCredentials";
}
catch (Exception)
{
TempData[WellKnownTempData.ErrorMessage] = "The provided Example Url was invalid.";
return View("Integrations", vm);
}
}
switch (command)
{
case "ShopifySaveCredentials":
{
var shopify = vm.Shopify;
var validCreds = shopify != null && shopify?.CredentialsPopulated() == true;
if (!validCreds)
{
TempData[WellKnownTempData.ErrorMessage] = "Please provide valid Shopify credentials";
return View("Integrations", vm);
}
var apiClient = new ShopifyApiClient(clientFactory, shopify.CreateShopifyApiCredentials());
try
{
await apiClient.OrdersCount();
}
catch (ShopifyApiException)
{
TempData[WellKnownTempData.ErrorMessage] =
"Shopify rejected provided credentials, please correct values and try again";
return View("Integrations", vm);
}
var scopesGranted = await apiClient.CheckScopes();
if (!scopesGranted.Contains("read_orders") || !scopesGranted.Contains("write_orders"))
{
TempData[WellKnownTempData.ErrorMessage] =
"Please grant the private app permissions for read_orders, write_orders";
return View("Integrations", vm);
}
// everything ready, proceed with saving Shopify integration credentials
shopify.IntegratedAt = DateTimeOffset.Now;
var blob = CurrentStore.GetStoreBlob();
blob.Shopify = shopify;
if (CurrentStore.SetStoreBlob(blob))
{
await _Repo.UpdateStore(CurrentStore);
}
TempData[WellKnownTempData.SuccessMessage] = "Shopify integration successfully updated";
break;
}
case "ShopifyClearCredentials":
{
var blob = CurrentStore.GetStoreBlob();
blob.Shopify = null;
if (CurrentStore.SetStoreBlob(blob))
{
await _Repo.UpdateStore(CurrentStore);
}
TempData[WellKnownTempData.SuccessMessage] = "Shopify integration credentials cleared";
break;
}
}
return RedirectToAction(nameof(Integrations), new {storeId = CurrentStore.Id});
return File(delivery.GetBlob().Request, "application/json");
}
}
}

@ -14,118 +14,78 @@ namespace BTCPayServer.Controllers
{
public partial class StoresController
{
[HttpGet]
[Route("{storeId}/lightning/{cryptoCode}")]
[HttpGet("{storeId}/lightning/{cryptoCode}")]
public IActionResult AddLightningNode(string storeId, string cryptoCode)
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
LightningNodeViewModel vm = new LightningNodeViewModel
var vm = new LightningNodeViewModel
{
CryptoCode = cryptoCode,
InternalLightningNode = GetInternalLighningNode(cryptoCode)?.ToString(),
StoreId = storeId
};
SetExistingValues(store, vm);
return View(vm);
}
private void SetExistingValues(StoreData store, LightningNodeViewModel vm)
{
vm.ConnectionString = GetExistingLightningSupportedPaymentMethod(vm.CryptoCode, store)?.GetLightningUrl()?.ToString();
vm.Enabled = !store.GetStoreBlob().IsExcluded(new PaymentMethodId(vm.CryptoCode, PaymentTypes.LightningLike));
}
private LightningSupportedPaymentMethod GetExistingLightningSupportedPaymentMethod(string cryptoCode, StoreData store)
{
var id = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
var existing = store.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<LightningSupportedPaymentMethod>()
.FirstOrDefault(d => d.PaymentId == id);
return existing;
}
private LightningConnectionString GetInternalLighningNode(string cryptoCode)
{
if (_lightningNetworkOptions.Value.InternalLightningByCryptoCode.TryGetValue(cryptoCode, out var connectionString))
{
return CanUseInternalLightning() ? connectionString : null;
}
return null;
}
[HttpPost]
[Route("{storeId}/lightning/{cryptoCode}")]
[HttpPost("{storeId}/lightning/{cryptoCode}")]
public async Task<IActionResult> AddLightningNode(string storeId, LightningNodeViewModel vm, string command, string cryptoCode)
{
vm.CryptoCode = cryptoCode;
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
var network = vm.CryptoCode == null ? null : _ExplorerProvider.GetNetwork(vm.CryptoCode);
var internalLightning = GetInternalLighningNode(network.CryptoCode);
vm.InternalLightningNode = internalLightning?.ToString();
vm.CanUseInternalNode = CanUseInternalLightning();
var network = vm.CryptoCode == null ? null : _ExplorerProvider.GetNetwork(vm.CryptoCode);
if (network == null)
{
ModelState.AddModelError(nameof(vm.CryptoCode), "Invalid network");
return View(vm);
}
PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.LightningLike);
Payments.Lightning.LightningSupportedPaymentMethod paymentMethod = null;
if (!string.IsNullOrEmpty(vm.ConnectionString))
var paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.LightningLike);
LightningSupportedPaymentMethod paymentMethod = null;
if (vm.LightningNodeType == LightningNodeType.Internal)
{
if (!CanUseInternalLightning())
{
ModelState.AddModelError(nameof(vm.ConnectionString), "You are not authorized to use the internal lightning node");
return View(vm);
}
paymentMethod = new LightningSupportedPaymentMethod
{
CryptoCode = paymentMethodId.CryptoCode
};
paymentMethod.SetInternalNode();
}
else
{
if (string.IsNullOrEmpty(vm.ConnectionString))
{
ModelState.AddModelError(nameof(vm.ConnectionString), "Please provide a connection string");
return View(vm);
}
if (!LightningConnectionString.TryParse(vm.ConnectionString, false, out var connectionString, out var error))
{
ModelState.AddModelError(nameof(vm.ConnectionString), $"Invalid URL ({error})");
return View(vm);
}
if (connectionString.ConnectionType == LightningConnectionType.LndGRPC)
{
ModelState.AddModelError(nameof(vm.ConnectionString), $"BTCPay does not support gRPC connections");
return View(vm);
}
bool isInternalNode = connectionString.IsInternalNode(internalLightning);
if (connectionString.BaseUri.Scheme == "http")
if (!User.IsInRole(Roles.ServerAdmin) && !connectionString.IsSafe())
{
if (!isInternalNode && !connectionString.AllowInsecure)
{
ModelState.AddModelError(nameof(vm.ConnectionString), "The url must be HTTPS");
return View(vm);
}
}
if (connectionString.MacaroonFilePath != null)
{
if (!CanUseInternalLightning())
{
ModelState.AddModelError(nameof(vm.ConnectionString), "You are not authorized to use macaroonfilepath");
return View(vm);
}
if (!System.IO.File.Exists(connectionString.MacaroonFilePath))
{
ModelState.AddModelError(nameof(vm.ConnectionString), "The macaroonfilepath file does not exist");
return View(vm);
}
if (!System.IO.Path.IsPathRooted(connectionString.MacaroonFilePath))
{
ModelState.AddModelError(nameof(vm.ConnectionString), "The macaroonfilepath should be fully rooted");
return View(vm);
}
}
if (isInternalNode && !CanUseInternalLightning())
{
ModelState.AddModelError(nameof(vm.ConnectionString), "Unauthorized url");
ModelState.AddModelError(nameof(vm.ConnectionString), "You are not a server admin, so the connection string should not contain 'cookiefilepath', 'macaroondirectorypath', 'macaroonfilepath', and should not point to a local ip or to a dns name ending with '.internal', '.local', '.lan' or '.'.");
return View(vm);
}
paymentMethod = new Payments.Lightning.LightningSupportedPaymentMethod()
paymentMethod = new LightningSupportedPaymentMethod
{
CryptoCode = paymentMethodId.CryptoCode
};
@ -141,24 +101,20 @@ namespace BTCPayServer.Controllers
store.SetStoreBlob(storeBlob);
store.SetSupportedPaymentMethod(paymentMethodId, paymentMethod);
await _Repo.UpdateStore(store);
TempData[WellKnownTempData.SuccessMessage] = $"Lightning node modified ({network.CryptoCode})";
return RedirectToAction(nameof(UpdateStore), new { storeId = storeId });
case "test" when paymentMethod == null:
ModelState.AddModelError(nameof(vm.ConnectionString), "Missing url parameter");
return View(vm);
TempData[WellKnownTempData.SuccessMessage] = $"{network.CryptoCode} Lightning node modified.";
return RedirectToAction(nameof(UpdateStore), new { storeId });
case "test":
var handler = _ServiceProvider.GetRequiredService<LightningLikePaymentHandler>();
try
{
var info = await handler.GetNodeInfo(this.Request.IsOnion(), paymentMethod, network);
var info = await handler.GetNodeInfo(Request.IsOnion(), paymentMethod, network);
if (!vm.SkipPortTest)
{
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)))
{
await handler.TestConnection(info, cts.Token);
}
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20));
await handler.TestConnection(info, cts.Token);
}
TempData[WellKnownTempData.SuccessMessage] = $"Connection to the lightning node succeeded. Your node address: {info}";
TempData[WellKnownTempData.SuccessMessage] = $"Connection to the Lightning node succeeded. Your node address: {info}";
}
catch (Exception ex)
{
@ -166,6 +122,7 @@ namespace BTCPayServer.Controllers
return View(vm);
}
return View(vm);
default:
return View(vm);
}
@ -173,7 +130,28 @@ namespace BTCPayServer.Controllers
private bool CanUseInternalLightning()
{
return (_BTCPayEnv.IsDeveloping || User.IsInRole(Roles.ServerAdmin) || _CssThemeManager.AllowLightningInternalNodeForAll);
return User.IsInRole(Roles.ServerAdmin) || _CssThemeManager.AllowLightningInternalNodeForAll;
}
private void SetExistingValues(StoreData store, LightningNodeViewModel vm)
{
var lightning = GetExistingLightningSupportedPaymentMethod(vm.CryptoCode, store);
if (lightning != null)
{
vm.LightningNodeType = lightning.IsInternalNode ? LightningNodeType.Internal : LightningNodeType.Custom;
vm.ConnectionString = lightning.GetDisplayableConnectionString();
}
vm.Enabled = !store.GetStoreBlob().IsExcluded(new PaymentMethodId(vm.CryptoCode, PaymentTypes.LightningLike)) && lightning != null;
vm.CanUseInternalNode = CanUseInternalLightning();
}
private LightningSupportedPaymentMethod GetExistingLightningSupportedPaymentMethod(string cryptoCode, StoreData store)
{
var id = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
var existing = store.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<LightningSupportedPaymentMethod>()
.FirstOrDefault(d => d.PaymentId == id);
return existing;
}
}
}

@ -0,0 +1,600 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBXplorer;
using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
namespace BTCPayServer.Controllers
{
public partial class StoresController
{
[HttpGet("{storeId}/onchain/{cryptoCode}")]
public ActionResult SetupWallet(WalletSetupViewModel vm)
{
var checkResult = IsAvailable(vm.CryptoCode, out var store, out _);
if (checkResult != null)
{
return checkResult;
}
var derivation = GetExistingDerivationStrategy(vm.CryptoCode, store);
vm.DerivationScheme = derivation?.AccountDerivation.ToString();
return View(vm);
}
[HttpGet("{storeId}/onchain/{cryptoCode}/import/{method?}")]
public async Task<IActionResult> ImportWallet(WalletSetupViewModel vm)
{
var checkResult = IsAvailable(vm.CryptoCode, out _, out var network);
if (checkResult != null)
{
return checkResult;
}
var (hotWallet, rpcImport) = await CanUseHotWallet();
vm.Network = network;
vm.RootKeyPath = network.GetRootKeyPath();
vm.CanUseHotWallet = hotWallet;
vm.CanUseRPCImport = rpcImport;
if (vm.Method == null)
{
vm.Method = WalletSetupMethod.ImportOptions;
}
else if (vm.Method == WalletSetupMethod.Seed)
{
vm.SetupRequest = new GenerateWalletRequest();
}
return View(vm.ViewName, vm);
}
[HttpPost("{storeId}/onchain/{cryptoCode}/modify")]
[HttpPost("{storeId}/onchain/{cryptoCode}/import/{method}")]
public async Task<IActionResult> UpdateWallet(WalletSetupViewModel vm)
{
var checkResult = IsAvailable(vm.CryptoCode, out var store, out var network);
if (checkResult != null)
{
return checkResult;
}
vm.Network = network;
vm.RootKeyPath = network.GetRootKeyPath();
DerivationSchemeSettings strategy = null;
var wallet = _WalletProvider.GetWallet(network);
if (wallet == null)
{
return NotFound();
}
if (!string.IsNullOrEmpty(vm.Config))
{
if (!DerivationSchemeSettings.TryParseFromJson(vm.Config, network, out strategy))
{
ModelState.AddModelError(nameof(vm.Config), "Config file was not in the correct format");
return View(vm.ViewName, vm);
}
}
if (vm.WalletFile != null)
{
if (!DerivationSchemeSettings.TryParseFromWalletFile(await ReadAllText(vm.WalletFile), network, out strategy))
{
ModelState.AddModelError(nameof(vm.WalletFile), "Wallet file was not in the correct format");
return View(vm.ViewName, vm);
}
}
else if (!string.IsNullOrEmpty(vm.WalletFileContent))
{
if (!DerivationSchemeSettings.TryParseFromWalletFile(vm.WalletFileContent, network, out strategy))
{
ModelState.AddModelError(nameof(vm.WalletFileContent), "QR import was not in the correct format");
return View(vm.ViewName, vm);
}
}
else if (!string.IsNullOrEmpty(vm.DerivationScheme))
{
try
{
var newStrategy = ParseDerivationStrategy(vm.DerivationScheme, null, network);
if (newStrategy.AccountDerivation != strategy?.AccountDerivation)
{
var accountKey = string.IsNullOrEmpty(vm.AccountKey)
? null
: new BitcoinExtPubKey(vm.AccountKey, network.NBitcoinNetwork);
if (accountKey != null)
{
var accountSettings =
newStrategy.AccountKeySettings.FirstOrDefault(a => a.AccountKey == accountKey);
if (accountSettings != null)
{
accountSettings.AccountKeyPath =
vm.KeyPath == null ? null : KeyPath.Parse(vm.KeyPath);
accountSettings.RootFingerprint = string.IsNullOrEmpty(vm.RootFingerprint)
? (HDFingerprint?)null
: new HDFingerprint(
NBitcoin.DataEncoders.Encoders.Hex.DecodeData(vm.RootFingerprint));
}
}
strategy = newStrategy;
strategy.Source = vm.Source;
vm.DerivationScheme = strategy.AccountDerivation.ToString();
}
}
catch
{
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid wallet format");
return View(vm.ViewName, vm);
}
}
else
{
ModelState.AddModelError(nameof(vm.DerivationScheme), "Please provide your extended public key");
return View(vm.ViewName, vm);
}
var oldConfig = vm.Config;
vm.Config = strategy?.ToJson();
var configChanged = oldConfig != vm.Config;
PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
var storeBlob = store.GetStoreBlob();
var wasExcluded = storeBlob.GetExcludedPaymentMethods().Match(paymentMethodId);
var willBeExcluded = !vm.Enabled;
var excludedChanged = willBeExcluded != wasExcluded;
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 clicking on continue after changing the config
(!vm.Confirmation && configChanged);
showAddress = showAddress && strategy != null;
if (!showAddress)
{
try
{
if (strategy != null)
await wallet.TrackAsync(strategy.AccountDerivation);
store.SetSupportedPaymentMethod(paymentMethodId, strategy);
storeBlob.SetExcluded(paymentMethodId, willBeExcluded);
storeBlob.Hints.Wallet = false;
store.SetStoreBlob(storeBlob);
}
catch
{
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid derivation scheme");
return View(vm.ViewName, vm);
}
await _Repo.UpdateStore(store);
_EventAggregator.Publish(new WalletChangedEvent {WalletId = new WalletId(vm.StoreId, vm.CryptoCode)});
if (excludedChanged)
{
var label = willBeExcluded ? "disabled" : "enabled";
TempData[WellKnownTempData.SuccessMessage] =
$"On-Chain payments for {network.CryptoCode} have been {label}.";
}
else
{
TempData[WellKnownTempData.SuccessMessage] =
$"Derivation settings for {network.CryptoCode} have been modified.";
}
// This is success case when derivation scheme is added to the store
return RedirectToAction(nameof(UpdateStore), new {storeId = vm.StoreId});
}
if (!string.IsNullOrEmpty(vm.HintAddress))
{
BitcoinAddress address;
try
{
address = BitcoinAddress.Create(vm.HintAddress, network.NBitcoinNetwork);
}
catch
{
ModelState.AddModelError(nameof(vm.HintAddress), "Invalid hint address");
return ConfirmAddresses(vm, strategy);
}
try
{
var newStrategy = ParseDerivationStrategy(vm.DerivationScheme, address.ScriptPubKey, network);
if (newStrategy.AccountDerivation != strategy.AccountDerivation)
{
strategy.AccountDerivation = newStrategy.AccountDerivation;
strategy.AccountOriginal = null;
}
}
catch
{
ModelState.AddModelError(nameof(vm.HintAddress), "Impossible to find a match with this address. Are you sure the wallet and address provided are correct and from the same source?");
return ConfirmAddresses(vm, strategy);
}
vm.HintAddress = "";
TempData[WellKnownTempData.SuccessMessage] =
"Address successfully found, please verify that the rest is correct and click on \"Confirm\"";
ModelState.Remove(nameof(vm.HintAddress));
ModelState.Remove(nameof(vm.DerivationScheme));
}
return ConfirmAddresses(vm, strategy);
}
[HttpGet("{storeId}/onchain/{cryptoCode}/generate/{method?}")]
public async Task<IActionResult> GenerateWallet(WalletSetupViewModel vm)
{
var checkResult = IsAvailable(vm.CryptoCode, out var store, out var network);
if (checkResult != null)
{
return checkResult;
}
var isHotWallet = vm.Method == WalletSetupMethod.HotWallet;
var (hotWallet, rpcImport) = await CanUseHotWallet();
if (isHotWallet && !hotWallet)
{
return NotFound();
}
var derivation = GetExistingDerivationStrategy(vm.CryptoCode, store);
if (derivation != null)
{
vm.DerivationScheme = derivation.AccountDerivation.ToString();
vm.Config = derivation.ToJson();
}
vm.Enabled = !store.GetStoreBlob().IsExcluded(new PaymentMethodId(vm.CryptoCode, PaymentTypes.BTCLike));
vm.CanUseHotWallet = hotWallet;
vm.CanUseRPCImport = rpcImport;
vm.RootKeyPath = network.GetRootKeyPath();
vm.Network = network;
if (vm.Method == null)
{
vm.Method = WalletSetupMethod.GenerateOptions;
}
else
{
vm.SetupRequest = new GenerateWalletRequest { SavePrivateKeys = isHotWallet };
}
return View(vm.ViewName, vm);
}
[HttpPost("{storeId}/onchain/{cryptoCode}/generate/{method}")]
public async Task<IActionResult> GenerateWallet(string storeId, string cryptoCode, WalletSetupMethod method, GenerateWalletRequest request)
{
var checkResult = IsAvailable(cryptoCode, out var store, out var network);
if (checkResult != null)
{
return checkResult;
}
var (hotWallet, rpcImport) = await CanUseHotWallet();
if (!hotWallet && request.SavePrivateKeys || !rpcImport && request.ImportKeysToRPC)
{
return NotFound();
}
var client = _ExplorerProvider.GetExplorerClient(cryptoCode);
var isImport = method == WalletSetupMethod.Seed;
var vm = new WalletSetupViewModel
{
StoreId = storeId,
CryptoCode = cryptoCode,
Method = method,
SetupRequest = request,
Confirmation = string.IsNullOrEmpty(request.ExistingMnemonic),
Network = network,
RootKeyPath = network.GetRootKeyPath(),
Enabled = !store.GetStoreBlob().IsExcluded(new PaymentMethodId(cryptoCode, PaymentTypes.BTCLike)),
Source = "NBXplorer",
DerivationSchemeFormat = "BTCPay",
CanUseHotWallet = true,
CanUseRPCImport = rpcImport
};
if (isImport && string.IsNullOrEmpty(request.ExistingMnemonic))
{
ModelState.AddModelError(nameof(request.ExistingMnemonic), "Please provide your existing seed");
return View(vm.ViewName, vm);
}
GenerateWalletResponse response;
try
{
response = await client.GenerateWalletAsync(request);
if (response == null)
{
throw new Exception("Node unavailable");
}
}
catch (Exception e)
{
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Error,
Html = $"There was an error generating your wallet: {e.Message}"
});
return View(vm.ViewName, vm);
}
// Set wallet properties from generate response
vm.RootFingerprint = response.AccountKeyPath.MasterFingerprint.ToString();
vm.DerivationScheme = response.DerivationScheme.ToString();
vm.AccountKey = response.AccountHDKey.Neuter().ToWif();
vm.KeyPath = response.AccountKeyPath.KeyPath.ToString();
var result = await UpdateWallet(vm);
if (!ModelState.IsValid || !(result is RedirectToActionResult))
return result;
if (!isImport)
{
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Success,
Html = "<span class='text-centered'>Your wallet has been generated.</span>"
});
var seedVm = new RecoverySeedBackupViewModel
{
CryptoCode = cryptoCode,
Mnemonic = response.Mnemonic,
Passphrase = response.Passphrase,
IsStored = request.SavePrivateKeys,
ReturnUrl = Url.Action(nameof(GenerateWalletConfirm), new {storeId, cryptoCode})
};
return this.RedirectToRecoverySeedBackup(seedVm);
}
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Warning,
Html = "Please check your addresses and confirm."
});
return result;
}
// The purpose of this action is to show the user a success message, which confirms
// that the store settings have been updated after generating a new wallet.
[HttpGet("{storeId}/onchain/{cryptoCode}/generate/confirm")]
public ActionResult GenerateWalletConfirm(string storeId, string cryptoCode)
{
var checkResult = IsAvailable(cryptoCode, out _, out var network);
if (checkResult != null)
{
return checkResult;
}
TempData[WellKnownTempData.SuccessMessage] =
$"Derivation settings for {network.CryptoCode} have been modified.";
return RedirectToAction(nameof(UpdateStore), new {storeId});
}
[HttpGet("{storeId}/onchain/{cryptoCode}/modify")]
public async Task<IActionResult> ModifyWallet(WalletSetupViewModel vm)
{
var checkResult = IsAvailable(vm.CryptoCode, out var store, out var network);
if (checkResult != null)
{
return checkResult;
}
var derivation = GetExistingDerivationStrategy(vm.CryptoCode, store);
if (derivation == null)
{
return NotFound();
}
var (hotWallet, rpcImport) = await CanUseHotWallet();
var isHotWallet = await IsHotWallet(vm.CryptoCode, derivation);
vm.CanUseHotWallet = hotWallet;
vm.CanUseRPCImport = rpcImport;
vm.RootKeyPath = network.GetRootKeyPath();
vm.Network = network;
vm.Source = derivation.Source;
vm.RootFingerprint = derivation.GetSigningAccountKeySettings().RootFingerprint.ToString();
vm.DerivationScheme = derivation.AccountDerivation.ToString();
vm.KeyPath = derivation.GetSigningAccountKeySettings().AccountKeyPath?.ToString();
vm.Config = derivation.ToJson();
vm.Enabled = !store.GetStoreBlob().IsExcluded(new PaymentMethodId(vm.CryptoCode, PaymentTypes.BTCLike));
vm.IsHotWallet = isHotWallet;
return View(vm);
}
[HttpGet("{storeId}/onchain/{cryptoCode}/replace")]
public async Task<IActionResult> ReplaceWallet(string storeId, string cryptoCode)
{
var checkResult = IsAvailable(cryptoCode, out var store, out var network);
if (checkResult != null)
{
return checkResult;
}
var derivation = GetExistingDerivationStrategy(cryptoCode, store);
var isHotWallet = await IsHotWallet(cryptoCode, derivation);
var walletType = isHotWallet ? "hot" : "watch-only";
var additionalText = isHotWallet
? ""
: " or imported into an external wallet. If you no longer have access to your private key (recovery seed), immediately replace the wallet";
var description =
$"<p class=\"text-danger font-weight-bold\">Please note that this is a {walletType} wallet!</p>" +
$"<p class=\"text-danger font-weight-bold\">Do not replace the wallet if you have not backed it up{additionalText}.</p>" +
"<p class=\"text-left mb-0\">Replacing the wallet will erase the current wallet data from the server. " +
"The current wallet will be replaced once you finish the setup of the new wallet. If you cancel the setup, the current wallet will stay active .</p>";
return View("Confirm", new ConfirmModel
{
Title = $"Replace {network.CryptoCode} wallet",
Description = description,
DescriptionHtml = true,
Action = "Setup new wallet"
});
}
[HttpPost("{storeId}/onchain/{cryptoCode}/replace")]
public IActionResult ConfirmReplaceWallet(string storeId, string cryptoCode)
{
var checkResult = IsAvailable(cryptoCode, out var store, out _);
if (checkResult != null)
{
return checkResult;
}
var derivation = GetExistingDerivationStrategy(cryptoCode, store);
if (derivation == null)
{
return NotFound();
}
return RedirectToAction(nameof(SetupWallet), new {storeId, cryptoCode});
}
[HttpGet("{storeId}/onchain/{cryptoCode}/delete")]
public async Task<IActionResult> DeleteWallet(string storeId, string cryptoCode)
{
var checkResult = IsAvailable(cryptoCode, out var store, out var network);
if (checkResult != null)
{
return checkResult;
}
var derivation = GetExistingDerivationStrategy(cryptoCode, store);
var isHotWallet = await IsHotWallet(cryptoCode, derivation);
var walletType = isHotWallet ? "hot" : "watch-only";
var additionalText = isHotWallet
? ""
: " or imported into an external wallet. If you no longer have access to your private key (recovery seed), immediately replace the wallet";
var description =
$"<p class=\"text-danger font-weight-bold\">Please note that this is a {walletType} wallet!</p>" +
$"<p class=\"text-danger font-weight-bold\">Do not remove the wallet if you have not backed it up{additionalText}.</p>" +
"<p class=\"text-left mb-0\">Removing the wallet will erase the wallet data from the server. " +
$"The store won't be able to receive {network.CryptoCode} onchain payments until a new wallet is set up.</p>";
return View("Confirm", new ConfirmModel
{
Title = $"Remove {network.CryptoCode} wallet",
Description = description,
DescriptionHtml = true,
Action = "Remove"
});
}
[HttpPost("{storeId}/onchain/{cryptoCode}/delete")]
public async Task<IActionResult> ConfirmDeleteWallet(string storeId, string cryptoCode)
{
var checkResult = IsAvailable(cryptoCode, out var store, out var network);
if (checkResult != null)
{
return checkResult;
}
var derivation = GetExistingDerivationStrategy(cryptoCode, store);
if (derivation == null)
{
return NotFound();
}
PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
store.SetSupportedPaymentMethod(paymentMethodId, null);
await _Repo.UpdateStore(store);
_EventAggregator.Publish(new WalletChangedEvent {WalletId = new WalletId(storeId, cryptoCode)});
TempData[WellKnownTempData.SuccessMessage] =
$"On-Chain payment for {network.CryptoCode} has been removed.";
return RedirectToAction(nameof(UpdateStore), new {storeId});
}
private IActionResult ConfirmAddresses(WalletSetupViewModel vm, DerivationSchemeSettings strategy)
{
vm.DerivationScheme = strategy.AccountDerivation.ToString();
var deposit = new KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit);
if (!string.IsNullOrEmpty(vm.DerivationScheme))
{
var line = strategy.AccountDerivation.GetLineFor(deposit);
for (uint i = 0; i < 10; i++)
{
var keyPath = deposit.GetKeyPath(i);
var rootedKeyPath = vm.GetAccountKeypath()?.Derive(keyPath);
var derivation = line.Derive(i);
var address = strategy.Network.NBXplorerNetwork.CreateAddress(strategy.AccountDerivation,
line.KeyPathTemplate.GetKeyPath(i),
derivation.ScriptPubKey).ToString();
vm.AddressSamples.Add((keyPath.ToString(), address, rootedKeyPath));
}
}
vm.Confirmation = true;
ModelState.Remove(nameof(vm.Config)); // Remove the cached value
return View("ImportWallet/ConfirmAddresses", vm);
}
private ActionResult IsAvailable(string cryptoCode, out StoreData store, out BTCPayNetwork network)
{
store = HttpContext.GetStoreData();
network = cryptoCode == null ? null : _ExplorerProvider.GetNetwork(cryptoCode);
return store == null || network == null ? NotFound() : null;
}
private DerivationSchemeSettings GetExistingDerivationStrategy(string cryptoCode, StoreData store)
{
var id = new PaymentMethodId(cryptoCode, PaymentTypes.BTCLike);
var existing = store.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<DerivationSchemeSettings>()
.FirstOrDefault(d => d.PaymentId == id);
return existing;
}
private async Task<(bool HotWallet, bool RPCImport)> CanUseHotWallet()
{
var policies = await _settingsRepository.GetSettingAsync<PoliciesSettings>();
return await _authorizationService.CanUseHotWallet(policies, User);
}
private async Task<string> ReadAllText(IFormFile file)
{
using (var stream = new StreamReader(file.OpenReadStream()))
{
return await stream.ReadToEndAsync();
}
}
private async Task<bool> IsHotWallet(string cryptoCode, DerivationSchemeSettings derivation)
{
return derivation.IsHotWallet && await _ExplorerProvider.GetExplorerClient(cryptoCode)
.GetMetadataAsync<string>(derivation.AccountDerivation, WellknownMetadataKeys.MasterHDKey) != null;
}
}
}

@ -23,7 +23,6 @@ using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Shopify;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
using BundlerMinifier.TagHelpers;
@ -406,6 +405,7 @@ namespace BTCPayServer.Controllers
vm.LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi;
vm.LightningPrivateRouteHints = storeBlob.LightningPrivateRouteHints;
vm.OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback;
vm.LazyPaymentMethods = storeBlob.LazyPaymentMethods;
vm.RedirectAutomatically = storeBlob.RedirectAutomatically;
vm.ShowRecommendedFee = storeBlob.ShowRecommendedFee;
vm.RecommendedFeeBlockTarget = storeBlob.RecommendedFeeBlockTarget;
@ -477,6 +477,7 @@ namespace BTCPayServer.Controllers
}).ToList();
blob.RequiresRefundEmail = model.RequiresRefundEmail;
blob.LazyPaymentMethods = model.LazyPaymentMethods;
blob.LightningAmountInSatoshi = model.LightningAmountInSatoshi;
blob.LightningPrivateRouteHints = model.LightningPrivateRouteHints;
blob.OnChainWithLnInvoiceFallback = model.OnChainWithLnInvoiceFallback;
@ -546,8 +547,8 @@ namespace BTCPayServer.Controllers
vm.LightningNodes.Add(new StoreViewModel.LightningNode()
{
CryptoCode = paymentMethodId.CryptoCode,
Address = lightning?.GetLightningUrl()?.BaseUri.AbsoluteUri ?? string.Empty,
Enabled = !excludeFilters.Match(paymentMethodId) && lightning?.GetLightningUrl() != null
Address = lightning?.GetDisplayableConnectionString(),
Enabled = !excludeFilters.Match(paymentMethodId) && lightning != null
});
break;
}

@ -9,9 +9,11 @@ using BTCPayServer.HostedServices;
using BTCPayServer.ModelBinders;
using BTCPayServer.Models;
using BTCPayServer.Models.WalletViewModels;
using BTCPayServer.Payments.PayJoin.Sender;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using BTCPayServer.BIP78.Sender;
using NBitcoin.Payment;
using NBXplorer;
using NBXplorer.Models;
@ -171,7 +173,7 @@ namespace BTCPayServer.Controllers
var cloned = psbt.Clone();
cloned = cloned.Finalize();
await _broadcaster.Schedule(DateTimeOffset.UtcNow + TimeSpan.FromMinutes(2.0), cloned.ExtractTransaction(), btcPayNetwork);
return await _payjoinClient.RequestPayjoin(bip21, derivationSchemeSettings, psbt, cancellationToken);
return await _payjoinClient.RequestPayjoin(bip21, new PayjoinWallet(derivationSchemeSettings), psbt, cancellationToken);
}
[HttpGet]

@ -8,6 +8,7 @@ using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.ModelBinders;
@ -25,11 +26,14 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using BTCPayServer.BIP78.Sender;
using BTCPayServer.Payments.PayJoin;
using NBitcoin.DataEncoders;
using NBXplorer;
using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
using Newtonsoft.Json;
using StoreData = BTCPayServer.Data.StoreData;
namespace BTCPayServer.Controllers
{
@ -49,7 +53,7 @@ namespace BTCPayServer.Controllers
private readonly IAuthorizationService _authorizationService;
private readonly IFeeProviderFactory _feeRateProvider;
private readonly BTCPayWalletProvider _walletProvider;
private readonly WalletReceiveStateService _WalletReceiveStateService;
private readonly WalletReceiveService _walletReceiveService;
private readonly EventAggregator _EventAggregator;
private readonly SettingsRepository _settingsRepository;
private readonly DelayedTransactionBroadcaster _broadcaster;
@ -74,7 +78,7 @@ namespace BTCPayServer.Controllers
ExplorerClientProvider explorerProvider,
IFeeProviderFactory feeRateProvider,
BTCPayWalletProvider walletProvider,
WalletReceiveStateService walletReceiveStateService,
WalletReceiveService walletReceiveService,
EventAggregator eventAggregator,
SettingsRepository settingsRepository,
DelayedTransactionBroadcaster broadcaster,
@ -96,7 +100,7 @@ namespace BTCPayServer.Controllers
ExplorerClientProvider = explorerProvider;
_feeRateProvider = feeRateProvider;
_walletProvider = walletProvider;
_WalletReceiveStateService = walletReceiveStateService;
_walletReceiveService = walletReceiveService;
_EventAggregator = eventAggregator;
_settingsRepository = settingsRepository;
_broadcaster = broadcaster;
@ -359,13 +363,20 @@ namespace BTCPayServer.Controllers
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId?.CryptoCode);
if (network == null)
return NotFound();
var address = _WalletReceiveStateService.Get(walletId)?.Address;
var address = _walletReceiveService.Get(walletId)?.Address;
var allowedPayjoin = paymentMethod.IsHotWallet && CurrentStore.GetStoreBlob().PayJoinEnabled;
var bip21 = address is null ? null : network.GenerateBIP21(address.ToString(), null);
if (allowedPayjoin)
{
bip21 +=
$"?{PayjoinClient.BIP21EndpointKey}={Request.GetAbsoluteUri(Url.Action(nameof(PayJoinEndpointController.Submit), "PayJoinEndpoint", new {walletId.CryptoCode}))}";
}
return View(new WalletReceiveViewModel()
{
CryptoCode = walletId.CryptoCode,
Address = address?.ToString(),
CryptoImage = GetImage(paymentMethod.PaymentId, network)
CryptoImage = GetImage(paymentMethod.PaymentId, network),
PaymentLink = bip21
});
}
@ -382,29 +393,22 @@ namespace BTCPayServer.Controllers
var network = this.NetworkProvider.GetNetwork<BTCPayNetwork>(walletId?.CryptoCode);
if (network == null)
return NotFound();
var wallet = _walletProvider.GetWallet(network);
switch (command)
{
case "unreserve-current-address":
KeyPathInformation cachedAddress = _WalletReceiveStateService.Get(walletId);
if (cachedAddress == null)
var address = await _walletReceiveService.UnReserveAddress(walletId);
if (!string.IsNullOrEmpty(address))
{
break;
TempData.SetStatusMessageModel(new StatusMessageModel()
{
AllowDismiss = true,
Message = $"Address {address} was unreserved.",
Severity = StatusMessageModel.StatusSeverity.Success,
});
}
var address = cachedAddress.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork);
ExplorerClientProvider.GetExplorerClient(network)
.CancelReservation(cachedAddress.DerivationStrategy, new[] { cachedAddress.KeyPath });
this.TempData.SetStatusMessageModel(new StatusMessageModel()
{
AllowDismiss = true,
Message = $"Address {address} was unreserved.",
Severity = StatusMessageModel.StatusSeverity.Success,
});
_WalletReceiveStateService.Remove(walletId);
break;
case "generate-new-address":
var reserve = (await wallet.ReserveAddressAsync(paymentMethod.AccountDerivation));
_WalletReceiveStateService.Set(walletId, reserve);
await _walletReceiveService.GetOrGenerate(walletId, true);
break;
}
return RedirectToAction(nameof(WalletReceive), new { walletId });
@ -412,11 +416,8 @@ namespace BTCPayServer.Controllers
private async Task<bool> CanUseHotWallet()
{
var isAdmin = (await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings)).Succeeded;
if (isAdmin)
return true;
var policies = await _settingsRepository.GetSettingAsync<PoliciesSettings>();
return policies?.AllowHotWalletForAll is true;
return (await _authorizationService.CanUseHotWallet(policies, User)).HotWallet;
}
[HttpGet]
@ -476,10 +477,8 @@ namespace BTCPayServer.Controllers
})
.ToArray();
var balance = _walletProvider.GetWallet(network).GetBalance(paymentMethod.AccountDerivation);
model.NBXSeedAvailable = await CanUseHotWallet() && !string.IsNullOrEmpty(await ExplorerClientProvider.GetExplorerClient(network)
.GetMetadataAsync<string>(GetDerivationSchemeSettings(walletId).AccountDerivation,
WellknownMetadataKeys.MasterHDKey));
model.CurrentBalance = await balance;
model.NBXSeedAvailable = await GetSeed(walletId, network) != null;
model.CurrentBalance = (await balance).Total.GetValue(network);
await Task.WhenAll(recommendedFees);
model.RecommendedSatoshiPerByte =
@ -511,6 +510,15 @@ namespace BTCPayServer.Controllers
return View(model);
}
private async Task<string> GetSeed(WalletId walletId, BTCPayNetwork network)
{
return await CanUseHotWallet() &&
GetDerivationSchemeSettings(walletId) is DerivationSchemeSettings s &&
s.IsHotWallet &&
ExplorerClientProvider.GetExplorerClient(network) is ExplorerClient client &&
await client.GetMetadataAsync<string>(s.AccountDerivation, WellknownMetadataKeys.MasterHDKey) is string seed &&
!string.IsNullOrEmpty(seed) ? seed : null;
}
[HttpPost]
[Route("{walletId}/send")]
@ -527,9 +535,7 @@ namespace BTCPayServer.Controllers
if (network == null || network.ReadonlyWallet)
return NotFound();
vm.SupportRBF = network.SupportRBF;
vm.NBXSeedAvailable = await CanUseHotWallet() && !string.IsNullOrEmpty(await ExplorerClientProvider.GetExplorerClient(network)
.GetMetadataAsync<string>(GetDerivationSchemeSettings(walletId).AccountDerivation,
WellknownMetadataKeys.MasterHDKey, cancellation));
vm.NBXSeedAvailable = await GetSeed(walletId, network) != null;
if (!string.IsNullOrEmpty(bip21))
{
LoadFromBIP21(vm, bip21, network);
@ -661,7 +667,7 @@ namespace BTCPayServer.Controllers
if (!ModelState.IsValid)
return View(vm);
DerivationSchemeSettings derivationScheme = GetDerivationSchemeSettings(walletId);
CreatePSBTResponse psbt = null;
@ -724,7 +730,7 @@ namespace BTCPayServer.Controllers
{
new WalletSendModel.TransactionOutput()
{
Amount = uriBuilder.Amount.ToDecimal(MoneyUnit.BTC),
Amount = uriBuilder.Amount?.ToDecimal(MoneyUnit.BTC),
DestinationAddress = uriBuilder.Address.ToString(),
SubtractFeesFromOutput = false
}
@ -897,17 +903,17 @@ namespace BTCPayServer.Controllers
}
else
{
ModelState.AddModelError(nameof(viewModel.SeedOrKey), "The master fingerprint does not match the one set in your wallet settings. Probable cause are: wrong seed, wrong passphrase or wrong fingerprint in your wallet settings.");
ModelState.AddModelError(nameof(viewModel.SeedOrKey), "The master fingerprint does not match the one set in your wallet settings. Probable causes are: wrong seed, wrong passphrase or wrong fingerprint in your wallet settings.");
return View(nameof(SignWithSeed), viewModel);
}
var changed = PSBTChanged(psbt, () => psbt.SignAll(settings.AccountDerivation, signingKey, rootedKeyPath, new SigningOptions()
var changed = psbt.PSBTChanged( () => psbt.SignAll(settings.AccountDerivation, signingKey, rootedKeyPath, new SigningOptions()
{
EnforceLowR = !(viewModel.SigningContext?.EnforceLowR is false)
}));
if (!changed)
{
ModelState.AddModelError(nameof(viewModel.SeedOrKey), "Impossible to sign the transaction. Probable cause: Incorrect account key path in wallet settings, PSBT already signed.");
ModelState.AddModelError(nameof(viewModel.SeedOrKey), "Impossible to sign the transaction. Probable causes: Incorrect account key path in wallet settings or PSBT already signed.");
return View(nameof(SignWithSeed), viewModel);
}
ModelState.Remove(nameof(viewModel.SigningContext.PSBT));
@ -920,15 +926,6 @@ namespace BTCPayServer.Controllers
});
}
private bool PSBTChanged(PSBT psbt, Action act)
{
var before = psbt.ToBase64();
act();
var after = psbt.ToBase64();
return before != after;
}
private string ValueToString(Money v, BTCPayNetworkBase network)
{
return v.ToString() + " " + network.CryptoCode;
@ -1035,26 +1032,19 @@ namespace BTCPayServer.Controllers
internal DerivationSchemeSettings GetDerivationSchemeSettings(WalletId walletId)
{
var paymentMethod = CurrentStore
.GetSupportedPaymentMethods(NetworkProvider)
.OfType<DerivationSchemeSettings>()
.FirstOrDefault(p => p.PaymentId.PaymentType == Payments.PaymentTypes.BTCLike && p.PaymentId.CryptoCode == walletId.CryptoCode);
return paymentMethod;
return CurrentStore.GetDerivationSchemeSettings(NetworkProvider, walletId.CryptoCode);
}
private static async Task<string> GetBalanceString(BTCPayWallet wallet, DerivationStrategyBase derivationStrategy)
{
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)))
using CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
try
{
try
{
return (await wallet.GetBalance(derivationStrategy, cts.Token)).ShowMoney(wallet.Network
.Divisibility);
}
catch
{
return "--";
}
return (await wallet.GetBalance(derivationStrategy, cts.Token)).Total.ShowMoney(wallet.Network);
}
catch
{
return "--";
}
}
@ -1149,10 +1139,22 @@ namespace BTCPayServer.Controllers
}
else if (command == "view-seed" && await CanUseHotWallet())
{
var seed = await ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode)
if (await GetSeed(walletId, derivationScheme.Network) != null)
{
var mnemonic = await ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode)
.GetMetadataAsync<string>(derivationScheme.AccountDerivation,
WellknownMetadataKeys.Mnemonic, cancellationToken);
if (string.IsNullOrEmpty(seed))
var recoveryVm = new RecoverySeedBackupViewModel()
{
CryptoCode = walletId.CryptoCode,
Mnemonic = mnemonic,
IsStored = true,
RequireConfirm = false,
ReturnUrl = Url.Action(nameof(WalletSettings), new { walletId })
};
return this.RedirectToRecoverySeedBackup(recoveryVm);
}
else
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
@ -1160,18 +1162,6 @@ namespace BTCPayServer.Controllers
Message = "The seed was not found"
});
}
else
{
var recoveryVm = new RecoverySeedBackupViewModel()
{
CryptoCode = walletId.CryptoCode,
Mnemonic = seed,
IsStored = true,
RequireConfirm = false,
ReturnUrl = Url.Action(nameof(WalletSettings), new { walletId })
};
return this.RedirectToRecoverySeedBackup(recoveryVm);
}
return RedirectToAction(nameof(WalletSettings));
}
@ -1195,6 +1185,7 @@ namespace BTCPayServer.Controllers
public string CryptoImage { get; set; }
public string CryptoCode { get; set; }
public string Address { get; set; }
public string PaymentLink { get; set; }
}

@ -11,7 +11,6 @@ using BTCPayServer.Payments.CoinSwitch;
using BTCPayServer.Rating;
using BTCPayServer.Services.Mails;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Shopify.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
@ -29,7 +28,6 @@ namespace BTCPayServer.Data
PaymentMethodCriteria = new List<PaymentMethodCriteria>();
}
public ShopifySettings Shopify { get; set; }
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
public NetworkFeeMode NetworkFeeMode { get; set; }
@ -38,6 +36,7 @@ namespace BTCPayServer.Data
public bool LightningAmountInSatoshi { get; set; }
public bool LightningPrivateRouteHints { get; set; }
public bool OnChainWithLnInvoiceFallback { get; set; }
public bool LazyPaymentMethods { get; set; }
public bool RedirectAutomatically { get; set; }
public bool ShowRecommendedFee { get; set; }
public int RecommendedFeeBlockTarget { get; set; }

@ -69,14 +69,6 @@ namespace BTCPayServer.Data
#pragma warning disable CS0618
bool btcReturned = false;
// Legacy stuff which should go away
if (!string.IsNullOrEmpty(storeData.DerivationStrategy))
{
btcReturned = true;
yield return DerivationSchemeSettings.Parse(storeData.DerivationStrategy, networks.BTC);
}
if (!string.IsNullOrEmpty(storeData.DerivationStrategies))
{
JObject strategies = JObject.Parse(storeData.DerivationStrategies);
@ -129,11 +121,9 @@ namespace BTCPayServer.Data
bool existing = false;
foreach (var strat in strategies.Properties().ToList())
{
var stratId = PaymentMethodId.Parse(strat.Name);
if (stratId.IsBTCOnChain)
if (!PaymentMethodId.TryParse(strat.Name, out var stratId))
{
// Legacy stuff which should go away
storeData.DerivationStrategy = null;
continue;
}
if (stratId == paymentMethodId)
{
@ -149,12 +139,7 @@ namespace BTCPayServer.Data
break;
}
}
if (!existing && supportedPaymentMethod == null && paymentMethodId.IsBTCOnChain)
{
storeData.DerivationStrategy = null;
}
else if (!existing && supportedPaymentMethod != null)
if (!existing && supportedPaymentMethod != null)
strategies.Add(new JProperty(supportedPaymentMethod.PaymentId.ToString(), PaymentMethodExtensions.Serialize(supportedPaymentMethod)));
storeData.DerivationStrategies = strategies.ToString();
#pragma warning restore CS0618

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