Compare commits

..

88 Commits

Author SHA1 Message Date
4b135fdbe2 refund report 2024-02-23 13:35:09 +01:00
d55770cc16 Admin overview of the stores on the instance (#5745)
* Admin overview of the stores on the instance

POC/Draft for #5674.

* Enable admin to access foreign stores

* Remove stores list link

* UI updates

* Grant admins guest access to foreign stores

* Optimize cookie auth handler

* Test fix

* Revert changes related to StoreRepository.FindStore with isAdmin
2024-02-23 09:51:41 +01:00
5c98ca180a Webhook tests + FIXES + DOCS (#5686)
* webhook tests

* fixes and add docs

* Do not update FormResponse and StoreId in update/create PullPayment

---------

Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2024-02-23 09:44:42 +01:00
10bb75ce0e Add debug flaky test statement 2024-02-22 19:08:01 +09:00
b9e3686fcf Fix build warnings and flaky tests (#5780)
* Make checkout v2 selenium tests more robust

* Fix build warnings

* Make payjoin test more robust

* Make LNURL test more robust
2024-02-22 09:38:06 +09:00
4ae1046571 Server Settings: Customize instance name and add contact URL (#5718)
* Server Settings: Customize instance name and add contact URL

- The custom instance name would improve #5563
- Added contact URL closes #4806

* Fix custom logo display
2024-02-21 20:54:39 +01:00
147c6c4548 HTML Sanitizer updates (#5736)
* Update HTML sanitizer package

* Remove unused sanitizer from apps

* Allow mailto: links

Fixes #5728.
2024-02-21 20:53:24 +01:00
354338180b Store: Move support URL to Checkout Appearance and improve wording (#5717)
As discussed in the recent design meeting.
2024-02-21 18:50:38 +01:00
5939e19f72 Lightning: Replace user info in server URL when logging (#5750)
* Lightning: Replace user info in server URL when logging

Fixes #5747.

* Fix empty user info case
2024-02-21 14:45:05 +01:00
f72a6df55a Add legacy report (#5740) 2024-02-21 14:44:49 +01:00
9c95b98f3a Policies: Cleanup and improvements (#5731)
* Policies: Turn checkboxes into toggles

* Move email policy to server email settings

* Move maintenance policies to server maintenance settings

* Policies: Adjust spacings

* Policies: Remove DisableInstantNotifications setting

* Wording updates

* Move maintenance settings back
2024-02-21 14:43:44 +01:00
55a8ba0905 Adding link to API usage examples in docs. (#5772) 2024-02-21 14:42:15 +01:00
04037b3d2d Crowdfund : Add Buyer information / Additional information(forms) like POS (#5659)
* Crowfund : Add Buyer information / Additional information(forms) like POS

* PR 5659 - changes

* Cleanups

* fix perk

* Crowdfund form tests

* Add Selenium test for Crowfund

* Selenium update

* update Selenium

* selenium update

* update selenium

* Test fixes and view improvements

* Cleanups

* do not use hacky form element for form detection

---------

Co-authored-by: nisaba <infos@nisaba.solutions>
Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
Co-authored-by: Kukks <evilkukka@gmail.com>
2024-02-21 14:41:21 +01:00
4943c84655 Invoice: Improve events display (#5775)
Closes #5773.

- Adds seconds to the displayed date and time
- Adds a tooltip that displays the full date and time including milliseconds
- Reintroduced the colored text in case of unusual events/states (this didn't work before)
2024-02-21 14:08:28 +01:00
42a8160768 UI: Make store selector list scrollable if necessary (#5760)
Fixes #5754.
2024-02-21 13:34:47 +01:00
33d3a25928 Apps: Don't redirect .onion requests to canonical domain (#5776)
Fixes #5729.
2024-02-21 13:34:12 +01:00
c2acff81c6 Fix: Labels wouldn't be properly applied to some wallet's transactions (#5770) 2024-02-20 18:42:38 +09:00
214d4b0c3f Support 16mb psbts. Potentially fixes #5768 2024-02-19 14:14:41 +01:00
bd4cf61c2b potentially fix #5764 2024-02-19 13:01:08 +01:00
b592ee2fed Bumping LND to 0.17.4-beta (#5739)
* Clarifying that only onchain funds will be restored to the wallet

Off chain recovery would need to be done with channel.backup file which is not part of this process

* Adding powershell version of lncli invoker

* Bumping LND to 0.17.4-beta-rc1

* Bumping LND to 0.17.4-beta
2024-02-16 08:43:51 -06:00
c57e1cca25 Shopify: Improve instruction display (#5752) 2024-02-16 09:36:41 +01:00
335f345ce3 Update GeneralSettings.cshtml (#5748)
Removed trailing data
2024-02-13 09:48:16 +01:00
b7be93c569 Update NTag424 lib 2024-02-08 19:12:14 +09:00
cd01a7b727 Improve performance of payout db queries 2024-02-08 16:44:03 +09:00
b96e73a002 Fix: Payouts state could turn cancelled even if payment was successful 2024-02-08 16:32:41 +09:00
0bf22ddf29 Do not require user approval by default (#5733)
As discussed on Mattermost.
2024-02-06 17:04:18 +09:00
1c4dc382a8 Merge pull request #5683 from pavlenex/release-cycles-doc
Create RELEASE-CYCLES.md
2024-02-05 19:42:15 +05:00
71c5566f2b Dashboard: Tooltip for balance on a particular day (#5650)
Closes #5617.
2024-02-02 11:29:35 +01:00
6621859567 remove decimals for Colombian (COP) and Argentina's Peso (ARS) (#5710)
* remove decimals for Colombian (COP) and Argentina's Peso (ARS)

* remove js currency hardcoding

* Fixes removal of columbia and argentina's peso

* Refactor

---------

Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2024-02-02 17:16:13 +09:00
6437967e60 Fix: Closing Balance in Dashboard was showing incorrect value (#5716) 2024-02-01 15:13:05 +09:00
c5a926c50c Fix Kraken rate for LTC 2024-02-01 14:45:59 +09:00
85ab691b68 bump version 2024-02-01 14:17:14 +09:00
4d3e0ab599 Changelog 2024-02-01 10:13:18 +09:00
02663a149e Fix Kraken API 2024-02-01 10:09:32 +09:00
a8fdc4798d Remove randomize RBF from wallet UI advanced settings (#5709)
* Remove randomize RBF from wallet UI advanced settings

* remove support RBF and allow bump fee from wallet send model

* update psbt RBF
2024-01-31 21:04:19 +09:00
6290b0f3bf Admins can approve registered users (#5647)
* Users list: Cleanups

* Policies: Flip registration settings

* Policies: Add RequireUserApproval setting

* Add approval to user

* Require approval on login and for API key

* API handling

* AccountController cleanups

* Test fix

* Apply suggestions from code review

Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com>

* Add missing imports

* Communicate login requirements to user on account creation

* Add login requirements to basic auth handler

* Cleanups and test fix

* Encapsulate approval logic in user service and log approval changes

* Send follow up "Account approved" email

Closes #5656.

* Add notification for admins

* Fix creating a user via the admin view

* Update list: Unify flags into status column, add approve action

* Adjust "Resend email" wording

* Incorporate feedback from code review

* Remove duplicate test server policy reset

---------

Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com>
2024-01-31 14:45:54 +09:00
411e0334d0 Bitnob rate provider (#5705)
* Bitnob rate provider

* Add Bitnob as recommended exchange for NGN
2024-01-30 10:18:42 +09:00
b174977bc7 Store Email Settings: Improve configuration (#5629)
* Store Email Settings: Improve configuration

This works with the existing settings and provides better guidance about the different store email cases. Closes #5623.

* Split email and notification settings
2024-01-26 10:28:50 +01:00
2111b67e2c Update changelog 2024-01-25 21:03:27 +09:00
b96cfcd14d Apps: Allow authenticated, non-owner users permissioned access (#5702)
Fixes #5698. Before this, the app lookup was constrained by the user having at least `CanModifyStoreSettings` permissions. This changes it to require the user being associated with a store, leaving the fine-grained authorization checks up to the individual actions.
2024-01-25 21:00:33 +09:00
086f713752 Wizard UI: Constrain navigation width (#5697)
This way the back and close buttons stay within the regular container size on and don't stick to the left and right end on wide screens.

Closes #5693.
2024-01-25 16:38:05 +09:00
fd67e09cf0 In Wallet Send, label were not applied to transactions (#5700) 2024-01-25 16:37:49 +09:00
6f4ca47532 Add documentation for wallet export on sparrow 2024-01-25 16:37:15 +09:00
f97f23c8a5 Do not dispose connections created by EF 2024-01-25 10:45:02 +09:00
b62985faf4 Update changelog 2024-01-24 22:55:23 +09:00
09c761aa31 Fix: Sometimes importing a wallet file from Electrum would fail 2024-01-24 22:53:40 +09:00
8089a938f3 Guest role: Fix redirect after store creation (#5689)
This ensures that guests land on the invoices list, which tehy are allowed to see — rather than the dashboard, which they don't have permissions for.

Fixes #5688.
2024-01-24 11:34:16 +01:00
35b3fef7c5 Fix wallet import (#5695)
* Fix wallet import

* Improve error message for import of wallet file
2024-01-24 17:49:15 +09:00
f31aa43c6a Wallet file parsing: Add Wasabi test case and re-add Electrum distinction (#5694)
* Extend tests, add Wasabi file

* Re-add Electrum distinction

* Specter: Fix indentation

* Cleanups
2024-01-24 09:28:22 +09:00
b03f8db06b Refactor wallet file parsing (Fix: #5690) (#5692) 2024-01-23 21:33:45 +09:00
27e70a169e Do not show warning about browser compatibility to vault on confirm address 2024-01-23 21:30:29 +09:00
6a1d17dda2 Remove ESC as a supporter (#5685)
Closes #5684.
2024-01-22 09:00:10 +09:00
95bf60c252 Create RELEASE-CYCLES.md
This PR adds documentation around release cycles in BTCPay and tends to outline processes and ensures there's documented structure on roles and responsibilities. Feedback welcome.
2024-01-20 13:44:39 +01:00
31bc6dd48c More tests on interpolation 2024-01-20 12:21:58 +09:00
6054315d84 Add changelog 1.12.4, bump (#5678)
* Add changelog 1.12.4, bump

* Update Changelog.md

Co-authored-by: d11n <mail@dennisreimann.de>

* Update Changelog.md

---------

Co-authored-by: d11n <mail@dennisreimann.de>
2024-01-19 23:28:01 +09:00
2a7059ddeb Update languages updates from transifex (#5679)
* Update languages

* Update ChatGPT translator script

* Update translations
2024-01-19 21:45:14 +09:00
e2e7e59722 Fix webhook test for payment requests (#5680)
When testing the webhook for payment requests, we were incorrectly creating a payout webhook instead of a payment request. This would cause an error (but nothing fatal as it is only a test webhook(
2024-01-19 21:30:15 +09:00
8b373bda8e bump NBX 2024-01-18 17:21:15 +09:00
d6806dc1f6 Improve checkout page load time by fetching recommended fee in the background periodically (#5672) 2024-01-18 17:16:57 +09:00
a753698ae7 Various plugin fixes (#5577)
* Fix: Plugin updates do not work

* Offer install on disabled plugins when different version

This will:
* Clear any previous pending actions of a plugin if you click uninstall
* Show the plugin version that was disabled
* Show an update button on disabled plugins instead of install
* if a plugin is scheduled to be installed/updated, it will show which version was scheduled to be updated. If a newer version if available than the scheduled one, it will show an option to switch to that

* Ensure disabled plugins don't get loaded

* View fixes

---------

Co-authored-by: d11n <mail@dennisreimann.de>
2024-01-18 17:15:16 +09:00
3eec9cb0bb Refactor fee provider (#5643)
* Refactor fee provider

The fee provider ended up glued with a hardcoded factory. This PR:
* removes this glue and uses the DI to register fee provider for a network. (allows plugins to add their own fee providers, for any network
* Add a 10 second timeout to mempoolspace fee fetching as they are slow at times

* use linear interpolation for mempool space fee estimation

* fix upper bound

* Add tests, rollback pluginify FeeProvider

---------

Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2024-01-18 15:27:19 +09:00
cd8ef0c1ff Fix: Bitpay's API rate route wasn't backward for some queries (#5671) 2024-01-18 14:08:07 +09:00
bd196ad963 fix build 2024-01-18 12:31:59 +09:00
1ad93838c9 Remove reliance on static field 2024-01-18 11:13:32 +09:00
a9252fd741 Fix: Partial Payment shows 'Could not update BTC (LNURL-Pay)' in invoice logs (#5670) 2024-01-18 09:57:25 +09:00
376067324b Remove unused variables (#5669) 2024-01-18 09:47:39 +09:00
dd7ab2f647 Avoid exception storm when currency provider is initialized (#5668) 2024-01-18 09:31:35 +09:00
1d6d146fb2 Revert "Remove unused variables" (#5667)
This reverts commit f070b223552b92b1450a96f893883c488f985cea.
2024-01-18 00:05:50 +09:00
3ae1f13323 Bump libraries 2024-01-17 22:11:30 +09:00
0b0a8f8218 Fix: BTCPay Server fails to start the first time when installing a new plugin (#5595) 2024-01-17 19:26:22 +09:00
f070b22355 Remove unused variables 2024-01-17 18:46:28 +09:00
c5a0e28420 Refactor Wallet import code (#5638)
* Refactor Wallet import code

The code for wallet import was incredibly messy as it evolved over time from various requests.

This PR:
* splits up each supported format into its own file
* Supports taproot descriptors (through a hack until NBitcoin supports it internally) fixes #5518
* Reduces different paths for handling electrum/non-electrum xpubs
* Allows plugins to add their own import support formats for onchain wallets.

* Update NBitcoin to parse tr descriptors

* Fix warnings

* Use dedicated type OnChainWalletParsers

---------

Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2024-01-17 18:08:39 +09:00
70e9ea1d5e POS: Fix missing store branding property on form error case (#5658)
When a POS has a form, which results in an error state, the store branding property was not set. This adds the missing property and also does not render the store branding partial, in case the model property isn't present.

Fixes #5655.
2024-01-16 08:55:38 +01:00
89d294524a Checkout v2: Clicking QR code copies full payment URI (#5627)
* Checkout v2: Clicking QR code copies full payment URI

Before it copied only the destination value (Bitcoin address or Lightning BOLT11). This didn't include the BOLT11 in case of the unified QR code. Now it will copy the full payment URI, which is the same as the QR represents:

- Unified: `bitcoin:ADDRESS?amount=AMOUNT&lightning=BOLT11`
- Bitcoin: `bitcoin:ADDRESS?amount=AMOUNT`
- Lightning: `lightning:BOLT11`

Fixes #5625.

* Test fix
2024-01-16 08:54:59 +01:00
5e25ee2996 Checkout v1: Apply custom style (#5628)
Applies the custom CSS in Checkout v1 and prevents that it interferes with the styling of Checkout v2.

Fixes #5615 and fixes #5616.
2024-01-15 13:30:39 +01:00
5935dbf1d1 Store Emails: Fix test email with multiple recipients (#5649)
Fixes #5648.
2024-01-15 13:30:10 +01:00
f7542c988d Prevent payment request to be created when a wallet is not set up (#5620)
* Prevent payment request to be created when a wallet is not set up

* Created an extension method for store wallet checks

* fix for invoice and payment request selenium test

* refactoring payment request controller

* removing unused variable

* Unify behaviour across controllers

---------

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2024-01-11 16:25:56 +01:00
e90414bded Hide LN Balance when using internal node and not server admin (#5639)
* Hide LN Balance when using internal node and not server admin

* Minor updates

---------

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2024-01-06 08:46:19 +01:00
78882dcff0 Propose linking Greenfield API information within the Legacy API view (#5635)
* Propose linking Greenfield API information within the Legacy API view

* Propose linking Greenfield API information within the Legacy API view

* moved Greenfield API section up

* moved Greenfield API section up

* Fix link

* Wording

* Adjust button alignment

---------

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2024-01-03 11:33:54 +01:00
1ac1443070 README.md: .NET is not called "Core" anymore (#5636)
* README.md: .NET is not called "Core" anymore

Ever since version 5.x, the "Core" part of the name was removed.

* README.md: remove unneeded lang setting from URL
2024-01-02 12:29:40 +01:00
b5405e9313 Make tips and discount properties disabled in POS setting (#5619)
* Make tips and discount properties disabled in POS setting

* Update discount and tips boolean properties in model and swagger json

* update pos tests to cater for default tip and discount state

* Remove custom IDs and unify tests

---------

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2023-12-31 09:07:15 +01:00
c7eef01fd5 Removed what's new button and info #5608 (#5618) 2023-12-28 08:57:18 +01:00
26f61d35bb Bumping LND to 0.17.3-beta (#5614) 2023-12-25 00:27:46 -06:00
765776c429 Update .NET version in README.md (#5609)
Update the version of .NET requirement from 6.0 to 8.0 in README.md
2023-12-24 15:40:38 +01:00
9f54074d03 Startup: List configured networks in non-altcoin build warning (#5593) 2023-12-22 17:25:04 +09:00
f23078df1c Use buildx for creating and pushing docker images (#5592) 2023-12-22 14:23:04 +09:00
a35bf54a02 Changelog and bump 2023-12-22 14:21:12 +09:00
4867698ac9 AppService: Update inventory only for known app types (#5590)
There are apps, which do not have a template and hence no inventory. Accessing it via `settings[templatePath]!.Value` causes exceptions in those cases.
2023-12-22 14:21:01 +09:00
272 changed files with 6141 additions and 2769 deletions

View File

@ -31,79 +31,23 @@ jobs:
- run:
command: |
curl -X POST -H "Authorization: token $GH_PAT" -H "Accept: application/vnd.github.everest-preview+json" -H "Content-Type: application/json" https://api.github.com/repos/btcpayserver/btcpayserver-doc/dispatches --data '{"event_type": "build_docs"}'
# publish jobs require $DOCKERHUB_REPO, $DOCKERHUB_USER, $DOCKERHUB_PASS defined
amd64:
machine:
image: ubuntu-2004:202111-02
docker:
docker:
- image: cimg/base:stable
steps:
- setup_remote_docker
- checkout
- run:
command: |
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
GIT_COMMIT=$(git rev-parse HEAD)
#
sudo docker build --build-arg GIT_COMMIT=${GIT_COMMIT} --pull -t $DOCKERHUB_REPO:$LATEST_TAG-amd64 -f amd64.Dockerfile .
sudo docker build --build-arg GIT_COMMIT=${GIT_COMMIT} --pull --build-arg CONFIGURATION_NAME=Altcoins-Release -t $DOCKERHUB_REPO:$LATEST_TAG-altcoins-amd64 -f amd64.Dockerfile .
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-amd64
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-altcoins-amd64
arm32v7:
machine:
image: ubuntu-2004:202111-02
steps:
- checkout
- run:
command: |
sudo docker run --rm --privileged multiarch/qemu-user-static:register --reset
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
GIT_COMMIT=$(git rev-parse HEAD)
#
sudo docker build --build-arg GIT_COMMIT=${GIT_COMMIT} --pull -t $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 -f arm32v7.Dockerfile .
sudo docker build --build-arg GIT_COMMIT=${GIT_COMMIT} --pull --build-arg CONFIGURATION_NAME=Altcoins-Release -t $DOCKERHUB_REPO:$LATEST_TAG-altcoins-arm32v7 -f arm32v7.Dockerfile .
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-arm32v7
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-altcoins-arm32v7
arm64v8:
machine:
image: ubuntu-2004:202111-02
steps:
- checkout
- run:
command: |
sudo docker run --rm --privileged multiarch/qemu-user-static:register --reset
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
GIT_COMMIT=$(git rev-parse HEAD)
#
sudo docker build --build-arg GIT_COMMIT=${GIT_COMMIT} --pull -t $DOCKERHUB_REPO:$LATEST_TAG-arm64v8 -f arm64v8.Dockerfile .
sudo docker build --build-arg GIT_COMMIT=${GIT_COMMIT} --build-arg CONFIGURATION_NAME=Altcoins-Release --pull -t $DOCKERHUB_REPO:$LATEST_TAG-altcoins-arm64v8 -f arm64v8.Dockerfile .
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-arm64v8
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-altcoins-arm64v8
multiarch:
machine:
image: ubuntu-2004:202201-02
steps:
- run:
command: |
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
#
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
sudo docker manifest create --amend $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-amd64 $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 $DOCKERHUB_REPO:$LATEST_TAG-arm64v8
sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-amd64 --os linux --arch amd64
sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 --os linux --arch arm --variant v7
sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-arm64v8 --os linux --arch arm64 --variant v8
sudo docker manifest push $DOCKERHUB_REPO:$LATEST_TAG -p
sudo docker manifest create --amend $DOCKERHUB_REPO:$LATEST_TAG-altcoins $DOCKERHUB_REPO:$LATEST_TAG-altcoins-amd64 $DOCKERHUB_REPO:$LATEST_TAG-altcoins-arm32v7 $DOCKERHUB_REPO:$LATEST_TAG-altcoins-arm64v8
sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG-altcoins $DOCKERHUB_REPO:$LATEST_TAG-altcoins-amd64 --os linux --arch amd64
sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG-altcoins $DOCKERHUB_REPO:$LATEST_TAG-altcoins-arm32v7 --os linux --arch arm --variant v7
sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG-altcoins $DOCKERHUB_REPO:$LATEST_TAG-altcoins-arm64v8 --os linux --arch arm64 --variant v8
sudo docker manifest push $DOCKERHUB_REPO:$LATEST_TAG-altcoins -p
docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
docker buildx create --use
DOCKER_BUILDX_OPTS="--platform linux/amd64,linux/arm64,linux/arm/v7 --build-arg GIT_COMMIT=${GIT_COMMIT} --push"
docker buildx build $DOCKER_BUILDX_OPTS -t $DOCKERHUB_REPO:$LATEST_TAG .
docker buildx build $DOCKER_BUILDX_OPTS -t $DOCKERHUB_REPO:$LATEST_TAG-altcoins --build-arg CONFIGURATION_NAME=Altcoins-Release .
workflows:
version: 2
build_and_test:
@ -120,7 +64,7 @@ workflows:
# only act on version tags
tags:
only: /(v[1-9]+(\.[0-9]+)*(-[a-z0-9-]+)?)|(v[a-z0-9-]+)/
- amd64:
- docker:
filters:
# ignore any commit on any branch by default
branches:
@ -130,25 +74,3 @@ workflows:
# OR features on specific versions like v1.0.0.88-lndseedbackup-1
tags:
only: /(v[1-9]+(\.[0-9]+)*(-[a-z0-9-]+)?)|(v[a-z0-9-]+)/
- arm32v7:
filters:
branches:
ignore: /.*/
tags:
only: /(v[1-9]+(\.[0-9]+)*(-[a-z0-9-]+)?)|(v[a-z0-9-]+)/
- arm64v8:
filters:
branches:
ignore: /.*/
tags:
only: /(v[1-9]+(\.[0-9]+)*(-[a-z0-9-]+)?)|(v[a-z0-9-]+)/
- multiarch:
requires:
- amd64
- arm32v7
- arm64v8
filters:
branches:
ignore: /.*/
tags:
only: /(v[1-9]+(\.[0-9]+)*(-[a-z0-9-]+)?)|(v[a-z0-9-]+)/

1
.gitignore vendored
View File

@ -300,3 +300,4 @@ Plugins/packed
BTCPayServer/wwwroot/swagger/v1/openapi.json
BTCPayServer/appsettings.dev.json
BTCPayServer.Tests/monero_wallet
/BTCPayServer.Tests/NewBlocks.bat

View File

@ -31,9 +31,9 @@
<None Include="icon.png" Pack="true" PackagePath="\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="HtmlSanitizer" Version="8.0.723" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" />
<PackageReference Include="HtmlSanitizer" Version="8.0.838" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.1" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.0-beta.2" />
</ItemGroup>

View File

@ -101,6 +101,14 @@ namespace BTCPayServer.Abstractions.Extensions
return categoryAndPageMatch && idMatch ? ActivePageClass : null;
}
public static HtmlString ToBrowserDate(this DateTimeOffset date, string netFormat, string jsDateFormat = "short", string jsTimeFormat = "short")
{
var dateTime = date.ToString("o", CultureInfo.InvariantCulture);
var displayDate = date.ToString(netFormat, CultureInfo.InvariantCulture);
var tooltip = dateTime.Replace("T", " ");
return new HtmlString($"<time datetime=\"{dateTime}\" data-date-style=\"{jsDateFormat}\" data-time-style=\"{jsTimeFormat}\" data-initial=\"localized\" data-bs-toggle=\"tooltip\" data-bs-title=\"{tooltip}\">{displayDate}</time>");
}
public static HtmlString ToBrowserDate(this DateTimeOffset date, DateDisplayFormat format = DateDisplayFormat.Localized)
{
var relative = date.ToTimeAgo();

View File

@ -31,7 +31,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.5.1" />
<PackageReference Include="NBitcoin" Version="7.0.32" />
<PackageReference Include="NBitcoin" Version="7.0.34" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>

View File

@ -41,6 +41,14 @@ namespace BTCPayServer.Client
return response.IsSuccessStatusCode;
}
public virtual async Task<bool> ApproveUser(string idOrEmail, bool approved, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/users/{idOrEmail}/approve", null,
new ApproveUserRequest { Approved = approved }, HttpMethod.Post), token);
await HandleResponse(response);
return response.IsSuccessStatusCode;
}
public virtual async Task<ApplicationUserData[]> GetUsers(CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/users/", null, HttpMethod.Get), token);

View File

@ -25,6 +25,16 @@ namespace BTCPayServer.Client.Models
/// </summary>
public bool RequiresEmailConfirmation { get; set; }
/// <summary>
/// Whether the user was approved by an admin
/// </summary>
public bool Approved { get; set; }
/// <summary>
/// whether the user needed approval on account creation
/// </summary>
public bool RequiresApproval { get; set; }
/// <summary>
/// the roles of the user
/// </summary>

View File

@ -0,0 +1,6 @@
namespace BTCPayServer.Client;
public class ApproveUserRequest
{
public bool Approved { get; set; }
}

View File

@ -27,10 +27,10 @@ namespace BTCPayServer.Client.Models
[JsonConverter(typeof(StringEnumConverter))]
public PosViewType DefaultView { get; set; }
public bool ShowCustomAmount { get; set; } = false;
public bool ShowDiscount { get; set; } = true;
public bool ShowDiscount { get; set; } = false;
public bool ShowSearch { get; set; } = true;
public bool ShowCategories { get; set; } = true;
public bool EnableTips { get; set; } = true;
public bool EnableTips { get; set; } = false;
public string CustomAmountPayButtonText { get; set; } = null;
public string FixedAmountPayButtonText { get; set; } = null;
public string TipText { get; set; } = null;

View File

@ -7,11 +7,11 @@ namespace BTCPayServer.Client.Models
{
public class WebhookPayoutEvent : StoreWebhookEvent
{
public WebhookPayoutEvent(string evtType, string storeId)
public WebhookPayoutEvent(string type, string storeId)
{
if (!evtType.StartsWith("payout", StringComparison.InvariantCultureIgnoreCase))
throw new ArgumentException("Invalid event type", nameof(evtType));
Type = evtType;
if (!type.StartsWith("payout", StringComparison.InvariantCultureIgnoreCase))
throw new ArgumentException("Invalid event type", nameof(type));
Type = type;
StoreId = storeId;
}
@ -21,11 +21,11 @@ namespace BTCPayServer.Client.Models
}
public class WebhookPaymentRequestEvent : StoreWebhookEvent
{
public WebhookPaymentRequestEvent(string evtType, string storeId)
public WebhookPaymentRequestEvent(string type, string storeId)
{
if (!evtType.StartsWith("paymentrequest", StringComparison.InvariantCultureIgnoreCase))
throw new ArgumentException("Invalid event type", nameof(evtType));
Type = evtType;
if (!type.StartsWith("paymentrequest", StringComparison.InvariantCultureIgnoreCase))
throw new ArgumentException("Invalid event type", nameof(type));
Type = type;
StoreId = storeId;
}

View File

@ -31,18 +31,17 @@ namespace BTCPayServer
IEnumerable<BTCPayNetworkBase> networks,
SelectedChains selectedChains,
NBXplorerNetworkProvider nbxplorerNetworkProvider,
Logs logs,
IConfiguration configuration)
Logs logs)
{
var networksList = networks.ToList();
#if !ALTCOINS
var onlyBTC = networks.Count() == 1 && networks.First().IsBTC;
var onlyBTC = networksList.Count == 1 && networksList.First().IsBTC;
if (!onlyBTC)
throw new ConfigException($"This build of BTCPay Server does not support altcoins");
throw new ConfigException($"This build of BTCPay Server does not support altcoins. Configured networks: {string.Join(',', networksList.Select(n => n.CryptoCode).ToArray())}");
#endif
_NBXplorerNetworkProvider = nbxplorerNetworkProvider;
NetworkType = nbxplorerNetworkProvider.NetworkType;
foreach (var network in networks)
foreach (var network in networksList)
{
_Networks.Add(network.CryptoCode.ToUpperInvariant(), network);
}
@ -53,8 +52,7 @@ namespace BTCPayServer
throw new ConfigException($"Invalid chains \"{chain}\"");
}
logs.Configuration.LogInformation(
"Supported chains: " + String.Join(',', _Networks.Select(n => n.Key).ToArray()));
logs.Configuration.LogInformation("Supported chains: {Chains}", string.Join(',', _Networks.Select(n => n.Key).ToArray()));
}
public BTCPayNetwork BTC => GetNetwork<BTCPayNetwork>("BTC");

View File

@ -4,7 +4,7 @@
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="NBXplorer.Client" Version="4.2.6" />
<PackageReference Include="NBXplorer.Client" Version="4.3.0" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="2.0.1" />
</ItemGroup>
<ItemGroup Condition="'$(Altcoins)' != 'true'">

View File

@ -174,7 +174,6 @@ namespace BTCPayServer.Logging
logLevelColors = GetLogLevelConsoleColors(logLevel);
logLevelString = GetLogLevelString(logLevel);
// category and event id
var lenBefore = logBuilder.ToString().Length;
logBuilder.Append(_loglevelPadding);
logBuilder.Append(logName);
logBuilder.Append(": ");

View File

@ -3,11 +3,11 @@
<Import Project="../Build/Common.csproj" />
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0">
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BTCPayServer.Abstractions\BTCPayServer.Abstractions.csproj" />

View File

@ -11,6 +11,8 @@ namespace BTCPayServer.Data
public class ApplicationUser : IdentityUser, IHasBlob<UserBlob>
{
public bool RequiresEmailConfirmation { get; set; }
public bool RequiresApproval { get; set; }
public bool Approved { get; set; }
public List<StoredFile> StoredFiles { get; set; }
[Obsolete("U2F support has been replace with FIDO2")]
public List<U2FDevice> U2FDevices { get; set; }

View File

@ -40,7 +40,6 @@ namespace BTCPayServer.Data
{
return Severity switch
{
EventSeverity.Info => "info",
EventSeverity.Error => "danger",
EventSeverity.Success => "success",
EventSeverity.Warning => "warning",

View File

@ -0,0 +1,39 @@
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20240104155620_AddApprovalToApplicationUser")]
public partial class AddApprovalToApplicationUser : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "Approved",
table: "AspNetUsers",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "RequiresApproval",
table: "AspNetUsers",
nullable: false,
defaultValue: false);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Approved",
table: "AspNetUsers");
migrationBuilder.DropColumn(
name: "RequiresApproval",
table: "AspNetUsers");
}
}
}

View File

@ -0,0 +1,26 @@
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20240220000000_FixWalletObjectsWithEmptyWalletId")]
public partial class FixWalletObjectsWithEmptyWalletId : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
if (migrationBuilder.IsNpgsql())
{
migrationBuilder.Sql("DELETE FROM \"WalletObjects\" WHERE \"WalletId\"='';");
}
}
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

View File

@ -112,6 +112,9 @@ namespace BTCPayServer.Migrations
b.Property<int>("AccessFailedCount")
.HasColumnType("INTEGER");
b.Property<bool>("Approved")
.HasColumnType("INTEGER");
b.Property<byte[]>("Blob")
.HasColumnType("BLOB");
@ -158,6 +161,9 @@ namespace BTCPayServer.Migrations
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("INTEGER");
b.Property<bool>("RequiresApproval")
.HasColumnType("INTEGER");
b.Property<bool>("RequiresEmailConfirmation")
.HasColumnType("INTEGER");

View File

@ -4,9 +4,9 @@
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.7.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.9" />
<PackageReference Include="NBitcoin" Version="7.0.32" />
<PackageReference Include="NBitcoin" Version="7.0.34" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="1.0.4" />
</ItemGroup>

View File

@ -1,4 +1,4 @@
[
[
{
"name":"Afghan Afghani",
"code":"AFN",
@ -58,7 +58,7 @@
{
"name":"Argentine Peso",
"code":"ARS",
"divisibility":2,
"divisibility":0,
"symbol":null,
"crypto":false
},
@ -289,7 +289,7 @@
{
"name":"Colombian Peso",
"code":"COP",
"divisibility":2,
"divisibility":0,
"symbol":null,
"crypto":false
},

View File

@ -64,11 +64,28 @@ namespace BTCPayServer.Services.Rates
{
if (_CurrencyProviders.Count == 0)
{
foreach (var culture in CultureInfo.GetCultures(CultureTypes.AllCultures).Where(c => !c.IsNeutralCulture))
foreach (var culture in CultureInfo.GetCultures(CultureTypes.AllCultures))
{
// This avoid storms of exception throwing slowing up
// startup and debugging sessions
if (culture switch
{
{ LCID: 0x007F or 0x0000 or 0x0c00 or 0x1000 } => true,
{ IsNeutralCulture : true } => true,
_ => false
})
continue;
try
{
_CurrencyProviders.TryAdd(new RegionInfo(culture.LCID).ISOCurrencySymbol, culture);
var symbol = new RegionInfo(culture.LCID).ISOCurrencySymbol;
var c = symbol switch
{
// ARS and COP are officially 2 digits, but due to depreciation,
// nobody really use those anymore. (See https://github.com/btcpayserver/btcpayserver/issues/5708)
"ARS" or "COP" => ModifyCurrencyDecimalDigit(culture, 0),
_ => culture
};
_CurrencyProviders.TryAdd(symbol, c);
}
catch { }
}
@ -82,6 +99,15 @@ namespace BTCPayServer.Services.Rates
}
}
private CultureInfo ModifyCurrencyDecimalDigit(CultureInfo culture, int decimals)
{
var modifiedCulture = new CultureInfo(culture.Name);
NumberFormatInfo modifiedNumberFormat = (NumberFormatInfo)modifiedCulture.NumberFormat.Clone();
modifiedNumberFormat.CurrencyDecimalDigits = decimals;
modifiedCulture.NumberFormat = modifiedNumberFormat;
return modifiedCulture;
}
private void AddCurrency(Dictionary<string, IFormatProvider> currencyProviders, string code, int divisibility, string symbol)
{
var culture = new CultureInfo("en-US");

View File

@ -110,7 +110,7 @@ namespace BTCPayServer.Services.Rates
public void LoadState(BackgroundFetcherState state)
{
if (state.LastRequested is DateTimeOffset lastRequested)
if (state.LastRequested is DateTimeOffset)
this.LastRequested = state.LastRequested;
if (state.LastUpdated is DateTimeOffset updated && state.Rates is List<BackgroundFetcherRate> rates)
{

View File

@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Rating.Providers
{
public class BitnobRateProvider : IRateProvider
{
private readonly HttpClient _httpClient;
public BitnobRateProvider(HttpClient httpClient)
{
_httpClient = httpClient ?? new HttpClient();
}
public RateSourceInfo RateSourceInfo => new("bitnob", "Bitnob", "https://api.bitnob.co/api/v1/rates/bitcoin/price");
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{
using var response = await _httpClient.GetAsync("https://api.bitnob.co/api/v1/rates/bitcoin/price", cancellationToken);
JObject jobj = await response.Content.ReadAsAsync<JObject>(cancellationToken);
var dataObject = jobj["data"] as JObject;
if (dataObject == null)
{
return Array.Empty<PairRate>();
}
var pairRates = new List<PairRate>();
foreach (var property in dataObject.Properties())
{
string[] parts = property.Name.Split('_');
decimal value = property.Value.Value<decimal>();
pairRates.Add(new PairRate(new CurrencyPair("BTC", parts[1]), new BidAsk(value)));
}
return pairRates.ToArray();
}
}
}

View File

@ -8,6 +8,7 @@ using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Rating;
using ExchangeSharp;
using Microsoft.CodeAnalysis;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
@ -16,7 +17,7 @@ namespace BTCPayServer.Services.Rates
// Make sure that only one request is sent to kraken in general
public class KrakenExchangeRateProvider : IRateProvider
{
public RateSourceInfo RateSourceInfo => new("kraken", "Kraken", "https://api.kraken.com/0/public/Ticker?pair=ATOMETH,ATOMEUR,ATOMUSD,ATOMXBT,BATETH,BATEUR,BATUSD,BATXBT,BCHEUR,BCHUSD,BCHXBT,DAIEUR,DAIUSD,DAIUSDT,DASHEUR,DASHUSD,DASHXBT,EOSETH,EOSXBT,ETHCHF,ETHDAI,ETHUSDC,ETHUSDT,GNOETH,GNOXBT,ICXETH,ICXEUR,ICXUSD,ICXXBT,LINKETH,LINKEUR,LINKUSD,LINKXBT,LSKETH,LSKEUR,LSKUSD,LSKXBT,NANOETH,NANOEUR,NANOUSD,NANOXBT,OMGETH,OMGEUR,OMGUSD,OMGXBT,PAXGETH,PAXGEUR,PAXGUSD,PAXGXBT,SCETH,SCEUR,SCUSD,SCXBT,USDCEUR,USDCUSD,USDCUSDT,USDTCAD,USDTEUR,USDTGBP,USDTZUSD,WAVESETH,WAVESEUR,WAVESUSD,WAVESXBT,XBTCHF,XBTDAI,XBTUSDC,XBTUSDT,XDGEUR,XDGUSD,XETCXETH,XETCXXBT,XETCZEUR,XETCZUSD,XETHXXBT,XETHZCAD,XETHZEUR,XETHZGBP,XETHZJPY,XETHZUSD,XLTCXXBT,XLTCZEUR,XLTCZUSD,XMLNXETH,XMLNXXBT,XMLNZEUR,XMLNZUSD,XREPXETH,XREPXXBT,XREPZEUR,XXBTZCAD,XXBTZEUR,XXBTZGBP,XXBTZJPY,XXBTZUSD,XXDGXXBT,XXLMXXBT,XXMRXXBT,XXMRZEUR,XXMRZUSD,XXRPXXBT,XXRPZEUR,XXRPZUSD,XZECXXBT,XZECZEUR,XZECZUSD");
public RateSourceInfo RateSourceInfo => new("kraken", "Kraken", "https://api.kraken.com/0/public/Ticker");
public HttpClient HttpClient
{
get
@ -31,39 +32,6 @@ namespace BTCPayServer.Services.Rates
HttpClient _LocalClient;
static readonly HttpClient _Client = new HttpClient();
// ExchangeSymbolToGlobalSymbol throws exception which would kill perf
readonly ConcurrentDictionary<string, string> notFoundSymbols = new ConcurrentDictionary<string, string>(new Dictionary<string, string>()
{
{"ADAXBT","ADAXBT"},
{ "BSVUSD","BSVUSD"},
{ "QTUMEUR","QTUMEUR"},
{ "QTUMXBT","QTUMXBT"},
{ "EOSUSD","EOSUSD"},
{ "XTZUSD","XTZUSD"},
{ "XREPZUSD","XREPZUSD"},
{ "ADAEUR","ADAEUR"},
{ "ADAUSD","ADAUSD"},
{ "GNOEUR","GNOEUR"},
{ "XTZETH","XTZETH"},
{ "XXRPZJPY","XXRPZJPY"},
{ "XXRPZCAD","XXRPZCAD"},
{ "XTZEUR","XTZEUR"},
{ "QTUMETH","QTUMETH"},
{ "XXLMZUSD","XXLMZUSD"},
{ "QTUMCAD","QTUMCAD"},
{ "QTUMUSD","QTUMUSD"},
{ "XTZXBT","XTZXBT"},
{ "GNOUSD","GNOUSD"},
{ "ADAETH","ADAETH"},
{ "ADACAD","ADACAD"},
{ "XTZCAD","XTZCAD"},
{ "BSVEUR","BSVEUR"},
{ "XZECZJPY","XZECZJPY"},
{ "XXLMZEUR","XXLMZEUR"},
{"EOSEUR","EOSEUR"},
{"BSVXBT","BSVXBT"}
});
string[] _Symbols = Array.Empty<string>();
DateTimeOffset? _LastSymbolUpdate = null;
readonly Dictionary<string, string> _TickerMapping = new Dictionary<string, string>()
@ -76,48 +44,57 @@ namespace BTCPayServer.Services.Rates
{ "ZEUR", "EUR" },
{ "ZJPY", "JPY" },
{ "ZCAD", "CAD" },
{ "ZGBP", "GBP" }
{ "ZGBP", "GBP" },
{ "XXMR", "XMR" },
{ "XETH", "ETH" },
{ "USDC", "USDC" }, // On A=A purpose
{ "XZEC", "ZEC" },
{ "XLTC", "LTC" },
{ "XXRP", "XRP" },
};
string Normalize(string ticker)
{
_TickerMapping.TryGetValue(ticker, out var normalized);
return normalized ?? ticker;
}
readonly ConcurrentDictionary<string, CurrencyPair> CachedCurrencyPairs = new ConcurrentDictionary<string, CurrencyPair>();
private CurrencyPair GetCurrencyPair(string symbol)
{
if (CachedCurrencyPairs.TryGetValue(symbol, out var pair))
return pair;
var found = _TickerMapping.Where(t => symbol.StartsWith(t.Key, StringComparison.OrdinalIgnoreCase))
.Select(t => new { KrakenTicker = t.Key, PayTicker = t.Value }).FirstOrDefault();
if (found is not null)
{
pair = new CurrencyPair(found.PayTicker, Normalize(symbol.Substring(found.KrakenTicker.Length)));
}
if (pair is null)
{
found = _TickerMapping.Where(t => symbol.EndsWith(t.Key, StringComparison.OrdinalIgnoreCase))
.Select(t => new { KrakenTicker = t.Key, PayTicker = t.Value }).FirstOrDefault();
if (found is not null)
pair = new CurrencyPair(Normalize(symbol.Substring(0, symbol.Length - found.KrakenTicker.Length)), found.PayTicker);
}
if (pair is null)
CurrencyPair.TryParse(symbol, out pair);
CachedCurrencyPairs.TryAdd(symbol, pair);
return pair;
}
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{
var result = new List<PairRate>();
var symbols = await GetSymbolsAsync(cancellationToken);
var helper = (ExchangeKrakenAPI)await ExchangeAPI.GetExchangeAPIAsync<ExchangeKrakenAPI>();
var normalizedPairsList = symbols.Where(s => !notFoundSymbols.ContainsKey(s)).Select(s => helper.NormalizeMarketSymbol(s)).ToList();
var csvPairsList = string.Join(",", normalizedPairsList);
JToken apiTickers = await MakeJsonRequestAsync<JToken>("/0/public/Ticker", null, new Dictionary<string, object> { { "pair", csvPairsList } }, cancellationToken: cancellationToken);
var tickers = new List<KeyValuePair<string, ExchangeTicker>>();
JToken apiTickers = await MakeJsonRequestAsync<JToken>("/0/public/Ticker", null, null, cancellationToken: cancellationToken);
foreach (string symbol in symbols)
{
var ticker = ConvertToExchangeTicker(symbol, apiTickers[symbol]);
if (ticker != null)
{
try
{
string global = null;
var mapped1 = _TickerMapping.Where(t => symbol.StartsWith(t.Key, StringComparison.OrdinalIgnoreCase))
.Select(t => new { KrakenTicker = t.Key, PayTicker = t.Value }).SingleOrDefault();
if (mapped1 != null)
{
var p2 = symbol.Substring(mapped1.KrakenTicker.Length);
if (_TickerMapping.TryGetValue(p2, out var mapped2))
p2 = mapped2;
global = $"{mapped1.PayTicker}_{p2}";
}
else
{
global = await helper.ExchangeMarketSymbolToGlobalMarketSymbolAsync(symbol);
}
if (CurrencyPair.TryParse(global, out var pair))
result.Add(new PairRate(pair, new BidAsk(ticker.Bid, ticker.Ask)));
else
notFoundSymbols.TryAdd(symbol, symbol);
}
catch (ArgumentException)
{
notFoundSymbols.TryAdd(symbol, symbol);
}
var pair = GetCurrencyPair(symbol);
if (pair is not null)
result.Add(new PairRate(pair, new BidAsk(ticker.Bid, ticker.Ask)));
}
}
return result.ToArray();

View File

@ -45,7 +45,6 @@ namespace BTCPayServer.Services.Rates
var fetchingRates = new Dictionary<CurrencyPair, Task<RateResult>>();
var fetchingExchanges = new Dictionary<string, Task<QueryRateResult>>();
var consolidatedRates = new ExchangeRates();
foreach (var i in pairs.Select(p => (Pair: p, RateRule: rules.GetRuleFor(p))))
{

View File

@ -176,7 +176,7 @@ namespace BTCPayServer.Tests
#pragma warning disable CS0618 // Type or member is obsolete
.OfType<DerivationSchemeSettings>().First(o => o.PaymentId.IsBTCOnChain);
#pragma warning restore CS0618 // Type or member is obsolete
DerivationSchemeSettings.TryParseFromWalletFile(content, onchainBTC.Network, out var expected, out var error);
FastTests.GetParsers().TryParseWalletFile(content, onchainBTC.Network, out var expected, out var error);
Assert.Equal(expected.ToJson(), onchainBTC.ToJson());
Assert.Null(error);

View File

@ -24,9 +24,9 @@
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.15" />
<PackageReference Include="Selenium.Support" Version="4.1.1" />
<PackageReference Include="Selenium.WebDriver" Version="4.1.1" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="119.0.6045.10500" />
<PackageReference Include="xunit" Version="2.6.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.5">
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="121.0.6167.8500" />
<PackageReference Include="xunit" Version="2.6.6" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>

View File

@ -245,6 +245,9 @@ namespace BTCPayServer.Tests
rateProvider.Providers.Add("kraken", kraken);
}
// reset test server policies
var settings = GetService<SettingsRepository>();
await settings.UpdateSetting(new PoliciesSettings { LockSubscription = false, RequiresUserApproval = false });
TestLogs.LogInformation("Waiting site is operational...");
await WaitSiteIsOperational();

View File

@ -39,7 +39,6 @@ namespace BTCPayServer.Tests
var supportUrl = "https://support.satoshisteaks.com/{InvoiceId}/";
s.GoToStore();
s.Driver.FindElement(By.Id("StoreWebsite")).SendKeys(storeUrl);
s.Driver.FindElement(By.Id("StoreSupportUrl")).SendKeys(supportUrl);
s.Driver.FindElement(By.Id("Save")).Click();
Assert.Contains("Store successfully updated", s.FindAlertMessage().Text);
@ -47,6 +46,7 @@ namespace BTCPayServer.Tests
s.Driver.WaitForAndClick(By.Id("Presets"));
s.Driver.WaitForAndClick(By.Id("Presets_InStore"));
Assert.True(s.Driver.SetCheckbox(By.Id("ShowPayInWalletButton"), true));
s.Driver.FindElement(By.Id("SupportUrl")).SendKeys(supportUrl);
s.Driver.FindElement(By.Id("Save")).SendKeys(Keys.Enter);
Assert.Contains("Store successfully updated", s.FindAlertMessage().Text);
@ -60,13 +60,13 @@ namespace BTCPayServer.Tests
Assert.Contains("Bitcoin", s.Driver.FindElement(By.CssSelector(".payment-method.active")).Text);
Assert.Contains("LNURL", s.Driver.FindElement(By.CssSelector(".payment-method:nth-child(2)")).Text);
var qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
var address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
var clipboard = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
var payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
var copyAddress = s.Driver.FindElement(By.CssSelector("#Address_BTC .truncate-center-start")).Text;
Assert.Equal($"bitcoin:{address}", payUrl);
var address = s.Driver.FindElement(By.CssSelector("#Address_BTC .truncate-center-start")).Text;
Assert.StartsWith("bcrt", s.Driver.FindElement(By.CssSelector("#Address_BTC .truncate-center-start")).Text);
Assert.DoesNotContain("lightning=", payUrl);
Assert.Equal(address, copyAddress);
Assert.Equal($"bitcoin:{address}", payUrl);
Assert.Equal($"bitcoin:{address}", clipboard);
Assert.Equal($"bitcoin:{address.ToUpperInvariant()}", qrValue);
s.Driver.ElementDoesNotExist(By.Id("Lightning_BTC"));
@ -97,11 +97,11 @@ namespace BTCPayServer.Tests
Assert.Contains("Lightning", s.Driver.WaitForElement(By.CssSelector(".payment-method.active")).Text);
Assert.Contains("Bitcoin", s.Driver.WaitForElement(By.CssSelector(".payment-method")).Text);
qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
clipboard = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
copyAddress = s.Driver.FindElement(By.CssSelector("#Lightning_BTC_LightningLike .truncate-center-start")).Text;
address = s.Driver.FindElement(By.CssSelector("#Lightning_BTC_LightningLike .truncate-center-start")).Text;
Assert.Equal($"lightning:{address}", payUrl);
Assert.Equal(address, copyAddress);
Assert.Equal($"lightning:{address}", clipboard);
Assert.Equal($"lightning:{address.ToUpperInvariant()}", qrValue);
s.Driver.ElementDoesNotExist(By.Id("Address_BTC"));
@ -130,9 +130,12 @@ namespace BTCPayServer.Tests
expirySeconds.SendKeys("3");
s.Driver.FindElement(By.Id("Expire")).Click();
var paymentInfo = s.Driver.WaitForElement(By.Id("PaymentInfo"));
Assert.Contains("This invoice will expire in", paymentInfo.Text);
Assert.DoesNotContain("Please send", paymentInfo.Text);
TestUtils.Eventually(() =>
{
var paymentInfo = s.Driver.WaitForElement(By.Id("PaymentInfo"));
Assert.Contains("This invoice will expire in", paymentInfo.Text);
Assert.DoesNotContain("Please send", paymentInfo.Text);
});
TestUtils.Eventually(() =>
{
var expiredSection = s.Driver.FindElement(By.Id("unpaid"));
@ -140,7 +143,6 @@ namespace BTCPayServer.Tests
Assert.Contains("Invoice Expired", expiredSection.Text);
Assert.Contains("resubmit a payment", expiredSection.Text);
Assert.DoesNotContain("This invoice expired with partial payment", expiredSection.Text);
});
Assert.True(s.Driver.ElementDoesNotExist(By.Id("ContactLink")));
Assert.True(s.Driver.ElementDoesNotExist(By.Id("ReceiptLink")));
@ -153,7 +155,7 @@ namespace BTCPayServer.Tests
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
await Task.Delay(200);
address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
address = s.Driver.FindElement(By.CssSelector("#Address_BTC .truncate-center-start")).Text;
var amountFraction = "0.00001";
await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(address, Network.RegTest),
Money.Parse(amountFraction));
@ -164,9 +166,12 @@ namespace BTCPayServer.Tests
expirySeconds.SendKeys("3");
s.Driver.FindElement(By.Id("Expire")).Click();
paymentInfo = s.Driver.WaitForElement(By.Id("PaymentInfo"));
Assert.Contains("The invoice hasn't been paid in full.", paymentInfo.Text);
Assert.Contains("Please send", paymentInfo.Text);
TestUtils.Eventually(() =>
{
var paymentInfo = s.Driver.WaitForElement(By.Id("PaymentInfo"));
Assert.Contains("The invoice hasn't been paid in full.", paymentInfo.Text);
Assert.Contains("Please send", paymentInfo.Text);
});
TestUtils.Eventually(() =>
{
var expiredSection = s.Driver.FindElement(By.Id("unpaid"));
@ -202,16 +207,15 @@ namespace BTCPayServer.Tests
// Pay partial amount
await Task.Delay(200);
address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
amountFraction = "0.00001";
await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(address, Network.RegTest),
Money.Parse(amountFraction));
await s.Server.ExplorerNode.GenerateAsync(1);
s.Driver.FindElement(By.Id("test-payment-amount")).Clear();
s.Driver.FindElement(By.Id("test-payment-amount")).SendKeys("0.00001");
// Fake Pay
TestUtils.Eventually(() =>
{
paymentInfo = s.Driver.WaitForElement(By.Id("PaymentInfo"));
s.Driver.FindElement(By.Id("FakePayment")).Click();
s.Driver.FindElement(By.Id("mine-block")).Click();
var paymentInfo = s.Driver.WaitForElement(By.Id("PaymentInfo"));
Assert.Contains("The invoice hasn't been paid in full", paymentInfo.Text);
Assert.Contains("Please send", paymentInfo.Text);
});
@ -265,18 +269,19 @@ namespace BTCPayServer.Tests
Assert.Empty(s.Driver.FindElements(By.CssSelector(".payment-method")));
Assert.Contains("BTC", s.Driver.FindElement(By.Id("AmountDue")).Text);
qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
clipboard = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
var copyAddressOnchain = s.Driver.FindElement(By.CssSelector("#Address_BTC .truncate-center-start")).Text;
var copyAddressLightning = s.Driver.FindElement(By.CssSelector("#Lightning_BTC .truncate-center-start")).Text;
Assert.StartsWith($"bitcoin:{address}?amount=", payUrl);
Assert.StartsWith($"bitcoin:{copyAddressOnchain}?amount=", payUrl);
Assert.Contains("?amount=", payUrl);
Assert.Contains("&lightning=", payUrl);
Assert.StartsWith("bcrt", copyAddressOnchain);
Assert.Equal(address, copyAddressOnchain);
Assert.StartsWith("lnbcrt", copyAddressLightning);
Assert.StartsWith($"bitcoin:{address.ToUpperInvariant()}?amount=", qrValue);
Assert.StartsWith($"bitcoin:{copyAddressOnchain.ToUpperInvariant()}?amount=", qrValue);
Assert.Contains("&lightning=LNBCRT", qrValue);
Assert.Contains("&lightning=lnbcrt", clipboard);
Assert.Equal(clipboard, payUrl);
// Check details
s.Driver.ToggleCollapse("PaymentDetails");
@ -333,17 +338,18 @@ namespace BTCPayServer.Tests
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
Assert.Empty(s.Driver.FindElements(By.CssSelector(".payment-method")));
qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
clipboard = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
copyAddressOnchain = s.Driver.FindElement(By.CssSelector("#Address_BTC .truncate-center-start")).Text;
copyAddressLightning = s.Driver.FindElement(By.CssSelector("#Lightning_BTC .truncate-center-start")).Text;
Assert.StartsWith($"bitcoin:{address}", payUrl);
Assert.StartsWith($"bitcoin:{copyAddressOnchain}", payUrl);
Assert.Contains("?lightning=lnurl", payUrl);
Assert.DoesNotContain("amount=", payUrl);
Assert.StartsWith("bcrt", copyAddressOnchain);
Assert.Equal(address, copyAddressOnchain);
Assert.StartsWith("lnurl", copyAddressLightning);
Assert.StartsWith($"bitcoin:{address.ToUpperInvariant()}?lightning=LNURL", qrValue);
Assert.StartsWith($"bitcoin:{copyAddressOnchain.ToUpperInvariant()}?lightning=LNURL", qrValue);
Assert.Contains($"bitcoin:{copyAddressOnchain}?lightning=lnurl", clipboard);
Assert.Equal(clipboard, payUrl);
// Check details
s.Driver.ToggleCollapse("PaymentDetails");
@ -358,11 +364,13 @@ namespace BTCPayServer.Tests
expirySeconds.Clear();
expirySeconds.SendKeys("5");
s.Driver.FindElement(By.Id("Expire")).Click();
paymentInfo = s.Driver.WaitForElement(By.Id("PaymentInfo"));
Assert.Contains("This invoice will expire in", paymentInfo.Text);
Assert.Contains("00:0", paymentInfo.Text);
Assert.DoesNotContain("Please send", paymentInfo.Text);
TestUtils.Eventually(() =>
{
var paymentInfo = s.Driver.WaitForElement(By.Id("PaymentInfo"));
Assert.Contains("This invoice will expire in", paymentInfo.Text);
Assert.Contains("00:0", paymentInfo.Text);
Assert.DoesNotContain("Please send", paymentInfo.Text);
});
// Configure countdown timer
s.GoToHome();
@ -378,7 +386,7 @@ namespace BTCPayServer.Tests
s.GoToInvoiceCheckout(invoiceId);
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
paymentInfo = s.Driver.FindElement(By.Id("PaymentInfo"));
var paymentInfo = s.Driver.FindElement(By.Id("PaymentInfo"));
Assert.False(paymentInfo.Displayed);
Assert.DoesNotContain("This invoice will expire in", paymentInfo.Text);
@ -386,11 +394,13 @@ namespace BTCPayServer.Tests
expirySeconds.Clear();
expirySeconds.SendKeys("599");
s.Driver.FindElement(By.Id("Expire")).Click();
paymentInfo = s.Driver.WaitForElement(By.Id("PaymentInfo"));
Assert.True(paymentInfo.Displayed);
Assert.Contains("This invoice will expire in", paymentInfo.Text);
Assert.Contains("09:5", paymentInfo.Text);
TestUtils.Eventually(() =>
{
paymentInfo = s.Driver.WaitForElement(By.Id("PaymentInfo"));
Assert.True(paymentInfo.Displayed);
Assert.Contains("This invoice will expire in", paymentInfo.Text);
Assert.Contains("09:5", paymentInfo.Text);
});
// Disable LNURL again
s.GoToHome();
@ -456,13 +466,12 @@ namespace BTCPayServer.Tests
.GetPaymentMethodDetails().GetPaymentDestination(), Network.RegTest),
new Money(0.001m, MoneyUnit.BTC));
IWebElement closebutton = null;
TestUtils.Eventually(() =>
{
closebutton = iframe.FindElement(By.Id("close"));
Assert.True(closebutton.Displayed);
var closeButton = iframe.FindElement(By.Id("close"));
Assert.True(closeButton.Displayed);
closeButton.Click();
});
closebutton.Click();
s.Driver.AssertElementNotFound(By.Name("btcpay"));
Assert.Equal(s.Driver.Url,
new Uri(s.ServerUri, $"tests/index.html?invoice={invoiceId}").ToString());

View File

@ -1,15 +1,19 @@
using System;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Form;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Forms;
using BTCPayServer.Forms.Models;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Plugins.Crowdfund;
using BTCPayServer.Plugins.Crowdfund.Controllers;
using BTCPayServer.Plugins.Crowdfund.Models;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBitpayClient;
@ -303,5 +307,114 @@ namespace BTCPayServer.Tests
Assert.Equal(0.7m, model.Info.CurrentPendingAmount);
});
}
[Fact(Timeout = LongRunningTestTimeout)]
[Trait("Integration", "Integration")]
public async Task CrowdfundWithFormNoPerk()
{
using var tester = CreateServerTester();
await tester.StartAsync();
var user = tester.NewAccount();
await user.GrantAccessAsync();
user.RegisterDerivationScheme("BTC");
await user.SetNetworkFeeMode(NetworkFeeMode.Never);
var frmService = tester.PayTester.GetService<FormDataService>();
var appService = tester.PayTester.GetService<AppService>();
var crowdfund = user.GetController<UICrowdfundController>();
var apps = user.GetController<UIAppsController>();
var appData = new AppData { StoreDataId = user.StoreId, Name = "test", AppType = CrowdfundAppType.AppType };
await appService.UpdateOrCreateApp(appData);
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
var app = appList.Apps[0];
apps.HttpContext.SetAppData(appData);
crowdfund.HttpContext.SetAppData(appData);
var form = new Form
{
Fields =
[
Field.Create("Enter your email", "item1", "test@toto.com", true, null, "email"),
Field.Create("Name", "item2", 2.ToString(), true, null),
Field.Create("Item3", "invoice_item3", 3.ToString(), true, null)
]
};
var frmData = new FormData
{
StoreId = user.StoreId,
Name = "frmTest",
Config = form.ToString()
};
await frmService.AddOrUpdateForm(frmData);
var lstForms = await frmService.GetForms(user.StoreId);
Assert.NotEmpty(lstForms);
var crowdfundViewModel = await crowdfund.UpdateCrowdfund(app.Id).AssertViewModelAsync<UpdateCrowdfundViewModel>();
crowdfundViewModel.FormId = lstForms[0].Id;
crowdfundViewModel.TargetCurrency = "BTC";
crowdfundViewModel.Enabled = true;
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
var vm2 = await crowdfund.CrowdfundForm(app.Id, (decimal?)0.01).AssertViewModelAsync<FormViewModel>();
var res = await crowdfund.CrowdfundFormSubmit(app.Id, (decimal)0.01, "", vm2);
Assert.IsNotType<NotFoundObjectResult>(res);
Assert.IsNotType<BadRequest>(res);
}
[Fact(Timeout = LongRunningTestTimeout)]
[Trait("Integration", "Integration")]
public async Task CrowdfundWithFormAndPerk()
{
using var tester = CreateServerTester();
await tester.StartAsync();
var user = tester.NewAccount();
await user.GrantAccessAsync();
user.RegisterDerivationScheme("BTC");
await user.SetNetworkFeeMode(NetworkFeeMode.Never);
var frmService = tester.PayTester.GetService<FormDataService>();
var appService = tester.PayTester.GetService<AppService>();
var crowdfund = user.GetController<UICrowdfundController>();
var apps = user.GetController<UIAppsController>();
var appData = new AppData { StoreDataId = user.StoreId, Name = "test", AppType = CrowdfundAppType.AppType };
await appService.UpdateOrCreateApp(appData);
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
var app = appList.Apps[0];
apps.HttpContext.SetAppData(appData);
crowdfund.HttpContext.SetAppData(appData);
var form = new Form
{
Fields =
[
Field.Create("Enter your email", "item1", "test@toto.com", true, null, "email"),
Field.Create("Name", "item2", 2.ToString(), true, null),
Field.Create("Item3", "invoice_item3", 3.ToString(), true, null)
]
};
var frmData = new FormData
{
StoreId = user.StoreId,
Name = "frmTest",
Config = form.ToString()
};
await frmService.AddOrUpdateForm(frmData);
var lstForms = await frmService.GetForms(user.StoreId);
Assert.NotEmpty(lstForms);
var crowdfundViewModel = await crowdfund.UpdateCrowdfund(app.Id).AssertViewModelAsync<UpdateCrowdfundViewModel>();
crowdfundViewModel.FormId = lstForms[0].Id;
crowdfundViewModel.TargetCurrency = "BTC";
crowdfundViewModel.Enabled = true;
crowdfundViewModel.PerksTemplate = "[{\"id\": \"xxx\",\"title\": \"Perk 1\",\"priceType\": \"Fixed\",\"price\": \"0.001\",\"image\": \"\",\"description\": \"\",\"categories\": [],\"disabled\": false}]";
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
var vm2 = await crowdfund.CrowdfundForm(app.Id, (decimal?)0.01, "xxx").AssertViewModelAsync<FormViewModel>();
var res = await crowdfund.CrowdfundFormSubmit(app.Id, (decimal)0.01, "xxx", vm2);
Assert.IsNotType<NotFoundObjectResult>(res);
Assert.IsNotType<BadRequest>(res);
}
}
}

View File

@ -1,4 +1,4 @@
FROM mcr.microsoft.com/dotnet/sdk:8.0.100-bookworm-slim AS builder
FROM mcr.microsoft.com/dotnet/sdk:8.0.101-bookworm-slim AS builder
RUN apt-get update && apt-get install -y --no-install-recommends chromium-driver \
&& rm -rf /var/lib/apt/lists/*

View File

@ -28,6 +28,7 @@ using BTCPayServer.Plugins.Bitcoin;
using BTCPayServer.Rating;
using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Fees;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Labels;
using BTCPayServer.Services.Rates;
@ -46,6 +47,7 @@ using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OpenQA.Selenium.DevTools.V100.DOMSnapshot;
using Xunit;
using Xunit.Abstractions;
using StoreData = BTCPayServer.Data.StoreData;
@ -159,6 +161,43 @@ namespace BTCPayServer.Tests
Assert.Equal("Test", data.FromAsset);
}
[Fact]
public void CanInterpolateOrBound()
{
var testData = new ((int Blocks, decimal Fee)[] Data, int Target, decimal Expected) []
{
([(0, 0m), (10, 100m)], 5, 50m),
([(50, 0m), (100, 100m)], 5, 0.0m),
([(50, 0m), (100, 100m)], 101, 100.0m),
([(50, 100m), (50, 100m)], 101, 100.0m),
([(50, 0m), (100, 100m)], 75, 50m),
([(0, 0m), (50, 50m), (100, 100m)], 75, 75m),
([(0, 0m), (500, 50m), (1000, 100m)], 750, 75m),
([(0, 0m), (500, 50m), (1000, 100m)], 100, 10m),
([(0, 0m), (100, 100m)], 80, 80m),
([(0, 0m), (100, 100m)], 25, 25m),
([(0, 0m), (25, 25m), (50, 50m), (100, 100m), (110, 120m)], 75, 75m),
([(0, 0m), (25, 0m), (50, 50m), (100, 100m), (110, 0m)], 75, 75m),
([(0, 0m), (25, 0m), (50, 50m), (100, 100m), (110, 0m)], 50, 50m),
([(0, 0m), (25, 0m), (50, 50m), (100, 100m), (110, 0m)], 100, 100m),
([(0, 0m), (25, 0m), (50, 50m), (100, 100m), (110, 0m)], 102, 80m),
};
foreach (var t in testData)
{
var actual = MempoolSpaceFeeProvider.InterpolateOrBound(t.Data.Select(t => new MempoolSpaceFeeProvider.BlockFeeRate(t.Blocks, new FeeRate(t.Fee))).ToArray(), t.Target);
Assert.Equal(new FeeRate(t.Expected), actual);
}
}
[Fact]
public void CanRandomizeByPercentage()
{
var generated = Enumerable.Range(0, 1000).Select(_ => MempoolSpaceFeeProvider.RandomizeByPercentage(100.0m, 10.0m)).ToArray();
Assert.Empty(generated.Where(g => g < 90m));
Assert.Empty(generated.Where(g => g > 110m));
Assert.NotEmpty(generated.Where(g => g < 91m));
Assert.NotEmpty(generated.Where(g => g > 109m));
}
private void CanParseDecimalsCore(string str, decimal expected)
{
var d = JsonConvert.DeserializeObject<LedgerEntryData>(str);
@ -649,11 +688,6 @@ namespace BTCPayServer.Tests
public void CanAcceptInvoiceWithTolerance()
{
var networkProvider = CreateNetworkProvider(ChainName.Regtest);
var paymentMethodHandlerDictionary = new PaymentMethodHandlerDictionary(new IPaymentMethodHandler[]
{
new BitcoinLikePaymentHandler(null, networkProvider, null, null, null, null),
new LightningLikePaymentHandler(null, null, networkProvider, null, null, null),
});
var entity = new InvoiceEntity();
entity.Networks = networkProvider;
#pragma warning disable CS0618
@ -756,13 +790,15 @@ namespace BTCPayServer.Tests
(0.0005m, "0.0005 USD", "USD"), (0.001m, "0.001 USD", "USD"), (0.01m, "0.01 USD", "USD"),
(0.1m, "0.10 USD", "USD"), (0.1m, "0,10 EUR", "EUR"), (1000m, "1,000 JPY", "JPY"),
(1000.0001m, "1,000.00 INR", "INR"),
(0.0m, "0.00 USD", "USD")
(0.0m, "0.00 USD", "USD"), (1m, "1 COP", "COP"), (1m, "1 ARS", "ARS")
})
{
var actual = displayFormatter.Currency(test.Item1, test.Item3);
actual = actual.Replace("¥", "¥"); // Hack so JPY test pass on linux as well
Assert.Equal(test.Item2, actual);
}
Assert.Equal(0, CurrencyNameTable.Instance.GetNumberFormatInfo("ARS").CurrencyDecimalDigits);
Assert.Equal(0, CurrencyNameTable.Instance.GetNumberFormatInfo("COP").CurrencyDecimalDigits);
}
[Fact]
@ -882,6 +918,14 @@ namespace BTCPayServer.Tests
Assert.Equal(TradeQuantity.Parse(qty2.ToString()), TradeQuantity.Parse(" 1.3 "));
}
public static WalletFileParsers GetParsers()
{
var service = new ServiceCollection();
BTCPayServerServices.AddOnchainWalletParsers(service);
return service.BuildServiceProvider().GetRequiredService<WalletFileParsers>();
}
[Fact]
public void ParseDerivationSchemeSettings()
{
@ -890,13 +934,14 @@ namespace BTCPayServer.Tests
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();
var parsers = GetParsers();
// xpub
var tpub = "tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS";
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(tpub, testnet, out var settings, out var error));
Assert.Null(error);
Assert.True(parsers.TryParseWalletFile(tpub, testnet, out var settings, out var error));
Assert.True(settings.AccountDerivation is DirectDerivationStrategy { Segwit: false });
Assert.Equal($"{tpub}-[legacy]", ((DirectDerivationStrategy)settings.AccountDerivation).ToString());
Assert.Equal("GenericFile", settings.Source);
Assert.Null(error);
// xpub with fingerprint and account
tpub = "tpubDCXK98mNrPWuoWweaoUkqwxQF5NMWpQLy7n7XJgDCpwYfoZRXGafPaVM7mYqD7UKhsbMxkN864JY2PniMkt1Uk4dNuAMnWFVqdquyvZNyca";
@ -904,16 +949,18 @@ namespace BTCPayServer.Tests
var fingerprint = "e5746fd9";
var account = "84'/1'/0'";
var str = $"[{fingerprint}/{account}]{vpub}";
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(str, testnet, out settings, out error));
Assert.True(parsers.TryParseWalletFile(str, testnet, out settings, out error));
Assert.Null(error);
Assert.True(settings.AccountDerivation is DirectDerivationStrategy { Segwit: true });
Assert.Equal(vpub, settings.AccountOriginal);
Assert.Equal(tpub, ((DirectDerivationStrategy)settings.AccountDerivation).ToString());
Assert.Equal(HDFingerprint.TryParse(fingerprint, out var hd) ? hd : default, settings.AccountKeySettings[0].RootFingerprint);
Assert.Equal(account, settings.AccountKeySettings[0].AccountKeyPath.ToString());
Assert.Equal("GenericFile", settings.Source);
Assert.Null(error);
// ColdCard
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
Assert.True(parsers.TryParseWalletFile(
"{\"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 settings, out error));
Assert.Null(error);
@ -927,78 +974,169 @@ 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);
Assert.Equal("ElectrumFile", settings.Source);
Assert.Null(error);
// Should be legacy
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
Assert.True(parsers.TryParseWalletFile(
"{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"tpubDDWYqT3P24znfsaGX7kZcQhNc5LAjnQiKQvUCHF2jS6dsgJBRtymopEU5uGpMaR5YChjuiExZG1X2aTbqXkp82KqH5qnqwWHp6EWis9ZvKr\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/44'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}",
testnet, out settings, out error));
Assert.True(settings.AccountDerivation is DirectDerivationStrategy { Segwit: false });
Assert.Equal("ElectrumFile", settings.Source);
Assert.Null(error);
// Should be segwit p2sh
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
Assert.True(parsers.TryParseWalletFile(
"{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"upub5DSddA9NoRUyJrQ4p86nsCiTSY7kLHrSxx3joEJXjHd4HPARhdXUATuk585FdWPVC2GdjsMePHb6BMDmf7c6KG4K4RPX6LVqBLtDcWpQJmh\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}",
testnet, out settings, out error));
Assert.True(settings.AccountDerivation is P2SHDerivationStrategy { Inner: DirectDerivationStrategy { Segwit: true } });
Assert.Equal("ElectrumFile", settings.Source);
Assert.Null(error);
// Should be segwit
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
Assert.True(parsers.TryParseWalletFile(
"{\"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, out error));
Assert.True(settings.AccountDerivation is DirectDerivationStrategy { Segwit: true });
Assert.Equal("ElectrumFile", settings.Source);
Assert.Null(error);
// Specter
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
Assert.True(parsers.TryParseWalletFile(
"{\"label\": \"Specter\", \"blockheight\": 123456, \"descriptor\": \"wpkh([8bafd160/49h/0h/0h]xpub661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw/0/*)#9x4vkw48\"}",
mainnet, out var specter, out error));
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.True(specter.AccountDerivation is DirectDerivationStrategy { Segwit: true });
Assert.Equal("Specter", specter.Label);
Assert.Null(error);
//BSMS BIP129, Nunchuk
// Wasabi
var wasabiJson = @"{""EncryptedSecret"": ""6PYNUAZZLS1ShkhHhm9ayiNwXPAPLN669fN5mY2WbGm1Hqc88tomqWXabU"",""ChainCode"": ""UoHIB+2mDbZSowo11TfDQbsYK6q1DrZ2H2yqQBxu6m8="",""MasterFingerprint"": ""0f215605"",""ExtPubKey"": ""xpub6DUXFa6fMrFpg7x4nEd8jBU6xDN3vkSXsVUrSbUB2dadbYaPE31czwVdv146JRStGsc2U6TywdKnGoVcP8Rtp2AZQyzXxQb7HrgmR9LrqLA"",""TaprootExtPubKey"": ""xpub6D2thLU5KwUk3axkJu1UT3yKFshCGU7TMuxhPgZMd91VvrcDwHdRwdzLk61cSHtZC6BkaipPgfFwjoDBY4m1WxyznxZLukYgM4dC6iRJVf8"",""SkipSynchronization"": true,""UseTurboSync"": true,""MinGapLimit"": 21,""AccountKeyPath"": ""84'/0'/0'"",""TaprootAccountKeyPath"": ""86'/0'/0'"",""BlockchainState"": {""Network"": ""Main"",""Height"": ""503723"",""TurboSyncHeight"": ""503723""},""PreferPsbtWorkflow"": false,""AutoCoinJoin"": true,""PlebStopThreshold"": ""0.01"",""AnonScoreTarget"": 5,""FeeRateMedianTimeFrameHours"": 0,""IsCoinjoinProfileSelected"": true,""RedCoinIsolation"": false,""ExcludedCoinsFromCoinJoin"": [],""HdPubKeys"": [{""PubKey"": ""03f88b9c3e16e40a5a9eaf8b36b9bcee7bbc93fd9eea640b541efb931ac55f7ff5"",""FullKeyPath"": ""84'/0'/0'/1/0"",""Label"": """",""KeyState"": 0},{""PubKey"": ""03e5241fc28aa556d7cb826b9a9f5ecee85287e7476746126263574a5e27fbf569"",""FullKeyPath"": ""84'/0'/0'/0/0"",""Label"": """",""KeyState"": 0}]}";
Assert.True(parsers.TryParseWalletFile(wasabiJson, mainnet, out var wasabi, out error));
Assert.Null(error);
Assert.Equal("WasabiFile", wasabi.Source);
Assert.Single(wasabi.AccountKeySettings);
Assert.Equal("84'/0'/0'", wasabi.AccountKeySettings[0].AccountKeyPath.ToString());
Assert.Equal("0f215605", wasabi.AccountKeySettings[0].RootFingerprint.ToString());
Assert.True(wasabi.AccountDerivation is DirectDerivationStrategy { Segwit: true });
// BSMS BIP129, Nunchuk
var bsms = @"BSMS 1.0
wsh(sortedmulti(1,[5c9e228d/48'/0'/0'/2']xpub6EgGHjcvovyN3nK921zAGPfuB41cJXkYRdt3tLGmiMyvbgHpss4X1eRZwShbEBb1znz2e2bCkCED87QZpin3sSYKbmCzQ9Sc7LaV98ngdeX/**,[2b0e251e/48'/0'/0'/2']xpub6DrimHB8KUSkPvmJ8Pk8RE769EdDm2VEoZ8MBz76w9QupP8Py4wexs4Pa3aRB1LUEhc9GyY6ypDWEFFRCgqeDQePcyWQfjtmintrehq3JCL/**))
/0/*,/1/*
bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
";
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(bsms,
Assert.True(parsers.TryParseWalletFile(bsms,
mainnet, out var nunchuk, out error));
Assert.Equal(2, nunchuk.AccountKeySettings.Length);
Assert.Equal(2, nunchuk.AccountKeySettings.Length);
//check that the account key settings match those in bsms string
Assert.Equal("5c9e228d", nunchuk.AccountKeySettings[0].RootFingerprint.ToString());
Assert.Equal("48'/0'/0'/2'", nunchuk.AccountKeySettings[0].AccountKeyPath.ToString());
Assert.Equal("2b0e251e", nunchuk.AccountKeySettings[1].RootFingerprint.ToString());
Assert.Equal("2b0e251e", nunchuk.AccountKeySettings[1].RootFingerprint.ToString());
Assert.Equal("48'/0'/0'/2'", nunchuk.AccountKeySettings[1].AccountKeyPath.ToString());
var multsig = Assert.IsType < MultisigDerivationStrategy >
var multsig = Assert.IsType<MultisigDerivationStrategy>
(Assert.IsType<P2WSHDerivationStrategy>(nunchuk.AccountDerivation).Inner);
Assert.True(multsig.LexicographicOrder);
Assert.Equal(1, multsig.RequiredSignatures);
var deposit = new NBXplorer.KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit);
var line =nunchuk.AccountDerivation.GetLineFor(deposit).Derive(0);
Assert.Equal(BitcoinAddress.Create("bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku", Network.Main).ScriptPubKey,
line.ScriptPubKey);
Assert.Equal(1, multsig.RequiredSignatures);
var deposit = new NBXplorer.KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit);
var line = nunchuk.AccountDerivation.GetLineFor(deposit).Derive(0);
Assert.Equal(BitcoinAddress.Create("bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku", Network.Main).ScriptPubKey,
line.ScriptPubKey);
Assert.Equal("BSMS", nunchuk.Source);
Assert.Null(error);
// Failure case
Assert.False(DerivationSchemeSettings.TryParseFromWalletFile(
Assert.False(parsers.TryParseWalletFile(
"{\"keystore\": {\"ckcc_xpub\": \"tpubFailure\", \"xpub\": \"tpubFailure\", \"label\": \"Failure\"}, \"wallet_type\": \"standard\"}",
testnet, out settings, out error));
Assert.Null(settings);
Assert.NotNull(error);
//passport
var passportText =
"{\"Source\": \"Passport\", \"Descriptor\": \"tr([5c9e228d/86'/0'/0']xpub6EgGHjcvovyN3nK921zAGPfuB41cJXkYRdt3tLGmiMyvbgHpss4X1eRZwShbEBb1znz2e2bCkCED87QZpin3sSYKbmCzQ9Sc7LaV98ngdeX/0/*)\", \"FirmwareVersion\": \"v1.0.0\"}";
Assert.True(parsers.TryParseWalletFile(passportText, mainnet, out var passport, out error));
Assert.Equal("Passport", passport.Source);
Assert.True(passport.AccountDerivation is TaprootDerivationStrategy);
Assert.Equal("5c9e228d", passport.AccountKeySettings[0].RootFingerprint.ToString());
Assert.Equal("86'/0'/0'", passport.AccountKeySettings[0].AccountKeyPath.ToString());
//electrum
var electrumText =
"""
{
"keystore": {
"xpub": "vpub5Z14bnDNoEQeFdwZYSpVHcpzRpH99CnvSemzqTAvhjcgBTzPUVnaA5GhjgZc9J46duUprxQRUVUuqchazanXD6bLuVyarviNHBFUu6fBZNj",
"xprv": "vprv9ENJcv8RKwqMTqyhLSuBz5bEV7hpdZjisjUBuV9K8azz1vpop6xJFEDRdfDwgWBpYgUUhEVxdvpxgV3f8NircysfebnBaPu5y2dcnSDAEEw",
"type": "bip32",
"pw_hash_version": 1
},
"wallet_type": "standard",
"use_encryption": false,
"seed_type": "bip39"
}
""";
Assert.True(parsers.TryParseWalletFile(electrumText, testnet, out var electrum, out _));
Assert.Equal("ElectrumFile", electrum.Source);
electrumText =
"""
{
"keystore": {
"derivation": "m/0h",
"pw_hash_version": 1,
"root_fingerprint": "fbb5b37d",
"seed": "tiger room acoustic bracket thing film umbrella rather pepper tired vault remain",
"seed_type": "segwit",
"type": "bip32",
"xprv": "zprvAaQyp6mTAX53zY4j2BbecRNtmTq2kSEKgy2y4yK3bFPKgPJLxrMmPxzZdRkWq5XvmtH2R4ko5YmJYH2MgnVkWr32pHi4Dc5627WyML32KTW",
"xpub": "zpub6oQLDcJLztdMD29C8D8eyZKdKVfX9txB4BxZsMif9avJZBdVWPg1wmK3Uh3VxU7KXon1wm1xzvjyqmKWguYMqyjKP5f5Cho9f7uLfmRt2Br"
},
"wallet_type": "standard",
"use_encryption": false,
"seed_type": "bip39"
}
""";
Assert.True(parsers.TryParseWalletFile(electrumText, mainnet, out electrum, out _));
Assert.Equal("ElectrumFile", electrum.Source);
Assert.Equal("0'", electrum.GetSigningAccountKeySettings().AccountKeyPath.ToString());
Assert.True(electrum.AccountDerivation is DirectDerivationStrategy { Segwit: true });
Assert.Equal("fbb5b37d", electrum.GetSigningAccountKeySettings().RootFingerprint.ToString());
Assert.Equal("zpub6oQLDcJLztdMD29C8D8eyZKdKVfX9txB4BxZsMif9avJZBdVWPg1wmK3Uh3VxU7KXon1wm1xzvjyqmKWguYMqyjKP5f5Cho9f7uLfmRt2Br", electrum.AccountOriginal);
Assert.Equal(((DirectDerivationStrategy)electrum.AccountDerivation).GetExtPubKeys().First().ParentFingerprint.ToString(), electrum.GetSigningAccountKeySettings().RootFingerprint.ToString());
// Electrum with strange garbage at the end caused by the lightning support
electrumText =
"""
{
"keystore": {
"derivation": "m/0h",
"pw_hash_version": 1,
"root_fingerprint": "fbb5b37d",
"seed": "tiger room acoustic bracket thing film umbrella rather pepper tired vault remain",
"seed_type": "segwit",
"type": "bip32",
"xprv": "zprvAaQyp6mTAX53zY4j2BbecRNtmTq2kSEKgy2y4yK3bFPKgPJLxrMmPxzZdRkWq5XvmtH2R4ko5YmJYH2MgnVkWr32pHi4Dc5627WyML32KTW",
"xpub": "zpub6oQLDcJLztdMD29C8D8eyZKdKVfX9txB4BxZsMif9avJZBdVWPg1wmK3Uh3VxU7KXon1wm1xzvjyqmKWguYMqyjKP5f5Cho9f7uLfmRt2Br"
},
"wallet_type": "standard",
"use_encryption": false,
"seed_type": "bip39"
},
{"op": "remove", "path": "/channels"}
""";
Assert.True(parsers.TryParseWalletFile(electrumText, mainnet, out electrum, out _));
}
[Fact]
@ -1601,7 +1739,7 @@ Assert.Equal("2b0e251e", nunchuk.AccountKeySettings[1].RootFingerprint.ToString(
{
var b = JsonConvert.DeserializeObject<PullPaymentBlob>("{}");
Assert.Equal(TimeSpan.FromDays(30.0), b.BOLT11Expiration);
var aaa = JsonConvert.SerializeObject(b);
JsonConvert.SerializeObject(b);
}
[Fact]
@ -2110,7 +2248,7 @@ Assert.Equal("2b0e251e", nunchuk.AccountKeySettings[1].RootFingerprint.ToString(
[Fact]
public void AllPoliciesShowInUI()
{
var a = new BitpayRateProvider(new System.Net.Http.HttpClient()).GetRatesAsync(default).Result;
new BitpayRateProvider(new System.Net.Http.HttpClient()).GetRatesAsync(default).GetAwaiter().GetResult();
foreach (var policy in Policies.AllPolicies)
{
Assert.True(UIManageController.AddApiKeyViewModel.PermissionValueItem.PermissionDescriptions.ContainsKey(policy));
@ -2157,7 +2295,7 @@ Assert.Equal("2b0e251e", nunchuk.AccountKeySettings[1].RootFingerprint.ToString(
["derivationStrategy"] = "tpubDDLQZ1WMdy5YJAJWmRNoTJ3uQkavEPXCXnmD4eAuo9BKbzFUBbJmVHys5M3ku4Qw1C165wGpVWH55gZpHjdsCyntwNzhmCAzGejSL6rzbyf"
};
var scheme = DerivationSchemeSettings.Parse("tpubDDLQZ1WMdy5YJAJWmRNoTJ3uQkavEPXCXnmD4eAuo9BKbzFUBbJmVHys5M3ku4Qw1C165wGpVWH55gZpHjdsCyntwNzhmCAzGejSL6rzbyf", CreateNetworkProvider(ChainName.Regtest).BTC);
Assert.True(scheme.AccountDerivation is DirectDerivationStrategy { Segwit: true });
scheme.Source = "ManualDerivationScheme";
scheme.AccountOriginal = "tpubDDLQZ1WMdy5YJAJWmRNoTJ3uQkavEPXCXnmD4eAuo9BKbzFUBbJmVHys5M3ku4Qw1C165wGpVWH55gZpHjdsCyntwNzhmCAzGejSL6rzbyf";
var legacy2 = new JObject()

View File

@ -58,8 +58,8 @@ namespace BTCPayServer.Tests
var factory = tester.PayTester.GetService<IBTCPayServerClientFactory>();
Assert.NotNull(factory);
var client = await factory.Create(user.UserId, user.StoreId);
var u = await client.GetCurrentUser();
var s = await client.GetStores();
await client.GetCurrentUser();
await client.GetStores();
var store = await client.GetStore(user.StoreId);
Assert.NotNull(store);
var addr = await client.GetLightningDepositAddress(user.StoreId, "BTC");
@ -694,14 +694,10 @@ namespace BTCPayServer.Tests
// Try loading 1 user by email. Loading myself.
await AssertHttpError(403, async () => await badClient.GetUserByIdOrEmail(badUser.Email));
// Why is this line needed? I saw it in "CanDeleteUsersViaApi" as well. Is this part of the cleanup?
tester.Stores.Remove(adminUser.StoreId);
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanCreateUsersViaAPI()
@ -1138,7 +1134,7 @@ namespace BTCPayServer.Tests
var approved = await acc.CreateClient(Policies.CanCreatePullPayments);
await AssertPermissionError(Policies.CanCreatePullPayments, async () =>
{
var pullPayment = await nonApproved.CreatePullPayment(acc.StoreId, new CreatePullPaymentRequest()
await nonApproved.CreatePullPayment(acc.StoreId, new CreatePullPaymentRequest()
{
Amount = 100,
Currency = "USD",
@ -1149,7 +1145,7 @@ namespace BTCPayServer.Tests
});
await AssertPermissionError(Policies.CanCreatePullPayments, async () =>
{
var pullPayment = await nonApproved.CreatePayout(acc.StoreId, new CreatePayoutThroughStoreRequest()
await nonApproved.CreatePayout(acc.StoreId, new CreatePayoutThroughStoreRequest()
{
Amount = 100,
PaymentMethod = "BTC",
@ -1158,7 +1154,7 @@ namespace BTCPayServer.Tests
});
});
var pullPayment = await approved.CreatePullPayment(acc.StoreId, new CreatePullPaymentRequest()
await approved.CreatePullPayment(acc.StoreId, new CreatePullPaymentRequest()
{
Amount = 100,
Currency = "USD",
@ -1167,7 +1163,7 @@ namespace BTCPayServer.Tests
AutoApproveClaims = true
});
var p = await approved.CreatePayout(acc.StoreId, new CreatePayoutThroughStoreRequest()
await approved.CreatePayout(acc.StoreId, new CreatePayoutThroughStoreRequest()
{
Amount = 100,
PaymentMethod = "BTC",
@ -2335,7 +2331,7 @@ namespace BTCPayServer.Tests
if (marked == InvoiceStatus.Settled)
{
Assert.Equal(InvoiceStatus.Settled, result.Status);
user.AssertHasWebhookEvent<WebhookInvoiceSettledEvent>(WebhookEventType.InvoiceSettled,
await user.AssertHasWebhookEvent<WebhookInvoiceSettledEvent>(WebhookEventType.InvoiceSettled,
o =>
{
Assert.Equal(inv.Id, o.InvoiceId);
@ -2345,7 +2341,7 @@ namespace BTCPayServer.Tests
if (marked == InvoiceStatus.Invalid)
{
Assert.Equal(InvoiceStatus.Invalid, result.Status);
var evt = user.AssertHasWebhookEvent<WebhookInvoiceInvalidEvent>(WebhookEventType.InvoiceInvalid,
var evt = await user.AssertHasWebhookEvent<WebhookInvoiceInvalidEvent>(WebhookEventType.InvoiceInvalid,
o =>
{
Assert.Equal(inv.Id, o.InvoiceId);
@ -2537,7 +2533,6 @@ namespace BTCPayServer.Tests
Expiry = TimeSpan.FromSeconds(400),
PrivateRouteHints = false
});
var chargeInvoice = invoiceData;
Assert.NotNull(await client.GetLightningInvoice("BTC", invoiceData.Id));
// check list for internal node
@ -3572,6 +3567,78 @@ namespace BTCPayServer.Tests
await newUserBasicClient.GetCurrentUser();
}
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")]
public async Task ApproveUserTests()
{
using var tester = CreateServerTester();
await tester.StartAsync();
var admin = tester.NewAccount();
await admin.GrantAccessAsync(true);
var adminClient = await admin.CreateClient(Policies.Unrestricted);
Assert.False((await adminClient.GetUserByIdOrEmail(admin.UserId)).RequiresApproval);
Assert.Empty(await adminClient.GetNotifications());
// require approval
var settings = tester.PayTester.GetService<SettingsRepository>();
await settings.UpdateSetting(new PoliciesSettings { LockSubscription = false, RequiresUserApproval = true });
// new user needs approval
var unapprovedUser = tester.NewAccount();
await unapprovedUser.GrantAccessAsync();
var unapprovedUserBasicAuthClient = await unapprovedUser.CreateClient();
await AssertAPIError("unauthenticated", async () =>
{
await unapprovedUserBasicAuthClient.GetCurrentUser();
});
var unapprovedUserApiKeyClient = await unapprovedUser.CreateClient(Policies.Unrestricted);
await AssertAPIError("unauthenticated", async () =>
{
await unapprovedUserApiKeyClient.GetCurrentUser();
});
Assert.True((await adminClient.GetUserByIdOrEmail(unapprovedUser.UserId)).RequiresApproval);
Assert.False((await adminClient.GetUserByIdOrEmail(unapprovedUser.UserId)).Approved);
Assert.Single(await adminClient.GetNotifications(false));
// approve
Assert.True(await adminClient.ApproveUser(unapprovedUser.UserId, true, CancellationToken.None));
Assert.True((await adminClient.GetUserByIdOrEmail(unapprovedUser.UserId)).Approved);
Assert.True((await unapprovedUserApiKeyClient.GetCurrentUser()).Approved);
Assert.True((await unapprovedUserBasicAuthClient.GetCurrentUser()).Approved);
// un-approve
Assert.True(await adminClient.ApproveUser(unapprovedUser.UserId, false, CancellationToken.None));
Assert.False((await adminClient.GetUserByIdOrEmail(unapprovedUser.UserId)).Approved);
await AssertAPIError("unauthenticated", async () =>
{
await unapprovedUserApiKeyClient.GetCurrentUser();
});
await AssertAPIError("unauthenticated", async () =>
{
await unapprovedUserBasicAuthClient.GetCurrentUser();
});
// reset policies to not require approval
await settings.UpdateSetting(new PoliciesSettings { LockSubscription = false, RequiresUserApproval = false });
// new user does not need approval
var newUser = tester.NewAccount();
await newUser.GrantAccessAsync();
var newUserBasicAuthClient = await newUser.CreateClient();
var newUserApiKeyClient = await newUser.CreateClient(Policies.Unrestricted);
Assert.False((await newUserApiKeyClient.GetCurrentUser()).RequiresApproval);
Assert.False((await newUserApiKeyClient.GetCurrentUser()).Approved);
Assert.False((await newUserBasicAuthClient.GetCurrentUser()).RequiresApproval);
Assert.False((await newUserBasicAuthClient.GetCurrentUser()).Approved);
Assert.Single(await adminClient.GetNotifications(false));
// try unapproving user which does not have the RequiresApproval flag
await AssertAPIError("invalid-state", async () =>
{
await adminClient.ApproveUser(newUser.UserId, false, CancellationToken.None);
});
}
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")]
[Trait("Lightning", "Lightning")]
@ -3679,7 +3746,7 @@ namespace BTCPayServer.Tests
SavePrivateKeys = true
});
var preApprovedPayoutWithoutPullPayment = await adminClient.CreatePayout(admin.StoreId, new CreatePayoutThroughStoreRequest()
await adminClient.CreatePayout(admin.StoreId, new CreatePayoutThroughStoreRequest()
{
Amount = 0.0001m,
Approved = true,
@ -3809,8 +3876,9 @@ namespace BTCPayServer.Tests
Assert.True( settings.ProcessNewPayoutsInstantly);
var pluginHookService = tester.PayTester.GetService<IPluginHookService>();
var beforeHookTcs = new TaskCompletionSource();
var afterHookTcs = new TaskCompletionSource();
var beforeHookTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var afterHookTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
TestLogs.LogInformation("Adding hook...");
pluginHookService.ActionInvoked += (sender, tuple) =>
{
switch (tuple.hook)
@ -3841,7 +3909,9 @@ namespace BTCPayServer.Tests
PaymentMethod = "BTC",
Destination = (await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
});
TestLogs.LogInformation("Waiting before hook...");
await beforeHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
TestLogs.LogInformation("Waiting before after...");
await afterHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
payouts = await adminClient.GetStorePayouts(admin.StoreId);
try
@ -3911,7 +3981,7 @@ namespace BTCPayServer.Tests
beforeHookTcs = new TaskCompletionSource();
afterHookTcs = new TaskCompletionSource();
var payoutThatShouldNotBeProcessedStraightAway3 = await adminClient.CreatePayout(admin.StoreId, new CreatePayoutThroughStoreRequest()
await adminClient.CreatePayout(admin.StoreId, new CreatePayoutThroughStoreRequest()
{
Amount = 0.3m,
Approved = true,
@ -4331,7 +4401,7 @@ clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId, new StoreRateConfi
await admin.GrantAccessAsync(true);
var unauthClient = new BTCPayServerClient(tester.PayTester.ServerUri);
var authClientNoPermissions = await admin.CreateClient(Policies.CanViewInvoices);
await admin.CreateClient(Policies.CanViewInvoices);
var adminClient = await admin.CreateClient(Policies.Unrestricted);
var managerClient = await admin.CreateClient(Policies.CanManageCustodianAccounts);
var withdrawalClient = await admin.CreateClient(Policies.CanWithdrawFromCustodianAccounts);

View File

@ -29,7 +29,7 @@ namespace BTCPayServer.Tests
using var s = CreateSeleniumTester(newDb: true);
await s.StartAsync();
var u1 = s.RegisterNewUser(true);
s.RegisterNewUser(true);
var hot = s.CreateNewStore();
var seed = s.GenerateWallet(isHotWallet: true);
var cold = s.CreateNewStore();

View File

@ -2,6 +2,7 @@ using System;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.BIP78.Sender;
@ -68,7 +69,7 @@ namespace BTCPayServer.Tests
{
using var tester = CreateServerTester();
await tester.StartAsync();
var network = tester.NetworkProvider.GetNetwork<BTCPayNetwork>("BTC");
tester.NetworkProvider.GetNetwork<BTCPayNetwork>("BTC");
var repo = tester.PayTester.GetService<UTXOLocker>();
var outpoint = RandomOutpoint();
@ -189,10 +190,10 @@ namespace BTCPayServer.Tests
using var tester = CreateServerTester();
await tester.StartAsync();
var broadcaster = tester.PayTester.GetService<DelayedTransactionBroadcaster>();
var payjoinRepository = tester.PayTester.GetService<UTXOLocker>();
tester.PayTester.GetService<UTXOLocker>();
broadcaster.Disable();
var network = tester.NetworkProvider.GetNetwork<BTCPayNetwork>("BTC");
var btcPayWallet = tester.PayTester.GetService<BTCPayWalletProvider>().GetWallet(network);
tester.PayTester.GetService<BTCPayWalletProvider>().GetWallet(network);
var cashCow = tester.ExplorerNode;
cashCow.Generate(2); // get some money in case
@ -218,7 +219,7 @@ namespace BTCPayServer.Tests
receiverUser.GrantAccess(true);
receiverUser.RegisterDerivationScheme("BTC", receiverAddressType, true);
await receiverUser.ModifyOnchainPaymentSettings(p => p.PayJoinEnabled = true);
var receiverCoin = await receiverUser.ReceiveUTXO(Money.Satoshis(810), network);
await receiverUser.ReceiveUTXO(Money.Satoshis(810), network);
string errorCode = receiverAddressType == senderAddressType ? null : "unavailable|any UTXO available";
var invoice = receiverUser.BitPay.CreateInvoice(new Invoice() { Price = 50000, Currency = "SATS", FullNotifications = true });
@ -236,7 +237,7 @@ namespace BTCPayServer.Tests
txBuilder.SendEstimatedFees(new FeeRate(50m));
var psbt = txBuilder.BuildPSBT(false);
psbt = await senderUser.Sign(psbt);
var pj = await senderUser.SubmitPayjoin(invoice, psbt, errorCode, false);
await senderUser.SubmitPayjoin(invoice, psbt, errorCode, false);
}
}
}
@ -250,11 +251,11 @@ namespace BTCPayServer.Tests
s.RegisterNewUser(true);
var receiver = s.CreateNewStore();
s.EnableCheckout(CheckoutType.V1);
var receiverSeed = s.GenerateWallet("BTC", "", true, true, ScriptPubKeyType.Segwit);
s.GenerateWallet("BTC", "", true, true, ScriptPubKeyType.Segwit);
var receiverWalletId = new WalletId(receiver.storeId, "BTC");
var sender = s.CreateNewStore();
var senderSeed = s.GenerateWallet("BTC", "", true, true, ScriptPubKeyType.Segwit);
s.GenerateWallet("BTC", "", true, true, ScriptPubKeyType.Segwit);
var senderWalletId = new WalletId(sender.storeId, "BTC");
await s.Server.ExplorerNode.GenerateAsync(1);
@ -305,13 +306,13 @@ namespace BTCPayServer.Tests
var cryptoCode = "BTC";
var receiver = s.CreateNewStore();
s.EnableCheckout(CheckoutType.V1);
var receiverSeed = s.GenerateWallet(cryptoCode, "", true, true, format);
s.GenerateWallet(cryptoCode, "", true, true, format);
var receiverWalletId = new WalletId(receiver.storeId, cryptoCode);
//payjoin is enabled by default.
var invoiceId = s.CreateInvoice(receiver.storeId);
s.GoToInvoiceCheckout(invoiceId);
var bip21 = s.Driver.FindElement(By.ClassName("payment__details__instruction__open-wallet__btn"))
var bip21 = s.Driver.WaitForElement(By.ClassName("payment__details__instruction__open-wallet__btn"))
.GetAttribute("href");
Assert.Contains($"{PayjoinClient.BIP21EndpointKey}=", bip21);
@ -320,14 +321,14 @@ namespace BTCPayServer.Tests
Assert.True(s.Driver.FindElement(By.Id("PayJoinEnabled")).Selected);
var sender = s.CreateNewStore();
var senderSeed = s.GenerateWallet(cryptoCode, "", true, true, format);
s.GenerateWallet(cryptoCode, "", true, true, format);
var senderWalletId = new WalletId(sender.storeId, cryptoCode);
await s.Server.ExplorerNode.GenerateAsync(1);
await s.FundStoreWallet(senderWalletId);
invoiceId = s.CreateInvoice(receiver.storeId);
s.GoToInvoiceCheckout(invoiceId);
bip21 = s.Driver.FindElement(By.ClassName("payment__details__instruction__open-wallet__btn"))
bip21 = s.Driver.WaitForElement(By.ClassName("payment__details__instruction__open-wallet__btn"))
.GetAttribute("href");
Assert.Contains($"{PayjoinClient.BIP21EndpointKey}=", bip21);
@ -361,7 +362,7 @@ namespace BTCPayServer.Tests
//let's do it all again, except now the receiver has funds and is able to payjoin
invoiceId = s.CreateInvoice();
s.GoToInvoiceCheckout(invoiceId);
bip21 = s.Driver.FindElement(By.ClassName("payment__details__instruction__open-wallet__btn"))
bip21 = s.Driver.WaitForElement(By.ClassName("payment__details__instruction__open-wallet__btn"))
.GetAttribute("href");
Assert.Contains($"{PayjoinClient.BIP21EndpointKey}", bip21);
@ -374,7 +375,7 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("FeeSatoshiPerByte")).Clear();
s.Driver.FindElement(By.Id("FeeSatoshiPerByte")).SendKeys("2");
s.Driver.FindElement(By.Id("SignTransaction")).Click();
var txId = await s.Server.WaitForEvent<NewOnChainTransactionEvent>(() =>
await s.Server.WaitForEvent<NewOnChainTransactionEvent>(() =>
{
s.Driver.FindElement(By.CssSelector("button[value=payjoin]")).Click();
return Task.CompletedTask;
@ -406,7 +407,6 @@ namespace BTCPayServer.Tests
await TestUtils.EventuallyAsync(async () =>
{
var invoice = await s.Server.PayTester.GetService<InvoiceRepository>().GetInvoice(invoiceId);
var dto = invoice.EntityToDTO();
Assert.Equal(InvoiceStatusLegacy.Paid, invoice.Status);
});
s.GoToInvoices(receiver.storeId);
@ -415,13 +415,13 @@ namespace BTCPayServer.Tests
Assert.False(paymentValueRowColumn.Text.Contains("payjoin",
StringComparison.InvariantCultureIgnoreCase));
s.GoToWallet(receiverWalletId, WalletsNavPages.Transactions);
s.Driver.WaitForElement(By.CssSelector("#WalletTransactionsList tr"));
TestUtils.Eventually(() =>
{
s.GoToWallet(receiverWalletId, WalletsNavPages.Transactions);
Assert.Contains(invoiceId, s.Driver.PageSource);
Assert.Contains("payjoin", s.Driver.PageSource);
//this label does not always show since input gets used
// Assert.Contains("payjoin-exposed", s.Driver.PageSource);
// Either the invoice id or the payjoin-exposed label, depending on the input having been used
Assert.Matches(new Regex($"({invoiceId}|payjoin-exposed)"), s.Driver.PageSource);
});
}
}
@ -875,7 +875,6 @@ retry:
new Invoice() { Price = 0.02m, Currency = "BTC", FullNotifications = true });
cashCow.SendToAddress(BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network),
Money.Coins(0.06m));
var receiverWalletId = new WalletId(receiverUser.StoreId, "BTC");
//give the cow some cash
await cashCow.GenerateAsync(1);
@ -963,8 +962,6 @@ retry:
senderUser.GenerateWalletResponseV.MasterHDKey.Derive(signingKeySettings.GetRootedKeyPath()
.KeyPath);
var n = tester.ExplorerClient.Network.NBitcoinNetwork;
var Invoice1Coin1 = tester.ExplorerClient.Network.NBitcoinNetwork.CreateTransactionBuilder()
.SetChange(senderChange)
.Send(parsedBip21.Address, parsedBip21.Amount)
@ -973,7 +970,7 @@ retry:
.SendEstimatedFees(new FeeRate(100m))
.BuildTransaction(true);
var Invoice1Coin2 = tester.ExplorerClient.Network.NBitcoinNetwork.CreateTransactionBuilder()
tester.ExplorerClient.Network.NBitcoinNetwork.CreateTransactionBuilder()
.SetChange(senderChange)
.Send(parsedBip21.Address, parsedBip21.Amount)
.AddCoins(coin2.Coin)
@ -1133,8 +1130,7 @@ retry:
var invoice7Coin6Response1Tx = await senderUser.SubmitPayjoin(invoice7, invoice7Coin6Tx, btcPayNetwork);
var Invoice7Coin6Response1TxSigned = invoice7Coin6TxBuilder.SignTransaction(invoice7Coin6Response1Tx);
var contributedInputsInvoice7Coin6Response1TxSigned =
Invoice7Coin6Response1TxSigned.Inputs.Single(txin => coin6.OutPoint != txin.PrevOut);
Invoice7Coin6Response1TxSigned.Inputs.Single(txin => coin6.OutPoint != txin.PrevOut);
////var receiverWalletPayJoinState = payJoinStateProvider.Get(receiverWalletId);

View File

@ -8,6 +8,7 @@ using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client.Models;
using BTCPayServer.Lightning;
using BTCPayServer.Lightning.CLightning;
using BTCPayServer.Services;
using BTCPayServer.Views.Manage;
using BTCPayServer.Views.Server;
using BTCPayServer.Views.Stores;
@ -76,6 +77,7 @@ namespace BTCPayServer.Tests
// A bit less than test timeout
TimeSpan.FromSeconds(50));
}
ServerUri = Server.PayTester.ServerUri;
Driver.Manage().Window.Maximize();
@ -288,7 +290,7 @@ namespace BTCPayServer.Tests
/// </summary>
/// <param name="cryptoCode"></param>
/// <param name="derivationScheme"></param>
public void AddDerivationScheme(string cryptoCode = "BTC", string derivationScheme = "xpub661MyMwAqRbcGABgHMUXDzPzH1tU7eZaAaJQXhDXsSxsqyQzQeU6kznNfSuAyqAK9UaWSaZaMFdNiY5BCF4zBPAzSnwfUAwUhwttuAKwfRX-[legacy]")
public void AddDerivationScheme(string cryptoCode = "BTC", string derivationScheme = "tpubD6NzVbkrYhZ4XxNXjYTcRujMc8z8734diCthtFGgDMimbG5hUsKBuSTCuUyxWL7YwP7R4A5StMTRQiZnb6vE4pdHWPgy9hbiHuVJfBMumUu-[legacy]")
{
if (!Driver.PageSource.Contains($"Setup {cryptoCode} Wallet"))
{

View File

@ -97,7 +97,7 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("ViewApp")).Click();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
s.Driver.FindElement(By.CssSelector("button[type='submit']")).Click();
Assert.Contains("Enter your email", s.Driver.PageSource);
@ -108,7 +108,7 @@ namespace BTCPayServer.Tests
var invoiceId = s.Driver.Url[(s.Driver.Url.LastIndexOf("/", StringComparison.Ordinal) + 1)..];
s.Driver.Close();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
s.GoToInvoice(invoiceId);
Assert.Contains("aa@aa.com", s.Driver.PageSource);
@ -123,10 +123,10 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.XPath("//a[starts-with(@id, 'Edit-')]")).Click();
var editUrl = s.Driver.Url;
s.Driver.FindElement(By.Id("ViewPaymentRequest")).Click();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
s.Driver.FindElement(By.CssSelector("[data-test='form-button']")).Click();
Assert.Contains("Enter your email", s.Driver.PageSource);
@ -135,7 +135,7 @@ namespace BTCPayServer.Tests
invoiceId = s.Driver.Url.Split('/').Last();
s.Driver.Close();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
s.Driver.Navigate().GoToUrl(editUrl);
Assert.Contains("aa@aa.com", s.Driver.PageSource);
var invoice = await s.Server.PayTester.GetService<InvoiceRepository>().GetInvoice(invoiceId);
@ -147,13 +147,13 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("CreateForm")).Click();
s.Driver.FindElement(By.Name("Name")).SendKeys("Custom Form 1");
s.Driver.FindElement(By.Id("ApplyEmailTemplate")).Click();
s.Driver.FindElement(By.Id("CodeTabButton")).Click();
s.Driver.WaitForElement(By.Id("CodeTabPane"));
var config = s.Driver.FindElement(By.Name("FormConfig")).GetAttribute("value");
Assert.Contains("buyerEmail", config);
s.Driver.FindElement(By.Name("FormConfig")).Clear();
s.Driver.FindElement(By.Name("FormConfig"))
.SendKeys(config.Replace("Enter your email", "CustomFormInputTest"));
@ -179,10 +179,10 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("CreateForm")).Click();
s.Driver.FindElement(By.Name("Name")).SendKeys("Custom Form 2");
s.Driver.FindElement(By.Id("ApplyEmailTemplate")).Click();
s.Driver.FindElement(By.Id("CodeTabButton")).Click();
s.Driver.WaitForElement(By.Id("CodeTabPane"));
s.Driver.SetCheckbox(By.Name("Public"), true);
s.Driver.FindElement(By.Name("FormConfig")).Clear();
@ -405,6 +405,148 @@ namespace BTCPayServer.Tests
Assert.Contains("/login", s.Driver.Url);
}
[Fact(Timeout = TestTimeout)]
public async Task CanRequireApprovalForNewAccounts()
{
using var s = CreateSeleniumTester();
await s.StartAsync();
var settings = s.Server.PayTester.GetService<SettingsRepository>();
var policies = await settings.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
Assert.True(policies.EnableRegistration);
Assert.False(policies.RequiresUserApproval);
// Register admin and adapt policies
s.RegisterNewUser(true);
var admin = s.AsTestAccount();
s.GoToHome();
s.GoToServer(ServerNavPages.Policies);
Assert.True(s.Driver.FindElement(By.Id("EnableRegistration")).Selected);
Assert.False(s.Driver.FindElement(By.Id("RequiresUserApproval")).Selected);
s.Driver.FindElement(By.Id("RequiresUserApproval")).Click();
s.Driver.FindElement(By.Id("SaveButton")).Click();
Assert.Contains("Policies updated successfully", s.FindAlertMessage().Text);
Assert.True(s.Driver.FindElement(By.Id("RequiresUserApproval")).Selected);
// Check user create view has approval checkbox
s.GoToServer(ServerNavPages.Users);
s.Driver.FindElement(By.Id("CreateUser")).Click();
Assert.False(s.Driver.FindElement(By.Id("Approved")).Selected);
// Ensure there is no unread notification yet
s.Driver.ElementDoesNotExist(By.Id("NotificationsBadge"));
s.Logout();
// Register user and try to log in
s.GoToRegister();
s.RegisterNewUser();
s.Driver.AssertNoError();
Assert.Contains("Account created. The new account requires approval by an admin before you can log in", s.FindAlertMessage().Text);
Assert.Contains("/login", s.Driver.Url);
var unapproved = s.AsTestAccount();
s.LogIn(unapproved.RegisterDetails.Email, unapproved.RegisterDetails.Password);
Assert.Contains("Your user account requires approval by an admin before you can log in", s.FindAlertMessage(StatusMessageModel.StatusSeverity.Warning).Text);
Assert.Contains("/login", s.Driver.Url);
// Login with admin
s.GoToLogin();
s.LogIn(admin.RegisterDetails.Email, admin.RegisterDetails.Password);
s.GoToHome();
// Check notification
TestUtils.Eventually(() => Assert.Equal("1", s.Driver.FindElement(By.Id("NotificationsBadge")).Text));
s.Driver.FindElement(By.Id("NotificationsHandle")).Click();
Assert.Matches($"New user {unapproved.RegisterDetails.Email} requires approval", s.Driver.FindElement(By.CssSelector("#NotificationsList .notification")).Text);
s.Driver.FindElement(By.Id("NotificationsMarkAllAsSeen")).Click();
// Reset approval policy
s.GoToServer(ServerNavPages.Policies);
Assert.True(s.Driver.FindElement(By.Id("EnableRegistration")).Selected);
Assert.True(s.Driver.FindElement(By.Id("RequiresUserApproval")).Selected);
s.Driver.FindElement(By.Id("RequiresUserApproval")).Click();
s.Driver.FindElement(By.Id("SaveButton")).Click();
Assert.Contains("Policies updated successfully", s.FindAlertMessage().Text);
Assert.False(s.Driver.FindElement(By.Id("RequiresUserApproval")).Selected);
// Check user create view does not have approval checkbox
s.GoToServer(ServerNavPages.Users);
s.Driver.FindElement(By.Id("CreateUser")).Click();
s.Driver.ElementDoesNotExist(By.Id("Approved"));
s.Logout();
// Still requires approval for user who registered before
s.GoToLogin();
s.LogIn(unapproved.RegisterDetails.Email, unapproved.RegisterDetails.Password);
Assert.Contains("Your user account requires approval by an admin before you can log in", s.FindAlertMessage(StatusMessageModel.StatusSeverity.Warning).Text);
Assert.Contains("/login", s.Driver.Url);
// New user can register and gets in without approval
s.GoToRegister();
s.RegisterNewUser();
s.Driver.AssertNoError();
Assert.DoesNotContain("/login", s.Driver.Url);
var autoApproved = s.AsTestAccount();
s.CreateNewStore();
s.Logout();
// Login with admin and check list
s.GoToLogin();
s.LogIn(admin.RegisterDetails.Email, admin.RegisterDetails.Password);
s.GoToHome();
// No notification this time
s.Driver.ElementDoesNotExist(By.Id("NotificationsBadge"));
// Check users list
s.GoToServer(ServerNavPages.Users);
var rows = s.Driver.FindElements(By.CssSelector("#UsersList tr.user-overview-row"));
Assert.True(rows.Count >= 3);
// Check user which didn't require approval
s.Driver.FindElement(By.Id("SearchTerm")).Clear();
s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(autoApproved.RegisterDetails.Email);
s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(Keys.Enter);
rows = s.Driver.FindElements(By.CssSelector("#UsersList tr.user-overview-row"));
Assert.Single(rows);
Assert.Contains(autoApproved.RegisterDetails.Email, rows.First().Text);
s.Driver.ElementDoesNotExist(By.CssSelector("#UsersList tr.user-overview-row:first-child .user-approved"));
// Edit view does not contain approve toggle
s.Driver.FindElement(By.CssSelector("#UsersList tr.user-overview-row:first-child .user-edit")).Click();
s.Driver.ElementDoesNotExist(By.Id("Approved"));
// Check user which still requires approval
s.GoToServer(ServerNavPages.Users);
s.Driver.FindElement(By.Id("SearchTerm")).Clear();
s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(unapproved.RegisterDetails.Email);
s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(Keys.Enter);
rows = s.Driver.FindElements(By.CssSelector("#UsersList tr.user-overview-row"));
Assert.Single(rows);
Assert.Contains(unapproved.RegisterDetails.Email, rows.First().Text);
Assert.Contains("Pending Approval", s.Driver.FindElement(By.CssSelector("#UsersList tr.user-overview-row:first-child .user-status")).Text);
// Approve user
s.Driver.FindElement(By.CssSelector("#UsersList tr.user-overview-row:first-child .user-edit")).Click();
s.Driver.FindElement(By.Id("Approved")).Click();
s.Driver.FindElement(By.Id("SaveUser")).Click();
Assert.Contains("User successfully updated", s.FindAlertMessage().Text);
// Check list again
s.GoToServer(ServerNavPages.Users);
Assert.Contains(unapproved.RegisterDetails.Email, s.Driver.FindElement(By.Id("SearchTerm")).GetAttribute("value"));
rows = s.Driver.FindElements(By.CssSelector("#UsersList tr.user-overview-row"));
Assert.Single(rows);
Assert.Contains(unapproved.RegisterDetails.Email, rows.First().Text);
Assert.Contains("Active", s.Driver.FindElement(By.CssSelector("#UsersList tr.user-overview-row:first-child .user-status")).Text);
// Finally, login user that needed approval
s.Logout();
s.GoToLogin();
s.LogIn(unapproved.RegisterDetails.Email, unapproved.RegisterDetails.Password);
s.Driver.AssertNoError();
Assert.DoesNotContain("/login", s.Driver.Url);
s.CreateNewStore();
}
[Fact(Timeout = TestTimeout)]
public async Task CanUseSSHService()
{
@ -468,8 +610,16 @@ namespace BTCPayServer.Tests
s.RegisterNewUser(true);
s.CreateNewStore();
// Ensure empty server settings
s.Driver.Navigate().GoToUrl(s.Link("/server/emails"));
s.Driver.FindElement(By.Id("Settings_Login")).Clear();
s.Driver.FindElement(By.Id("Settings_Password")).Clear();
s.Driver.FindElement(By.Id("Settings_From")).Clear();
s.Driver.FindElement(By.Id("Save")).Submit();
// Store Emails without server fallback
s.GoToStore(StoreNavPages.Emails);
s.Driver.ElementDoesNotExist(By.Id("UseCustomSMTP"));
s.Driver.FindElement(By.Id("ConfigureEmailRules")).Click();
Assert.Contains("You need to configure email settings before this feature works", s.Driver.PageSource);
@ -481,13 +631,16 @@ namespace BTCPayServer.Tests
s.FindAlertMessage();
}
CanSetupEmailCore(s);
// Store Emails with server fallback
s.GoToStore(StoreNavPages.Emails);
Assert.False(s.Driver.FindElement(By.Id("UseCustomSMTP")).Selected);
s.Driver.FindElement(By.Id("ConfigureEmailRules")).Click();
Assert.Contains("Emails will be sent with the email settings of the server", s.Driver.PageSource);
Assert.DoesNotContain("You need to configure email settings before this feature works", s.Driver.PageSource);
s.GoToStore(StoreNavPages.Emails);
s.Driver.FindElement(By.Id("UseCustomSMTP")).Click();
Thread.Sleep(250);
CanSetupEmailCore(s);
// Store Email Rules
@ -495,7 +648,6 @@ namespace BTCPayServer.Tests
Assert.Contains("There are no rules yet.", s.Driver.PageSource);
Assert.DoesNotContain("id=\"SaveEmailRules\"", s.Driver.PageSource);
Assert.DoesNotContain("You need to configure email settings before this feature works", s.Driver.PageSource);
Assert.DoesNotContain("Emails will be sent with the email settings of the server", s.Driver.PageSource);
s.Driver.FindElement(By.Id("CreateEmailRule")).Click();
var select = new SelectElement(s.Driver.FindElement(By.Id("Rules_0__Trigger")));
@ -506,6 +658,9 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.ClassName("note-editable")).SendKeys("Your invoice is settled");
s.Driver.FindElement(By.Id("SaveEmailRules")).Click();
Assert.Contains("Store email rules saved", s.FindAlertMessage().Text);
s.GoToStore(StoreNavPages.Emails);
Assert.True(s.Driver.FindElement(By.Id("UseCustomSMTP")).Selected);
}
[Fact(Timeout = TestTimeout)]
@ -565,9 +720,11 @@ namespace BTCPayServer.Tests
s.RegisterNewUser(true);
s.CreateNewStore();
s.GoToInvoices();
s.Driver.FindElement(By.Id("CreateNewInvoice")).Click();
// Should give us an error message if we try to create an invoice before adding a wallet
s.Driver.FindElement(By.Id("CreateNewInvoice")).Click();
Assert.Contains("To create an invoice, you need to", s.Driver.PageSource);
s.AddDerivationScheme();
s.GoToInvoices();
s.CreateInvoice();
@ -1135,34 +1292,33 @@ namespace BTCPayServer.Tests
s.Driver.ExecuteJavaScript("document.getElementById('EndDate').value = ''");
s.Driver.FindElement(By.Id("SaveSettings")).Click();
Assert.Contains("App updated", s.FindAlertMessage().Text);
var appId = s.Driver.Url.Split('/')[4];
var editUrl = s.Driver.Url;
var appId = editUrl.Split('/')[4];
// CHeck public page
// Check public page
s.Driver.FindElement(By.Id("ViewApp")).Click();
var windows = s.Driver.WindowHandles;
Assert.Equal(2, windows.Count);
s.Driver.SwitchTo().Window(windows[1]);
var cfUrl = s.Driver.Url;
Assert.Equal("Currently active!",
s.Driver.FindElement(By.CssSelector("[data-test='time-state']")).Text);
Assert.Equal("Currently active!", s.Driver.FindElement(By.CssSelector("[data-test='time-state']")).Text);
// Contribute
s.Driver.FindElement(By.Id("crowdfund-body-header-cta")).Click();
s.Driver.WaitUntilAvailable(By.Name("btcpay"));
var frameElement = s.Driver.FindElement(By.Name("btcpay"));
Assert.True(frameElement.Displayed);
var iframe = s.Driver.SwitchTo().Frame(frameElement);
iframe.WaitUntilAvailable(By.Id("Checkout-v2"));
IWebElement closebutton = null;
TestUtils.Eventually(() =>
{
closebutton = iframe.FindElement(By.Id("close"));
Assert.True(closebutton.Displayed);
s.Driver.WaitUntilAvailable(By.Name("btcpay"));
var frameElement = s.Driver.FindElement(By.Name("btcpay"));
Assert.True(frameElement.Displayed);
var iframe = s.Driver.SwitchTo().Frame(frameElement);
iframe.WaitUntilAvailable(By.Id("Checkout-v2"));
var closeButton = iframe.FindElement(By.Id("close"));
Assert.True(closeButton.Displayed);
closeButton.Click();
});
closebutton.Click();
s.Driver.AssertElementNotFound(By.Name("btcpay"));
// Back to admin view
@ -1186,6 +1342,56 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id($"App-{appId}")).Click();
s.Driver.FindElement(By.Id("btn-archive-toggle")).Click();
Assert.Contains("The app has been unarchived and will appear in the apps list by default again.", s.FindAlertMessage().Text);
// Crowdfund with form
s.GoToUrl(editUrl);
new SelectElement(s.Driver.FindElement(By.Id("FormId"))).SelectByValue("Email");
s.Driver.FindElement(By.Id("SaveSettings")).Click();
Assert.Contains("App updated", s.FindAlertMessage().Text);
s.Driver.FindElement(By.Id("ViewApp")).Click();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
s.Driver.FindElement(By.Id("crowdfund-body-header-cta")).Click();
Assert.Contains("Enter your email", s.Driver.PageSource);
s.Driver.FindElement(By.Name("buyerEmail")).SendKeys("test-without-perk@crowdfund.com");
s.Driver.FindElement(By.CssSelector("input[type='submit']")).Click();
s.PayInvoice(true, 10);
var invoiceId = s.Driver.Url[(s.Driver.Url.LastIndexOf("/", StringComparison.Ordinal) + 1)..];
s.Driver.Close();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
s.GoToInvoice(invoiceId);
Assert.Contains("test-without-perk@crowdfund.com", s.Driver.PageSource);
// Crowdfund with perk
s.GoToUrl(editUrl);
s.Driver.ScrollTo(By.Id("btAddItem"));
s.Driver.FindElement(By.Id("btAddItem")).Click();
s.Driver.FindElement(By.Id("EditorTitle")).SendKeys("Perk 1");
s.Driver.FindElement(By.Id("EditorId")).SendKeys("Perk-1");
s.Driver.FindElement(By.Id("EditorAmount")).SendKeys("20");
s.Driver.FindElement(By.Id("ApplyItemChanges")).Click();
s.Driver.FindElement(By.Id("SaveSettings")).Click();
Assert.Contains("App updated", s.FindAlertMessage().Text);
s.Driver.FindElement(By.Id("ViewApp")).Click();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
s.Driver.WaitForElement(By.Id("Perk-1")).Click();
s.Driver.WaitForElement(By.CssSelector("#Perk-1 button[type=\"submit\"]")).Submit();
Assert.Contains("Enter your email", s.Driver.PageSource);
s.Driver.FindElement(By.Name("buyerEmail")).SendKeys("test-with-perk@crowdfund.com");
s.Driver.FindElement(By.CssSelector("input[type='submit']")).Click();
s.PayInvoice(true, 20);
invoiceId = s.Driver.Url[(s.Driver.Url.LastIndexOf("/", StringComparison.Ordinal) + 1)..];
s.Driver.Close();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
s.GoToInvoice(invoiceId);
Assert.Contains("test-with-perk@crowdfund.com", s.Driver.PageSource);
}
[Fact(Timeout = TestTimeout)]
@ -1195,8 +1401,13 @@ namespace BTCPayServer.Tests
await s.StartAsync();
s.RegisterNewUser();
s.CreateNewStore();
s.AddDerivationScheme();
s.Driver.FindElement(By.Id("StoreNav-PaymentRequests")).Click();
// Should give us an error message if we try to create a payment request before adding a wallet
s.Driver.FindElement(By.Id("CreatePaymentRequest")).Click();
Assert.Contains("To create a payment request, you need to", s.Driver.PageSource);
s.AddDerivationScheme();
s.Driver.FindElement(By.Id("StoreNav-PaymentRequests")).Click();
s.Driver.FindElement(By.Id("CreatePaymentRequest")).Click();
s.Driver.FindElement(By.Id("Title")).SendKeys("Pay123");
@ -1626,7 +1837,6 @@ namespace BTCPayServer.Tests
var mnemonic = s.GenerateWallet(cryptoCode, "", true, true);
//lets import and save private keys
var root = mnemonic.DeriveExtKey();
invoiceId = s.CreateInvoice(storeId);
invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice(invoiceId);
address = invoice.EntityToDTO().Addresses["BTC"];
@ -2326,19 +2536,42 @@ namespace BTCPayServer.Tests
s.Server.ActivateLightning();
await s.StartAsync();
await s.Server.EnsureChannelsSetup();
// Create users
var user = s.RegisterNewUser();
var userAccount = s.AsTestAccount();
s.GoToHome();
s.Logout();
s.GoToRegister();
s.RegisterNewUser(true);
// Setup store and associate user
s.CreateNewStore();
s.GoToStore();
s.AddLightningNode(LightningConnectionType.CLightning, false);
s.GoToStore(StoreNavPages.Users);
s.Driver.FindElement(By.Id("Email")).Clear();
s.Driver.FindElement(By.Id("Email")).SendKeys(user);
new SelectElement(s.Driver.FindElement(By.Id("Role"))).SelectByValue("Guest");
s.Driver.FindElement(By.Id("AddUser")).Click();
Assert.Contains("User added successfully", s.FindAlertMessage().Text);
// Setup POS
s.Driver.FindElement(By.Id("StoreNav-CreatePointOfSale")).Click();
s.Driver.FindElement(By.Id("AppName")).SendKeys(Guid.NewGuid().ToString());
s.Driver.FindElement(By.Id("Create")).Click();
TestUtils.Eventually(() => Assert.Contains("App successfully created", s.FindAlertMessage().Text));
s.Driver.FindElement(By.CssSelector("label[for='DefaultView_Light']")).Click();
s.Driver.FindElement(By.Id("Currency")).SendKeys("EUR");
Assert.False(s.Driver.FindElement(By.Id("EnableTips")).Selected);
s.Driver.FindElement(By.Id("EnableTips")).Click();
Assert.True(s.Driver.FindElement(By.Id("EnableTips")).Selected);
Thread.Sleep(250);
s.Driver.FindElement(By.Id("CustomTipPercentages")).Clear();
s.Driver.FindElement(By.Id("CustomTipPercentages")).SendKeys("10,21");
Assert.False(s.Driver.FindElement(By.Id("ShowDiscount")).Selected);
s.Driver.FindElement(By.Id("ShowDiscount")).Click();
s.Driver.FindElement(By.Id("SaveSettings")).Click();
Assert.Contains("App updated", s.FindAlertMessage().Text);
s.Driver.FindElement(By.Id("ViewApp")).Click();
@ -2346,15 +2579,17 @@ namespace BTCPayServer.Tests
Assert.Equal(2, windows.Count);
s.Driver.SwitchTo().Window(windows[1]);
s.Driver.WaitForElement(By.ClassName("keypad"));
// basic checks
var keypadUrl = s.Driver.Url;
s.Driver.FindElement(By.Id("RecentTransactionsToggle"));
Assert.Contains("EUR", s.Driver.FindElement(By.Id("Currency")).Text);
Assert.Contains("0,00", s.Driver.FindElement(By.Id("Amount")).Text);
Assert.Equal("", s.Driver.FindElement(By.Id("Calculation")).Text);
Assert.True(s.Driver.FindElement(By.Id("ModeTablist-amounts")).Selected);
Assert.False(s.Driver.FindElement(By.Id("ModeTablist-discount")).Enabled);
Assert.False(s.Driver.FindElement(By.Id("ModeTablist-tip")).Enabled);
// Amount: 1234,56
s.Driver.FindElement(By.CssSelector(".keypad [data-key='1']")).Click();
s.Driver.FindElement(By.CssSelector(".keypad [data-key='2']")).Click();
@ -2371,7 +2606,7 @@ namespace BTCPayServer.Tests
Assert.True(s.Driver.FindElement(By.Id("ModeTablist-discount")).Enabled);
Assert.True(s.Driver.FindElement(By.Id("ModeTablist-tip")).Enabled);
Assert.Equal("1.234,00 € + 0,56 €", s.Driver.FindElement(By.Id("Calculation")).Text);
// Discount: 10%
s.Driver.FindElement(By.CssSelector("label[for='ModeTablist-discount']")).Click();
s.Driver.FindElement(By.CssSelector(".keypad [data-key='1']")).Click();
@ -2379,20 +2614,33 @@ namespace BTCPayServer.Tests
Assert.Contains("1.111,10", s.Driver.FindElement(By.Id("Amount")).Text);
Assert.Contains("10% discount", s.Driver.FindElement(By.Id("Discount")).Text);
Assert.Contains("1.234,00 € + 0,56 € - 123,46 € (10%)", s.Driver.FindElement(By.Id("Calculation")).Text);
// Tip: 10%
s.Driver.FindElement(By.CssSelector("label[for='ModeTablist-tip']")).Click();
s.Driver.WaitForElement(By.Id("Tip-Custom"));
s.Driver.FindElement(By.Id("Tip-10")).Click();
Assert.Contains("1.222,21", s.Driver.FindElement(By.Id("Amount")).Text);
Assert.Contains("1.234,00 € + 0,56 € - 123,46 € (10%) + 111,11 € (10%)", s.Driver.FindElement(By.Id("Calculation")).Text);
// Pay
s.Driver.FindElement(By.Id("pay-button")).Click();
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
s.Driver.FindElement(By.Id("DetailsToggle")).Click();
s.Driver.WaitForElement(By.Id("PaymentDetails-TotalFiat"));
Assert.Contains("1 222,21 €", s.Driver.FindElement(By.Id("PaymentDetails-TotalFiat")).Text);
// Guest user can access recent transactions
s.GoToHome();
s.Logout();
s.LogIn(user, userAccount.RegisterDetails.Password);
s.GoToUrl(keypadUrl);
s.Driver.FindElement(By.Id("RecentTransactionsToggle"));
s.GoToHome();
s.Logout();
// Unauthenticated user can't access recent transactions
s.GoToUrl(keypadUrl);
s.Driver.ElementDoesNotExist(By.Id("RecentTransactionsToggle"));
}
[Fact]
@ -2415,8 +2663,14 @@ namespace BTCPayServer.Tests
Assert.Contains("App successfully created", s.FindAlertMessage().Text);
s.Driver.FindElement(By.CssSelector("label[for='DefaultView_Cart']")).Click();
s.Driver.FindElement(By.Id("Currency")).SendKeys("EUR");
Assert.False(s.Driver.FindElement(By.Id("EnableTips")).Selected);
s.Driver.FindElement(By.Id("EnableTips")).Click();
Assert.True(s.Driver.FindElement(By.Id("EnableTips")).Selected);
Thread.Sleep(250);
s.Driver.FindElement(By.Id("CustomTipPercentages")).Clear();
s.Driver.FindElement(By.Id("CustomTipPercentages")).SendKeys("10,21");
Assert.False(s.Driver.FindElement(By.Id("ShowDiscount")).Selected);
s.Driver.FindElement(By.Id("ShowDiscount")).Click();
s.Driver.FindElement(By.Id("SaveSettings")).Click();
Assert.Contains("App updated", s.FindAlertMessage().Text);
s.Driver.FindElement(By.Id("ViewApp")).Click();
@ -2483,19 +2737,19 @@ namespace BTCPayServer.Tests
Thread.Sleep(250);
Assert.Equal(7, s.Driver.FindElements(By.CssSelector("#CartItems tr")).Count);
Assert.Equal("10,00 €", s.Driver.FindElement(By.Id("CartTotal")).Text);
// Discount: 10%
s.Driver.ElementDoesNotExist(By.Id("CartDiscount"));
s.Driver.FindElement(By.Id("Discount")).SendKeys("10");
Assert.Contains("10% = 1,00 €", s.Driver.FindElement(By.Id("CartDiscount")).Text);
Assert.Equal("9,00 €", s.Driver.FindElement(By.Id("CartTotal")).Text);
// Tip: 10%
s.Driver.ElementDoesNotExist(By.Id("CartTip"));
s.Driver.FindElement(By.Id("Tip-10")).Click();
Assert.Contains("10% = 0,90 €", s.Driver.FindElement(By.Id("CartTip")).Text);
Assert.Equal("9,90 €", s.Driver.FindElement(By.Id("CartTotal")).Text);
// Check values on checkout page
s.Driver.FindElement(By.Id("CartSubmit")).Click();
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
@ -2538,7 +2792,7 @@ namespace BTCPayServer.Tests
// Topup Invoice test
var i = s.CreateInvoice(storeId, null, cryptoCode);
s.GoToInvoiceCheckout(i);
s.Driver.FindElement(By.Id("copy-tab")).Click();
s.Driver.WaitForElement(By.Id("copy-tab")).Click();
var lnurl = s.Driver.FindElement(By.CssSelector("input.checkoutTextbox")).GetAttribute("value");
var parsed = LNURL.LNURL.Parse(lnurl, out var tag);
var fetchedReuqest =
@ -2577,9 +2831,9 @@ namespace BTCPayServer.Tests
s.GoToInvoiceCheckout(i);
s.Driver.FindElement(By.ClassName("payment__currencies_noborder")).Click();
// BOLT11 is also displayed for standard invoice (not LNURL, even if it is available)
s.Driver.FindElement(By.Id("copy-tab")).Click();
s.Driver.WaitForElement(By.Id("copy-tab")).Click();
var bolt11 = s.Driver.FindElement(By.CssSelector("input.checkoutTextbox")).GetAttribute("value");
var bolt11Parsed = Lightning.BOLT11PaymentRequest.Parse(bolt11, s.Server.ExplorerNode.Network);
Lightning.BOLT11PaymentRequest.Parse(bolt11, s.Server.ExplorerNode.Network);
var invoiceId = s.Driver.Url.Split('/').Last();
using (var resp = await s.Server.PayTester.HttpClient.GetAsync("BTC/lnurl/pay/i/" + invoiceId))
{
@ -2634,7 +2888,7 @@ namespace BTCPayServer.Tests
i = s.CreateInvoice(storeId, null, cryptoCode);
s.GoToInvoiceCheckout(i);
s.Driver.FindElement(By.ClassName("payment__currencies_noborder"));
s.Driver.FindElement(By.Id("copy-tab")).Click();
s.Driver.WaitForElement(By.Id("copy-tab")).Click();
lnurl = s.Driver.FindElement(By.CssSelector("input.checkoutTextbox")).GetAttribute("value");
Assert.StartsWith("lnurlp", lnurl);
LNURL.LNURL.Parse(lnurl, out tag);
@ -2649,7 +2903,7 @@ namespace BTCPayServer.Tests
Assert.Contains($"{cryptoCode} Lightning settings successfully updated", s.FindAlertMessage().Text);
var invForPP = s.CreateInvoice(null, cryptoCode);
s.GoToInvoiceCheckout(invForPP);
s.Driver.FindElement(By.Id("copy-tab")).Click();
s.Driver.WaitForElement(By.Id("copy-tab")).Click();
lnurl = s.Driver.FindElement(By.CssSelector("input.checkoutTextbox")).GetAttribute("value");
LNURL.LNURL.Parse(lnurl, out tag);
@ -2790,7 +3044,6 @@ namespace BTCPayServer.Tests
}
invoices = await repo.GetInvoices(new InvoiceQuery() { StoreId = new[] { s.StoreId } });
Assert.Equal(2, invoices.Length);
var emailSuffix = $"@{s.Server.PayTester.HostName}:{s.Server.PayTester.Port}";
foreach (var i in invoices)
{
var lightningPaymentMethod = i.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.LNURLPay));
@ -3110,15 +3363,13 @@ retry:
private static void CanSetupEmailCore(SeleniumTester s)
{
s.Driver.ScrollTo(By.Id("QuickFillDropdownToggle"));
s.Driver.FindElement(By.Id("QuickFillDropdownToggle")).Click();
s.Driver.FindElement(By.CssSelector("#quick-fill .dropdown-menu .dropdown-item:first-child")).Click();
s.Driver.FindElement(By.Id("Settings_Login")).Clear();
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")).Clear();
s.Driver.FindElement(By.Id("Settings_Password")).SendKeys("mypassword");
s.Driver.FindElement(By.Id("Settings_From")).Clear();
s.Driver.FindElement(By.Id("Settings_From")).SendKeys("Firstname Lastname <email@example.com>");
s.Driver.FindElement(By.Id("Save")).SendKeys(Keys.Enter);

View File

@ -39,7 +39,7 @@ namespace BTCPayServer.Tests
if (!Directory.Exists(_Directory))
Directory.CreateDirectory(_Directory);
NetworkProvider = networkProvider;
_NetworkProvider = networkProvider;
ExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_BTCRPCCONNECTION", "server=http://127.0.0.1:43782;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork<BTCPayNetwork>("BTC").NBitcoinNetwork);
ExplorerNode.ScanRPCCapabilities();
@ -214,8 +214,14 @@ namespace BTCPayServer.Tests
{
return new TestAccount(this);
}
public BTCPayNetworkProvider NetworkProvider { get; private set; }
BTCPayNetworkProvider _NetworkProvider;
public BTCPayNetworkProvider NetworkProvider
{
get
{
return PayTester?.Networks ?? _NetworkProvider;
}
}
public RPCClient ExplorerNode
{
get; set;

View File

@ -73,7 +73,7 @@ namespace BTCPayServer.Tests
public async Task<BTCPayServerClient> CreateClient(params string[] permissions)
{
var manageController = parent.PayTester.GetController<UIManageController>(UserId, StoreId, IsAdmin);
var x = Assert.IsType<RedirectToActionResult>(await manageController.AddApiKey(
Assert.IsType<RedirectToActionResult>(await manageController.AddApiKey(
new UIManageController.AddApiKeyViewModel()
{
PermissionValues = permissions.Select(s =>
@ -339,7 +339,6 @@ namespace BTCPayServer.Tests
public async Task<BitcoinAddress> GetNewAddress(BTCPayNetwork network)
{
var cashCow = parent.ExplorerNode;
var btcPayWallet = parent.PayTester.GetService<BTCPayWalletProvider>().GetWallet(network);
var address = (await btcPayWallet.ReserveAddressAsync(this.DerivationScheme)).Address;
return address;
@ -347,7 +346,7 @@ namespace BTCPayServer.Tests
public async Task<PSBT> Sign(PSBT psbt)
{
var btcPayWallet = parent.PayTester.GetService<BTCPayWalletProvider>()
parent.PayTester.GetService<BTCPayWalletProvider>()
.GetWallet(psbt.Network.NetworkSet.CryptoCode);
var explorerClient = parent.PayTester.GetService<ExplorerClientProvider>()
.GetExplorerClient(psbt.Network.NetworkSet.CryptoCode);
@ -444,7 +443,7 @@ namespace BTCPayServer.Tests
var parsedBip21 = new BitcoinUrlBuilder(
invoice.CryptoInfo.First(c => c.CryptoCode == network.NetworkSet.CryptoCode).PaymentUrls.BIP21,
network);
if (!parsedBip21.TryGetPayjoinEndpoint(out var endpoint))
if (!parsedBip21.TryGetPayjoinEndpoint(out _))
return null;
return parsedBip21;
}
@ -453,9 +452,9 @@ namespace BTCPayServer.Tests
{
private Client.Models.StoreWebhookData _wh;
private FakeServer _server;
private readonly List<WebhookInvoiceEvent> _webhookEvents;
private readonly List<StoreWebhookEvent> _webhookEvents;
private CancellationTokenSource _cts;
public WebhookListener(Client.Models.StoreWebhookData wh, FakeServer server, List<WebhookInvoiceEvent> webhookEvents)
public WebhookListener(Client.Models.StoreWebhookData wh, FakeServer server, List<StoreWebhookEvent> webhookEvents)
{
_wh = wh;
_server = server;
@ -473,7 +472,7 @@ namespace BTCPayServer.Tests
var callback = Encoding.UTF8.GetString(bytes);
lock (_webhookEvents)
{
_webhookEvents.Add(JsonConvert.DeserializeObject<WebhookInvoiceEvent>(callback));
_webhookEvents.Add(JsonConvert.DeserializeObject<DummyStoreWebhookEvent>(callback));
}
req.Response.StatusCode = 200;
_server.Done();
@ -486,8 +485,13 @@ namespace BTCPayServer.Tests
}
}
public List<WebhookInvoiceEvent> WebhookEvents { get; set; } = new List<WebhookInvoiceEvent>();
public TEvent AssertHasWebhookEvent<TEvent>(string eventType, Action<TEvent> assert) where TEvent : class
public class DummyStoreWebhookEvent : StoreWebhookEvent
{
}
public List<StoreWebhookEvent> WebhookEvents { get; set; } = new List<StoreWebhookEvent>();
public async Task<TEvent> AssertHasWebhookEvent<TEvent>(string eventType, Action<TEvent> assert) where TEvent : class
{
int retry = 0;
retry:
@ -511,7 +515,7 @@ retry:
}
if (retry < 3)
{
Thread.Sleep(1000);
await Task.Delay(1000);
retry++;
goto retry;
}

View File

@ -10,6 +10,7 @@ using System.Threading.Tasks;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Models.WalletViewModels;
using BTCPayServer.Rating;
using BTCPayServer.Services.Fees;
using BTCPayServer.Services.Rates;
@ -90,9 +91,43 @@ namespace BTCPayServer.Tests
"test" + isTestnet,
prov.GetService<IHttpClientFactory>(),
isTestnet);
mempoolSpaceFeeProvider.CachedOnly = true;
await Assert.ThrowsAsync<InvalidOperationException>(() => mempoolSpaceFeeProvider.GetFeeRateAsync());
mempoolSpaceFeeProvider.CachedOnly = false;
var rates = await mempoolSpaceFeeProvider.GetFeeRatesAsync();
mempoolSpaceFeeProvider.CachedOnly = true;
await mempoolSpaceFeeProvider.GetFeeRateAsync();
mempoolSpaceFeeProvider.CachedOnly = false;
Assert.NotEmpty(rates);
await mempoolSpaceFeeProvider.GetFeeRateAsync(20);
var recommendedFees =
await Task.WhenAll(new[]
{
TimeSpan.FromMinutes(10.0), TimeSpan.FromMinutes(60.0), TimeSpan.FromHours(6.0),
TimeSpan.FromHours(24.0),
}.Select(async time =>
{
try
{
var result = await mempoolSpaceFeeProvider.GetFeeRateAsync(
(int)Network.Main.Consensus.GetExpectedBlocksFor(time));
return new WalletSendModel.FeeRateOption()
{
Target = time,
FeeRate = result.SatoshiPerByte
};
}
catch (Exception)
{
return null;
}
})
.ToArray());
//ENSURE THESE ARE LOGICAL
Assert.True(recommendedFees[0].FeeRate >= recommendedFees[1].FeeRate, $"{recommendedFees[0].Target}:{recommendedFees[0].FeeRate} >= {recommendedFees[1].Target}:{recommendedFees[1].FeeRate}");
Assert.True(recommendedFees[1].FeeRate >= recommendedFees[2].FeeRate, $"{recommendedFees[1].Target}:{recommendedFees[1].FeeRate} >= {recommendedFees[2].Target}:{recommendedFees[2].FeeRate}");
Assert.True(recommendedFees[2].FeeRate >= recommendedFees[3].FeeRate, $"{recommendedFees[2].Target}:{recommendedFees[2].FeeRate} >= {recommendedFees[3].Target}:{recommendedFees[3].FeeRate}");
}
}
[Fact]
@ -156,6 +191,12 @@ namespace BTCPayServer.Tests
// Ripio keeps changing their pair, so anything is fine...
Assert.NotEmpty(exchangeRates.ByExchange[name]);
}
else if (name == "bitnob")
{
Assert.Contains(exchangeRates.ByExchange[name],
e => e.CurrencyPair == new CurrencyPair("BTC", "NGN") &&
e.BidAsk.Bid > 1.0m); // 1 BTC will always be more than 1 NGN
}
else if (name == "cryptomarket")
{
Assert.Contains(exchangeRates.ByExchange[name],
@ -278,7 +319,6 @@ retry:
}
catch (Exception ex) when (ex is MatchesException)
{
var details = ex.Message;
TestLogs.LogInformation($"FAILED: {url} ({file}) anchor not found: {uri.Fragment}");
throw;

View File

@ -73,6 +73,9 @@ using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;
using CreateInvoiceRequest = BTCPayServer.Client.Models.CreateInvoiceRequest;
using CreatePaymentRequestRequest = BTCPayServer.Client.Models.CreatePaymentRequestRequest;
using MarkPayoutRequest = BTCPayServer.Client.Models.MarkPayoutRequest;
using PaymentRequestData = BTCPayServer.Client.Models.PaymentRequestData;
using RatesViewModel = BTCPayServer.Models.StoreViewModels.RatesViewModel;
namespace BTCPayServer.Tests
@ -267,7 +270,6 @@ namespace BTCPayServer.Tests
}
catch (Exception ex) when (ex is MatchesException)
{
var details = ex.Message;
TestLogs.LogInformation($"FAILED: {url} ({file}) anchor not found: {uri.Fragment}");
throw;
@ -347,7 +349,7 @@ namespace BTCPayServer.Tests
try
{
var throwsBitpay404Error = user.BitPay.GetInvoice(invoice.Id + "123");
user.BitPay.GetInvoice(invoice.Id + "123");
}
catch (BitPayException ex)
{
@ -885,7 +887,7 @@ namespace BTCPayServer.Tests
Assert.Equal("LTC", GetCurrencyPairRateResult.Data.Code);
// Should be OK because the request is signed, so we can know the store
var rates = acc.BitPay.GetRates();
acc.BitPay.GetRates();
HttpClient client = new HttpClient();
// Unauthentified requests should also be ok
var response =
@ -1072,7 +1074,7 @@ namespace BTCPayServer.Tests
var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(0.01m, "BTC"));
await tester.WaitForEvent<InvoiceEvent>(async () =>
{
var tx = await tester.ExplorerNode.SendToAddressAsync(
await tester.ExplorerNode.SendToAddressAsync(
BitcoinAddress.Create(invoice.BitcoinAddress, Network.RegTest),
Money.Coins(0.01m));
});
@ -1148,6 +1150,14 @@ namespace BTCPayServer.Tests
bitpay = new Bitpay(k, tester.PayTester.ServerUri);
Assert.True(bitpay.TestAccess(Facade.Merchant));
Assert.True(bitpay.TestAccess(Facade.PointOfSale));
HttpClient client = new HttpClient();
var token = (await bitpay.GetAccessTokenAsync(Facade.Merchant)).Value;
var getRates = tester.PayTester.ServerUri.AbsoluteUri + $"rates/?cryptoCode=BTC&token={token}";
var req = new HttpRequestMessage(HttpMethod.Get, getRates);
req.Headers.Add("x-signature", NBitpayClient.Extensions.BitIdExtensions.GetBitIDSignature(k, getRates, null));
req.Headers.Add("x-identity", k.PubKey.ToHex());
var resp = await client.SendAsync(req);
resp.EnsureSuccessStatusCode();
// Can generate API Key
var repo = tester.PayTester.GetService<TokenRepository>();
@ -1168,7 +1178,6 @@ namespace BTCPayServer.Tests
apiKey = apiKey2;
// Can create an invoice with this new API Key
HttpClient client = new HttpClient();
HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Post,
tester.PayTester.ServerUri.AbsoluteUri + "invoices");
message.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic",
@ -1457,7 +1466,7 @@ namespace BTCPayServer.Tests
// via UI
var controller = user.GetController<UIInvoiceController>();
var model = await controller.CreateInvoice();
await controller.CreateInvoice();
(await controller.CreateInvoice(new CreateInvoiceModel(), default)).AssertType<RedirectToActionResult>();
invoice = await client.GetInvoice(user.StoreId, controller.CreatedInvoiceId);
Assert.Equal("EUR", invoice.Currency);
@ -1937,6 +1946,173 @@ namespace BTCPayServer.Tests
entity.GetPaymentMethods().First().Calculate();
}
[Fact()]
[Trait("Integration", "Integration")]
public async Task EnsureWebhooksTrigger()
{
using var tester = CreateServerTester();
await tester.StartAsync();
var user = tester.NewAccount();
await user.GrantAccessAsync();
user.RegisterDerivationScheme("BTC");
await user.SetupWebhook();
var client = await user.CreateClient();
var invoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest()
{
Amount = 0.00m,
Currency = "BTC"
});;
await user.AssertHasWebhookEvent(WebhookEventType.InvoiceCreated, (WebhookInvoiceEvent x)=> Assert.Equal(invoice.Id, x.InvoiceId));
//invoice payment webhooks
invoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest()
{
Amount = 0.01m,
Currency = "BTC"
});
var invoicePaymentRequest = new BitcoinUrlBuilder((await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id)).Single(model =>
PaymentMethodId.Parse(model.PaymentMethod) ==
new PaymentMethodId("BTC", BitcoinPaymentType.Instance))
.PaymentLink, tester.ExplorerNode.Network);
var halfPaymentTx = await tester.ExplorerNode.SendToAddressAsync(invoicePaymentRequest.Address, Money.Coins(invoicePaymentRequest.Amount.ToDecimal(MoneyUnit.BTC)/2m));
invoicePaymentRequest = new BitcoinUrlBuilder((await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id)).Single(model =>
PaymentMethodId.Parse(model.PaymentMethod) ==
new PaymentMethodId("BTC", BitcoinPaymentType.Instance))
.PaymentLink, tester.ExplorerNode.Network);
var remainingPaymentTx = await tester.ExplorerNode.SendToAddressAsync(invoicePaymentRequest.Address, Money.Coins(invoicePaymentRequest.Amount.ToDecimal(MoneyUnit.BTC)));
await user.AssertHasWebhookEvent(WebhookEventType.InvoiceCreated, (WebhookInvoiceEvent x)=> Assert.Equal(invoice.Id, x.InvoiceId));
await user.AssertHasWebhookEvent(WebhookEventType.InvoiceProcessing, (WebhookInvoiceEvent x)=> Assert.Equal(invoice.Id, x.InvoiceId));
await user.AssertHasWebhookEvent(WebhookEventType.InvoiceReceivedPayment,
(WebhookInvoiceReceivedPaymentEvent x) =>
{
Assert.Equal(invoice.Id, x.InvoiceId);
Assert.Contains(halfPaymentTx.ToString(), x.Payment.Id);
});
await user.AssertHasWebhookEvent(WebhookEventType.InvoiceReceivedPayment,
(WebhookInvoiceReceivedPaymentEvent x) =>
{
Assert.Equal(invoice.Id, x.InvoiceId);
Assert.Contains(remainingPaymentTx.ToString(), x.Payment.Id);
});
await tester.ExplorerNode.GenerateAsync(1);
await user.AssertHasWebhookEvent(WebhookEventType.InvoicePaymentSettled,
(WebhookInvoiceReceivedPaymentEvent x) =>
{
Assert.Equal(invoice.Id, x.InvoiceId);
Assert.Contains(halfPaymentTx.ToString(), x.Payment.Id);
});
await user.AssertHasWebhookEvent(WebhookEventType.InvoicePaymentSettled,
(WebhookInvoiceReceivedPaymentEvent x) =>
{
Assert.Equal(invoice.Id, x.InvoiceId);
Assert.Contains(remainingPaymentTx.ToString(), x.Payment.Id);
});
await user.AssertHasWebhookEvent(WebhookEventType.InvoiceSettled, (WebhookInvoiceEvent x)=> Assert.Equal(invoice.Id, x.InvoiceId));
invoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest()
{
Amount = 0.01m,
Currency = "BTC",
});
invoicePaymentRequest = new BitcoinUrlBuilder((await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id)).Single(model =>
PaymentMethodId.Parse(model.PaymentMethod) ==
new PaymentMethodId("BTC", BitcoinPaymentType.Instance))
.PaymentLink, tester.ExplorerNode.Network);
halfPaymentTx = await tester.ExplorerNode.SendToAddressAsync(invoicePaymentRequest.Address, Money.Coins(invoicePaymentRequest.Amount.ToDecimal(MoneyUnit.BTC)/2m));
await user.AssertHasWebhookEvent(WebhookEventType.InvoiceCreated, (WebhookInvoiceEvent x)=> Assert.Equal(invoice.Id, x.InvoiceId));
await user.AssertHasWebhookEvent(WebhookEventType.InvoiceReceivedPayment,
(WebhookInvoiceReceivedPaymentEvent x) =>
{
Assert.Equal(invoice.Id, x.InvoiceId);
Assert.Contains(halfPaymentTx.ToString(), x.Payment.Id);
});
invoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest()
{
Amount = 0.01m,
Currency = "BTC"
});
await user.AssertHasWebhookEvent(WebhookEventType.InvoiceCreated, (WebhookInvoiceEvent x)=> Assert.Equal(invoice.Id, x.InvoiceId));
await client.MarkInvoiceStatus(user.StoreId, invoice.Id, new MarkInvoiceStatusRequest() { Status = InvoiceStatus.Invalid});
await user.AssertHasWebhookEvent(WebhookEventType.InvoiceInvalid, (WebhookInvoiceEvent x)=> Assert.Equal(invoice.Id, x.InvoiceId));
//payment request webhook test
var pr = await client.CreatePaymentRequest(user.StoreId, new CreatePaymentRequestRequest()
{
Amount = 100m,
Currency = "USD",
Title = "test pr",
//TODO: this is a bug, we should not have these props in create request
StoreId = user.StoreId,
FormResponse = new JObject(),
//END todo
Description = "lala baba"
});
await user.AssertHasWebhookEvent(WebhookEventType.PaymentRequestCreated, (WebhookPaymentRequestEvent x)=> Assert.Equal(pr.Id, x.PaymentRequestId));
pr = await client.UpdatePaymentRequest(user.StoreId, pr.Id,
new UpdatePaymentRequestRequest() { Title = "test pr updated", Amount = 100m,
Currency = "USD",
//TODO: this is a bug, we should not have these props in create request
StoreId = user.StoreId,
FormResponse = new JObject(),
//END todo
Description = "lala baba"});
await user.AssertHasWebhookEvent(WebhookEventType.PaymentRequestUpdated, (WebhookPaymentRequestEvent x)=> Assert.Equal(pr.Id, x.PaymentRequestId));
var inv = await client.PayPaymentRequest(user.StoreId, pr.Id, new PayPaymentRequestRequest() {});
await client.MarkInvoiceStatus(user.StoreId, inv.Id, new MarkInvoiceStatusRequest() { Status = InvoiceStatus.Settled});
await user.AssertHasWebhookEvent(WebhookEventType.PaymentRequestStatusChanged, (WebhookPaymentRequestEvent x)=>
{
Assert.Equal(PaymentRequestData.PaymentRequestStatus.Completed, x.Status);
Assert.Equal(pr.Id, x.PaymentRequestId);
});
await client.ArchivePaymentRequest(user.StoreId, pr.Id);
await user.AssertHasWebhookEvent(WebhookEventType.PaymentRequestArchived, (WebhookPaymentRequestEvent x)=> Assert.Equal(pr.Id, x.PaymentRequestId));
//payoyt webhooks test
var payout = await client.CreatePayout(user.StoreId,
new CreatePayoutThroughStoreRequest()
{
Amount = 0.0001m,
Destination = (await tester.ExplorerNode.GetNewAddressAsync()).ToString(),
Approved = true,
PaymentMethod = "BTC"
});
await user.AssertHasWebhookEvent(WebhookEventType.PayoutCreated, (WebhookPayoutEvent x)=> Assert.Equal(payout.Id, x.PayoutId));
await client.MarkPayout(user.StoreId, payout.Id, new MarkPayoutRequest(){ State = PayoutState.AwaitingApproval});
await user.AssertHasWebhookEvent(WebhookEventType.PayoutUpdated, (WebhookPayoutEvent x)=>
{
Assert.Equal(payout.Id, x.PayoutId);
Assert.Equal(PayoutState.AwaitingApproval, x.PayoutState);
});
await client.ApprovePayout(user.StoreId, payout.Id, new ApprovePayoutRequest(){});
await user.AssertHasWebhookEvent(WebhookEventType.PayoutApproved, (WebhookPayoutEvent x)=>
{
Assert.Equal(payout.Id, x.PayoutId);
Assert.Equal(PayoutState.AwaitingPayment, x.PayoutState);
});
await client.CancelPayout(user.StoreId, payout.Id );
await user.AssertHasWebhookEvent(WebhookEventType.PayoutUpdated, (WebhookPayoutEvent x)=>
{
Assert.Equal(payout.Id, x.PayoutId);
Assert.Equal(PayoutState.Cancelled, x.PayoutState);
});
}
[Fact(Timeout = LongRunningTestTimeout)]
[Trait("Integration", "Integration")]
public async Task InvoiceFlowThroughDifferentStatesCorrectly()
@ -2139,8 +2315,7 @@ namespace BTCPayServer.Tests
user.RegisterDerivationScheme("BTC");
var serverController = user.GetController<UIServerController>();
var vm = Assert.IsType<LogsViewModel>(
Assert.IsType<ViewResult>(await serverController.LogsView()).Model);
Assert.IsType<LogsViewModel>(Assert.IsType<ViewResult>(await serverController.LogsView()).Model);
}
[Fact(Timeout = LongRunningTestTimeout)]
@ -2320,17 +2495,21 @@ namespace BTCPayServer.Tests
using var tester = CreateServerTester(newDb: true);
await tester.StartAsync();
var f = tester.PayTester.GetService<ApplicationDbContextFactory>();
const string id = "BTCPayServer.Services.PoliciesSettings";
using (var ctx = f.CreateContext())
{
var setting = new SettingData() { Id = "BTCPayServer.Services.PoliciesSettings" };
setting.Value = JObject.Parse("{\"RootAppId\": null, \"RootAppType\": 1, \"Experimental\": false, \"PluginSource\": null, \"LockSubscription\": false, \"DisableSSHService\": false, \"PluginPreReleases\": false, \"BlockExplorerLinks\": [],\"DomainToAppMapping\": [{\"AppId\": \"87kj5yKay8mB4UUZcJhZH5TqDKMD3CznjwLjiu1oYZXe\", \"Domain\": \"donate.nicolas-dorier.com\", \"AppType\": 0}], \"CheckForNewVersions\": false, \"AllowHotWalletForAll\": false, \"RequiresConfirmedEmail\": false, \"DiscourageSearchEngines\": false, \"DisableInstantNotifications\": false, \"DisableNonAdminCreateUserApi\": false, \"AllowHotWalletRPCImportForAll\": false, \"AllowLightningInternalNodeForAll\": false, \"DisableStoresToUseServerEmailSettings\": false}").ToString();
// remove existing policies setting
var setting = await ctx.Settings.FirstOrDefaultAsync(c => c.Id == id);
if (setting != null) ctx.Settings.Remove(setting);
// create legacy policies setting that needs migration
setting = new SettingData { Id = id, Value = JObject.Parse("{\"RootAppId\": null, \"RootAppType\": 1, \"Experimental\": false, \"PluginSource\": null, \"LockSubscription\": false, \"DisableSSHService\": false, \"PluginPreReleases\": false, \"BlockExplorerLinks\": [],\"DomainToAppMapping\": [{\"AppId\": \"87kj5yKay8mB4UUZcJhZH5TqDKMD3CznjwLjiu1oYZXe\", \"Domain\": \"donate.nicolas-dorier.com\", \"AppType\": 0}], \"CheckForNewVersions\": false, \"AllowHotWalletForAll\": false, \"RequiresConfirmedEmail\": false, \"DiscourageSearchEngines\": false, \"DisableNonAdminCreateUserApi\": false, \"AllowHotWalletRPCImportForAll\": false, \"AllowLightningInternalNodeForAll\": false, \"DisableStoresToUseServerEmailSettings\": false}").ToString() };
ctx.Settings.Add(setting);
await ctx.SaveChangesAsync();
}
await RestartMigration(tester);
using (var ctx = f.CreateContext())
{
var setting = await ctx.Settings.FirstOrDefaultAsync(c => c.Id == "BTCPayServer.Services.PoliciesSettings");
var setting = await ctx.Settings.FirstOrDefaultAsync(c => c.Id == id);
var o = JObject.Parse(setting.Value);
Assert.Equal("Crowdfund", o["RootAppType"].Value<string>());
o = (JObject)((JArray)o["DomainToAppMapping"])[0];
@ -2394,7 +2573,7 @@ namespace BTCPayServer.Tests
Assert.NotNull(lnMethod.GetExternalLightningUrl());
var url = lnMethod.GetExternalLightningUrl();
var kv = LightningConnectionStringHelper.ExtractValues(url, out var connType);
LightningConnectionStringHelper.ExtractValues(url, out var connType);
Assert.Equal(LightningConnectionType.Charge, connType);
var client = Assert.IsType<ChargeClient>(tester.PayTester.GetService<LightningClientFactoryService>()
.Create(url, tester.NetworkProvider.GetNetwork<BTCPayNetwork>("BTC")));
@ -2771,7 +2950,7 @@ namespace BTCPayServer.Tests
Assert.Equal(fileContent, data);
//create a temporary link to file
var tmpLinkGenerate = Assert.IsType<RedirectToActionResult>(await controller.CreateTemporaryFileUrl(fileId,
Assert.IsType<RedirectToActionResult>(await controller.CreateTemporaryFileUrl(fileId,
new UIServerController.CreateTemporaryFileUrlViewModel
{
IsDownload = true,

View File

@ -200,26 +200,33 @@ namespace BTCPayServer.Tests
if (!askedPrompt)
{
driver.FindElement(By.XPath("//a[contains(text(), \"New chat\")]")).Click();
driver.FindElements(By.XPath("//button[contains(@class,'text-token-text-primary')]")).Where(e => e.Displayed).First().Click();
Thread.Sleep(200);
var input = driver.FindElement(By.XPath("//textarea[@data-id]"));
input.SendKeys($"I am translating a checkout crypto payment page, and I want you to translate it from English (en-US) to {languageCurrent} ({jsonLangCode}).");
input.SendKeys(Keys.LeftShift + Keys.Enter);
input.SendKeys("Reply only with the translation of the sentences I will give you and nothing more." + Keys.Enter);
input.SendKeys("Reply only with the translation of the sentences I will give you and nothing more, and do not translate what is inside `{{` and `}}`." + Keys.Enter);
WaitCanWritePrompt(driver);
askedPrompt = true;
}
english = english.Replace('\n', ' ');
driver.FindElement(By.XPath("//textarea[@data-id]")).SendKeys(english + Keys.Enter);
WaitCanWritePrompt(driver);
var elements = driver.FindElements(By.XPath("//div[contains(@class,'markdown') and contains(@class,'prose')]//p"));
var result = elements.Last().Text;
string result = GetLastResponse(driver);
langFile.Words[translation.Key] = result;
}
langFile.Save();
}
}
private static string GetLastResponse(ChromeDriver driver)
{
var elements = driver.FindElements(By.XPath("//div[contains(@class,'markdown') and contains(@class,'prose')]//p"));
var result = elements.LastOrDefault()?.Text;
return result;
}
private static TransifexClient GetTransifexClient()
{
return new TransifexClient(FactWithSecretAttribute.GetFromSecrets("TransifexAPIToken"));
@ -227,12 +234,27 @@ namespace BTCPayServer.Tests
private void WaitCanWritePrompt(IWebDriver driver)
{
bool stopGenerating = false;
retry:
Thread.Sleep(200);
try
{
driver.FindElement(By.XPath("//*[contains(text(), \"Regenerate response\")]"));
var el = driver.FindElement(By.XPath("//button[contains(@aria-label, 'Stop generating')]"));
if (!el.Enabled)
goto retry;
stopGenerating = true;
goto retry;
}
catch
{
if (!stopGenerating)
goto retry;
}
try
{
var el = driver.FindElement(By.XPath("//button[contains(@data-testid, 'send-button')]"));
if (!el.Displayed)
goto retry;
}
catch
{
@ -409,7 +431,7 @@ retry:
content.Headers.TryAddWithoutValidation("Content-Type", "application/vnd.api+json;profile=\"bulk\"");
message.Content = content;
using var response = await Client.SendAsync(message);
var str = await response.Content.ReadAsStringAsync();
await response.Content.ReadAsStringAsync();
}).ToArray());
}

View File

@ -99,7 +99,7 @@ services:
custom:
nbxplorer:
image: nicolasdorier/nbxplorer:2.4.0
image: nicolasdorier/nbxplorer:2.5.0
restart: unless-stopped
ports:
- "32838:32838"
@ -224,7 +224,7 @@ services:
- "5432"
merchant_lnd:
image: btcpayserver/lnd:v0.17.2-beta
image: btcpayserver/lnd:v0.17.4-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -259,7 +259,7 @@ services:
- bitcoind
customer_lnd:
image: btcpayserver/lnd:v0.17.2-beta
image: btcpayserver/lnd:v0.17.4-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"

View File

@ -96,7 +96,7 @@ services:
custom:
nbxplorer:
image: nicolasdorier/nbxplorer:2.4.0
image: nicolasdorier/nbxplorer:2.5.0
restart: unless-stopped
ports:
- "32838:32838"
@ -211,7 +211,7 @@ services:
- "5432"
merchant_lnd:
image: btcpayserver/lnd:v0.17.2-beta
image: btcpayserver/lnd:v0.17.4-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -248,7 +248,7 @@ services:
- bitcoind
customer_lnd:
image: btcpayserver/lnd:v0.17.2-beta
image: btcpayserver/lnd:v0.17.4-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"

View File

@ -0,0 +1,2 @@
$container_id="$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=customer_lnd)"
docker exec -ti $container_id lncli --no-macaroons --rpcserver localhost:10008 $args

View File

@ -0,0 +1,2 @@
$container_id="$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=merchant_lnd)"
docker exec -ti $container_id lncli --no-macaroons --rpcserver localhost:10008 $args

View File

@ -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" />
@ -46,13 +46,13 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="BTCPayServer.NTag424" Version="1.0.19" />
<PackageReference Include="BTCPayServer.NTag424" Version="1.0.22" />
<PackageReference Include="YamlDotNet" Version="8.0.0" />
<PackageReference Include="BIP78.Sender" Version="0.2.2" />
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.2" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.5.3" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.5.4" />
<PackageReference Include="CsvHelper" Version="15.0.5" />
<PackageReference Include="Dapper" Version="2.1.24" />
<PackageReference Include="Dapper" Version="2.1.28" />
<PackageReference Include="Fido2" Version="2.0.2" />
<PackageReference Include="Fido2.AspNet" Version="2.0.2" />
<PackageReference Include="LNURL" Version="0.0.34" />
@ -77,8 +77,8 @@
<PackageReference Include="TwentyTwenty.Storage.Azure" Version="2.12.1" />
<PackageReference Include="TwentyTwenty.Storage.Google" Version="2.12.1" />
<PackageReference Include="TwentyTwenty.Storage.Local" Version="2.12.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.1" />
</ItemGroup>
<ItemGroup>

View File

@ -30,7 +30,7 @@ public class AppSales : ViewComponent
public async Task<IViewComponentResult> InvokeAsync(string appId, string appType)
{
var type = _appService.GetAppType(appType);
if (type is not IHasSaleStatsAppType salesAppType || type is not AppBaseType appBaseType)
if (type is not IHasSaleStatsAppType || type is not AppBaseType appBaseType)
return new HtmlContentViewComponentResult(new StringHtmlContent(string.Empty));
var vm = new AppSalesViewModel
{

View File

@ -252,7 +252,7 @@
{
<ul id="mainNavSettings" class="navbar-nav border-top p-3 px-lg-4">
<li class="nav-item" permission="@Policies.CanModifyServerSettings">
<a asp-area="" asp-controller="UIServer" asp-action="ListUsers" class="nav-link @ViewData.IsActivePage(ServerNavPages.Users) @ViewData.IsActivePage(ServerNavPages.Emails) @ViewData.IsActivePage(ServerNavPages.Policies) @ViewData.IsActivePage(ServerNavPages.Services) @ViewData.IsActivePage(ServerNavPages.Theme) @ViewData.IsActivePage(ServerNavPages.Maintenance) @ViewData.IsActivePage(ServerNavPages.Logs) @ViewData.IsActivePage(ServerNavPages.Files)" id="Nav-ServerSettings">
<a asp-area="" asp-controller="UIServer" asp-action="ListUsers" class="nav-link @ViewData.IsActivePage(ServerNavPages.Users) @ViewData.IsActivePage(ServerNavPages.Emails) @ViewData.IsActivePage(ServerNavPages.Policies) @ViewData.IsActivePage(ServerNavPages.Services) @ViewData.IsActivePage(ServerNavPages.Branding) @ViewData.IsActivePage(ServerNavPages.Maintenance) @ViewData.IsActivePage(ServerNavPages.Logs) @ViewData.IsActivePage(ServerNavPages.Files)" id="Nav-ServerSettings">
<vc:icon symbol="server-settings"/>
<span>Server Settings</span>
</a>

View File

@ -1,5 +1,8 @@
@model BTCPayServer.Components.StoreLightningBalance.StoreLightningBalanceViewModel
@if(!Model.InitialRendering && Model.Balance == null)
{
return;
}
<div id="StoreLightningBalance-@Model.Store.Id" class="widget store-lightning-balance">
<div class="d-flex gap-3 align-items-center justify-content-between mb-2">
<h6>Lightning Balance</h6>

View File

@ -2,6 +2,7 @@ using System;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client;
using BTCPayServer.Configuration;
using BTCPayServer.Data;
using BTCPayServer.Lightning;
@ -9,9 +10,11 @@ using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Security;
using BTCPayServer.Services;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
@ -26,6 +29,7 @@ public class StoreLightningBalance : ViewComponent
private readonly LightningClientFactoryService _lightningClientFactory;
private readonly IOptions<LightningNetworkOptions> _lightningNetworkOptions;
private readonly IOptions<ExternalServicesOptions> _externalServiceOptions;
private readonly IAuthorizationService _authorizationService;
public StoreLightningBalance(
StoreRepository storeRepo,
@ -34,13 +38,15 @@ public class StoreLightningBalance : ViewComponent
BTCPayServerOptions btcpayServerOptions,
LightningClientFactoryService lightningClientFactory,
IOptions<LightningNetworkOptions> lightningNetworkOptions,
IOptions<ExternalServicesOptions> externalServiceOptions)
IOptions<ExternalServicesOptions> externalServiceOptions,
IAuthorizationService authorizationService)
{
_storeRepo = storeRepo;
_currencies = currencies;
_networkProvider = networkProvider;
_btcpayServerOptions = btcpayServerOptions;
_externalServiceOptions = externalServiceOptions;
_authorizationService = authorizationService;
_lightningClientFactory = lightningClientFactory;
_lightningNetworkOptions = lightningNetworkOptions;
}
@ -54,13 +60,19 @@ public class StoreLightningBalance : ViewComponent
vm.DefaultCurrency = vm.Store.GetStoreBlob().DefaultCurrency;
vm.CurrencyData = _currencies.GetCurrencyData(vm.DefaultCurrency, true);
if (vm.InitialRendering)
return View(vm);
try
{
var lightningClient = GetLightningClient(vm.Store, vm.CryptoCode);
var lightningClient = await GetLightningClient(vm.Store, vm.CryptoCode);
if (lightningClient == null)
{
vm.InitialRendering = false;
return View(vm);
}
if (vm.InitialRendering)
return View(vm);
var balance = await lightningClient.GetBalance();
vm.Balance = balance;
vm.TotalOnchain = balance.OnchainBalance != null
@ -72,7 +84,8 @@ public class StoreLightningBalance : ViewComponent
(balance.OffchainBalance.Closing ?? 0)
: null;
}
catch (NotSupportedException)
catch (Exception ex) when (ex is NotImplementedException or NotSupportedException)
{
// not all implementations support balance fetching
vm.ProblemDescription = "Your node does not support balance fetching.";
@ -85,7 +98,7 @@ public class StoreLightningBalance : ViewComponent
return View(vm);
}
private ILightningClient GetLightningClient(StoreData store, string cryptoCode)
private async Task<ILightningClient> GetLightningClient(StoreData store, string cryptoCode )
{
var network = _networkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
var id = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
@ -101,7 +114,9 @@ public class StoreLightningBalance : ViewComponent
}
if (existing.IsInternalNode && _lightningNetworkOptions.Value.InternalLightningByCryptoCode.TryGetValue(cryptoCode, out var internalLightningNode))
{
return internalLightningNode;
var result = await _authorizationService.AuthorizeAsync(HttpContext.User, null,
new PolicyRequirement(Policies.CanUseInternalLightningNode));
return result.Succeeded ? internalLightningNode : null;
}
return null;

View File

@ -2,6 +2,7 @@
@using BTCPayServer.Client
@using BTCPayServer.Components.MainLogo
@using BTCPayServer.Services
@using BTCPayServer.Views.Server
@using BTCPayServer.Views.Stores
@inject BTCPayServerEnvironment Env
@inject IFileService FileService
@ -53,19 +54,23 @@ else
@foreach (var option in Model.Options)
{
<li>
<a asp-controller="UIStores" asp-action="Index" asp-route-storeId="@option.Value" class="dropdown-item@(option.Selected ? " active" : "")" id="StoreSelectorMenuItem-@option.Value">@StoreName(option.Text)</a>
<a asp-controller="UIStores" asp-action="Index" asp-route-storeId="@option.Value" class="dropdown-item@(option.Selected && ViewData.IsActivePage(ServerNavPages.Stores) != "active" ? " active" : "")" id="StoreSelectorMenuItem-@option.Value">@StoreName(option.Text)</a>
</li>
}
@if (Model.Options.Any())
{
<li><hr class="dropdown-divider"></li>
}
<li ><a asp-controller="UIUserStores" asp-action="CreateStore" class="dropdown-item @ViewData.IsActivePage(StoreNavPages.Create)" id="StoreSelectorCreate">Create Store</a></li>
<li><a asp-controller="UIUserStores" asp-action="CreateStore" class="dropdown-item @ViewData.IsActivePage(StoreNavPages.Create)" id="StoreSelectorCreate">Create Store</a></li>
@if (Model.ArchivedCount > 0)
{
<li><hr class="dropdown-divider"></li>
<li><a asp-controller="UIUserStores" asp-action="ListStores" asp-route-archived="true" class="dropdown-item @ViewData.IsActivePage(StoreNavPages.Index)" id="StoreSelectorArchived">@Model.ArchivedCount Archived Store@(Model.ArchivedCount == 1 ? "" : "s")</a></li>
}
@*
<li permission="@Policies.CanModifyServerSettings"><hr class="dropdown-divider"></li>
<li permission="@Policies.CanModifyServerSettings"><a asp-controller="UIServer" asp-action="ListStores" class="dropdown-item @ViewData.IsActivePage(ServerNavPages.Stores)" id="StoreSelectorAdminStores">Admin Store Overview</a></li>
*@
</ul>
</div>
</div>

View File

@ -64,13 +64,14 @@
const id = `StoreWalletBalance-${storeId}`;
const baseUrl = @Safe.Json(Url.Action("WalletHistogram", "UIWallets", new { walletId = Model.WalletId, type = WalletHistogramType.Week }));
const valueTransform = value => rate
? DashboardUtils.displayDefaultCurrency(value, rate, defaultCurrency, divisibility).toString()
: value
const chartOpts = {
fullWidth: true,
showArea: true,
axisY: {
labelInterpolationFnc: value => rate
? DashboardUtils.displayDefaultCurrency(value, rate, defaultCurrency, divisibility).toString()
: value
labelInterpolationFnc: valueTransform
}
};
@ -80,16 +81,22 @@
document.querySelectorAll(`#${id} .currency`).forEach(c => c.innerText = currency)
document.querySelectorAll(`#${id} [data-balance]`).forEach(c => {
const value = Number.parseFloat(c.dataset.balance);
c.innerText = rate
? DashboardUtils.displayDefaultCurrency(value, rate, currency, divisibility)
: value
c.innerText = valueTransform(value)
});
if (!series) return;
const min = Math.min(...series);
const max = Math.max(...series);
const low = Math.max(min - ((max - min) / 5), 0);
const renderOpts = Object.assign({}, chartOpts, { low });
const tooltip = Chartist.plugins.tooltip2({
template: '{{value}}',
offset: {
x: 0,
y: -16
},
valueTransformFunction: valueTransform
})
const renderOpts = Object.assign({}, chartOpts, { low, plugins: [tooltip] });
const chart = new Chartist.Line(`#${id} .ct-chart`, {
labels,
series: [series]

View File

@ -48,7 +48,7 @@ namespace BTCPayServer.Controllers
[Route("rates/{baseCurrency}")]
[HttpGet]
[BitpayAPIConstraint]
public async Task<IActionResult> GetBaseCurrencyRates(string baseCurrency, CancellationToken cancellationToken)
public async Task<IActionResult> GetBaseCurrencyRates(string baseCurrency, string cryptoCode = null, CancellationToken cancellationToken = default)
{
var supportedMethods = CurrentStore.GetSupportedPaymentMethods(_networkProvider);
@ -57,16 +57,16 @@ namespace BTCPayServer.Controllers
var currencypairs = BuildCurrencyPairs(currencyCodes, baseCurrency);
var result = await GetRates2(currencypairs, null, cancellationToken);
var result = await GetRates2(currencypairs, null, cryptoCode, cancellationToken);
var rates = (result as JsonResult)?.Value as Rate[];
return rates == null ? result : Json(new DataWrapper<Rate[]>(rates));
}
[HttpGet("rates/{baseCurrency}/{currency}")]
[BitpayAPIConstraint]
public async Task<IActionResult> GetCurrencyPairRate(string baseCurrency, string currency, CancellationToken cancellationToken)
public async Task<IActionResult> GetCurrencyPairRate(string baseCurrency, string currency, string cryptoCode = null, CancellationToken cancellationToken = default)
{
var result = await GetRates2($"{baseCurrency}_{currency}", null, cancellationToken);
var result = await GetRates2($"{baseCurrency}_{currency}", null, cryptoCode, cancellationToken);
return (result as JsonResult)?.Value is not Rate[] rates
? result
: Json(new DataWrapper<Rate>(rates.First()));
@ -74,9 +74,9 @@ namespace BTCPayServer.Controllers
[HttpGet("rates")]
[BitpayAPIConstraint]
public async Task<IActionResult> GetRates(string currencyPairs, string storeId = null, CancellationToken cancellationToken = default)
public async Task<IActionResult> GetRates(string currencyPairs, string storeId = null, string cryptoCode = null, CancellationToken cancellationToken = default)
{
var result = await GetRates2(currencyPairs, storeId, cancellationToken);
var result = await GetRates2(currencyPairs, storeId, cryptoCode, cancellationToken);
return (result as JsonResult)?.Value is not Rate[] rates
? result
: Json(new DataWrapper<Rate[]>(rates));
@ -84,7 +84,7 @@ namespace BTCPayServer.Controllers
[AllowAnonymous]
[HttpGet("api/rates")]
public async Task<IActionResult> GetRates2(string currencyPairs, string storeId, CancellationToken cancellationToken)
public async Task<IActionResult> GetRates2(string currencyPairs, string storeId, string cryptoCode = null, CancellationToken cancellationToken = default)
{
var store = CurrentStore ?? await _storeRepo.FindStore(storeId);
if (store == null)
@ -95,7 +95,12 @@ namespace BTCPayServer.Controllers
}
if (currencyPairs == null)
{
currencyPairs = store.GetStoreBlob().GetDefaultCurrencyPairString();
var blob = store.GetStoreBlob();
currencyPairs = blob.GetDefaultCurrencyPairString();
if (string.IsNullOrEmpty(currencyPairs) && !string.IsNullOrWhiteSpace(cryptoCode))
{
currencyPairs = $"{blob.DefaultCurrency}_{cryptoCode}".ToUpperInvariant();
}
if (string.IsNullOrEmpty(currencyPairs))
{
var result = Json(new BitpayErrorsModel() { Error = "You need to setup the default currency pairs in 'Store Settings / Rates' or specify 'currencyPairs' query parameter (eg. BTC_USD,LTC_CAD)." });

View File

@ -168,6 +168,8 @@ namespace BTCPayServer.Controllers.Greenfield
Status = Client.Models.PaymentRequestData.PaymentRequestStatus.Pending,
Created = DateTimeOffset.UtcNow
};
request.FormResponse = null;
request.StoreId = storeId;
pr.SetBlob(request);
pr = await _paymentRequestRepository.CreateOrUpdatePaymentRequest(pr);
return Ok(FromModel(pr));
@ -196,6 +198,9 @@ namespace BTCPayServer.Controllers.Greenfield
}
var updatedPr = pr.First();
var blob = updatedPr.GetBlob();
request.FormResponse = blob.FormResponse;
request.StoreId = storeId;
updatedPr.SetBlob(request);
return Ok(FromModel(await _paymentRequestRepository.CreateOrUpdatePaymentRequest(updatedPr)));

View File

@ -386,7 +386,8 @@ namespace BTCPayServer.Controllers.Greenfield
Destination = destination.destination,
PullPaymentId = pullPaymentId,
Value = request.Amount,
PaymentMethodId = paymentMethodId
PaymentMethodId = paymentMethodId,
StoreId = pp.StoreId
});
return HandleClaimResult(result);

View File

@ -23,7 +23,7 @@ namespace BTCPayServer.Controllers.Greenfield
GenerateWalletRequest request)
{
AssertCryptoCodeWallet(cryptoCode, out var network, out var wallet);
AssertCryptoCodeWallet(cryptoCode, out var network, out _);
if (!_walletProvider.IsAvailable(network))
{

View File

@ -122,10 +122,11 @@ namespace BTCPayServer.Controllers.Greenfield
try
{
var strategy = DerivationSchemeSettings.Parse(paymentMethod.DerivationScheme, network);
var strategy = network.GetDerivationSchemeParser().Parse(paymentMethod.DerivationScheme);
var deposit = new NBXplorer.KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit);
var line = strategy.AccountDerivation.GetLineFor(deposit);
var line = strategy.GetLineFor(deposit);
var result = new OnChainPaymentMethodPreviewResultData();
for (var i = offset; i < amount; i++)
{
@ -134,8 +135,9 @@ namespace BTCPayServer.Controllers.Greenfield
new OnChainPaymentMethodPreviewResultData.OnChainPaymentMethodPreviewResultAddressItem()
{
KeyPath = deposit.GetKeyPath((uint)i).ToString(),
Address = address.ScriptPubKey.GetDestinationAddress(strategy.Network.NBitcoinNetwork)
.ToString()
Address =
network.NBXplorerNetwork.CreateAddress(strategy,deposit.GetKeyPath((uint)i), address.ScriptPubKey)
.ToString()
});
}
@ -168,10 +170,10 @@ namespace BTCPayServer.Controllers.Greenfield
if (!ModelState.IsValid)
return this.CreateValidationError(ModelState);
DerivationSchemeSettings strategy;
DerivationStrategyBase strategy;
try
{
strategy = DerivationSchemeSettings.Parse(paymentMethodData.DerivationScheme, network);
strategy = network.GetDerivationSchemeParser().Parse(paymentMethodData.DerivationScheme);
}
catch
{
@ -181,7 +183,7 @@ namespace BTCPayServer.Controllers.Greenfield
}
var deposit = new NBXplorer.KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit);
var line = strategy.AccountDerivation.GetLineFor(deposit);
var line = strategy.GetLineFor(deposit);
var result = new OnChainPaymentMethodPreviewResultData();
for (var i = offset; i < amount; i++)
{
@ -192,9 +194,9 @@ namespace BTCPayServer.Controllers.Greenfield
OnChainPaymentMethodPreviewResultAddressItem()
{
KeyPath = deposit.GetKeyPath((uint)i).ToString(),
Address = strategy.Network.NBXplorerNetwork.CreateAddress(strategy.AccountDerivation,
line.KeyPathTemplate.GetKeyPath((uint)i),
derivation.ScriptPubKey).ToString()
Address =
network.NBXplorerNetwork.CreateAddress(strategy,deposit.GetKeyPath((uint)i), derivation.ScriptPubKey)
.ToString()
});
}
@ -244,12 +246,13 @@ namespace BTCPayServer.Controllers.Greenfield
{
var store = Store;
var storeBlob = store.GetStoreBlob();
var strategy = DerivationSchemeSettings.Parse(request.DerivationScheme, network);
var strategy = network.GetDerivationSchemeParser().Parse(request.DerivationScheme);
if (strategy != null)
await wallet.TrackAsync(strategy.AccountDerivation);
strategy.Label = request.Label;
var signing = strategy.GetSigningAccountKeySettings();
if (request.AccountKeyPath is RootedKeyPath r)
await wallet.TrackAsync(strategy);
var dss = new DerivationSchemeSettings(strategy, network) {Label = request.Label,};
var signing = dss.GetSigningAccountKeySettings();
if (request.AccountKeyPath is { } r)
{
signing.AccountKeyPath = r.KeyPath;
signing.RootFingerprint = r.MasterFingerprint;
@ -260,7 +263,7 @@ namespace BTCPayServer.Controllers.Greenfield
signing.RootFingerprint = null;
}
store.SetSupportedPaymentMethod(id, strategy);
store.SetSupportedPaymentMethod(id, dss);
storeBlob.SetExcluded(id, !request.Enabled);
store.SetStoreBlob(storeBlob);
await _storeRepository.UpdateStore(store);

View File

@ -117,7 +117,7 @@ namespace BTCPayServer.Controllers.Greenfield
public async Task<IActionResult> GetOnChainFeeRate(string storeId, string cryptoCode, int? blockTarget = null)
{
if (IsInvalidWalletRequest(cryptoCode, out var network,
out var derivationScheme, out var actionResult))
out _, out var actionResult))
return actionResult;
var feeRateTarget = blockTarget ?? Store.GetStoreBlob().RecommendedFeeBlockTarget;
@ -164,8 +164,8 @@ namespace BTCPayServer.Controllers.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 var network,
out var derivationScheme, out var actionResult))
if (IsInvalidWalletRequest(cryptoCode, out _,
out _, out var actionResult))
return actionResult;
var addr = await _walletReceiveService.UnReserveAddress(new WalletId(storeId, cryptoCode));
@ -518,10 +518,6 @@ namespace BTCPayServer.Controllers.Greenfield
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
},

View File

@ -78,7 +78,7 @@ namespace BTCPayServer.Controllers.Greenfield
private void ValidateWebhookRequest(StoreWebhookBaseData create)
{
if (!Uri.TryCreate(create?.Url, UriKind.Absolute, out var uri))
if (!Uri.TryCreate(create?.Url, UriKind.Absolute, out _))
ModelState.AddModelError(nameof(Url), "Invalid Url");
}

View File

@ -240,7 +240,7 @@ namespace BTCPayServer.Controllers.Greenfield
}
if (!string.IsNullOrEmpty(request.DefaultPaymentMethod) &&
!PaymentMethodId.TryParse(request.DefaultPaymentMethod, out var defaultPaymentMethodId))
!PaymentMethodId.TryParse(request.DefaultPaymentMethod, out _))
{
ModelState.AddModelError(nameof(request.Name), "DefaultPaymentMethod is invalid");
}

View File

@ -92,6 +92,26 @@ namespace BTCPayServer.Controllers.Greenfield
$"{(request.Locked ? "Locking" : "Unlocking")} user failed");
}
[Authorize(Policy = Policies.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/users/{idOrEmail}/approve")]
public async Task<IActionResult> ApproveUser(string idOrEmail, ApproveUserRequest request)
{
var user = await _userManager.FindByIdOrEmail(idOrEmail);
if (user is null)
{
return this.UserNotFound();
}
var success = false;
if (user.RequiresApproval)
{
success = await _userService.SetUserApproval(user.Id, request.Approved, Request.GetAbsoluteRootUri());
}
return success ? Ok() : this.CreateAPIError("invalid-state",
$"{(request.Approved ? "Approving" : "Unapproving")} user failed");
}
[Authorize(Policy = Policies.CanViewUsers, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/users/")]
public async Task<ActionResult<ApplicationUserData[]>> GetUsers()
@ -163,7 +183,9 @@ namespace BTCPayServer.Controllers.Greenfield
UserName = request.Email,
Email = request.Email,
RequiresEmailConfirmation = policies.RequiresConfirmedEmail,
RequiresApproval = policies.RequiresUserApproval,
Created = DateTimeOffset.UtcNow,
Approved = !anyAdmin && isAdmin // auto-approve first admin
};
var passwordValidation = await this._passwordValidator.ValidateAsync(_userManager, user, request.Password);
if (!passwordValidation.Succeeded)

View File

@ -1084,6 +1084,13 @@ namespace BTCPayServer.Controllers.Greenfield
new LockUserRequest { Locked = disabled }));
}
public override async Task<bool> ApproveUser(string idOrEmail, bool approved, CancellationToken token = default)
{
return GetFromActionResult<bool>(
await GetController<GreenfieldUsersController>().ApproveUser(idOrEmail,
new ApproveUserRequest { Approved = approved }));
}
public override async Task<OnChainWalletTransactionData> PatchOnChainWalletTransaction(string storeId,
string cryptoCode, string transactionId,
PatchOnChainTransactionRequest request, bool force = false, CancellationToken token = default)

View File

@ -2,7 +2,6 @@ using System;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Security.Claims;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
@ -109,6 +108,7 @@ namespace BTCPayServer.Controllers
{
if (User.Identity.IsAuthenticated && string.IsNullOrEmpty(returnUrl))
return RedirectToLocal();
// Clear the existing external cookie to ensure a clean login process
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
@ -118,15 +118,13 @@ namespace BTCPayServer.Controllers
}
ViewData["ReturnUrl"] = returnUrl;
return View(nameof(Login), new LoginViewModel() { Email = email });
return View(nameof(Login), new LoginViewModel { Email = email });
}
[HttpPost("/login/code")]
[AllowAnonymous]
[ValidateAntiForgeryToken]
[RateLimitsFilter(ZoneLimits.Login, Scope = RateLimitsScope.RemoteAddress)]
public async Task<IActionResult> LoginWithCode(string loginCode, string returnUrl = null)
{
if (!string.IsNullOrEmpty(loginCode))
@ -134,17 +132,26 @@ namespace BTCPayServer.Controllers
var userId = _userLoginCodeService.Verify(loginCode);
if (userId is null)
{
ModelState.AddModelError(string.Empty,
"Login code was invalid");
return await Login(returnUrl, null);
TempData[WellKnownTempData.ErrorMessage] = "Login code was invalid";
return await Login(returnUrl);
}
var user = await _userManager.FindByIdAsync(userId);
_logger.LogInformation("User with ID {UserId} logged in with a login code.", user.Id);
var user = await _userManager.FindByIdAsync(userId);
if (!UserService.TryCanLogin(user, out var message))
{
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = user == null ? StatusMessageModel.StatusSeverity.Error : StatusMessageModel.StatusSeverity.Warning,
Message = message
});
return await Login(returnUrl);
}
_logger.LogInformation("User with ID {UserId} logged in with a login code", user!.Id);
await _signInManager.SignInAsync(user, false, "LoginCode");
return RedirectToLocal(returnUrl);
}
return await Login(returnUrl, null);
return await Login(returnUrl);
}
[HttpPost("/login")]
@ -161,24 +168,20 @@ namespace BTCPayServer.Controllers
ViewData["ReturnUrl"] = returnUrl;
if (ModelState.IsValid)
{
// Require the user to have a confirmed email before they can log on.
// Require the user to pass basic checks (approval, confirmed email, not disabled) before they can log on
var user = await _userManager.FindByEmailAsync(model.Email);
if (user != null)
const string errorMessage = "Invalid login attempt.";
if (!UserService.TryCanLogin(user, out var message))
{
if (user.RequiresEmailConfirmation && !await _userManager.IsEmailConfirmedAsync(user))
TempData.SetStatusMessageModel(new StatusMessageModel
{
ModelState.AddModelError(string.Empty,
"You must have a confirmed email to log in.");
return View(model);
}
}
else
{
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
Severity = user == null ? StatusMessageModel.StatusSeverity.Error : StatusMessageModel.StatusSeverity.Warning,
Message = message
});
return View(model);
}
var fido2Devices = await _fido2Service.HasCredentials(user.Id);
var fido2Devices = await _fido2Service.HasCredentials(user!.Id);
var lnurlAuthCredentials = await _lnurlAuthService.HasCredentials(user.Id);
if (!await _userManager.IsLockedOutAsync(user) && (fido2Devices || lnurlAuthCredentials))
{
@ -196,33 +199,30 @@ namespace BTCPayServer.Controllers
};
}
return View("SecondaryLogin", new SecondaryLoginViewModel()
return View("SecondaryLogin", new SecondaryLoginViewModel
{
LoginWith2FaViewModel = twoFModel,
LoginWithFido2ViewModel = fido2Devices ? await BuildFido2ViewModel(model.RememberMe, user) : null,
LoginWithLNURLAuthViewModel = lnurlAuthCredentials ? await BuildLNURLAuthViewModel(model.RememberMe, user) : null,
});
}
else
{
var incrementAccessFailedResult = await _userManager.AccessFailedAsync(user);
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return View(model);
}
await _userManager.AccessFailedAsync(user);
ModelState.AddModelError(string.Empty, errorMessage!);
return View(model);
}
var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: true);
if (result.Succeeded)
{
_logger.LogInformation($"User '{user.Id}' logged in.");
_logger.LogInformation("User {UserId} logged in", user.Id);
return RedirectToLocal(returnUrl);
}
if (result.RequiresTwoFactor)
{
return View("SecondaryLogin", new SecondaryLoginViewModel()
return View("SecondaryLogin", new SecondaryLoginViewModel
{
LoginWith2FaViewModel = new LoginWith2faViewModel()
LoginWith2FaViewModel = new LoginWith2faViewModel
{
RememberMe = model.RememberMe
}
@ -230,14 +230,12 @@ namespace BTCPayServer.Controllers
}
if (result.IsLockedOut)
{
_logger.LogWarning($"User '{user.Id}' account locked out.");
_logger.LogWarning("User {UserId} account locked out", user.Id);
return RedirectToAction(nameof(Lockout), new { user.LockoutEnd });
}
else
{
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return View(model);
}
ModelState.AddModelError(string.Empty, errorMessage);
return View(model);
}
// If we got this far, something failed, redisplay form
@ -253,7 +251,7 @@ namespace BTCPayServer.Controllers
{
return null;
}
return new LoginWithFido2ViewModel()
return new LoginWithFido2ViewModel
{
Data = r,
UserId = user.Id,
@ -263,7 +261,6 @@ namespace BTCPayServer.Controllers
return null;
}
private async Task<LoginWithLNURLAuthViewModel> BuildLNURLAuthViewModel(bool rememberMe, ApplicationUser user)
{
if (_btcPayServerEnvironment.IsSecure(HttpContext))
@ -273,15 +270,14 @@ namespace BTCPayServer.Controllers
{
return null;
}
return new LoginWithLNURLAuthViewModel()
return new LoginWithLNURLAuthViewModel
{
RememberMe = rememberMe,
UserId = user.Id,
LNURLEndpoint = new Uri(_linkGenerator.GetUriByAction(
action: nameof(UILNURLAuthController.LoginResponse),
controller: "UILNURLAuth",
values: new { userId = user.Id, action = "login", tag = "login", k1 = Encoders.Hex.EncodeData(r) }, Request.Scheme, Request.Host, Request.PathBase))
action: nameof(UILNURLAuthController.LoginResponse),
controller: "UILNURLAuth",
values: new { userId = user.Id, action = "login", tag = "login", k1 = Encoders.Hex.EncodeData(r) }, Request.Scheme, Request.Host, Request.PathBase) ?? string.Empty)
};
}
return null;
@ -298,14 +294,18 @@ namespace BTCPayServer.Controllers
}
ViewData["ReturnUrl"] = returnUrl;
var errorMessage = "Invalid login attempt.";
var user = await _userManager.FindByIdAsync(viewModel.UserId);
if (user == null)
if (!UserService.TryCanLogin(user, out var message))
{
return NotFound();
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = user == null ? StatusMessageModel.StatusSeverity.Error : StatusMessageModel.StatusSeverity.Warning,
Message = message
});
return RedirectToAction("Login");
}
var errorMessage = string.Empty;
try
{
var k1 = Encoders.Hex.DecodeData(viewModel.LNURLEndpoint.ParseQueryString().Get("k1"));
@ -313,34 +313,33 @@ namespace BTCPayServer.Controllers
storedk1.SequenceEqual(k1))
{
_lnurlAuthService.FinalLoginStore.TryRemove(viewModel.UserId, out _);
await _signInManager.SignInAsync(user, viewModel.RememberMe, "FIDO2");
_logger.LogInformation("User logged in.");
await _signInManager.SignInAsync(user!, viewModel.RememberMe, "FIDO2");
_logger.LogInformation("User logged in");
return RedirectToLocal(returnUrl);
}
errorMessage = "Invalid login attempt.";
}
catch (Exception e)
{
errorMessage = e.Message;
}
ModelState.AddModelError(string.Empty, errorMessage);
return View("SecondaryLogin", new SecondaryLoginViewModel()
if (!string.IsNullOrEmpty(errorMessage))
{
LoginWithFido2ViewModel = (await _fido2Service.HasCredentials(user.Id)) ? await BuildFido2ViewModel(viewModel.RememberMe, user) : null,
ModelState.AddModelError(string.Empty, errorMessage);
}
return View("SecondaryLogin", new SecondaryLoginViewModel
{
LoginWithFido2ViewModel = await _fido2Service.HasCredentials(user!.Id) ? await BuildFido2ViewModel(viewModel.RememberMe, user) : null,
LoginWithLNURLAuthViewModel = viewModel,
LoginWith2FaViewModel = !user.TwoFactorEnabled
? null
: new LoginWith2faViewModel()
: new LoginWith2faViewModel
{
RememberMe = viewModel.RememberMe
}
});
}
[HttpPost("/login/fido2")]
[AllowAnonymous]
[ValidateAntiForgeryToken]
@ -352,44 +351,50 @@ namespace BTCPayServer.Controllers
}
ViewData["ReturnUrl"] = returnUrl;
var errorMessage = "Invalid login attempt.";
var user = await _userManager.FindByIdAsync(viewModel.UserId);
if (user == null)
if (!UserService.TryCanLogin(user, out var message))
{
return NotFound();
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = user == null ? StatusMessageModel.StatusSeverity.Error : StatusMessageModel.StatusSeverity.Warning,
Message = message
});
return RedirectToAction("Login");
}
var errorMessage = string.Empty;
try
{
if (await _fido2Service.CompleteLogin(viewModel.UserId, JObject.Parse(viewModel.Response).ToObject<AuthenticatorAssertionRawResponse>()))
{
await _signInManager.SignInAsync(user, viewModel.RememberMe, "FIDO2");
_logger.LogInformation("User logged in.");
await _signInManager.SignInAsync(user!, viewModel.RememberMe, "FIDO2");
_logger.LogInformation("User logged in");
return RedirectToLocal(returnUrl);
}
errorMessage = "Invalid login attempt.";
}
catch (Fido2VerificationException e)
{
errorMessage = e.Message;
}
ModelState.AddModelError(string.Empty, errorMessage);
if (!string.IsNullOrEmpty(errorMessage))
{
ModelState.AddModelError(string.Empty, errorMessage);
}
viewModel.Response = null;
return View("SecondaryLogin", new SecondaryLoginViewModel()
return View("SecondaryLogin", new SecondaryLoginViewModel
{
LoginWithFido2ViewModel = viewModel,
LoginWithLNURLAuthViewModel = (await _lnurlAuthService.HasCredentials(user.Id)) ? await BuildLNURLAuthViewModel(viewModel.RememberMe, user) : null,
LoginWithLNURLAuthViewModel = await _lnurlAuthService.HasCredentials(user!.Id) ? await BuildLNURLAuthViewModel(viewModel.RememberMe, user) : null,
LoginWith2FaViewModel = !user.TwoFactorEnabled
? null
: new LoginWith2faViewModel()
: new LoginWith2faViewModel
{
RememberMe = viewModel.RememberMe
}
});
}
[HttpGet("/login/2fa")]
[AllowAnonymous]
public async Task<IActionResult> LoginWith2fa(bool rememberMe, string returnUrl = null)
@ -401,7 +406,6 @@ namespace BTCPayServer.Controllers
// Ensure the user has gone through the username & password screen first
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
if (user == null)
{
throw new ApplicationException($"Unable to load two-factor authentication user.");
@ -409,11 +413,11 @@ namespace BTCPayServer.Controllers
ViewData["ReturnUrl"] = returnUrl;
return View("SecondaryLogin", new SecondaryLoginViewModel()
return View("SecondaryLogin", new SecondaryLoginViewModel
{
LoginWith2FaViewModel = new LoginWith2faViewModel { RememberMe = rememberMe },
LoginWithFido2ViewModel = (await _fido2Service.HasCredentials(user.Id)) ? await BuildFido2ViewModel(rememberMe, user) : null,
LoginWithLNURLAuthViewModel = (await _lnurlAuthService.HasCredentials(user.Id)) ? await BuildLNURLAuthViewModel(rememberMe, user) : null,
LoginWithFido2ViewModel = await _fido2Service.HasCredentials(user.Id) ? await BuildFido2ViewModel(rememberMe, user) : null,
LoginWithLNURLAuthViewModel = await _lnurlAuthService.HasCredentials(user.Id) ? await BuildLNURLAuthViewModel(rememberMe, user) : null,
});
}
@ -437,32 +441,32 @@ namespace BTCPayServer.Controllers
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
if (!UserService.TryCanLogin(user, out var message))
{
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Warning,
Message = message
});
return View(model);
}
var authenticatorCode = model.TwoFactorCode.Replace(" ", string.Empty, StringComparison.InvariantCulture).Replace("-", string.Empty, StringComparison.InvariantCulture);
var result = await _signInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, rememberMe, model.RememberMachine);
if (result.Succeeded)
{
_logger.LogInformation("User with ID {UserId} logged in with 2fa.", user.Id);
_logger.LogInformation("User with ID {UserId} logged in with 2fa", user.Id);
return RedirectToLocal(returnUrl);
}
else if (result.IsLockedOut)
_logger.LogWarning("Invalid authenticator code entered for user with ID {UserId}", user.Id);
ModelState.AddModelError(string.Empty, "Invalid authenticator code.");
return View("SecondaryLogin", new SecondaryLoginViewModel
{
_logger.LogWarning("User with ID {UserId} account locked out.", user.Id);
return RedirectToAction(nameof(Lockout), new { user.LockoutEnd });
}
else
{
_logger.LogWarning("Invalid authenticator code entered for user with ID {UserId}.", user.Id);
ModelState.AddModelError(string.Empty, "Invalid authenticator code.");
return View("SecondaryLogin", new SecondaryLoginViewModel()
{
LoginWith2FaViewModel = model,
LoginWithFido2ViewModel = (await _fido2Service.HasCredentials(user.Id)) ? await BuildFido2ViewModel(rememberMe, user) : null,
LoginWithLNURLAuthViewModel = (await _lnurlAuthService.HasCredentials(user.Id)) ? await BuildLNURLAuthViewModel(rememberMe, user) : null,
});
}
LoginWith2FaViewModel = model,
LoginWithFido2ViewModel = await _fido2Service.HasCredentials(user.Id) ? await BuildFido2ViewModel(rememberMe, user) : null,
LoginWithLNURLAuthViewModel = await _lnurlAuthService.HasCredentials(user.Id) ? await BuildLNURLAuthViewModel(rememberMe, user) : null,
});
}
[HttpGet("/login/recovery-code")]
@ -504,30 +508,35 @@ namespace BTCPayServer.Controllers
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
if (user == null)
{
throw new ApplicationException($"Unable to load two-factor authentication user.");
throw new ApplicationException("Unable to load two-factor authentication user.");
}
if (!UserService.TryCanLogin(user, out var message))
{
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Warning,
Message = message
});
return View(model);
}
var recoveryCode = model.RecoveryCode.Replace(" ", string.Empty, StringComparison.InvariantCulture);
var result = await _signInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode);
if (result.Succeeded)
{
_logger.LogInformation("User with ID {UserId} logged in with a recovery code.", user.Id);
_logger.LogInformation("User with ID {UserId} logged in with a recovery code", user.Id);
return RedirectToLocal(returnUrl);
}
if (result.IsLockedOut)
{
_logger.LogWarning("User with ID {UserId} account locked out.", user.Id);
_logger.LogWarning("User with ID {UserId} account locked out", user.Id);
return RedirectToAction(nameof(Lockout), new { user.LockoutEnd });
}
else
{
_logger.LogWarning("Invalid recovery code entered for user with ID {UserId}", user.Id);
ModelState.AddModelError(string.Empty, "Invalid recovery code entered.");
return View();
}
_logger.LogWarning("Invalid recovery code entered for user with ID {UserId}", user.Id);
ModelState.AddModelError(string.Empty, "Invalid recovery code entered.");
return View();
}
[HttpGet("/login/lockout")]
@ -540,7 +549,7 @@ namespace BTCPayServer.Controllers
[HttpGet("/register")]
[AllowAnonymous]
[RateLimitsFilter(ZoneLimits.Register, Scope = RateLimitsScope.RemoteAddress)]
public IActionResult Register(string returnUrl = null, bool logon = true)
public IActionResult Register(string returnUrl = null)
{
if (!CanLoginOrRegister())
{
@ -567,32 +576,36 @@ namespace BTCPayServer.Controllers
var policies = await _SettingsRepository.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
if (policies.LockSubscription && !User.IsInRole(Roles.ServerAdmin))
return RedirectToAction(nameof(UIHomeController.Index), "UIHome");
if (ModelState.IsValid)
{
var anyAdmin = (await _userManager.GetUsersInRoleAsync(Roles.ServerAdmin)).Any();
var isFirstAdmin = !anyAdmin || (model.IsAdmin && _Options.CheatMode);
var user = new ApplicationUser
{
UserName = model.Email,
Email = model.Email,
RequiresEmailConfirmation = policies.RequiresConfirmedEmail,
Created = DateTimeOffset.UtcNow
RequiresApproval = policies.RequiresUserApproval,
Created = DateTimeOffset.UtcNow,
Approved = isFirstAdmin // auto-approve first admin
};
var result = await _userManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
var admin = await _userManager.GetUsersInRoleAsync(Roles.ServerAdmin);
if (admin.Count == 0 || (model.IsAdmin && _Options.CheatMode))
if (isFirstAdmin)
{
await _RoleManager.CreateAsync(new IdentityRole(Roles.ServerAdmin));
await _userManager.AddToRoleAsync(user, Roles.ServerAdmin);
var settings = await _SettingsRepository.GetSettingAsync<ThemeSettings>();
var settings = await _SettingsRepository.GetSettingAsync<ThemeSettings>() ?? new ThemeSettings();
settings.FirstRun = false;
await _SettingsRepository.UpdateSetting<ThemeSettings>(settings);
await _SettingsRepository.UpdateSetting(settings);
await _SettingsRepository.FirstAdminRegistered(policies, _Options.UpdateUrl != null, _Options.DisableRegistration, Logs);
RegisteredAdmin = true;
}
_eventAggregator.Publish(new UserRegisteredEvent()
_eventAggregator.Publish(new UserRegisteredEvent
{
RequestUri = Request.GetAbsoluteRootUri(),
User = user,
@ -600,19 +613,29 @@ namespace BTCPayServer.Controllers
});
RegisteredUserId = user.Id;
if (!policies.RequiresConfirmedEmail)
TempData[WellKnownTempData.SuccessMessage] = "Account created.";
if (policies.RequiresConfirmedEmail)
{
if (logon)
await _signInManager.SignInAsync(user, isPersistent: false);
TempData[WellKnownTempData.SuccessMessage] += " Please confirm your email.";
}
if (policies.RequiresUserApproval)
{
TempData[WellKnownTempData.SuccessMessage] += " The new account requires approval by an admin before you can log in.";
}
if (policies.RequiresConfirmedEmail || policies.RequiresUserApproval)
{
return RedirectToAction(nameof(Login));
}
if (logon)
{
await _signInManager.SignInAsync(user, isPersistent: false);
return RedirectToLocal(returnUrl);
}
else
{
TempData[WellKnownTempData.SuccessMessage] = "Account created, please confirm your email";
return View();
}
}
AddErrors(result);
else
{
AddErrors(result);
}
}
// If we got this far, something failed, redisplay form
@ -628,8 +651,8 @@ namespace BTCPayServer.Controllers
{
await _signInManager.SignOutAsync();
HttpContext.DeleteUserPrefsCookie();
_logger.LogInformation("User logged out.");
return RedirectToAction(nameof(UIAccountController.Login));
_logger.LogInformation("User logged out");
return RedirectToAction(nameof(Login));
}
[HttpGet("/register/confirm-email")]
@ -650,7 +673,7 @@ namespace BTCPayServer.Controllers
if (!await _userManager.HasPasswordAsync(user))
{
TempData.SetStatusMessageModel(new StatusMessageModel()
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Info,
Message = "Your email has been confirmed but you still need to set your password."
@ -660,7 +683,7 @@ namespace BTCPayServer.Controllers
if (result.Succeeded)
{
TempData.SetStatusMessageModel(new StatusMessageModel()
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Success,
Message = "Your email has been confirmed."
@ -687,12 +710,12 @@ namespace BTCPayServer.Controllers
if (ModelState.IsValid)
{
var user = await _userManager.FindByEmailAsync(model.Email);
if (user == null || (user.RequiresEmailConfirmation && !(await _userManager.IsEmailConfirmedAsync(user))))
if (!UserService.TryCanLogin(user, out _))
{
// Don't reveal that the user does not exist or is not confirmed
return RedirectToAction(nameof(ForgotPasswordConfirmation));
}
_eventAggregator.Publish(new UserPasswordResetRequestedEvent()
_eventAggregator.Publish(new UserPasswordResetRequestedEvent
{
User = user,
RequestUri = Request.GetAbsoluteRootUri()
@ -740,16 +763,16 @@ namespace BTCPayServer.Controllers
return View(model);
}
var user = await _userManager.FindByEmailAsync(model.Email);
if (user == null)
if (!UserService.TryCanLogin(user, out _))
{
// Don't reveal that the user does not exist
return RedirectToAction(nameof(Login));
}
var result = await _userManager.ResetPasswordAsync(user, model.Code, model.Password);
var result = await _userManager.ResetPasswordAsync(user!, model.Code, model.Password);
if (result.Succeeded)
{
TempData.SetStatusMessageModel(new StatusMessageModel()
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Success,
Message = "Password successfully set."
@ -800,7 +823,7 @@ namespace BTCPayServer.Controllers
private void SetInsecureFlags()
{
TempData.SetStatusMessageModel(new StatusMessageModel()
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Error,
Message = "You cannot login over an insecure connection. Please use HTTPS or Tor."

View File

@ -391,10 +391,6 @@ namespace BTCPayServer.Controllers
return NotFound();
}
var store = GetCurrentStore();
var storeBlob = BTCPayServer.Data.StoreDataExtensions.GetStoreBlob(store);
var defaultCurrency = storeBlob.DefaultCurrency;
try
{
var assetBalancesData =
@ -583,7 +579,7 @@ namespace BTCPayServer.Controllers
try
{
if (custodian is ICanWithdraw withdrawableCustodian)
if (custodian is ICanWithdraw)
{
var config = custodianAccount.GetBlob();

View File

@ -1,11 +1,8 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Mime;
using System.Net.WebSockets;
using System.Reflection.Metadata;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
@ -32,10 +29,8 @@ using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Routing;
using Microsoft.EntityFrameworkCore;
using NBitcoin;
using NBitpayClient;
using NBXplorer;
using Newtonsoft.Json.Linq;
using BitpayCreateInvoiceRequest = BTCPayServer.Models.BitpayCreateInvoiceRequest;
using StoreData = BTCPayServer.Data.StoreData;
namespace BTCPayServer.Controllers
@ -617,7 +612,7 @@ namespace BTCPayServer.Controllers
await _InvoiceRepository.MassArchive(selectedItems, false);
TempData[WellKnownTempData.SuccessMessage] = $"{selectedItems.Length} invoice{(selectedItems.Length == 1 ? "" : "s")} unarchived.";
break;
case "cpfp":
case "cpfp" when storeId is not null:
var network = _NetworkProvider.DefaultNetwork;
var explorer = _ExplorerClients.GetExplorerClient(network);
if (explorer is null)
@ -856,10 +851,8 @@ namespace BTCPayServer.Controllers
DefaultLang = lang ?? invoice.DefaultLanguage ?? storeBlob.DefaultLang ?? "en",
ShowPayInWalletButton = storeBlob.ShowPayInWalletButton,
ShowStoreHeader = storeBlob.ShowStoreHeader,
StoreBranding = new StoreBrandingViewModel(storeBlob)
{
CustomCSSLink = storeBlob.CustomCSS
},
StoreBranding = new StoreBrandingViewModel(storeBlob),
CustomCSSLink = storeBlob.CustomCSS,
CustomLogoLink = storeBlob.CustomLogo,
CheckoutType = invoice.CheckoutType ?? storeBlob.CheckoutType,
HtmlTitle = storeBlob.HtmlTitle ?? "BTCPay Invoice",
@ -1149,63 +1142,34 @@ namespace BTCPayServer.Controllers
};
}
private SelectList GetPaymentMethodsSelectList()
{
var store = GetCurrentStore();
var excludeFilter = store.GetStoreBlob().GetExcludedPaymentMethods();
return new SelectList(store.GetSupportedPaymentMethods(_NetworkProvider)
.Where(s => !excludeFilter.Match(s.PaymentId))
.Select(method => new SelectListItem(method.PaymentId.ToPrettyString(), method.PaymentId.ToString())),
nameof(SelectListItem.Value),
nameof(SelectListItem.Text));
}
private bool AnyPaymentMethodAvailable(StoreData store)
{
var storeBlob = store.GetStoreBlob();
var excludeFilter = storeBlob.GetExcludedPaymentMethods();
return store.GetSupportedPaymentMethods(_NetworkProvider).Where(s => !excludeFilter.Match(s.PaymentId)).Any();
}
[HttpGet("/stores/{storeId}/invoices/create")]
[HttpGet("invoices/create")]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[BitpayAPIConstraint(false)]
public async Task<IActionResult> CreateInvoice(InvoicesModel? model = null)
{
if (model?.StoreId != null)
{
var store = await _StoreRepository.FindStore(model.StoreId, GetUserId());
if (store == null)
return NotFound();
if (!AnyPaymentMethodAvailable(store))
{
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Error,
Html = $"To create an invoice, you need to <a href='{Url.Action(nameof(UIStoresController.SetupWallet), "UIStores", new { cryptoCode = _NetworkProvider.DefaultNetwork.CryptoCode, storeId = store.Id })}' class='alert-link'>set up a wallet</a> first",
AllowDismiss = false
});
}
HttpContext.SetStoreData(store);
}
else
if (string.IsNullOrEmpty(model?.StoreId))
{
TempData[WellKnownTempData.ErrorMessage] = "You need to select a store before creating an invoice.";
return RedirectToAction(nameof(UIHomeController.Index), "UIHome");
}
var storeBlob = HttpContext.GetStoreData()?.GetStoreBlob();
var store = await _StoreRepository.FindStore(model.StoreId, GetUserId());
if (store == null)
return NotFound();
if (!store.AnyPaymentMethodAvailable(_NetworkProvider))
{
return NoPaymentMethodResult(store.Id);
}
var storeBlob = store.GetStoreBlob();
var vm = new CreateInvoiceModel
{
StoreId = model.StoreId,
Currency = storeBlob?.DefaultCurrency,
CheckoutType = storeBlob?.CheckoutType ?? CheckoutType.V2,
AvailablePaymentMethods = GetPaymentMethodsSelectList()
Currency = storeBlob.DefaultCurrency,
CheckoutType = storeBlob.CheckoutType,
AvailablePaymentMethods = GetPaymentMethodsSelectList(store)
};
return View(vm);
@ -1218,9 +1182,14 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> CreateInvoice(CreateInvoiceModel model, CancellationToken cancellationToken)
{
var store = HttpContext.GetStoreData();
if (!store.AnyPaymentMethodAvailable(_NetworkProvider))
{
return NoPaymentMethodResult(store.Id);
}
var storeBlob = store.GetStoreBlob();
model.CheckoutType = storeBlob.CheckoutType;
model.AvailablePaymentMethods = GetPaymentMethodsSelectList();
model.AvailablePaymentMethods = GetPaymentMethodsSelectList(store);
JObject? metadataObj = null;
if (!string.IsNullOrEmpty(model.Metadata))
@ -1239,18 +1208,6 @@ namespace BTCPayServer.Controllers
{
return View(model);
}
if (!AnyPaymentMethodAvailable(store))
{
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Error,
Html = $"To create an invoice, you need to <a href='{Url.Action(nameof(UIStoresController.SetupWallet), "UIStores", new { cryptoCode = _NetworkProvider.DefaultNetwork.CryptoCode, storeId = store.Id })}' class='alert-link'>set up a wallet</a> first",
AllowDismiss = false
});
return View(model);
}
try
{
var metadata = metadataObj is null ? new InvoiceMetadata() : InvoiceMetadata.FromJObject(metadataObj);
@ -1396,5 +1353,26 @@ namespace BTCPayServer.Controllers
}
}
}
private SelectList GetPaymentMethodsSelectList(StoreData store)
{
var excludeFilter = store.GetStoreBlob().GetExcludedPaymentMethods();
return new SelectList(store.GetSupportedPaymentMethods(_NetworkProvider)
.Where(s => !excludeFilter.Match(s.PaymentId))
.Select(method => new SelectListItem(method.PaymentId.ToPrettyString(), method.PaymentId.ToString())),
nameof(SelectListItem.Value),
nameof(SelectListItem.Text));
}
private IActionResult NoPaymentMethodResult(string storeId)
{
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Error,
Html = $"To create an invoice, you need to <a href='{Url.Action(nameof(UIStoresController.SetupWallet), "UIStores", new { cryptoCode = _NetworkProvider.DefaultNetwork.CryptoCode, storeId })}' class='alert-link'>set up a wallet</a> first",
AllowDismiss = false
});
return RedirectToAction(nameof(ListInvoices), new { storeId });
}
}
}

View File

@ -227,7 +227,6 @@ namespace BTCPayServer.Controllers
entity.Status = InvoiceStatusLegacy.New;
entity.UpdateTotals();
HashSet<CurrencyPair> currencyPairsToFetch = new HashSet<CurrencyPair>();
var rules = storeBlob.GetRateRules(_NetworkProvider);
var excludeFilter = storeBlob.GetExcludedPaymentMethods(); // Here we can compose filters from other origin with PaymentFilter.Any()
if (invoicePaymentMethodFilter != null)
{

View File

@ -3,7 +3,9 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Form;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
@ -39,6 +41,7 @@ namespace BTCPayServer.Controllers
private readonly DisplayFormatter _displayFormatter;
private readonly InvoiceRepository _InvoiceRepository;
private readonly StoreRepository _storeRepository;
private readonly BTCPayNetworkProvider _networkProvider;
private FormComponentProviders FormProviders { get; }
public FormDataService FormDataService { get; }
@ -54,7 +57,8 @@ namespace BTCPayServer.Controllers
StoreRepository storeRepository,
InvoiceRepository invoiceRepository,
FormComponentProviders formProviders,
FormDataService formDataService)
FormDataService formDataService,
BTCPayNetworkProvider networkProvider)
{
_InvoiceController = invoiceController;
_UserManager = userManager;
@ -67,6 +71,7 @@ namespace BTCPayServer.Controllers
_InvoiceRepository = invoiceRepository;
FormProviders = formProviders;
FormDataService = formDataService;
_networkProvider = networkProvider;
}
[HttpGet("/stores/{storeId}/payment-requests")]
@ -107,12 +112,20 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> EditPaymentRequest(string storeId, string payReqId)
{
var store = GetCurrentStore();
if (store == null)
{
return NotFound();
}
var paymentRequest = GetCurrentPaymentRequest();
if (paymentRequest == null && !string.IsNullOrEmpty(payReqId))
{
return NotFound();
}
if (!store.AnyPaymentMethodAvailable(_networkProvider))
{
return NoPaymentMethodResult(storeId);
}
var storeBlob = store.GetStoreBlob();
var prInvoices = payReqId is null ? null : (await _PaymentRequestService.GetPaymentRequest(payReqId, GetUserId())).Invoices;
var vm = new UpdatePaymentRequestViewModel(paymentRequest)
@ -143,7 +156,11 @@ namespace BTCPayServer.Controllers
{
return NotFound();
}
if (!store.AnyPaymentMethodAvailable(_networkProvider))
{
return NoPaymentMethodResult(store.Id);
}
if (paymentRequest?.Archived is true && viewModel.Archived)
{
ModelState.AddModelError(string.Empty, "You cannot edit an archived payment request.");
@ -441,5 +458,16 @@ namespace BTCPayServer.Controllers
private StoreData GetCurrentStore() => HttpContext.GetStoreData();
private PaymentRequestData GetCurrentPaymentRequest() => HttpContext.GetPaymentRequestData();
private IActionResult NoPaymentMethodResult(string storeId)
{
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Error,
Html = $"To create a payment request, you need to <a href='{Url.Action(nameof(UIStoresController.SetupWallet), "UIStores", new { cryptoCode = _networkProvider.DefaultNetwork.CryptoCode, storeId })}' class='alert-link'>set up a wallet</a> first",
AllowDismiss = false
});
return RedirectToAction(nameof(GetPaymentRequests), new { storeId });
}
}
}

View File

@ -261,7 +261,8 @@ namespace BTCPayServer.Controllers
Destination = destination,
PullPaymentId = pullPaymentId,
Value = vm.ClaimedAmount,
PaymentMethodId = paymentMethodId
PaymentMethodId = paymentMethodId,
StoreId = pp.StoreId
});
if (result.Result != ClaimRequest.ClaimResult.Ok)

View File

@ -55,7 +55,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; }
public Dictionary<string, Version> Disabled { get; set; }
public Dictionary<string, AvailablePlugin> DownloadedPluginsByIdentifier { get; set; } = new Dictionary<string, AvailablePlugin>();
}

View File

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
@ -8,25 +7,21 @@ using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Models;
using BTCPayServer.Models.ServerViewModels;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.EntityFrameworkCore;
using MimeKit;
namespace BTCPayServer.Controllers
{
public partial class UIServerController
{
[Route("server/users")]
[HttpGet("server/users")]
public async Task<IActionResult> ListUsers(
[FromServices] RoleManager<IdentityRole> roleManager,
UsersViewModel model,
string sortOrder = null
)
UsersViewModel model,
string sortOrder = null)
{
model = this.ParseListQuery(model ?? new UsersViewModel());
@ -57,6 +52,8 @@ namespace BTCPayServer.Controllers
model.Roles = roleManager.Roles.ToDictionary(role => role.Id, role => role.Name);
model.Users = await usersQuery
.Include(user => user.UserRoles)
.Include(user => user.UserStores)
.ThenInclude(data => data.StoreData)
.Skip(model.Skip)
.Take(model.Count)
.Select(u => new UsersViewModel.UserViewModel
@ -64,54 +61,79 @@ namespace BTCPayServer.Controllers
Name = u.UserName,
Email = u.Email,
Id = u.Id,
Verified = u.EmailConfirmed || !u.RequiresEmailConfirmation,
EmailConfirmed = u.RequiresEmailConfirmation ? u.EmailConfirmed : null,
Approved = u.RequiresApproval ? u.Approved : null,
Created = u.Created,
Roles = u.UserRoles.Select(role => role.RoleId),
Disabled = u.LockoutEnabled && u.LockoutEnd != null && DateTimeOffset.UtcNow < u.LockoutEnd.Value.UtcDateTime
Disabled = u.LockoutEnabled && u.LockoutEnd != null && DateTimeOffset.UtcNow < u.LockoutEnd.Value.UtcDateTime,
Stores = u.UserStores.OrderBy(s => !s.StoreData.Archived).ToList()
})
.ToListAsync();
return View(model);
}
[Route("server/users/{userId}")]
[HttpGet("server/users/{userId}")]
public new async Task<IActionResult> User(string userId)
{
var user = await _UserManager.FindByIdAsync(userId);
if (user == null)
return NotFound();
var roles = await _UserManager.GetRolesAsync(user);
var userVM = new UsersViewModel.UserViewModel
var model = new UsersViewModel.UserViewModel
{
Id = user.Id,
Email = user.Email,
Verified = user.EmailConfirmed || !user.RequiresEmailConfirmation,
EmailConfirmed = user.RequiresEmailConfirmation ? user.EmailConfirmed : null,
Approved = user.RequiresApproval ? user.Approved : null,
IsAdmin = Roles.HasServerAdmin(roles)
};
return View(userVM);
return View(model);
}
[Route("server/users/{userId}")]
[HttpPost]
[HttpPost("server/users/{userId}")]
public new async Task<IActionResult> User(string userId, UsersViewModel.UserViewModel viewModel)
{
var user = await _UserManager.FindByIdAsync(userId);
if (user == null)
return NotFound();
bool? propertiesChanged = null;
bool? adminStatusChanged = null;
bool? approvalStatusChanged = null;
if (user.RequiresApproval && viewModel.Approved.HasValue)
{
approvalStatusChanged = await _userService.SetUserApproval(user.Id, viewModel.Approved.Value, Request.GetAbsoluteRootUri());
}
if (user.RequiresEmailConfirmation && viewModel.EmailConfirmed.HasValue && user.EmailConfirmed != viewModel.EmailConfirmed)
{
user.EmailConfirmed = viewModel.EmailConfirmed.Value;
propertiesChanged = true;
}
var admins = await _UserManager.GetUsersInRoleAsync(Roles.ServerAdmin);
var roles = await _UserManager.GetRolesAsync(user);
var wasAdmin = Roles.HasServerAdmin(roles);
if (!viewModel.IsAdmin && admins.Count == 1 && wasAdmin)
{
TempData[WellKnownTempData.ErrorMessage] = "This is the only Admin, so their role can't be removed until another Admin is added.";
return View(viewModel); // return
return View(viewModel);
}
if (viewModel.IsAdmin != wasAdmin)
{
var success = await _userService.SetAdminUser(user.Id, viewModel.IsAdmin);
if (success)
adminStatusChanged = await _userService.SetAdminUser(user.Id, viewModel.IsAdmin);
}
if (propertiesChanged is true)
{
propertiesChanged = await _UserManager.UpdateAsync(user) is { Succeeded: true };
}
if (propertiesChanged.HasValue || adminStatusChanged.HasValue || approvalStatusChanged.HasValue)
{
if (propertiesChanged is not false && adminStatusChanged is not false && approvalStatusChanged is not false)
{
TempData[WellKnownTempData.SuccessMessage] = "User successfully updated";
}
@ -121,23 +143,22 @@ namespace BTCPayServer.Controllers
}
}
return RedirectToAction(nameof(User), new { userId = userId });
return RedirectToAction(nameof(User), new { userId });
}
[Route("server/users/new")]
[HttpGet]
[HttpGet("server/users/new")]
public IActionResult CreateUser()
{
ViewData["AllowRequestApproval"] = _policiesSettings.RequiresUserApproval;
ViewData["AllowRequestEmailConfirmation"] = _policiesSettings.RequiresConfirmedEmail;
return View();
}
[Route("server/users/new")]
[HttpPost]
[HttpPost("server/users/new")]
public async Task<IActionResult> CreateUser(RegisterFromAdminViewModel model)
{
var requiresConfirmedEmail = _policiesSettings.RequiresConfirmedEmail;
ViewData["AllowRequestEmailConfirmation"] = requiresConfirmedEmail;
ViewData["AllowRequestApproval"] = _policiesSettings.RequiresUserApproval;
ViewData["AllowRequestEmailConfirmation"] = _policiesSettings.RequiresConfirmedEmail;
if (!_Options.CheatMode)
model.IsAdmin = false;
if (ModelState.IsValid)
@ -148,7 +169,9 @@ namespace BTCPayServer.Controllers
UserName = model.Email,
Email = model.Email,
EmailConfirmed = model.EmailConfirmed,
RequiresEmailConfirmation = requiresConfirmedEmail,
RequiresEmailConfirmation = _policiesSettings.RequiresConfirmedEmail,
RequiresApproval = _policiesSettings.RequiresUserApproval,
Approved = model.Approved,
Created = DateTimeOffset.UtcNow
};
@ -223,7 +246,6 @@ namespace BTCPayServer.Controllers
{
if (await _userService.IsUserTheOnlyOneAdmin(user))
{
// return
return View("Confirm", new ConfirmModel("Delete admin",
$"Unable to proceed: As the user <strong>{Html.Encode(user.Email)}</strong> is the last enabled admin, it cannot be removed."));
}
@ -281,6 +303,29 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(ListUsers));
}
[HttpGet("server/users/{userId}/approve")]
public async Task<IActionResult> ApproveUser(string userId, bool approved)
{
var user = userId == null ? null : await _UserManager.FindByIdAsync(userId);
if (user == null)
return NotFound();
return View("Confirm", new ConfirmModel($"{(approved ? "Approve" : "Unapprove")} user", $"The user <strong>{Html.Encode(user.Email)}</strong> will be {(approved ? "approved" : "unapproved")}. Are you sure?", (approved ? "Approve" : "Unapprove")));
}
[HttpPost("server/users/{userId}/approve")]
public async Task<IActionResult> ApproveUserPost(string userId, bool approved)
{
var user = userId == null ? null : await _UserManager.FindByIdAsync(userId);
if (user == null)
return NotFound();
await _userService.SetUserApproval(userId, approved, Request.GetAbsoluteRootUri());
TempData[WellKnownTempData.SuccessMessage] = $"User {(approved ? "approved" : "unapproved")}";
return RedirectToAction(nameof(ListUsers));
}
[HttpGet("server/users/{userId}/verification-email")]
public async Task<IActionResult> SendVerificationEmail(string userId)
{
@ -332,5 +377,8 @@ namespace BTCPayServer.Controllers
[Display(Name = "Email confirmed?")]
public bool EmailConfirmed { get; set; }
[Display(Name = "User approved?")]
public bool Approved { get; set; }
}
}

View File

@ -20,6 +20,7 @@ using BTCPayServer.Hosting;
using BTCPayServer.Logging;
using BTCPayServer.Models;
using BTCPayServer.Models.ServerViewModels;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
@ -122,21 +123,44 @@ namespace BTCPayServer.Controllers
_transactionLinkProviders = transactionLinkProviders;
}
[Route("server/maintenance")]
public IActionResult Maintenance()
[HttpGet("server/stores")]
public async Task<IActionResult> ListStores()
{
MaintenanceViewModel vm = new MaintenanceViewModel();
vm.CanUseSSH = _sshState.CanUseSSH;
if (!vm.CanUseSSH)
TempData[WellKnownTempData.ErrorMessage] = "Maintenance feature requires access to SSH properly configured in BTCPay Server configuration.";
vm.DNSDomain = this.Request.Host.Host;
if (IPAddress.TryParse(vm.DNSDomain, out var unused))
vm.DNSDomain = null;
var stores = await _StoreRepository.GetStores();
var vm = new ListStoresViewModel
{
Stores = stores
.Select(s => new ListStoresViewModel.StoreViewModel
{
StoreId = s.Id,
StoreName = s.StoreName,
Archived = s.Archived,
Users = s.UserStores
})
.OrderBy(s => !s.Archived)
.ToList()
};
return View(vm);
}
[Route("server/maintenance")]
[HttpPost]
[HttpGet("server/maintenance")]
public IActionResult Maintenance()
{
var vm = new MaintenanceViewModel
{
CanUseSSH = _sshState.CanUseSSH,
DNSDomain = Request.Host.Host
};
if (!vm.CanUseSSH)
TempData[WellKnownTempData.ErrorMessage] = "Maintenance feature requires access to SSH properly configured in BTCPay Server configuration.";
if (IPAddress.TryParse(vm.DNSDomain, out var unused))
vm.DNSDomain = null;
return View(vm);
}
[HttpPost("server/maintenance")]
public async Task<IActionResult> Maintenance(MaintenanceViewModel vm, string command)
{
vm.CanUseSSH = _sshState.CanUseSSH;
@ -147,6 +171,7 @@ namespace BTCPayServer.Controllers
}
if (!ModelState.IsValid)
return View(vm);
if (command == "changedomain")
{
if (string.IsNullOrWhiteSpace(vm.DNSDomain))
@ -168,37 +193,31 @@ namespace BTCPayServer.Controllers
return View(vm);
}
var builder = new UriBuilder();
using (var client = new HttpClient(new HttpClientHandler()
try
{
ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
}))
{
try
{
builder.Scheme = this.Request.Scheme;
builder.Host = vm.DNSDomain;
var addresses1 = GetAddressAsync(this.Request.Host.Host);
var addresses2 = GetAddressAsync(vm.DNSDomain);
await Task.WhenAll(addresses1, addresses2);
builder.Scheme = this.Request.Scheme;
builder.Host = vm.DNSDomain;
var addresses1 = GetAddressAsync(this.Request.Host.Host);
var addresses2 = GetAddressAsync(vm.DNSDomain);
await Task.WhenAll(addresses1, addresses2);
var addressesSet = addresses1.GetAwaiter().GetResult().Select(c => c.ToString()).ToHashSet();
var hasCommonAddress = addresses2.GetAwaiter().GetResult().Select(c => c.ToString()).Any(s => addressesSet.Contains(s));
if (!hasCommonAddress)
{
ModelState.AddModelError(nameof(vm.DNSDomain), $"Invalid host ({vm.DNSDomain} is not pointing to this BTCPay instance)");
return View(vm);
}
}
catch (Exception ex)
var addressesSet = addresses1.GetAwaiter().GetResult().Select(c => c.ToString()).ToHashSet();
var hasCommonAddress = addresses2.GetAwaiter().GetResult().Select(c => c.ToString()).Any(s => addressesSet.Contains(s));
if (!hasCommonAddress)
{
var messages = new List<object>();
messages.Add(ex.Message);
if (ex.InnerException != null)
messages.Add(ex.InnerException.Message);
ModelState.AddModelError(nameof(vm.DNSDomain), $"Invalid domain ({string.Join(", ", messages.ToArray())})");
ModelState.AddModelError(nameof(vm.DNSDomain), $"Invalid host ({vm.DNSDomain} is not pointing to this BTCPay instance)");
return View(vm);
}
}
catch (Exception ex)
{
var messages = new List<object>();
messages.Add(ex.Message);
if (ex.InnerException != null)
messages.Add(ex.InnerException.Message);
ModelState.AddModelError(nameof(vm.DNSDomain), $"Invalid domain ({string.Join(", ", messages.ToArray())})");
return View(vm);
}
var error = await RunSSH(vm, $"changedomain.sh {vm.DNSDomain}");
if (error != null)
@ -308,8 +327,8 @@ namespace BTCPayServer.Controllers
[Route("server/policies")]
public async Task<IActionResult> Policies()
{
ViewBag.AppsList = await GetAppSelectList();
ViewBag.UpdateUrlPresent = _Options.UpdateUrl != null;
ViewBag.AppsList = await GetAppSelectList();
return View(_policiesSettings);
}
@ -685,7 +704,7 @@ namespace BTCPayServer.Controllers
[HttpPost]
public async Task<IActionResult> ServicePost(string serviceName, string cryptoCode)
{
if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
if (!_dashBoard.IsFullySynched(cryptoCode, out _))
{
TempData[WellKnownTempData.ErrorMessage] = $"{cryptoCode} is not fully synched";
return RedirectToAction(nameof(Services));
@ -998,155 +1017,183 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(Services));
}
[HttpGet("server/theme")]
public async Task<IActionResult> Theme()
[HttpGet("server/branding")]
public async Task<IActionResult> Branding()
{
var data = await _SettingsRepository.GetSettingAsync<ThemeSettings>() ?? new ThemeSettings();
return View(data);
var server = await _SettingsRepository.GetSettingAsync<ServerSettings>() ?? new ServerSettings();
var theme = await _SettingsRepository.GetSettingAsync<ThemeSettings>() ?? new ThemeSettings();
var vm = new BrandingViewModel
{
ServerName = server.ServerName,
ContactUrl = server.ContactUrl,
CustomTheme = theme.CustomTheme,
CustomThemeExtension = theme.CustomThemeExtension,
CustomThemeCssUri = theme.CustomThemeCssUri,
CustomThemeFileId = theme.CustomThemeFileId,
LogoFileId = theme.LogoFileId
};
return View(vm);
}
[HttpPost("server/theme")]
public async Task<IActionResult> Theme(
ThemeSettings model,
[HttpPost("server/branding")]
public async Task<IActionResult> Branding(
BrandingViewModel vm,
[FromForm] bool RemoveLogoFile,
[FromForm] bool RemoveCustomThemeFile)
{
var settingsChanged = false;
var settings = await _SettingsRepository.GetSettingAsync<ThemeSettings>() ?? new ThemeSettings();
var server = await _SettingsRepository.GetSettingAsync<ServerSettings>() ?? new ServerSettings();
var theme = await _SettingsRepository.GetSettingAsync<ThemeSettings>() ?? new ThemeSettings();
var userId = GetUserId();
if (userId is null)
return NotFound();
if (model.CustomThemeFile != null)
vm.LogoFileId = theme.LogoFileId;
vm.CustomThemeFileId = theme.CustomThemeFileId;
if (server.ServerName != vm.ServerName || server.ContactUrl != vm.ContactUrl)
{
if (model.CustomThemeFile.ContentType.Equals("text/css", StringComparison.InvariantCulture))
server.ServerName = vm.ServerName;
server.ContactUrl = vm.ContactUrl;
settingsChanged = true;
await _SettingsRepository.UpdateSetting(server);
}
if (vm.CustomThemeFile != null)
{
if (vm.CustomThemeFile.ContentType.Equals("text/css", StringComparison.InvariantCulture))
{
// delete existing file
if (!string.IsNullOrEmpty(settings.CustomThemeFileId))
if (!string.IsNullOrEmpty(theme.CustomThemeFileId))
{
await _fileService.RemoveFile(settings.CustomThemeFileId, userId);
await _fileService.RemoveFile(theme.CustomThemeFileId, userId);
}
// add new file
try
{
var storedFile = await _fileService.AddFile(model.CustomThemeFile, userId);
settings.CustomThemeFileId = storedFile.Id;
var storedFile = await _fileService.AddFile(vm.CustomThemeFile, userId);
vm.CustomThemeFileId = theme.CustomThemeFileId = storedFile.Id;
settingsChanged = true;
}
catch (Exception e)
{
ModelState.AddModelError(nameof(settings.CustomThemeFile), $"Could not save theme file: {e.Message}");
ModelState.AddModelError(nameof(vm.CustomThemeFile), $"Could not save theme file: {e.Message}");
}
}
else
{
ModelState.AddModelError(nameof(settings.CustomThemeFile), "The uploaded theme file needs to be a CSS file");
ModelState.AddModelError(nameof(vm.CustomThemeFile), "The uploaded theme file needs to be a CSS file");
}
}
else if (RemoveCustomThemeFile && !string.IsNullOrEmpty(settings.CustomThemeFileId))
else if (RemoveCustomThemeFile && !string.IsNullOrEmpty(theme.CustomThemeFileId))
{
await _fileService.RemoveFile(settings.CustomThemeFileId, userId);
settings.CustomThemeFileId = null;
await _fileService.RemoveFile(theme.CustomThemeFileId, userId);
vm.CustomThemeFileId = theme.CustomThemeFileId = null;
settingsChanged = true;
}
if (model.LogoFile != null)
if (vm.LogoFile != null)
{
if (model.LogoFile.Length > 1_000_000)
if (vm.LogoFile.Length > 1_000_000)
{
ModelState.AddModelError(nameof(model.LogoFile), "The uploaded logo file should be less than 1MB");
ModelState.AddModelError(nameof(vm.LogoFile), "The uploaded logo file should be less than 1MB");
}
else if (!model.LogoFile.ContentType.StartsWith("image/", StringComparison.InvariantCulture))
else if (!vm.LogoFile.ContentType.StartsWith("image/", StringComparison.InvariantCulture))
{
ModelState.AddModelError(nameof(model.LogoFile), "The uploaded logo file needs to be an image");
ModelState.AddModelError(nameof(vm.LogoFile), "The uploaded logo file needs to be an image");
}
else
{
var formFile = await model.LogoFile.Bufferize();
var formFile = await vm.LogoFile.Bufferize();
if (!FileTypeDetector.IsPicture(formFile.Buffer, formFile.FileName))
{
ModelState.AddModelError(nameof(model.LogoFile), "The uploaded logo file needs to be an image");
ModelState.AddModelError(nameof(vm.LogoFile), "The uploaded logo file needs to be an image");
}
else
{
model.LogoFile = formFile;
vm.LogoFile = formFile;
// delete existing file
if (!string.IsNullOrEmpty(settings.LogoFileId))
if (!string.IsNullOrEmpty(theme.LogoFileId))
{
await _fileService.RemoveFile(settings.LogoFileId, userId);
await _fileService.RemoveFile(theme.LogoFileId, userId);
}
// add new file
try
{
var storedFile = await _fileService.AddFile(model.LogoFile, userId);
settings.LogoFileId = storedFile.Id;
var storedFile = await _fileService.AddFile(vm.LogoFile, userId);
vm.LogoFileId = theme.LogoFileId = storedFile.Id;
settingsChanged = true;
}
catch (Exception e)
{
ModelState.AddModelError(nameof(settings.LogoFile), $"Could not save logo: {e.Message}");
ModelState.AddModelError(nameof(vm.LogoFile), $"Could not save logo: {e.Message}");
}
}
}
}
else if (RemoveLogoFile && !string.IsNullOrEmpty(settings.LogoFileId))
else if (RemoveLogoFile && !string.IsNullOrEmpty(theme.LogoFileId))
{
await _fileService.RemoveFile(settings.LogoFileId, userId);
settings.LogoFileId = null;
await _fileService.RemoveFile(theme.LogoFileId, userId);
vm.LogoFileId = theme.LogoFileId = null;
settingsChanged = true;
}
if (model.CustomTheme && !string.IsNullOrEmpty(model.CustomThemeCssUri) && !Uri.IsWellFormedUriString(model.CustomThemeCssUri, UriKind.RelativeOrAbsolute))
if (vm.CustomTheme && !string.IsNullOrEmpty(vm.CustomThemeCssUri) && !Uri.IsWellFormedUriString(vm.CustomThemeCssUri, UriKind.RelativeOrAbsolute))
{
ModelState.AddModelError(nameof(settings.CustomThemeCssUri), "Please provide a non-empty theme URI");
ModelState.AddModelError(nameof(theme.CustomThemeCssUri), "Please provide a non-empty theme URI");
}
else if (settings.CustomThemeCssUri != model.CustomThemeCssUri)
else if (theme.CustomThemeCssUri != vm.CustomThemeCssUri)
{
settings.CustomThemeCssUri = model.CustomThemeCssUri;
theme.CustomThemeCssUri = vm.CustomThemeCssUri;
settingsChanged = true;
}
if (settings.CustomThemeExtension != model.CustomThemeExtension)
if (theme.CustomThemeExtension != vm.CustomThemeExtension)
{
// Require a custom theme to be defined in that case
if (string.IsNullOrEmpty(model.CustomThemeCssUri) && string.IsNullOrEmpty(settings.CustomThemeFileId))
if (string.IsNullOrEmpty(vm.CustomThemeCssUri) && string.IsNullOrEmpty(theme.CustomThemeFileId))
{
ModelState.AddModelError(nameof(settings.CustomThemeFile), "Please provide a custom theme");
ModelState.AddModelError(nameof(vm.CustomThemeFile), "Please provide a custom theme");
}
else
{
settings.CustomThemeExtension = model.CustomThemeExtension;
theme.CustomThemeExtension = vm.CustomThemeExtension;
settingsChanged = true;
}
}
if (settings.CustomTheme != model.CustomTheme)
if (theme.CustomTheme != vm.CustomTheme)
{
settings.CustomTheme = model.CustomTheme;
theme.CustomTheme = vm.CustomTheme;
settingsChanged = true;
}
if (settingsChanged)
{
await _SettingsRepository.UpdateSetting(settings);
TempData[WellKnownTempData.SuccessMessage] = "Theme settings updated successfully";
await _SettingsRepository.UpdateSetting(theme);
TempData[WellKnownTempData.SuccessMessage] = "Settings updated successfully";
}
return View(settings);
return View(vm);
}
[Route("server/emails")]
public async Task<IActionResult> Emails()
{
var data = (await _SettingsRepository.GetSettingAsync<EmailSettings>()) ?? new EmailSettings();
return View(new EmailsViewModel(data));
var email = await _SettingsRepository.GetSettingAsync<EmailSettings>() ?? new EmailSettings();
var vm = new ServerEmailsViewModel(email)
{
EnableStoresToUseServerEmailSettings = !_policiesSettings.DisableStoresToUseServerEmailSettings
};
return View(vm);
}
[Route("server/emails")]
[HttpPost]
public async Task<IActionResult> Emails(EmailsViewModel model, string command)
public async Task<IActionResult> Emails(ServerEmailsViewModel model, string command)
{
if (command == "Test")
{
@ -1176,6 +1223,13 @@ namespace BTCPayServer.Controllers
}
return View(model);
}
if (_policiesSettings.DisableStoresToUseServerEmailSettings == model.EnableStoresToUseServerEmailSettings)
{
_policiesSettings.DisableStoresToUseServerEmailSettings = !model.EnableStoresToUseServerEmailSettings;
await _SettingsRepository.UpdateSetting(_policiesSettings);
}
if (command == "ResetPassword")
{
var settings = await _SettingsRepository.GetSettingAsync<EmailSettings>() ?? new EmailSettings();
@ -1184,22 +1238,22 @@ namespace BTCPayServer.Controllers
TempData[WellKnownTempData.SuccessMessage] = "Email server password reset";
return RedirectToAction(nameof(Emails));
}
else // if (command == "Save")
// save
if (model.Settings.From is not null && !MailboxAddressValidator.IsMailboxAddress(model.Settings.From))
{
if (model.Settings.From is not null && !MailboxAddressValidator.IsMailboxAddress(model.Settings.From))
{
ModelState.AddModelError("Settings.From", "Invalid email");
return View(model);
}
var oldSettings = await _SettingsRepository.GetSettingAsync<EmailSettings>() ?? new EmailSettings();
if (new EmailsViewModel(oldSettings).PasswordSet)
{
model.Settings.Password = oldSettings.Password;
}
await _SettingsRepository.UpdateSetting(model.Settings);
TempData[WellKnownTempData.SuccessMessage] = "Email settings saved";
return RedirectToAction(nameof(Emails));
ModelState.AddModelError("Settings.From", "Invalid email");
return View(model);
}
var oldSettings = await _SettingsRepository.GetSettingAsync<EmailSettings>() ?? new EmailSettings();
if (new EmailsViewModel(oldSettings).PasswordSet)
{
model.Settings.Password = oldSettings.Password;
}
await _SettingsRepository.UpdateSetting(model.Settings);
TempData[WellKnownTempData.SuccessMessage] = "Email settings saved";
return RedirectToAction(nameof(Emails));
}
[Route("server/logs/{file?}")]

View File

@ -7,11 +7,9 @@ using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Models.ServerViewModels;
using BTCPayServer.Models;
using BTCPayServer.Services.Mails;
using BTCPayServer.Validation;
using Microsoft.AspNetCore.Mvc;
using MimeKit;
@ -27,22 +25,20 @@ namespace BTCPayServer.Controllers
return NotFound();
var blob = store.GetStoreBlob();
var storeSetupComplete = blob.EmailSettings?.IsComplete() is true;
if (!storeSetupComplete && !TempData.HasStatusMessage())
if (blob.EmailSettings?.IsComplete() is not true && !TempData.HasStatusMessage())
{
var emailSender = await _emailSenderFactory.GetEmailSender(store.Id) as StoreEmailSender;
var hasServerFallback = await IsSetupComplete(emailSender?.FallbackSender);
var message = hasServerFallback
? "Emails will be sent with the email settings of the server"
: "You need to configure email settings before this feature works";
TempData.SetStatusMessageModel(new StatusMessageModel
if (!await IsSetupComplete(emailSender?.FallbackSender))
{
Severity = hasServerFallback ? StatusMessageModel.StatusSeverity.Info : StatusMessageModel.StatusSeverity.Warning,
Html = $"{message}. <a class='alert-link' href='{Url.Action("StoreEmailSettings", new { storeId })}'>Configure store email settings</a>."
});
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Warning,
Html = $"You need to configure email settings before this feature works. <a class='alert-link' href='{Url.Action("StoreEmailSettings", new { storeId })}'>Configure store email settings</a>."
});
}
}
var vm = new StoreEmailRuleViewModel { Rules = blob.EmailRules ?? new List<StoreEmailRule>() };
var vm = new StoreEmailRuleViewModel { Rules = blob.EmailRules ?? [] };
return View(vm);
}
@ -74,11 +70,10 @@ namespace BTCPayServer.Controllers
var rule = vm.Rules[i];
if (!string.IsNullOrEmpty(rule.To) && (rule.To.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Any(s => !MailboxAddressValidator.TryParse(s, out var mb))))
.Any(s => !MailboxAddressValidator.TryParse(s, out _))))
{
ModelState.AddModelError($"{nameof(vm.Rules)}[{i}].{nameof(rule.To)}",
"Invalid mailbox address provided. Valid formats are: 'test@example.com' or 'Firstname Lastname <test@example.com>'");
}
else if (!rule.CustomerEmail && string.IsNullOrEmpty(rule.To))
ModelState.AddModelError($"{nameof(vm.Rules)}[{i}].{nameof(rule.To)}",
@ -114,8 +109,17 @@ namespace BTCPayServer.Controllers
var emailSender = await _emailSenderFactory.GetEmailSender(store.Id);
if (await IsSetupComplete(emailSender))
{
emailSender.SendEmail(MailboxAddress.Parse(rule.To), $"({store.StoreName} test) {rule.Subject}", rule.Body);
message += $"Test email sent to {rule.To} — please verify you received it.";
var recipients = rule.To.Split(",", StringSplitOptions.RemoveEmptyEntries)
.Select(o =>
{
MailboxAddressValidator.TryParse(o, out var mb);
return mb;
})
.Where(o => o != null)
.ToArray();
emailSender.SendEmail(recipients.ToArray(), null, null, $"({store.StoreName} test) {rule.Subject}", rule.Body);
message += "Test email sent — please verify you received it.";
}
else
{
@ -164,13 +168,20 @@ namespace BTCPayServer.Controllers
}
[HttpGet("{storeId}/email-settings")]
public IActionResult StoreEmailSettings()
public async Task<IActionResult> StoreEmailSettings(string storeId)
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
var data = store.GetStoreBlob().EmailSettings ?? new EmailSettings();
return View(new EmailsViewModel(data));
var blob = store.GetStoreBlob();
var data = blob.EmailSettings ?? new EmailSettings();
var fallbackSettings = await _emailSenderFactory.GetEmailSender(store.Id) is StoreEmailSender { FallbackSender: not null } storeSender
? await storeSender.FallbackSender.GetEmailSettings()
: null;
var vm = new EmailsViewModel(data, fallbackSettings);
return View(vm);
}
[HttpPost("{storeId}/email-settings")]
@ -179,7 +190,13 @@ namespace BTCPayServer.Controllers
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
var emailSender = await _emailSenderFactory.GetEmailSender(store.Id) as StoreEmailSender;
var fallbackSettings = await _emailSenderFactory.GetEmailSender(store.Id) is StoreEmailSender { FallbackSender: not null } storeSender
? await storeSender.FallbackSender.GetEmailSettings()
: null;
model.FallbackSettings = fallbackSettings;
if (command == "Test")
{
try
@ -222,7 +239,7 @@ namespace BTCPayServer.Controllers
return View(model);
}
var storeBlob = store.GetStoreBlob();
if (new EmailsViewModel(storeBlob.EmailSettings).PasswordSet && storeBlob.EmailSettings != null)
if (storeBlob.EmailSettings != null && new EmailsViewModel(storeBlob.EmailSettings, fallbackSettings).PasswordSet)
{
model.Settings.Password = storeBlob.EmailSettings.Password;
}

View File

@ -90,15 +90,23 @@ namespace BTCPayServer.Controllers
if (vm.WalletFile != null)
{
if (!DerivationSchemeSettings.TryParseFromWalletFile(await ReadAllText(vm.WalletFile), network, out strategy, out var error))
string fileContent = null;
try
{
ModelState.AddModelError(nameof(vm.WalletFile), $"Importing wallet failed: {error}");
fileContent = await ReadAllText(vm.WalletFile);
}
catch
{
}
if (fileContent is null || !_onChainWalletParsers.TryParseWalletFile(fileContent, network, out strategy, out _))
{
ModelState.AddModelError(nameof(vm.WalletFile), $"Import failed, make sure you import a compatible wallet format");
return View(vm.ViewName, vm);
}
}
else if (!string.IsNullOrEmpty(vm.WalletFileContent))
{
if (!DerivationSchemeSettings.TryParseFromWalletFile(vm.WalletFileContent, network, out strategy, out var error))
if (!_onChainWalletParsers.TryParseWalletFile(vm.WalletFileContent, network, out strategy, out var error))
{
ModelState.AddModelError(nameof(vm.WalletFileContent), $"QR import failed: {error}");
return View(vm.ViewName, vm);
@ -191,7 +199,7 @@ namespace BTCPayServer.Controllers
[HttpGet("{storeId}/onchain/{cryptoCode}/generate/{method?}")]
public async Task<IActionResult> GenerateWallet(WalletSetupViewModel vm)
{
var checkResult = IsAvailable(vm.CryptoCode, out var store, out var network);
var checkResult = IsAvailable(vm.CryptoCode, out _, out var network);
if (checkResult != null)
{
return checkResult;
@ -231,7 +239,7 @@ namespace BTCPayServer.Controllers
[HttpPost("{storeId}/onchain/{cryptoCode}/generate/{method}")]
public async Task<IActionResult> GenerateWallet(string storeId, string cryptoCode, WalletSetupMethod method, WalletSetupRequest request)
{
var checkResult = IsAvailable(cryptoCode, out var store, out var network);
var checkResult = IsAvailable(cryptoCode, out _, out var network);
if (checkResult != null)
{
return checkResult;

View File

@ -71,7 +71,8 @@ namespace BTCPayServer.Controllers
IOptions<ExternalServicesOptions> externalServiceOptions,
IHtmlHelper html,
LightningClientFactoryService lightningClientFactoryService,
EmailSenderFactory emailSenderFactory)
EmailSenderFactory emailSenderFactory,
WalletFileParsers onChainWalletParsers)
{
_RateFactory = rateFactory;
_Repo = repo;
@ -97,6 +98,7 @@ namespace BTCPayServer.Controllers
_externalServiceOptions = externalServiceOptions;
_lightningClientFactoryService = lightningClientFactoryService;
_emailSenderFactory = emailSenderFactory;
_onChainWalletParsers = onChainWalletParsers;
Html = html;
}
@ -121,6 +123,7 @@ namespace BTCPayServer.Controllers
private readonly IOptions<ExternalServicesOptions> _externalServiceOptions;
private readonly LightningClientFactoryService _lightningClientFactoryService;
private readonly EmailSenderFactory _emailSenderFactory;
private readonly WalletFileParsers _onChainWalletParsers;
public string? GeneratedPairingCode { get; set; }
public WebhookSender WebhookNotificationManager { get; }
@ -147,6 +150,7 @@ namespace BTCPayServer.Controllers
{
return Forbid();
}
HttpContext.SetStoreData(store);
if (store.GetPermissionSet(userId).Contains(Policies.CanModifyStoreSettings, storeId))
{
return RedirectToAction("Dashboard", new { storeId });
@ -155,7 +159,6 @@ namespace BTCPayServer.Controllers
{
return RedirectToAction("ListInvoices", "UIInvoice", new { storeId });
}
HttpContext.SetStoreData(store);
return View();
}
@ -435,6 +438,7 @@ namespace BTCPayServer.Controllers
vm.CustomLogo = storeBlob.CustomLogo;
vm.SoundFileId = storeBlob.SoundFileId;
vm.HtmlTitle = storeBlob.HtmlTitle;
vm.SupportUrl = storeBlob.StoreSupportUrl;
vm.DisplayExpirationTimer = (int)storeBlob.DisplayExpirationTimer.TotalMinutes;
vm.ReceiptOptions = CheckoutAppearanceViewModel.ReceiptOptionsViewModel.Create(storeBlob.ReceiptOptions);
vm.AutoDetectLanguage = storeBlob.AutoDetectLanguage;
@ -501,7 +505,7 @@ namespace BTCPayServer.Controllers
var methodCriterion = model.PaymentMethodCriteria[index];
if (!string.IsNullOrWhiteSpace(methodCriterion.Value))
{
if (!CurrencyValue.TryParse(methodCriterion.Value, out var value))
if (!CurrencyValue.TryParse(methodCriterion.Value, out _))
{
model.AddModelError(viewModel => viewModel.PaymentMethodCriteria[index].Value,
$"{methodCriterion.PaymentMethod}: Invalid format. Make sure to enter a valid amount and currency code. Examples: '5 USD', '0.001 BTC'", this);
@ -610,6 +614,7 @@ namespace BTCPayServer.Controllers
blob.CustomLogo = model.CustomLogo;
blob.CustomCSS = model.CustomCSS;
blob.HtmlTitle = string.IsNullOrWhiteSpace(model.HtmlTitle) ? null : model.HtmlTitle;
blob.StoreSupportUrl = string.IsNullOrWhiteSpace(model.SupportUrl) ? null : model.SupportUrl;
blob.DisplayExpirationTimer = TimeSpan.FromMinutes(model.DisplayExpirationTimer);
blob.AutoDetectLanguage = model.AutoDetectLanguage;
blob.DefaultLang = model.DefaultLang;
@ -671,7 +676,7 @@ namespace BTCPayServer.Controllers
});
break;
case LNURLPayPaymentType lnurlPayPaymentType:
case LNURLPayPaymentType:
break;
case LightningPaymentType _:
@ -701,7 +706,6 @@ namespace BTCPayServer.Controllers
Id = store.Id,
StoreName = store.StoreName,
StoreWebsite = store.StoreWebsite,
StoreSupportUrl = storeBlob.StoreSupportUrl,
LogoFileId = storeBlob.LogoFileId,
CssFileId = storeBlob.CssFileId,
BrandColor = storeBlob.BrandColor,
@ -738,7 +742,6 @@ namespace BTCPayServer.Controllers
}
var blob = CurrentStore.GetStoreBlob();
blob.StoreSupportUrl = model.StoreSupportUrl;
blob.AnyoneCanInvoice = model.AnyoneCanCreateInvoice;
blob.NetworkFeeMode = model.NetworkFeeMode;
blob.PaymentTolerance = model.PaymentTolerance;
@ -997,7 +1000,7 @@ namespace BTCPayServer.Controllers
var store = model.StoreId switch
{
null => CurrentStore,
string id => await _Repo.FindStore(storeId, userId)
_ => await _Repo.FindStore(storeId, userId)
};
if (store == null)
return Challenge(AuthenticationSchemes.Cookie);

View File

@ -35,7 +35,7 @@ namespace BTCPayServer.Controllers
_rateFactory = rateFactory;
}
[HttpGet()]
[HttpGet]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanModifyStoreSettingsUnscoped)]
public async Task<IActionResult> ListStores(bool archived = false)
{
@ -90,7 +90,7 @@ namespace BTCPayServer.Controllers
await _repo.CreateStore(GetUserId(), store);
CreatedStoreId = store.Id;
TempData[WellKnownTempData.SuccessMessage] = "Store successfully created";
return RedirectToAction(nameof(UIStoresController.Dashboard), "UIStores", new
return RedirectToAction(nameof(UIStoresController.Index), "UIStores", new
{
storeId = store.Id
});

View File

@ -42,15 +42,7 @@ namespace BTCPayServer.Controllers
psbtDestination.Amount = Money.Coins(transactionOutput.Amount.Value);
psbtDestination.SubstractFees = transactionOutput.SubtractFeesFromOutput;
}
if (network.SupportRBF)
{
if (sendModel.AllowFeeBump is WalletSendModel.ThreeStateBool.Yes)
psbtRequest.RBF = true;
if (sendModel.AllowFeeBump is WalletSendModel.ThreeStateBool.No)
psbtRequest.RBF = false;
}
psbtRequest.RBF = network.SupportRBF ? true : null;
psbtRequest.AlwaysIncludeNonWitnessUTXO = sendModel.AlwaysIncludeNonWitnessUTXO;
psbtRequest.FeePreference = new FeePreference();

View File

@ -32,6 +32,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.DependencyInjection;
using NBitcoin;
using NBXplorer;
@ -47,6 +48,8 @@ namespace BTCPayServer.Controllers
[Route("wallets")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[AutoValidateAntiforgeryToken]
//16mb psbts
[RequestFormLimits(ValueLengthLimit = FormReader.DefaultValueLengthLimit * 4)]
public partial class UIWalletsController : Controller
{
private StoreRepository Repository { get; }
@ -146,7 +149,6 @@ namespace BTCPayServer.Controllers
return NotFound();
var txObjId = new WalletObjectId(walletId, WalletObjectData.Types.Tx, transactionId);
var wallet = _walletProvider.GetWallet(paymentMethod.Network);
if (addlabel != null)
{
await WalletRepository.AddWalletObjectLabels(txObjId, addlabel);
@ -414,7 +416,6 @@ namespace BTCPayServer.Controllers
await ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode).WaitServerStartedAsync();
await Task.Delay(1000);
await using var conn = await factory.OpenConnection();
var wallet_id = paymentMethod.GetNBXWalletId();
var txIds = sending.Select(s => s.Result.ToString()).ToArray();
await conn.ExecuteAsync(
@ -515,8 +516,6 @@ namespace BTCPayServer.Controllers
recommendedFees.Select(tuple => tuple.GetAwaiter().GetResult()).Where(option => option != null).ToList();
model.FeeSatoshiPerByte = recommendedFees[1].GetAwaiter().GetResult()?.FeeRate;
model.SupportRBF = network.SupportRBF;
model.CryptoDivisibility = network.Divisibility;
using (CancellationTokenSource cts = new CancellationTokenSource())
{
@ -572,7 +571,6 @@ namespace BTCPayServer.Controllers
if (network == null || network.ReadonlyWallet)
return NotFound();
vm.SupportRBF = network.SupportRBF;
vm.NBXSeedAvailable = await GetSeed(walletId, network) != null;
if (!string.IsNullOrEmpty(bip21))
{

View File

@ -225,7 +225,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
{
if (payout.State != PayoutState.AwaitingPayment)
{
_eventAggregator.Publish(new PayoutEvent(null, payout));
_eventAggregator.Publish(new PayoutEvent(PayoutEvent.PayoutEventType.Updated, payout));
}
}
}

View File

@ -202,7 +202,8 @@ namespace BTCPayServer.Data
{ "JPY", "bitbank" },
{ "TRY", "btcturk" },
{ "UGX", "yadio"},
{ "RSD", "bitpay"}
{ "RSD", "bitpay"},
{ "NGN", "bitnob"}
};
public string GetRecommendedExchange() =>

View File

@ -60,6 +60,14 @@ namespace BTCPayServer.Data
return result;
}
public static bool AnyPaymentMethodAvailable(this StoreData storeData, BTCPayNetworkProvider networkProvider)
{
var storeBlob = GetStoreBlob(storeData);
var excludeFilter = storeBlob.GetExcludedPaymentMethods();
return GetSupportedPaymentMethods(storeData, networkProvider).Where(s => !excludeFilter.Match(s.PaymentId)).Any();
}
public static bool SetStoreBlob(this StoreData storeData, StoreBlob storeBlob)
{
var original = new Serializer(null).ToString(storeData.GetStoreBlob());

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using NBitcoin;
using NBitcoin.Scripting;
@ -53,6 +54,8 @@ namespace BTCPayServer
ArgumentNullException.ThrowIfNull(str);
str = str.Trim();
//nbitcoin output descriptor does not support taproot, so let's check if it is a taproot descriptor and fake until it is supported
var outputDescriptor = OutputDescriptor.Parse(str, Network);
switch (outputDescriptor)
{
@ -81,8 +84,10 @@ namespace BTCPayServer
return (Parse(ds.Item1 + suffix), ds.Item2);
};
throw new FormatException("sh descriptors are only supported with multsig(legacy or p2wsh) and segwit(p2wpkh)");
case OutputDescriptor.Tr tr:
return ExtractFromPkProvider(tr.InnerPubkey, "-[taproot]");
case OutputDescriptor.WPKH wpkh:
return ExtractFromPkProvider(wpkh.PkProvider, "");
return ExtractFromPkProvider(wpkh.PkProvider);
case OutputDescriptor.WSH { Inner: OutputDescriptor.Multi multi }:
return ExtractFromMulti(multi);
case OutputDescriptor.WSH:
@ -91,42 +96,11 @@ namespace BTCPayServer
throw new ArgumentOutOfRangeException(nameof(outputDescriptor));
}
}
public DerivationStrategyBase ParseElectrum(string str)
{
ArgumentNullException.ThrowIfNull(str);
str = str.Trim();
var data = Network.GetBase58CheckEncoder().DecodeData(str);
if (data.Length < 4)
throw new FormatException();
var prefix = Utils.ToUInt32(data, false);
var standardPrefix = Utils.ToBytes(0x0488b21eU, false);
for (int ii = 0; ii < 4; ii++)
data[ii] = standardPrefix[ii];
var extPubKey = GetBitcoinExtPubKeyByNetwork(Network, data);
if (!BtcPayNetwork.ElectrumMapping.TryGetValue(prefix, out var type))
{
throw new FormatException();
}
if (type == DerivationType.Segwit)
return new DirectDerivationStrategy(extPubKey, true);
if (type == DerivationType.Legacy)
return new DirectDerivationStrategy(extPubKey, false);
if (type == DerivationType.SegwitP2SH)
return BtcPayNetwork.NBXplorerNetwork.DerivationStrategyFactory.Parse(extPubKey.ToString() + "-[p2sh]");
throw new FormatException();
}
public DerivationStrategyBase Parse(string str)
{
ArgumentNullException.ThrowIfNull(str);
str = str.Trim();
HashSet<string> hintedLabels = new HashSet<string>();
if (!Network.Consensus.SupportSegwit)
{
hintedLabels.Add("legacy");
@ -163,7 +137,6 @@ namespace BTCPayServer
if (data.Length < 4)
continue;
var prefix = Utils.ToUInt32(data, false);
var standardPrefix = Utils.ToBytes(0x0488b21eU, false);
for (int ii = 0; ii < 4; ii++)
data[ii] = standardPrefix[ii];
@ -192,10 +165,35 @@ namespace BTCPayServer
{
str = $"{str}-[{label}]";
}
return BtcPayNetwork.NBXplorerNetwork.DerivationStrategyFactory.Parse(str);
}
internal DerivationStrategyBase ParseElectrum(string str)
{
ArgumentNullException.ThrowIfNull(str);
str = str.Trim();
var data = Network.GetBase58CheckEncoder().DecodeData(str);
if (data.Length < 4)
throw new FormatException();
var prefix = Utils.ToUInt32(data, false);
var standardPrefix = Utils.ToBytes(0x0488b21eU, false);
for (int ii = 0; ii < 4; ii++)
data[ii] = standardPrefix[ii];
var extPubKey = GetBitcoinExtPubKeyByNetwork(Network, data);
if (!BtcPayNetwork.ElectrumMapping.TryGetValue(prefix, out var type))
{
throw new FormatException();
}
if (type == DerivationType.Segwit)
return new DirectDerivationStrategy(extPubKey, true);
if (type == DerivationType.Legacy)
return new DirectDerivationStrategy(extPubKey, false);
if (type == DerivationType.SegwitP2SH)
return BtcPayNetwork.NBXplorerNetwork.DerivationStrategyFactory.Parse(extPubKey.ToString() + "-[p2sh]");
throw new FormatException();
}
public static BitcoinExtPubKey GetBitcoinExtPubKeyByNetwork(Network network, byte[] data)
{
try

View File

@ -1,15 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using BTCPayServer.Payments;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBXplorer.Client;
using NBXplorer.DerivationStrategy;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer
{
@ -17,18 +13,17 @@ namespace BTCPayServer
{
public static DerivationSchemeSettings Parse(string derivationStrategy, BTCPayNetwork network)
{
string error = null;
ArgumentNullException.ThrowIfNull(network);
ArgumentNullException.ThrowIfNull(derivationStrategy);
var result = new DerivationSchemeSettings();
result.Network = network;
var parser = new DerivationSchemeParser(network);
if (TryParseXpub(derivationStrategy, parser, ref result, ref error, false) || TryParseXpub(derivationStrategy, parser, ref result, ref error, true))
var result = new DerivationSchemeSettings { Network = network };
var parser = network.GetDerivationSchemeParser();
if (parser.TryParseXpub(derivationStrategy, ref result) ||
parser.TryParseXpub(derivationStrategy, ref result, electrum: true))
{
return result;
}
throw new FormatException($"Invalid Derivation Scheme: {error}");
throw new FormatException($"Invalid Derivation Scheme");
}
public static bool TryParseFromJson(string config, BTCPayNetwork network, out DerivationSchemeSettings strategy)
@ -50,299 +45,6 @@ namespace BTCPayServer
return AccountDerivation is null ? null : DBUtils.nbxv1_get_wallet_id(Network.CryptoCode, AccountDerivation.ToString());
}
private static bool TryParseXpub(string xpub, DerivationSchemeParser derivationSchemeParser, ref DerivationSchemeSettings derivationSchemeSettings, ref string error, bool electrum = true)
{
if (!electrum)
{
var isOD = Regex.Match(xpub, @"\(.*?\)").Success;
try
{
var result = derivationSchemeParser.ParseOutputDescriptor(xpub);
derivationSchemeSettings.AccountOriginal = xpub.Trim();
derivationSchemeSettings.AccountDerivation = result.Item1;
derivationSchemeSettings.AccountKeySettings = result.Item2.Select((path, i) => new AccountKeySettings()
{
RootFingerprint = path?.MasterFingerprint,
AccountKeyPath = path?.KeyPath,
AccountKey = result.Item1.GetExtPubKeys().ElementAt(i).GetWif(derivationSchemeParser.Network)
}).ToArray();
return true;
}
catch (Exception exception)
{
error = exception.Message;
if (isOD)
{
return false;
} // otherwise continue and try to parse input as xpub
}
}
try
{
// Extract fingerprint and account key path from export formats that contain them.
// Possible formats: [fingerprint/account_key_path]xpub, [fingerprint]xpub, xpub
HDFingerprint? rootFingerprint = null;
KeyPath accountKeyPath = null;
var derivationRegex = new Regex(@"^(?:\[(\w+)(?:\/(.*?))?\])?(\w+)$", RegexOptions.IgnoreCase);
var match = derivationRegex.Match(xpub.Trim());
if (match.Success)
{
if (!string.IsNullOrEmpty(match.Groups[1].Value))
rootFingerprint = HDFingerprint.Parse(match.Groups[1].Value);
if (!string.IsNullOrEmpty(match.Groups[2].Value))
accountKeyPath = KeyPath.Parse(match.Groups[2].Value);
if (!string.IsNullOrEmpty(match.Groups[3].Value))
xpub = match.Groups[3].Value;
}
derivationSchemeSettings.AccountOriginal = xpub.Trim();
derivationSchemeSettings.AccountDerivation = electrum ? derivationSchemeParser.ParseElectrum(derivationSchemeSettings.AccountOriginal) : derivationSchemeParser.Parse(derivationSchemeSettings.AccountOriginal);
derivationSchemeSettings.AccountKeySettings = derivationSchemeSettings.AccountDerivation.GetExtPubKeys()
.Select(key => new AccountKeySettings
{
AccountKey = key.GetWif(derivationSchemeParser.Network)
}).ToArray();
if (derivationSchemeSettings.AccountDerivation is DirectDerivationStrategy direct && !direct.Segwit)
derivationSchemeSettings.AccountOriginal = null; // Saving this would be confusing for user, as xpub of electrum is legacy derivation, but for btcpay, it is segwit derivation
// apply initial matches if there were no results from parsing
if (rootFingerprint != null && derivationSchemeSettings.AccountKeySettings[0].RootFingerprint == null)
{
derivationSchemeSettings.AccountKeySettings[0].RootFingerprint = rootFingerprint;
}
if (accountKeyPath != null && derivationSchemeSettings.AccountKeySettings[0].AccountKeyPath == null)
{
derivationSchemeSettings.AccountKeySettings[0].AccountKeyPath = accountKeyPath;
}
return true;
}
catch (Exception exception)
{
error = exception.Message;
return false;
}
}
public static bool TryParseBSMSFile(string filecontent, DerivationSchemeParser derivationSchemeParser, ref DerivationSchemeSettings derivationSchemeSettings,
out string error)
{
error = null;
try
{
string[] lines = filecontent.Split(
new[] {"\r\n", "\r", "\n"},
StringSplitOptions.None
);
if (!lines[0].Trim().Equals("BSMS 1.0"))
{;
return false;
}
var descriptor = lines[1];
var derivationPath = lines[2].Split(',', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault()?? "/0/*";
if (derivationPath == "No path restrictions")
{
derivationPath = "/0/*";
}
if(derivationPath != "/0/*")
{
error = "BTCPay Server can only derive address to the deposit and change paths";
return false;
}
descriptor = descriptor.Replace("/**", derivationPath);
var testAddress = BitcoinAddress.Create( lines[3], derivationSchemeParser.Network);
var result = derivationSchemeParser.ParseOutputDescriptor(descriptor);
var deposit = new NBXplorer.KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit);
var line = result.Item1.GetLineFor(deposit).Derive(0);
if (testAddress.ScriptPubKey != line.ScriptPubKey)
{
error = "BSMS test address did not match our generated address";
return false;
}
derivationSchemeSettings.Source = "BSMS";
derivationSchemeSettings.AccountDerivation = result.Item1;
derivationSchemeSettings.AccountOriginal = descriptor.Trim();
derivationSchemeSettings.AccountKeySettings = result.Item2.Select((path, i) => new AccountKeySettings()
{
RootFingerprint = path?.MasterFingerprint,
AccountKeyPath = path?.KeyPath,
AccountKey = result.Item1.GetExtPubKeys().ElementAt(i).GetWif(derivationSchemeParser.Network)
}).ToArray();
return true;
}
catch (Exception e)
{
error = $"BSMS parse error: {e.Message}";
return false;
}
}
public static bool TryParseFromWalletFile(string fileContents, BTCPayNetwork network, out DerivationSchemeSettings settings, out string error)
{
settings = null;
error = null;
ArgumentNullException.ThrowIfNull(fileContents);
ArgumentNullException.ThrowIfNull(network);
var result = new DerivationSchemeSettings();
var derivationSchemeParser = new DerivationSchemeParser(network);
JObject jobj;
try
{
if (HexEncoder.IsWellFormed(fileContents))
{
fileContents = Encoding.UTF8.GetString(Encoders.Hex.DecodeData(fileContents));
}
jobj = JObject.Parse(fileContents);
}
catch
{
if (TryParseBSMSFile(fileContents, derivationSchemeParser,ref result, out var bsmsError))
{
settings = result;
settings.Network = network;
return true;
}
if (bsmsError is not null)
{
error = bsmsError;
return false;
}
result.Source = "GenericFile";
if (TryParseXpub(fileContents, derivationSchemeParser, ref result, ref error) ||
TryParseXpub(fileContents, derivationSchemeParser, ref result, ref error, false))
{
settings = result;
settings.Network = network;
return true;
}
return false;
}
// Electrum
if (jobj.ContainsKey("keystore"))
{
result.Source = "ElectrumFile";
jobj = (JObject)jobj["keystore"];
if (!jobj.ContainsKey("xpub") ||
!TryParseXpub(jobj["xpub"].Value<string>(), derivationSchemeParser, ref result, ref error))
{
return false;
}
if (jobj.ContainsKey("label"))
{
try
{
result.Label = jobj["label"].Value<string>();
}
catch { return false; }
}
if (jobj.ContainsKey("ckcc_xfp"))
{
try
{
result.AccountKeySettings[0].RootFingerprint = new HDFingerprint(jobj["ckcc_xfp"].Value<uint>());
}
catch { return false; }
}
if (jobj.ContainsKey("derivation"))
{
try
{
result.AccountKeySettings[0].AccountKeyPath = new KeyPath(jobj["derivation"].Value<string>());
}
catch { return false; }
}
}
// Specter
else if (jobj.ContainsKey("descriptor") && jobj.ContainsKey("blockheight"))
{
result.Source = "SpecterFile";
if (!TryParseXpub(jobj["descriptor"].Value<string>(), derivationSchemeParser, ref result, ref error, false))
{
return false;
}
if (jobj.ContainsKey("label"))
{
try
{
result.Label = jobj["label"].Value<string>();
}
catch { return false; }
}
}
// Wasabi
else
{
result.Source = "WasabiFile";
if (!jobj.ContainsKey("ExtPubKey") ||
!TryParseXpub(jobj["ExtPubKey"].Value<string>(), derivationSchemeParser, ref result, ref error, false))
{
return false;
}
if (jobj.ContainsKey("MasterFingerprint"))
{
try
{
var mfpString = jobj["MasterFingerprint"].ToString().Trim();
// https://github.com/zkSNACKs/WalletWasabi/pull/1663#issuecomment-508073066
if (uint.TryParse(mfpString, out var fingerprint))
{
result.AccountKeySettings[0].RootFingerprint = new HDFingerprint(fingerprint);
}
else
{
var shouldReverseMfp = jobj.ContainsKey("ColdCardFirmwareVersion") &&
jobj["ColdCardFirmwareVersion"].ToString() == "2.1.0";
var bytes = Encoders.Hex.DecodeData(mfpString);
result.AccountKeySettings[0].RootFingerprint = shouldReverseMfp ? new HDFingerprint(bytes.Reverse().ToArray()) : new HDFingerprint(bytes);
}
}
catch { return false; }
}
if (jobj.ContainsKey("AccountKeyPath"))
{
try
{
result.AccountKeySettings[0].AccountKeyPath = new KeyPath(jobj["AccountKeyPath"].Value<string>());
}
catch { return false; }
}
if (jobj.ContainsKey("DerivationPath"))
{
try
{
result.AccountKeySettings[0].AccountKeyPath = new KeyPath(jobj["DerivationPath"].Value<string>().ToLowerInvariant());
}
catch { return false; }
}
if (jobj.ContainsKey("ColdCardFirmwareVersion"))
{
result.Source = "ColdCard";
}
if (jobj.ContainsKey("CoboVaultFirmwareVersion"))
{
result.Source = "CoboVault";
}
}
settings = result;
settings.Network = network;
return true;
}
public DerivationSchemeSettings()
{

View File

@ -0,0 +1,12 @@
using System;
using BTCPayServer.Data;
namespace BTCPayServer.Events
{
public class UserApprovedEvent
{
public ApplicationUser User { get; set; }
public bool Approved { get; set; }
public Uri RequestUri { get; set; }
}
}

View File

@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
@ -9,6 +10,7 @@ using System.Net.WebSockets;
using System.Reflection;
using System.Security.Claims;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.BIP78.Sender;
@ -16,7 +18,6 @@ using BTCPayServer.Configuration;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.Lightning;
using BTCPayServer.Logging;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.NTag424;
@ -30,10 +31,9 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NBitcoin;
using NBitcoin.Payment;
using NBitpayClient;
using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
using Newtonsoft.Json;
using InvoiceCryptoInfo = BTCPayServer.Services.Invoices.InvoiceCryptoInfo;
@ -42,6 +42,81 @@ namespace BTCPayServer
{
public static class Extensions
{
public static DerivationSchemeParser GetDerivationSchemeParser(this BTCPayNetwork network)
{
return new DerivationSchemeParser(network);
}
public static bool TryParseXpub(this DerivationSchemeParser derivationSchemeParser, string xpub,
ref DerivationSchemeSettings derivationSchemeSettings, bool electrum = false)
{
if (!electrum)
{
var isOD = Regex.Match(xpub, @"\(.*?\)").Success;
try
{
var result = derivationSchemeParser.ParseOutputDescriptor(xpub);
derivationSchemeSettings.AccountOriginal = xpub.Trim();
derivationSchemeSettings.AccountDerivation = result.Item1;
derivationSchemeSettings.AccountKeySettings = result.Item2.Select((path, i) => new AccountKeySettings()
{
RootFingerprint = path?.MasterFingerprint,
AccountKeyPath = path?.KeyPath,
AccountKey = result.Item1.GetExtPubKeys().ElementAt(i).GetWif(derivationSchemeParser.Network)
}).ToArray();
return true;
}
catch (Exception)
{
if (isOD)
{
return false;
} // otherwise continue and try to parse input as xpub
}
}
try
{
// Extract fingerprint and account key path from export formats that contain them.
// Possible formats: [fingerprint/account_key_path]xpub, [fingerprint]xpub, xpub
HDFingerprint? rootFingerprint = null;
KeyPath accountKeyPath = null;
var derivationRegex = new Regex(@"^(?:\[(\w+)(?:\/(.*?))?\])?(\w+)$", RegexOptions.IgnoreCase);
var match = derivationRegex.Match(xpub.Trim());
if (match.Success)
{
if (!string.IsNullOrEmpty(match.Groups[1].Value))
rootFingerprint = HDFingerprint.Parse(match.Groups[1].Value);
if (!string.IsNullOrEmpty(match.Groups[2].Value))
accountKeyPath = KeyPath.Parse(match.Groups[2].Value);
if (!string.IsNullOrEmpty(match.Groups[3].Value))
xpub = match.Groups[3].Value;
}
derivationSchemeSettings.AccountOriginal = xpub.Trim();
derivationSchemeSettings.AccountDerivation = electrum ? derivationSchemeParser.ParseElectrum(derivationSchemeSettings.AccountOriginal) : derivationSchemeParser.Parse(derivationSchemeSettings.AccountOriginal);
derivationSchemeSettings.AccountKeySettings = derivationSchemeSettings.AccountDerivation.GetExtPubKeys()
.Select(key => new AccountKeySettings
{
AccountKey = key.GetWif(derivationSchemeParser.Network)
}).ToArray();
if (derivationSchemeSettings.AccountDerivation is DirectDerivationStrategy direct && !direct.Segwit)
derivationSchemeSettings.AccountOriginal = null; // Saving this would be confusing for user, as xpub of electrum is legacy derivation, but for btcpay, it is segwit derivation
// apply initial matches if there were no results from parsing
if (rootFingerprint != null && derivationSchemeSettings.AccountKeySettings[0].RootFingerprint == null)
{
derivationSchemeSettings.AccountKeySettings[0].RootFingerprint = rootFingerprint;
}
if (accountKeyPath != null && derivationSchemeSettings.AccountKeySettings[0].AccountKeyPath == null)
{
derivationSchemeSettings.AccountKeySettings[0].AccountKeyPath = accountKeyPath;
}
return true;
}
catch (Exception)
{
return false;
}
}
public static CardKey CreatePullPaymentCardKey(this IssuerKey issuerKey, byte[] uid, int version, string pullPaymentId)
{
var data = Encoding.UTF8.GetBytes(pullPaymentId);
@ -92,7 +167,7 @@ namespace BTCPayServer
public static Uri GetServerUri(this ILightningClient client)
{
var kv = LightningConnectionStringHelper.ExtractValues(client.ToString(), out var type);
var kv = LightningConnectionStringHelper.ExtractValues(client.ToString(), out _);
return !kv.TryGetValue("server", out var server) ? null : new Uri(server, UriKind.Absolute);
}
@ -111,7 +186,7 @@ namespace BTCPayServer
public static bool IsSafe(this ILightningClient client)
{
var kv = LightningConnectionStringHelper.ExtractValues(client.ToString(), out var type);
var kv = LightningConnectionStringHelper.ExtractValues(client.ToString(), out _);
if (kv.TryGetValue("cookiefilepath", out _) ||
kv.TryGetValue("macaroondirectorypath", out _) ||
kv.TryGetValue("macaroonfilepath", out _) )

View File

@ -1,11 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Configuration;
using BTCPayServer.Logging;
using BTCPayServer.NTag424;
using BTCPayServer.Services;
using Microsoft.Extensions.Logging;

View File

@ -23,6 +23,12 @@ namespace BTCPayServer.Services
$"Please confirm your account by clicking this link: <a href='{HtmlEncoder.Default.Encode(link)}'>link</a>");
}
public static void SendApprovalConfirmation(this IEmailSender emailSender, MailboxAddress address, string link)
{
emailSender.SendEmail(address, "Your account has been approved",
$"Your account has been approved and you can now <a href='{HtmlEncoder.Default.Encode(link)}'>login here</a>");
}
public static void SendSetPasswordConfirmation(this IEmailSender emailSender, MailboxAddress address, string link, bool newPassword)
{
var subject = $"{(newPassword ? "Set" : "Update")} Password";

View File

@ -12,6 +12,8 @@ namespace BTCPayServer
{
switch (m)
{
case null:
return 0m;
case Money money:
return money.ToDecimal(MoneyUnit.BTC);
case MoneyBag mb:

View File

@ -29,6 +29,11 @@ namespace Microsoft.AspNetCore.Mvc
new { userId, code }, scheme, host, pathbase);
}
public static string LoginLink(this LinkGenerator urlHelper, string scheme, HostString host, string pathbase)
{
return urlHelper.GetUriByAction(nameof(UIAccountController.Login), "UIAccount", null , scheme, host, pathbase);
}
public static string ResetPasswordCallbackLink(this LinkGenerator urlHelper, string userId, string code, string scheme, HostString host, string pathbase)
{
return urlHelper.GetUriByAction(

View File

@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Mvc.ActionConstraints;
using Microsoft.Extensions.DependencyInjection;
@ -25,7 +25,7 @@ namespace BTCPayServer.Filters
{
var req = context.RouteContext.HttpContext.Request;
var policies = context.RouteContext.HttpContext.RequestServices.GetService<PoliciesSettings>();
var mapping = policies?.DomainToAppMapping?.ToList() ?? new List<PoliciesSettings.DomainToAppMappingItem>();
var mapping = policies?.DomainToAppMapping?.ToList() ?? [];
if (policies is { RootAppId: { } rootAppId, RootAppType: { } rootAppType })
{
mapping.Add(new PoliciesSettings.DomainToAppMappingItem
@ -37,7 +37,7 @@ namespace BTCPayServer.Filters
}
// If we have an appId, we can redirect to the canonical domain
if ((string)context.RouteContext.RouteData.Values["appId"] is string appId)
if ((string)context.RouteContext.RouteData.Values["appId"] is { } appId && !req.IsOnion())
{
var redirectDomain = mapping.FirstOrDefault(item => item.AppId == appId)?.Domain;
// App is accessed via path, redirect to canonical domain

View File

@ -87,7 +87,7 @@ namespace BTCPayServer.HostedServices
var remotePluginsList = remotePlugins
.GroupBy(plugin => plugin.Identifier)
.Select(group => group.OrderByDescending(plugin => plugin.Version).First())
.Where(pair => installedPlugins.ContainsKey(pair.Identifier) || disabledPlugins.Contains(pair.Name))
.Where(pair => installedPlugins.ContainsKey(pair.Identifier) || disabledPlugins.ContainsKey(pair.Name))
.ToDictionary(plugin => plugin.Identifier, plugin => plugin.Version);
var notify = new HashSet<string>();
foreach (var pair in remotePluginsList)
@ -95,8 +95,10 @@ namespace BTCPayServer.HostedServices
if (dh.LastVersions.TryGetValue(pair.Key, out var lastVersion) && lastVersion >= pair.Value)
continue;
if (installedPlugins.TryGetValue(pair.Key, out var installedVersion) && installedVersion < pair.Value)
{
notify.Add(pair.Key);
if (disabledPlugins.Contains(pair.Key))
}
else if (disabledPlugins.TryGetValue(pair.Key, out var disabledVersion) && disabledVersion < pair.Value)
{
notify.Add(pair.Key);
}

View File

@ -173,7 +173,15 @@ namespace BTCPayServer.HostedServices
var query = ctx.Payouts.AsQueryable();
if (payoutQuery.States is not null)
{
query = query.Where(data => payoutQuery.States.Contains(data.State));
if (payoutQuery.States.Length == 1)
{
var state = payoutQuery.States[0];
query = query.Where(data => data.State == state);
}
else
{
query = query.Where(data => payoutQuery.States.Contains(data.State));
}
}
if (payoutQuery.PullPayments is not null)
@ -196,12 +204,28 @@ namespace BTCPayServer.HostedServices
if (payoutQuery.PaymentMethods is not null)
{
query = query.Where(data => payoutQuery.PaymentMethods.Contains(data.PaymentMethodId));
if (payoutQuery.PaymentMethods.Length == 1)
{
var pm = payoutQuery.PaymentMethods[0];
query = query.Where(data => pm == data.PaymentMethodId);
}
else
{
query = query.Where(data => payoutQuery.PaymentMethods.Contains(data.PaymentMethodId));
}
}
if (payoutQuery.Stores is not null)
{
query = query.Where(data => payoutQuery.Stores.Contains(data.StoreDataId));
if (payoutQuery.Stores.Length == 1)
{
var store = payoutQuery.Stores[0];
query = query.Where(data => store == data.StoreDataId);
}
else
{
query = query.Where(data => payoutQuery.Stores.Contains(data.StoreDataId));
}
}
if (payoutQuery.IncludeStoreData)
{
@ -493,7 +517,7 @@ namespace BTCPayServer.HostedServices
}
payout.State = req.Request.State;
await ctx.SaveChangesAsync();
_eventAggregator.Publish(new PayoutEvent(null, payout));
_eventAggregator.Publish(new PayoutEvent(PayoutEvent.PayoutEventType.Updated, payout));
req.Completion.SetResult(MarkPayoutRequest.PayoutPaidResult.Ok);
}
catch (Exception ex)
@ -715,7 +739,7 @@ namespace BTCPayServer.HostedServices
foreach (var keyValuePair in result.Where(pair => pair.Value == MarkPayoutRequest.PayoutPaidResult.Ok))
{
var payout = payouts.First(p => p.Id == keyValuePair.Key);
_eventAggregator.Publish(new PayoutEvent(null, payout));
_eventAggregator.Publish(new PayoutEvent(PayoutEvent.PayoutEventType.Updated, payout));
}
cancel.Completion.TrySetResult(result);
}
@ -935,7 +959,7 @@ namespace BTCPayServer.HostedServices
public JObject Metadata { get; set; }
}
public record PayoutEvent(PayoutEvent.PayoutEventType? Type, PayoutData Payout)
public record PayoutEvent(PayoutEvent.PayoutEventType Type, PayoutData Payout)
{
public enum PayoutEventType
{

View File

@ -51,17 +51,31 @@ namespace BTCPayServer.HostedServices
// find all wallet objects that fit this transaction
// that means see if there are any utxo objects that match in/outs and scripts/addresses that match outs
var matchedObjects = transactionEvent.NewTransactionEvent.TransactionData.Transaction.Inputs
.Select<TxIn, ObjectTypeId>(txIn => new ObjectTypeId(WalletObjectData.Types.Utxo, txIn.PrevOut.ToString()))
.Concat(transactionEvent.NewTransactionEvent.Outputs.SelectMany<NBXplorer.Models.MatchedOutput, ObjectTypeId>(txOut =>
new[]{
new ObjectTypeId(WalletObjectData.Types.Address, GetAddress(derivation, txOut, network).ToString()),
new ObjectTypeId(WalletObjectData.Types.Utxo, new OutPoint(transactionEvent.NewTransactionEvent.TransactionData.TransactionHash, (uint)txOut.Index).ToString())
var matchedObjects = new List<ObjectTypeId>();
// Check if inputs match some UTXOs
foreach (var txIn in transactionEvent.NewTransactionEvent.TransactionData.Transaction.Inputs)
{
matchedObjects.Add(new ObjectTypeId(WalletObjectData.Types.Utxo, txIn.PrevOut.ToString()));
}
})).Distinct().ToArray();
// Check if outputs match some UTXOs
var walletOutputsByIndex = transactionEvent.NewTransactionEvent.Outputs.ToDictionary(o => (uint)o.Index);
foreach (var txOut in transactionEvent.NewTransactionEvent.TransactionData.Transaction.Outputs.AsIndexedOutputs())
{
BitcoinAddress? address = null;
// Technically, walletTxOut.Address can be calculated.
// However in liquid for example, this returns the blinded address
// rather than the unblinded one.
if (walletOutputsByIndex.TryGetValue(txOut.N, out var walletTxOut))
address = walletTxOut.Address;
address ??= txOut.TxOut.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork);
if (address is not null)
matchedObjects.Add(new ObjectTypeId(WalletObjectData.Types.Address, address.ToString()));
matchedObjects.Add(new ObjectTypeId(WalletObjectData.Types.Utxo, new OutPoint(transactionEvent.NewTransactionEvent.TransactionData.TransactionHash, txOut.N).ToString()));
}
var objs = await _walletRepository.GetWalletObjects(new GetWalletObjectsQuery() { TypesIds = matchedObjects });
var objs = await _walletRepository.GetWalletObjects(new GetWalletObjectsQuery() { TypesIds = matchedObjects.Distinct().ToArray() });
var links = new List<WalletObjectLinkData>();
foreach (var walletObjectDatas in objs.GroupBy(data => data.Key.WalletId))
{
@ -112,11 +126,5 @@ namespace BTCPayServer.HostedServices
}
}
}
private BitcoinAddress GetAddress(DerivationStrategyBase derivationStrategy, NBXplorer.Models.MatchedOutput txOut, BTCPayNetwork network)
{
// Old version of NBX doesn't give address in the event, so we need to guess
return (txOut.Address ?? network.NBXplorerNetwork.CreateAddress(derivationStrategy, txOut.KeyPath, txOut.ScriptPubKey));
}
}
}

View File

@ -6,6 +6,8 @@ using BTCPayServer.Events;
using BTCPayServer.Logging;
using BTCPayServer.Services;
using BTCPayServer.Services.Mails;
using BTCPayServer.Services.Notifications;
using BTCPayServer.Services.Notifications.Blobs;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
@ -19,20 +21,27 @@ namespace BTCPayServer.HostedServices
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly EmailSenderFactory _emailSenderFactory;
private readonly NotificationSender _notificationSender;
private readonly LinkGenerator _generator;
public UserEventHostedService(EventAggregator eventAggregator, UserManager<ApplicationUser> userManager,
EmailSenderFactory emailSenderFactory, LinkGenerator generator, Logs logs) : base(eventAggregator, logs)
public UserEventHostedService(
EventAggregator eventAggregator,
UserManager<ApplicationUser> userManager,
EmailSenderFactory emailSenderFactory,
NotificationSender notificationSender,
LinkGenerator generator,
Logs logs) : base(eventAggregator, logs)
{
_userManager = userManager;
_emailSenderFactory = emailSenderFactory;
_notificationSender = notificationSender;
_generator = generator;
}
protected override void SubscribeToEvents()
{
Subscribe<UserRegisteredEvent>();
Subscribe<UserApprovedEvent>();
Subscribe<UserPasswordResetRequestedEvent>();
}
@ -40,30 +49,39 @@ namespace BTCPayServer.HostedServices
{
string code;
string callbackUrl;
Uri uri;
HostString host;
ApplicationUser user;
MailboxAddress address;
IEmailSender emailSender;
UserPasswordResetRequestedEvent userPasswordResetRequestedEvent;
switch (evt)
{
case UserRegisteredEvent userRegisteredEvent:
user = userRegisteredEvent.User;
Logs.PayServer.LogInformation(
$"A new user just registered {userRegisteredEvent.User.Email} {(userRegisteredEvent.Admin ? "(admin)" : "")}");
if (!userRegisteredEvent.User.EmailConfirmed && userRegisteredEvent.User.RequiresEmailConfirmation)
$"A new user just registered {user.Email} {(userRegisteredEvent.Admin ? "(admin)" : "")}");
if (user.RequiresApproval && !user.Approved)
{
code = await _userManager.GenerateEmailConfirmationTokenAsync(userRegisteredEvent.User);
callbackUrl = _generator.EmailConfirmationLink(userRegisteredEvent.User.Id, code,
userRegisteredEvent.RequestUri.Scheme,
new HostString(userRegisteredEvent.RequestUri.Host, userRegisteredEvent.RequestUri.Port),
userRegisteredEvent.RequestUri.PathAndQuery);
await _notificationSender.SendNotification(new AdminScope(), new NewUserRequiresApprovalNotification(user));
}
if (!user.EmailConfirmed && user.RequiresEmailConfirmation)
{
uri = userRegisteredEvent.RequestUri;
host = new HostString(uri.Host, uri.Port);
code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
callbackUrl = _generator.EmailConfirmationLink(user.Id, code, uri.Scheme, host, uri.PathAndQuery);
userRegisteredEvent.CallbackUrlGenerated?.SetResult(new Uri(callbackUrl));
address = userRegisteredEvent.User.GetMailboxAddress();
(await _emailSenderFactory.GetEmailSender()).SendEmailConfirmation(address, callbackUrl);
address = user.GetMailboxAddress();
emailSender = await _emailSenderFactory.GetEmailSender();
emailSender.SendEmailConfirmation(address, callbackUrl);
}
else if (!await _userManager.HasPasswordAsync(userRegisteredEvent.User))
{
userPasswordResetRequestedEvent = new UserPasswordResetRequestedEvent()
userPasswordResetRequestedEvent = new UserPasswordResetRequestedEvent
{
CallbackUrlGenerated = userRegisteredEvent.CallbackUrlGenerated,
User = userRegisteredEvent.User,
User = user,
RequestUri = userRegisteredEvent.RequestUri
};
goto passwordSetter;
@ -72,22 +90,33 @@ namespace BTCPayServer.HostedServices
{
userRegisteredEvent.CallbackUrlGenerated?.SetResult(null);
}
break;
case UserApprovedEvent userApprovedEvent:
if (userApprovedEvent.Approved)
{
uri = userApprovedEvent.RequestUri;
host = new HostString(uri.Host, uri.Port);
address = userApprovedEvent.User.GetMailboxAddress();
callbackUrl = _generator.LoginLink(uri.Scheme, host, uri.PathAndQuery);
emailSender = await _emailSenderFactory.GetEmailSender();
emailSender.SendApprovalConfirmation(address, callbackUrl);
}
break;
case UserPasswordResetRequestedEvent userPasswordResetRequestedEvent2:
userPasswordResetRequestedEvent = userPasswordResetRequestedEvent2;
passwordSetter:
code = await _userManager.GeneratePasswordResetTokenAsync(userPasswordResetRequestedEvent.User);
var newPassword = await _userManager.HasPasswordAsync(userPasswordResetRequestedEvent.User);
callbackUrl = _generator.ResetPasswordCallbackLink(userPasswordResetRequestedEvent.User.Id, code,
userPasswordResetRequestedEvent.RequestUri.Scheme,
new HostString(userPasswordResetRequestedEvent.RequestUri.Host,
userPasswordResetRequestedEvent.RequestUri.Port),
userPasswordResetRequestedEvent.RequestUri.PathAndQuery);
uri = userPasswordResetRequestedEvent.RequestUri;
host = new HostString(uri.Host, uri.Port);
user = userPasswordResetRequestedEvent.User;
code = await _userManager.GeneratePasswordResetTokenAsync(user);
var newPassword = await _userManager.HasPasswordAsync(user);
callbackUrl = _generator.ResetPasswordCallbackLink(user.Id, code, uri.Scheme, host, uri.PathAndQuery);
userPasswordResetRequestedEvent.CallbackUrlGenerated?.SetResult(new Uri(callbackUrl));
address = userPasswordResetRequestedEvent.User.GetMailboxAddress();
(await _emailSenderFactory.GetEmailSender())
.SendSetPasswordConfirmation(address, callbackUrl, newPassword);
address = user.GetMailboxAddress();
emailSender = await _emailSenderFactory.GetEmailSender();
emailSender.SendSetPasswordConfirmation(address, callbackUrl, newPassword);
break;
}
}

View File

@ -28,9 +28,9 @@ public class PaymentRequestWebhookProvider: WebhookProvider<PaymentRequestEvent>
public override WebhookEvent CreateTestEvent(string type, object[] args)
{
var storeId = args[0].ToString();
return new WebhookPayoutEvent(type, storeId)
return new WebhookPaymentRequestEvent(type, storeId)
{
PayoutId = "__test__" + Guid.NewGuid() + "__test__"
PaymentRequestId = "__test__" + Guid.NewGuid() + "__test__"
};
}

View File

@ -73,6 +73,8 @@ using Newtonsoft.Json;
using NicolasDorier.RateLimits;
using Serilog;
using BTCPayServer.Services.Reporting;
using BTCPayServer.Services.WalletFileParsing;
#if ALTCOINS
using BTCPayServer.Services.Altcoins.Monero;
using BTCPayServer.Services.Altcoins.Zcash;
@ -129,7 +131,7 @@ namespace BTCPayServer.Hosting
services.AddSingleton<IHostedService>(provider => provider.GetRequiredService<TorServices>());
services.AddSingleton<ISwaggerProvider, DefaultSwaggerProvider>();
services.TryAddSingleton<SocketFactory>();
services.AddSingleton<Func<HttpClient, ILightningConnectionStringHandler>>(client =>
new ChargeLightningConnectionStringHandler(client));
services.AddSingleton<Func<HttpClient, ILightningConnectionStringHandler>>(_ =>
@ -145,8 +147,8 @@ namespace BTCPayServer.Hosting
services.TryAddSingleton<LightningClientFactoryService>();
services.AddHttpClient(LightningClientFactoryService.OnionNamedClient)
.ConfigurePrimaryHttpMessageHandler<Socks5HttpClientHandler>();
services.TryAddSingleton<InvoicePaymentNotification>();
services.TryAddSingleton<BTCPayServerOptions>(o =>
o.GetRequiredService<IOptions<BTCPayServerOptions>>().Value);
@ -159,6 +161,9 @@ namespace BTCPayServer.Hosting
AddSettingsAccessor<PoliciesSettings>(services);
AddSettingsAccessor<ThemeSettings>(services);
//
AddOnchainWalletParsers(services);
services.AddStartupTask<BlockExplorerLinkStartupTask>();
services.TryAddSingleton<InvoiceRepository>();
services.AddSingleton<PaymentService>();
@ -246,7 +251,7 @@ namespace BTCPayServer.Hosting
{
error = e.Message;
}
if (error is not null)
{
logs.Configuration.LogWarning($"Invalid setting {net.CryptoCode}.lightning, " +
@ -312,10 +317,8 @@ namespace BTCPayServer.Hosting
services.TryAddTransient<DisplayFormatter>();
services.TryAddSingleton<Ganss.Xss.HtmlSanitizer>(o =>
{
var htmlSanitizer = new Ganss.Xss.HtmlSanitizer();
htmlSanitizer.RemovingAtRule += (sender, args) =>
{
};
@ -348,6 +351,7 @@ namespace BTCPayServer.Hosting
htmlSanitizer.AllowedTags.Remove("img");
htmlSanitizer.AllowedAttributes.Add("webkitallowfullscreen");
htmlSanitizer.AllowedAttributes.Add("allowfullscreen");
htmlSanitizer.AllowedSchemes.Add("mailto");
return htmlSanitizer;
});
@ -363,7 +367,8 @@ namespace BTCPayServer.Hosting
services.TryAddSingleton<WalletReceiveService>();
services.AddSingleton<IHostedService>(provider => provider.GetService<WalletReceiveService>());
services.TryAddSingleton<CurrencyNameTable>(CurrencyNameTable.Instance);
services.TryAddSingleton<IFeeProviderFactory,FeeProviderFactory>();
services.AddScheduledTask<FeeProviderFactory>(TimeSpan.FromMinutes(3.0));
services.AddSingleton<IFeeProviderFactory, FeeProviderFactory>(f => f.GetRequiredService<FeeProviderFactory>());
services.Configure<MvcOptions>((o) =>
{
@ -384,6 +389,8 @@ namespace BTCPayServer.Hosting
services.AddReportProvider<OnChainWalletReportProvider>();
services.AddReportProvider<ProductsReportProvider>();
services.AddReportProvider<PayoutsReportProvider>();
services.AddReportProvider<LegacyInvoiceExportReportProvider>();
services.AddReportProvider<RefundReportProvider>();
services.AddWebhooks();
services.AddSingleton<BitcoinLikePayoutHandler>();
services.AddSingleton<IPayoutHandler>(provider => provider.GetRequiredService<BitcoinLikePayoutHandler>());
@ -412,7 +419,7 @@ namespace BTCPayServer.Hosting
services.AddSingleton<NotificationManager>();
services.AddScoped<NotificationSender>();
services.AddSingleton<IHostedService, NBXplorerWaiters>();
services.AddSingleton<IHostedService, InvoiceEventSaverService>();
services.AddSingleton<IHostedService, BitpayIPNSender>();
@ -430,8 +437,8 @@ namespace BTCPayServer.Hosting
services.AddScoped<IAuthorizationHandler, BitpayAuthorizationHandler>();
services.AddSingleton<INotificationHandler, NewVersionNotification.Handler>();
services.AddSingleton<INotificationHandler, NewUserRequiresApprovalNotification.Handler>();
services.AddSingleton<INotificationHandler, PluginUpdateNotification.Handler>();
services.AddSingleton<INotificationHandler, InvoiceEventNotification.Handler>();
services.AddSingleton<INotificationHandler, PayoutNotification.Handler>();
services.AddSingleton<INotificationHandler, ExternalPayoutTransactionNotification.Handler>();
@ -513,6 +520,19 @@ namespace BTCPayServer.Hosting
return services;
}
public static void AddOnchainWalletParsers(IServiceCollection services)
{
services.AddSingleton<WalletFileParsers>();
services.AddSingleton<IWalletFileParser, BSMSWalletFileParser>();
services.AddSingleton<IWalletFileParser, NBXDerivGenericWalletFileParser>();
services.AddSingleton<IWalletFileParser, ElectrumWalletFileParser>();
services.AddSingleton<IWalletFileParser, OutputDescriptorWalletFileParser>(provider => provider.GetService<OutputDescriptorWalletFileParser>());
services.AddSingleton<OutputDescriptorWalletFileParser>();
services.AddSingleton<IWalletFileParser, SpecterWalletFileParser>();
services.AddSingleton<IWalletFileParser, OutputDescriptorJsonWalletFileParser>();
services.AddSingleton<IWalletFileParser, WasabiWalletFileParser>();
}
internal static void RegisterRateSources(IServiceCollection services)
{
// We need to be careful to only add exchanges which OnGetTickers implementation make only 1 request
@ -532,6 +552,7 @@ namespace BTCPayServer.Hosting
services.AddRateProvider<ByllsRateProvider>();
services.AddRateProvider<BudaRateProvider>();
services.AddRateProvider<BitbankRateProvider>();
services.AddRateProvider<BitnobRateProvider>();
services.AddRateProvider<BitpayRateProvider>();
services.AddRateProvider<RipioExchangeProvider>();
services.AddRateProvider<CryptoMarketExchangeRateProvider>();

View File

@ -475,9 +475,6 @@ namespace BTCPayServer.Hosting
continue;
var obj = new JObject();
obj.Add("color", label.Value);
var labelObjId = new WalletObjectId(WalletId.Parse(wallet.Id),
WalletObjectData.Types.Label,
labelId);
ctx.WalletObjects.Add(new WalletObjectData()
{
WalletId = wallet.Id,

View File

@ -139,7 +139,7 @@ namespace BTCPayServer.Hosting
services.AddSingleton<UserLoginCodeService>();
services.AddSingleton<LnurlAuthService>();
services.AddSingleton<LightningAddressService>();
var mvcBuilder = services.AddMvc(o =>
services.AddMvc(o =>
{
o.Filters.Add(new XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.Deny));
o.Filters.Add(new XContentTypeOptionsAttribute("nosniff"));

View File

@ -172,7 +172,6 @@ namespace BTCPayServer.Hosting
if (await otherContext.Settings.FirstOrDefaultAsync() == null)
return;
{
var postgres = new NpgsqlConnectionStringBuilder(p);
using var postgresContext = new ApplicationDbContext(new DbContextOptionsBuilder<ApplicationDbContext>().UseNpgsql(p, o =>
{
o.CommandTimeout(60 * 60 * 10);

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