Compare commits

..

152 Commits

Author SHA1 Message Date
597116bb26 Allow Users to be disabled/enabled 2022-04-14 19:16:55 +02:00
64534efe71 Fix broken "CanCreateRefunds" test (#3635) 2022-04-14 14:07:10 +09:00
1235ced355 htmlcoin altcoin updates (#3601)
* remove old logo

* add new logo

* switch name from Althash to Htmlcoin

* increment version NBXplorer.Client

* fix KeyPath BIP-0044, fix name

https://github.com/BEPAL/slips/blob/master/slip-0044.md

* revert increment version
2022-04-14 13:18:12 +09:00
23d383be67 Add transaction info PATCH endpoint (#3561)
* Add transaction info patch endpoint

* Add "#nullable enable" to LabelFactory

* Add Swagger docs

* Update OnChain to onchain

* update feeRate to feerate

* Add test

* replace "Onchain" with "onchain"
2022-04-14 13:17:22 +09:00
fb90ff2fbb updates (#3631) 2022-04-14 13:09:37 +09:00
4706aa95e6 Minor Dashboard Adjustments (#3629)
* improves border-radius

* adjusts CF widget wording
2022-04-14 13:08:43 +09:00
8981414705 API: Add Lightning Payment info endpoint (#3557)
* Upgrade Lightning lib

* API: Add Lightning Payment info endpoint
2022-04-12 18:01:58 +09:00
7ec978fcdb Dashboard (#3530)
* Add dashboard and chart basics

* More widgets

* Make widgets responsive

* Layout dashboard

* Prepare ExplorerClient

* Switch to Chartist

* Dynamic data for store numbers and recent transactions tiles

* Dynamic data for recent invoices tile

* Improvements

* Plug NBXPlorer DB

* Properly filter by code

* Reorder cheat mode button

* AJAX update for graph data

* Fix create invoice button

* Retry connection on transient issues

* App Top Items stats

* Design updates

* App Sales stats

* Add points for weekly histogram, set last point to current balance

Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2022-04-12 16:55:10 +09:00
d58803a058 Specify PayJoin enabled in Payment Link heading (#3614)
* Specify PayJoin enabled in Payment Link heading

* Fix for non bitcoin payments
2022-04-12 11:05:09 +09:00
fe6b7dc1e3 Fix visual bug when user clicks on "Create refund" without selecting an option (#3624)
* Add missing refund option validation error element

* Add missing hidden inputs for text elements

* Move validation element above button

* Update validation error message
2022-04-11 17:53:52 +09:00
c9f0988b95 Handle possible error when bumping fee (#3608)
fix #3600
2022-04-11 17:53:10 +09:00
cd9a52706c Use the store's default currency when creating entities (#3585)
* Use default currency for new pull payments

Closes #3582.

* Pull payment: Improve create form

* Use default currency for new invoices

Closes  #3581.

* Clean up old invoice form code

* Use default currency for new payment requests

* Test fixes
2022-04-11 17:50:30 +09:00
bfdb1b4af9 Design updates (#3565)
* Design updates

* Improve table styles

* Form input color improvements

* Form input shadows

* Increase accordion button padding

* Hover transition for checkboxes and radio buttons

* Improve checkbox and radio button spacings

* Improve input styles

* Secondary button updates

* Clear pager floats

* Link improvements

* Don't display border for last table row
2022-04-11 17:49:57 +09:00
e5174b4a29 Lightning: Link to services directly (#3593)
* Allow to access fake LN services in dev mode

* Link directly to Lightning services

Closes #3552.

* Fix typo
2022-04-11 17:49:28 +09:00
8feb60c30d Add ability to set default payment method for pay button (#3606)
* Add ability to set default payment method for pay button

close #3604

* Add "#nullable enable" to UIStoresController

* Add PaymentMethodOptionViewModel

* Add explicit "Use the store’s default" option
2022-04-11 17:48:12 +09:00
6bd7fb64ab CI test fixes (#3609)
* Test fix


Logs

* Add test logs

* Test change

* Use async overloads in CanPayWithTwoCurrencies test

* Bump NBXplorer

* More test updates

* More logs

* More waiting

* More waiting

* Update GoToUrl calls

* Log request status

* More logs

* More logs, more waits, idk

* Click checkboxes using JS

* Go to url directly

* Double timeout
2022-04-08 18:58:01 +09:00
b9602243d3 Fix tests for litecoin 2022-04-08 13:03:51 +09:00
b7a930ef18 Adjust user search input width (#3577)
Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2022-04-05 07:34:56 -04:00
add206ae2d Fixes #3598 by adding overflow scrolling to StoreSelectorMenu (#3599)
Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2022-04-05 07:34:14 -04:00
28ce095fb4 Merge pull request #3602 from phershbe/master
Link to local development environment instructions corrected
2022-04-05 09:01:00 +02:00
13952a4b79 Bump NBX (#3607) 2022-04-05 14:46:42 +09:00
05ec398346 Update CoinGeckoRateProvider 2022-04-04 14:47:50 +09:00
dea2dd52be Link to local development environment instructions corrected
As noted here: https://github.com/btcpayserver/btcpayserver/issues/3590

The link to the local development environment instructions was broken and now is correct.
2022-04-01 22:18:43 -04:00
debe3cda4b fix typo 2022-04-01 13:45:30 +02:00
5b5aa2c721 bump btcpay 2022-04-01 13:22:24 +02:00
e201ddd74c Plugins: Fix plugin installer 2022-04-01 13:20:19 +02:00
4a1580169d Merge pull request #3595 from pavlenex/shopify 2022-03-31 15:15:26 +02:00
001ca7de60 Fix UI to match Shopify's 2022-03-31 14:51:16 +02:00
184be4e27b Merge pull request #3592 from Kukks/updaart 2022-03-31 13:36:11 +02:00
7652645dda 1.4.8 update 2022-03-31 12:48:10 +02:00
ef6016857b FileService: AddFile from URL (#3566) 2022-03-31 11:54:25 +02:00
e449ca2c95 Fix shopify 2022-03-31 10:13:07 +02:00
b0f00773d6 fix issues around local btcpay client and no request obj 2022-03-30 15:04:51 +02:00
451eee549b added additional inputmode attr to relevant form input fields (#3578)
* added inputmode attr to relevant input fields

* missed some numerical form inputs

* removed inputmode attribute from checkout appearnce invoice settings input field
2022-03-30 09:58:50 +02:00
77da261fea Allow plugins to extend swagger docs and fix pull payment test 2022-03-29 20:29:27 +02:00
e23c9ee608 Add missing policies in api key UI 2022-03-29 20:12:02 +02:00
37cb87a9c6 Update CircleCI config (#3586) 2022-03-29 16:35:02 +01:00
211db8e0f0 Adjust pull payment badge color (#3584)
Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2022-03-29 12:09:30 +02:00
d074d60dad Change "was confirmed paid" to "is settled"
As detailed here: https://github.com/btcpayserver/btcpayserver/issues/3572
2022-03-27 07:56:32 +01:00
0bff5e2236 Update BTCPayServer/wwwroot/swagger/v1/swagger.template.stores-email.json
Co-authored-by: d11n <mail@dennisreimann.de>
2022-03-26 21:46:40 +00:00
dca986eb2e Add Greenfield Store Email API 2022-03-26 21:46:40 +00:00
326eb1135b added inputmode attr to relevant input fields 2022-03-26 18:27:22 +00:00
b2f7b4e6b9 Update BTCPayServer/Services/Invoices/InvoiceRepository.cs
Co-authored-by: Andrew Camilleri <evilkukka@gmail.com>
2022-03-25 13:21:43 +00:00
5129d6aa6b Update BTCPayServer/Services/Invoices/InvoiceRepository.cs
Co-authored-by: Andrew Camilleri <evilkukka@gmail.com>
2022-03-25 13:21:43 +00:00
a8cf334616 Update BTCPayServer/Services/Invoices/InvoiceRepository.cs
Co-authored-by: Andrew Camilleri <evilkukka@gmail.com>
2022-03-25 13:21:43 +00:00
3b4d06a1e5 Bugfix: Could not find an order by OrderId after it's OrderId was changed through the API 2022-03-25 13:21:43 +00:00
c7969476b0 Apply suggestions from code review
Co-authored-by: d11n <mail@dennisreimann.de>
2022-03-25 10:45:56 +00:00
7bf24df03a Change payment method name from "Wallet" to "Bitcoin"
As discussed here: https://github.com/btcpayserver/btcpayserver/issues/3571
2022-03-25 10:45:56 +00:00
5ef41294e4 Optimize github plugin fetching 2022-03-23 15:03:39 +00:00
2eb68655c7 FileService: Add method to check availability 2022-03-21 12:38:25 +01:00
23049439c0 API: Add description hash to CreateLightningInvoiceRequest (#3559)
Co-authored-by: Andrew Camilleri <evilkukka@gmail.com>
2022-03-17 10:15:27 +01:00
ce6cd40b92 Adjust wallet receive page layout (#3553)
Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2022-03-11 10:43:31 +01:00
c36b0c16b0 New API endpoint: Send email using store SMTP (#3181)
Co-authored-by: Kukks <evilkukka@gmail.com>
2022-03-11 10:17:40 +01:00
c15f182377 Streamline JS/CSS bundles (#3520) 2022-03-11 08:41:48 +01:00
165cb345b4 Merge pull request #3551 from dennisreimann/acinq
Remove ACINQ from README
2022-03-10 11:52:57 +03:00
9b6d2beb4d Remove ACINQ from README
Closes #3546.
2022-03-10 09:50:40 +01:00
722c39a6ff Exclude ChainCoin in rate provider test 2022-03-08 12:12:01 +01:00
e344749d2f Remove Polis
As per [this comment](https://github.com/btcpayserver/btcpayserver/pull/3514#issuecomment-1059583143)
2022-03-08 12:12:01 +01:00
fe782bc3b6 Merge pull request #3519 from dennisreimann/2fa-layout
Use simple layout for 2FA views
2022-03-08 13:42:28 +03:00
d372cbad74 Fix null reference error when "destinations" field is not specified or empty when creating a new wallet transaction
fix #3537
2022-03-08 11:14:34 +01:00
1e1198f4ec Remove Kraken from README 2022-03-08 11:13:19 +01:00
5effc96cff Remove Kraken as a supporter
Closes #3539.
2022-03-08 11:13:19 +01:00
b8d4a1be05 Delete Plugins/packed directory 2022-03-08 09:57:43 +01:00
36a25e6efa Merge pull request #3543 from dennisreimann/manage-plugins
Rename Add plugin to Manage plugins
2022-03-08 11:26:35 +03:00
1240e7914d Fixes 2022-03-08 08:17:39 +00:00
e4683b1ea1 Refactoring: Extract ITempDataDictionary extensions 2022-03-08 08:17:39 +00:00
749c22a0c3 Refactoring: Extract HttpRequest extensions 2022-03-08 08:17:39 +00:00
6867774627 Refactoring: Extract StringExtensions 2022-03-08 08:17:39 +00:00
80944972e9 Rename Add plugin to Manage plugins
Closes #3540.
2022-03-08 08:04:15 +01:00
c1f608c0d8 Upgrade Lightning lib (#3531) 2022-03-08 10:02:48 +09:00
4dfbb08db3 Try test fix 2022-03-04 10:49:36 +00:00
ead1dffd98 QR container display fixes
Consistently centers the container contents.
2022-03-04 10:49:36 +00:00
5a16e4d132 Use simple layout for 2FA views 2022-03-03 13:43:22 +01:00
a89491e343 Skip Polis and OKEx in rate fetching tests 2022-03-03 10:57:55 +01:00
b1b00ae886 Scroll to checkbox before click 2022-03-03 10:57:55 +01:00
cde5bd87d8 update 1.4.7 2022-03-02 12:37:28 +01:00
3231d5d179 Allow file service to be used in plugins 2022-03-02 12:22:46 +01:00
03e49ea2bf Allow access to global invoices list
The recent changes in 19eea3a6154e73f7fc9d62ab20480134e95be3d5 prevent it to access a global/unfiltered list of invoices across all stores. This removes the fallback to the current store, which brings the global list back at `/invoices`.
2022-03-02 11:00:25 +01:00
7d3eef092c Add missing generate wallet greenfield docs 2022-03-02 10:54:05 +01:00
30d0410b49 fix shopify settings 2022-03-01 09:19:28 +01:00
eb2a887f77 Fix missing store in export for invoices
fixes #3505
2022-03-01 09:18:46 +01:00
e77b8d29cf Remove policy, set store context manually 2022-03-01 09:18:23 +01:00
490ec299c5 Fix missing store context for Lightning payouts 2022-03-01 09:18:23 +01:00
e47c2aa24d Fix redirect 2022-03-01 09:18:23 +01:00
3eb9fdca6a Syntax improvements 2022-03-01 09:18:23 +01:00
a4173a93b7 Improve display on payout confirm page 2022-03-01 09:18:23 +01:00
ad762cf239 Fix back link on payout confirm page
Fixes #3490.
2022-03-01 09:18:23 +01:00
5a478607dc Fix "Copy Link" button on pull payment and payment request pages
close #3499
2022-03-01 09:15:15 +01:00
4abc6eb387 Refactoring: Allow GreenfieldExtensions to be used by plugins 2022-03-01 09:14:51 +01:00
c313bba288 Fix mobile content z-index
I think we added this before taking proper care of the main menu z-index. Now that that is fixed we can remove the z-index of the content area, which fixes #3504.
2022-02-26 13:51:59 +01:00
73eaf97afb Fix pos print view
fixes #3503
2022-02-26 13:22:27 +01:00
8d25df5d4e Fix Pay Button code copying
Fixes #3489.
2022-02-24 12:23:03 +01:00
4a05f16050 Refactoring: Move WellKnownTempData into Abstractions.Constants
This allows plugins to reuse the status message mechanism.
2022-02-21 16:39:08 +01:00
3ef1423263 Update Changelog.md 2022-02-21 13:06:13 +01:00
898652189b Changelog: Fix usernames 2022-02-21 13:06:13 +01:00
2976edf333 Sticky header JS fix
The old version lead to an error on pages that do not contain a sticky header.
2022-02-21 13:05:50 +01:00
248be11e4d fix local client http accessor overrider 2022-02-21 11:48:40 +01:00
19ec8c36e2 Remove debug line in selenium tests 2022-02-21 16:17:36 +09:00
90d989e358 Bump 1.4.6 2022-02-21 14:54:43 +09:00
19eea3a615 Refactor how we get storeids list in invoice filter (#3483) 2022-02-21 14:53:48 +09:00
7b81b9786d Fix LNUrl comment truncating 2022-02-21 13:47:00 +09:00
292d302a3d Allow only 2k chars in LNURL comments 2022-02-21 13:27:02 +09:00
557594e34d Test LNUrl Pay payment method information, add doc 2022-02-21 13:22:17 +09:00
48393c3765 Add border for mobile menu (#3477)
In addition to #3469.
2022-02-21 12:20:04 +09:00
022cd666eb Sticky header updates (#3471)
* Add tag helper for sticky header

Encapsulates some of the CSS and JS required and makes the usage easier.

* Make sticky header span full content area horizontally

* Use sticky header on remaining list views

* Use sticky header on remaining edit and detail views

* Adapt pull payments view to be consistent with other list views

* Fix form markup

* PSBT test fix

* Update header actions

* Remove sticky header tag helper
2022-02-21 11:05:42 +09:00
2d0eedb132 CircleCI: Update build image (#3475)
CircleCI recently [deprecated build images](https://circleci.com/blog/ubuntu-14-16-image-deprecation/), including the classic ones we are using. I found this config working for me on another repo.
2022-02-21 11:04:15 +09:00
5d3d664ce6 Update BTCPayServer/Views/UIWallets/WalletTransactions.cshtml
Co-authored-by: d11n <mail@dennisreimann.de>
2022-02-18 10:13:50 +01:00
d1c12d8294 Add ability to clear tx label filter 2022-02-18 10:13:50 +01:00
947a67fcd2 adds border (#3469) 2022-02-18 10:56:05 +09:00
9b9540b857 PSBT test fix (#3472) 2022-02-18 10:55:54 +09:00
a3b748ffe3 Remove payment methods not currently configured when creating invoice (#3394)
* Add error message when wallet is not configured

* Adjust payment methods based on available ones

* Disable "Create invoice" page if there is an error

* Add test

* update HasErrorMessage

* Add method for checking if payment methods are available

* small pr fixes

Co-authored-by: Kukks <evilkukka@gmail.com>
2022-02-17 18:22:09 +09:00
9a3a7a3444 Add pull payment grouping options (#3177)
* Add grouping by payment methods

* Add filtering by pull payment state

* Hide "Archive" button for archived pull payments

* Don't show payment methods bar if there is only one

* Add "All" payment method option

* Remove filtering by payment method

* Update state queries to not run on the client

* Add filtering by future pull payments
2022-02-17 18:13:28 +09:00
5c8ca15ee2 Redesign Wallet UI (#3441)
* Update wallet navigation

* Find matching text color for label bg color

* Cleanup

* Extract WalletNav component

* Move PSBT link to Send and Rescan link to Settings

* Update transactions view

* Test fixes

* Adapt invoices list actions

* Show invoice actions only if there are any invoices

* Link wallet name and balance to tranactions list

* Move wallet related actions from list to settings

* Fix main menu z-index

Needs a value between fixed and the offcanvas backdrop, see https://getbootstrap.com/docs/5.1/layout/z-index/

* Update receive and send views
2022-02-17 18:07:41 +09:00
cd3807a3d8 Lightning payment info and fee handling (#3454)
* Lightning payment info and fee handling

Builds on the additions in btcpayserver/BTCPayServer.Lightning#59 and btcpayserver/BTCPayServer.Lightning#61.

Adds payment information (total amount and fees) to the API response and allows to set an optional maximum fee percentage when paying.

* Add max fee flat
2022-02-17 18:01:39 +09:00
2a884d6f38 removes redundant header (#3470) 2022-02-17 17:59:21 +09:00
6efeb60c41 Fix the PSBT signing flow (#3465) 2022-02-17 17:58:56 +09:00
dcdab5b218 Do not show storeid in the invoice filter 2022-02-16 13:43:33 +09:00
288fbda54f New API endpoint: Find 1 user by ID or by email, or list all users. (#3176)
Co-authored-by: Kukks <evilkukka@gmail.com>
2022-02-15 16:19:52 +01:00
03bc91fd1e switch to jobj 2022-02-15 12:23:54 +01:00
1c5cf29540 Greenfield: Invoices Payment Methods: Additional Data
closes #3154
2022-02-15 12:23:54 +01:00
631ddc0af2 Bump version 2022-02-15 14:33:25 +09:00
1d593df5af Bump lightning lib (#3453)
* Bump lightning lib

* Fix endianness of description hash text
2022-02-14 20:33:27 +09:00
1d3a8bb7bf App updates (#3437)
* Fix toggle alignment

* Crowdfund: Change defaults (disable sounds and Disqus)

* Crowdfund: Move callback notification URL into additional options

Same as with the Point Of Sale options.

* Crowdfund: Make enable toggle more prominent

* Crowdfund: Improve start and end date form group display

* Template Editor: Improve button spacing

* adjusts currency + target

* removes "other actions"

* adjusts text

* adjusts status message bottom margin

Co-authored-by: dstrukt <gfxdsign@gmail.com>
2022-02-14 18:14:21 +09:00
0dc9c183b5 Store guest shouldn't see the bump fee option 2022-02-14 18:13:22 +09:00
05ab43f309 Redirect to transactions list after wallet creation (#3451)
Closes #3450.
2022-02-14 17:09:57 +09:00
f4153ade92 Zcash integration (#3400)
* zcash

* Use Channel instead of Queue
2022-02-14 17:04:34 +09:00
44e84b46b8 Upgrade Lightning lib (#3446)
Prerequisite for btcpayserver/btcpayserver#3423.
2022-02-10 19:33:57 +09:00
3fe71e7bdc Avoid NRE on PermissionTagHelper 2022-02-10 18:37:07 +09:00
5d4d8a3422 Simplify swagger 2022-02-10 17:09:55 +09:00
f06199230c Add ability to add description to pull payment (#3363)
* Add ability to add description to pull payment

close #2625

* Add API support

* Remove 'Model.Description != "<br>"'
2022-02-10 14:54:00 +09:00
da9a6b835a Greenfield: Store Users (#3425)
* Greenfield: Store Users

* fixups

Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2022-02-10 14:51:10 +09:00
0afc2cd2cb Add NetworkProvider.DefaultNetwork 2022-02-10 12:43:26 +09:00
9e2f7fb048 Do not register services of altcoins if not used 2022-02-10 12:36:07 +09:00
3fa694c65f Improve Lightning test scripts (#3435) 2022-02-10 12:25:14 +09:00
300d84c5d8 [UX/UI] Add CPFP (#3395)
* Add CPFP

* Sign PSBT should go back to the initial page
2022-02-10 12:24:28 +09:00
efed00f58b Setup guide: Link to first crypto if BTC not available 2022-02-10 12:23:45 +09:00
99c4ebe046 Setup guide: Link wallet setup always to BTC (#3442) 2022-02-10 12:17:15 +09:00
699231fd92 Fix payment request archival actions (#3443) 2022-02-09 23:37:15 +09:00
c18f112f31 Invoice status lookup fix (#3444)
See 4ca152da7cac87ecff7065ae077d72e0985be70b.
2022-02-09 23:35:17 +09:00
605d04580c Fix subnav on store pairing view (#3438)
Fixes #3431.
2022-02-09 12:24:25 +09:00
143211f276 Filter 'complete' = 'confirmed or settled' 2022-02-09 12:17:21 +09:00
4ca152da7c Fix paid invoice filter
Fixes #3434 by reverting the filter changes done [here](ec68d2a0e6 (diff-b7a89b0b45f062f004cdfe6ca8484f6ca519044f63485fd15986af5f7dd5ec76L219)).

The new labels are only used in the UI ­— when filtering one needs to use the old labels, as the filter docs in the view already suggest.
2022-02-09 12:12:32 +09:00
1c1f69fa50 bump 1.4.4 2022-02-08 12:21:58 +09:00
147ccd6c96 Ensure compresed public key is used for SIN generation even if uncompressed key was provided (fix #3432) (#3433) 2022-02-08 11:24:58 +09:00
c8b9906ef3 After login, redirect user to the main page even if root app configured (#3429) 2022-02-07 21:18:22 +09:00
cd94a9fac1 Make CheckNoDeadLink more reliable 2022-02-07 21:15:39 +09:00
20a9472ee2 Sticky headers (#3416)
* Make headers sticky

Closes #3344.

* Decrease headline margin bottom on mobile

* increases gap

* adds bottom padding

* Update BTCPayServer/Views/UIApps/UpdatePointOfSale.cshtml

* add "_blank" to view action

* Fix markup and tests

* Spacing updates

* Try test fix

* Re-add sticky account header and add test logs for timeout check

* Fix timeout issues

* Apply scroll padding on pages with sticky header

Co-authored-by: dstrukt <gfxdsign@gmail.com>
Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2022-02-07 18:37:45 +09:00
c652a2f122 Bumping LND to 0.14.2-beta (#3424) 2022-02-07 16:30:17 +09:00
ce174d507d Prevent initial scroll to section nav (#3411)
* Prevent initial scroll to section nav

On pages taht contain a section nav (sub navigation) the previous `scrollIntoView` led to an initial scroll position start at the section nav. See the store or server settings for examples.

This fixes it so that the scroll vertical position always starts at 0.

* Mobile: Prevent anchors from disappearing underneath the fixed header

* Add content padding top to scroll padding
2022-02-06 20:39:52 +09:00
f66b1b644f docker-entrypoint would crash if missing ssh pubkey 2022-02-06 20:39:10 +09:00
5b460f0b4e Error messages when starting BTCPay Server where not shown (Fix #3404) 2022-02-06 14:37:31 +09:00
ab8d116f11 Fix null reference exception when going to LN node settings before it's been set-up (#3410)
* Fix null reference exception when going to LN node settings before it's been set-up

* Fix-up remaining null reference errors

* Update error message
2022-02-06 12:19:42 +09:00
348 changed files with 10368 additions and 3083 deletions

View File

@ -2,7 +2,7 @@ version: 2
jobs:
fast_tests:
machine:
enabled: true
image: ubuntu-2004:202111-02
steps:
- checkout
- run:
@ -10,7 +10,7 @@ jobs:
cd .circleci && ./run-tests.sh "Fast=Fast|ThirdParty=ThirdParty" && ./can-build.sh
selenium_tests:
machine:
enabled: true
image: ubuntu-2004:202111-02
steps:
- checkout
- run:
@ -18,7 +18,7 @@ jobs:
cd .circleci && ./run-tests.sh "Selenium=Selenium"
integration_tests:
machine:
enabled: true
image: ubuntu-2004:202111-02
steps:
- checkout
- run:
@ -26,19 +26,18 @@ jobs:
cd .circleci && ./run-tests.sh "Integration=Integration"
trigger_docs_build:
machine:
enabled: true
image: circleci/classic:201808-01
image: ubuntu-2004:202111-02
steps:
- 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"}'
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:
enabled: true
image: ubuntu-2004:202111-02
steps:
- checkout
- checkout
- run:
command: |
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
@ -51,9 +50,9 @@ jobs:
arm32v7:
machine:
enabled: true
image: ubuntu-2004:202111-02
steps:
- checkout
- checkout
- run:
command: |
sudo docker run --rm --privileged multiarch/qemu-user-static:register --reset
@ -67,9 +66,9 @@ jobs:
arm64v8:
machine:
enabled: true
image: ubuntu-2004:202111-02
steps:
- checkout
- checkout
- run:
command: |
sudo docker run --rm --privileged multiarch/qemu-user-static:register --reset
@ -83,15 +82,10 @@ jobs:
multiarch:
machine:
enabled: true
image: circleci/classic:201808-01
image: ubuntu-2004:202201-02
steps:
- run:
command: |
# Turn on Experimental features
sudo mkdir $HOME/.docker
sudo sh -c 'echo "{ \"experimental\": \"enabled\" }" >> $HOME/.docker/config.json'
#
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
#
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
@ -100,8 +94,7 @@ jobs:
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

1
.gitignore vendored
View File

@ -299,3 +299,4 @@ BTCPayServer/wwwroot/bundles/*
BTCPayServer/testpwd
.DS_Store
Packed Plugins
Plugins/packed

View File

@ -6,7 +6,6 @@ namespace BTCPayServer.Configuration
public string PluginDir { get; set; }
public string TempStorageDir { get; set; }
public string StorageDir { get; set; }
public string TempDir { get; set; }
}
}

View File

@ -0,0 +1,7 @@
namespace BTCPayServer.Abstractions.Constants;
public class WellKnownTempData
{
public const string SuccessMessage = nameof(SuccessMessage);
public const string ErrorMessage = nameof(ErrorMessage);
}

View File

@ -1,10 +1,12 @@
using System.Threading.Tasks;
using BTCPayServer.Client;
using Microsoft.AspNetCore.Http;
namespace BTCPayServer.Abstractions.Contracts
{
public interface IBTCPayServerClientFactory
{
Task<BTCPayServerClient> Create(string userId, params string[] storeIds);
Task<BTCPayServerClient> Create(string userId, string[] storeIds, HttpContext httpRequest);
}
}

View File

@ -0,0 +1,17 @@
#nullable enable
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace BTCPayServer.Abstractions.Contracts;
public interface IFileService
{
Task<bool> IsAvailable();
Task<IStoredFile> AddFile(IFormFile file, string userId);
Task<IStoredFile> AddFile(Uri file, string userId);
Task<string?> GetFileUrl(Uri baseUri, string fileId);
Task<string?> GetTemporaryFileUrl(Uri baseUri, string fileId, DateTimeOffset expiry,
bool isDownload);
Task RemoveFile(string fileId, string userId);
}

View File

@ -0,0 +1,12 @@
using System;
namespace BTCPayServer.Abstractions.Contracts;
public interface IStoredFile
{
string Id { get; set; }
string FileName { get; set; }
string StorageFileName { get; set; }
DateTime Timestamp { get; set; }
string ApplicationUserId { get; set; }
}

View File

@ -0,0 +1,9 @@
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Abstractions.Contracts;
public interface ISwaggerProvider
{
Task<JObject> Fetch();
}

View File

@ -1,20 +0,0 @@
using System.Text.Json;
using BTCPayServer.Abstractions.Models;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
namespace BTCPayServer.Abstractions.Extensions
{
public static class SetStatusMessageModelExtensions
{
public static void SetStatusMessageModel(this ITempDataDictionary tempData, StatusMessageModel statusMessage)
{
if (statusMessage == null)
{
tempData.Remove("StatusMessageModel");
return;
}
tempData["StatusMessageModel"] = JsonSerializer.Serialize(statusMessage, new JsonSerializerOptions());
}
}
}

View File

@ -0,0 +1,43 @@
using System.Collections.Generic;
using BTCPayServer.Client.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace BTCPayServer.Abstractions.Extensions;
public static class GreenfieldExtensions
{
public static IActionResult CreateValidationError(this ControllerBase controller, ModelStateDictionary modelState)
{
return controller.UnprocessableEntity(modelState.ToGreenfieldValidationError());
}
public static List<GreenfieldValidationError> ToGreenfieldValidationError(this ModelStateDictionary modelState)
{
List<GreenfieldValidationError> errors = new List<GreenfieldValidationError>();
foreach (var error in modelState)
{
foreach (var errorMessage in error.Value.Errors)
{
errors.Add(new GreenfieldValidationError(error.Key, errorMessage.ErrorMessage));
}
}
return errors;
}
public static IActionResult CreateAPIError(this ControllerBase controller, string errorCode, string errorMessage)
{
return controller.BadRequest(new GreenfieldAPIError(errorCode, errorMessage));
}
public static IActionResult CreateAPIError(this ControllerBase controller, int httpCode, string errorCode, string errorMessage)
{
return controller.StatusCode(httpCode, new GreenfieldAPIError(errorCode, errorMessage));
}
public static IActionResult CreateAPIPermissionError(this ControllerBase controller, string missingPermission, string message = null)
{
return controller.StatusCode(403, new GreenfieldPermissionAPIError(missingPermission, message));
}
}

View File

@ -0,0 +1,120 @@
using System;
using Microsoft.AspNetCore.Http;
namespace BTCPayServer.Abstractions.Extensions;
public static class HttpRequestExtensions
{
public static bool IsOnion(this HttpRequest request)
{
if (request?.Host.Host == null)
return false;
return request.Host.Host.EndsWith(".onion", StringComparison.OrdinalIgnoreCase);
}
public static string GetAbsoluteRoot(this HttpRequest request)
{
return string.Concat(
request.Scheme,
"://",
request.Host.ToUriComponent(),
request.PathBase.ToUriComponent());
}
public static Uri GetAbsoluteRootUri(this HttpRequest request)
{
return new Uri(request.GetAbsoluteRoot());
}
public static string GetCurrentUrl(this HttpRequest request)
{
return string.Concat(
request.Scheme,
"://",
request.Host.ToUriComponent(),
request.PathBase.ToUriComponent(),
request.Path.ToUriComponent());
}
public static string GetCurrentPath(this HttpRequest request)
{
return string.Concat(
request.PathBase.ToUriComponent(),
request.Path.ToUriComponent());
}
public static string GetCurrentPathWithQueryString(this HttpRequest request)
{
return request.PathBase + request.Path + request.QueryString;
}
/// <summary>
/// If 'toto' and RootPath is 'rootpath' returns '/rootpath/toto'
/// If 'toto' and RootPath is empty returns '/toto'
/// </summary>
/// <param name="request"></param>
/// <param name="path"></param>
/// <returns></returns>
public static string GetRelativePath(this HttpRequest request, string path)
{
if (path.Length > 0 && path[0] != '/')
path = $"/{path}";
return string.Concat(
request.PathBase.ToUriComponent(),
path);
}
/// <summary>
/// If 'https://example.com/toto' returns 'https://example.com/toto'
/// If 'toto' and RootPath is 'rootpath' returns '/rootpath/toto'
/// If 'toto' and RootPath is empty returns '/toto'
/// </summary>
/// <param name="request"></param>
/// <param name="path"></param>
/// <returns></returns>
public static string GetRelativePathOrAbsolute(this HttpRequest request, string path)
{
if (!Uri.TryCreate(path, UriKind.RelativeOrAbsolute, out var uri) ||
uri.IsAbsoluteUri)
return path;
if (path.Length > 0 && path[0] != '/')
path = $"/{path}";
return string.Concat(
request.PathBase.ToUriComponent(),
path);
}
public static string GetAbsoluteUri(this HttpRequest request, string redirectUrl)
{
bool isRelative =
(redirectUrl.Length > 0 && redirectUrl[0] == '/')
|| !new Uri(redirectUrl, UriKind.RelativeOrAbsolute).IsAbsoluteUri;
return isRelative ? request.GetAbsoluteRoot() + redirectUrl : redirectUrl;
}
/// <summary>
/// Will return an absolute URL.
/// If `relativeOrAsbolute` is absolute, returns it.
/// If `relativeOrAsbolute` is relative, send absolute url based on the HOST of this request (without PathBase)
/// </summary>
/// <param name="request"></param>
/// <param name="relativeOrAbsolte"></param>
/// <returns></returns>
public static Uri GetAbsoluteUriNoPathBase(this HttpRequest request, Uri relativeOrAbsolute = null)
{
if (relativeOrAbsolute == null)
{
return new Uri(string.Concat(
request.Scheme,
"://",
request.Host.ToUriComponent()), UriKind.Absolute);
}
if (relativeOrAbsolute.IsAbsoluteUri)
return relativeOrAbsolute;
return new Uri(string.Concat(
request.Scheme,
"://",
request.Host.ToUriComponent()) + relativeOrAbsolute.ToString().WithStartingSlash(), UriKind.Absolute);
}
}

View File

@ -0,0 +1,59 @@
using System.Text.Json;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Models;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Abstractions.Extensions;
public static class SetStatusMessageModelExtensions
{
public static void SetStatusMessageModel(this ITempDataDictionary tempData, StatusMessageModel statusMessage)
{
if (statusMessage == null)
{
tempData.Remove("StatusMessageModel");
return;
}
tempData["StatusMessageModel"] = JsonSerializer.Serialize(statusMessage, new JsonSerializerOptions());
}
public static StatusMessageModel GetStatusMessageModel(this ITempDataDictionary tempData)
{
tempData.TryGetValue(WellKnownTempData.SuccessMessage, out var successMessage);
tempData.TryGetValue(WellKnownTempData.ErrorMessage, out var errorMessage);
tempData.TryGetValue("StatusMessageModel", out var model);
if (successMessage != null || errorMessage != null)
{
var parsedModel = new StatusMessageModel();
parsedModel.Message = (string)successMessage ?? (string)errorMessage;
if (successMessage != null)
{
parsedModel.Severity = StatusMessageModel.StatusSeverity.Success;
}
else
{
parsedModel.Severity = StatusMessageModel.StatusSeverity.Error;
}
return parsedModel;
}
else if (model != null && model is string str)
{
return JObject.Parse(str).ToObject<StatusMessageModel>();
}
return null;
}
public static bool HasStatusMessage(this ITempDataDictionary tempData)
{
return (tempData.Peek(WellKnownTempData.SuccessMessage) ??
tempData.Peek(WellKnownTempData.ErrorMessage) ??
tempData.Peek("StatusMessageModel")) != null;
}
public static bool HasErrorMessage(this ITempDataDictionary tempData)
{
return GetStatusMessageModel(tempData)?.Severity == StatusMessageModel.StatusSeverity.Error;
}
}

View File

@ -0,0 +1,45 @@
using System;
using System.IO;
using System.Linq;
namespace BTCPayServer.Abstractions.Extensions;
public static class StringExtensions
{
public static bool IsValidFileName(this string fileName)
{
return !fileName.ToCharArray().Any(c => Path.GetInvalidFileNameChars().Contains(c)
|| c == Path.AltDirectorySeparatorChar
|| c == Path.DirectorySeparatorChar
|| c == Path.PathSeparator
|| c == '\\');
}
public static string Truncate(this string value, int maxLength)
{
if (string.IsNullOrEmpty(value))
return value;
return value.Length <= maxLength ? value : value.Substring(0, maxLength);
}
public static string WithTrailingSlash(this string str)
{
if (str.EndsWith("/", StringComparison.InvariantCulture))
return str;
return str + "/";
}
public static string WithStartingSlash(this string str)
{
if (str.StartsWith("/", StringComparison.InvariantCulture))
return str;
return $"/{str}";
}
public static string WithoutEndingSlash(this string str)
{
if (str.EndsWith("/", StringComparison.InvariantCulture))
return str.Substring(0, str.Length - 1);
return str;
}
}

View File

@ -28,8 +28,8 @@
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="NBitcoin" Version="6.0.19" />
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.2.8" />
<PackageReference Include="NBitcoin" Version="7.0.1" />
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.3.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>
<ItemGroup>

View File

@ -54,8 +54,7 @@ namespace BTCPayServer.Client
return await HandleResponse<string>(response);
}
public virtual async Task PayLightningInvoice(string cryptoCode, PayLightningInvoiceRequest request,
public virtual async Task<LightningPaymentData> PayLightningInvoice(string cryptoCode, PayLightningInvoiceRequest request,
CancellationToken token = default)
{
if (request == null)
@ -63,7 +62,18 @@ namespace BTCPayServer.Client
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/invoices/pay", bodyPayload: request,
method: HttpMethod.Post), token);
await HandleResponse(response);
return await HandleResponse<LightningPaymentData>(response);
}
public virtual async Task<LightningPaymentData> GetLightningPayment(string cryptoCode,
string paymentHash, CancellationToken token = default)
{
if (paymentHash == null)
throw new ArgumentNullException(nameof(paymentHash));
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/payments/{paymentHash}",
method: HttpMethod.Get), token);
return await HandleResponse<LightningPaymentData>(response);
}
public virtual async Task<LightningInvoiceData> GetLightningInvoice(string cryptoCode,

View File

@ -67,6 +67,17 @@ namespace BTCPayServer.Client
await HandleResponse(response);
}
public virtual async Task<LightningPaymentData> GetLightningPayment(string storeId, string cryptoCode,
string paymentHash, CancellationToken token = default)
{
if (paymentHash == null)
throw new ArgumentNullException(nameof(paymentHash));
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/payments/{paymentHash}",
method: HttpMethod.Get), token);
return await HandleResponse<LightningPaymentData>(response);
}
public virtual async Task<LightningInvoiceData> GetLightningInvoice(string storeId, string cryptoCode,
string invoiceId, CancellationToken token = default)
{

View File

@ -20,7 +20,7 @@ namespace BTCPayServer.Client
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain",
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain",
query), token);
return await HandleResponse<IEnumerable<OnChainPaymentMethodData>>(response);
}
@ -30,7 +30,7 @@ namespace BTCPayServer.Client
{
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}"), token);
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}"), token);
return await HandleResponse<OnChainPaymentMethodData>(response);
}
@ -39,7 +39,7 @@ namespace BTCPayServer.Client
{
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}",
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}",
method: HttpMethod.Delete), token);
await HandleResponse(response);
}
@ -49,7 +49,7 @@ namespace BTCPayServer.Client
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}",
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}",
bodyPayload: paymentMethod, method: HttpMethod.Put), token);
return await HandleResponse<OnChainPaymentMethodData>(response);
}
@ -61,7 +61,7 @@ namespace BTCPayServer.Client
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/preview",
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/preview",
bodyPayload: paymentMethod,
queryPayload: new Dictionary<string, object>() { { "offset", offset }, { "amount", amount } },
method: HttpMethod.Post), token);
@ -73,7 +73,7 @@ namespace BTCPayServer.Client
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/preview",
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/preview",
queryPayload: new Dictionary<string, object>() { { "offset", offset }, { "amount", amount } },
method: HttpMethod.Get), token);
return await HandleResponse<OnChainPaymentMethodPreviewResultData>(response);
@ -84,7 +84,7 @@ namespace BTCPayServer.Client
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/generate",
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/generate",
bodyPayload: request,
method: HttpMethod.Post), token);
return await HandleResponse<OnChainPaymentMethodDataWithSensitiveData>(response);

View File

@ -16,7 +16,7 @@ namespace BTCPayServer.Client
{
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/wallet"), token);
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet"), token);
return await HandleResponse<OnChainWalletOverviewData>(response);
}
public virtual async Task<OnChainWalletFeeRateData> GetOnChainFeeRate(string storeId, string cryptoCode, int? blockTarget = null,
@ -29,7 +29,7 @@ namespace BTCPayServer.Client
}
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/wallet/feeRate", queryParams), token);
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/feeRate", queryParams), token);
return await HandleResponse<OnChainWalletFeeRateData>(response);
}
@ -38,7 +38,7 @@ namespace BTCPayServer.Client
{
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/wallet/address", new Dictionary<string, object>()
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/address", new Dictionary<string, object>()
{
{"forceGenerate", forceGenerate}
}), token);
@ -50,7 +50,7 @@ namespace BTCPayServer.Client
{
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/wallet/address", method: HttpMethod.Delete), token);
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/address", method: HttpMethod.Delete), token);
await HandleResponse(response);
}
@ -65,7 +65,7 @@ namespace BTCPayServer.Client
}
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/wallet/transactions", query), token);
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/transactions", query), token);
return await HandleResponse<IEnumerable<OnChainWalletTransactionData>>(response);
}
@ -75,7 +75,18 @@ namespace BTCPayServer.Client
{
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/wallet/transactions/{transactionId}"), token);
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/transactions/{transactionId}"), token);
return await HandleResponse<OnChainWalletTransactionData>(response);
}
public virtual async Task<OnChainWalletTransactionData> PatchOnChainWalletTransaction(
string storeId, string cryptoCode, string transactionId,
PatchOnChainTransactionRequest request,
CancellationToken token = default)
{
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/transactions/{transactionId}", queryPayload: null, bodyPayload: request, HttpMethod.Patch), token);
return await HandleResponse<OnChainWalletTransactionData>(response);
}
@ -85,7 +96,7 @@ namespace BTCPayServer.Client
{
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/wallet/utxos"), token);
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/utxos"), token);
return await HandleResponse<IEnumerable<OnChainWalletUTXOData>>(response);
}
@ -100,7 +111,7 @@ namespace BTCPayServer.Client
}
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/wallet/transactions", null, request, HttpMethod.Post), token);
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/transactions", null, request, HttpMethod.Post), token);
return await HandleResponse<OnChainWalletTransactionData>(response);
}
@ -115,7 +126,7 @@ namespace BTCPayServer.Client
}
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/wallet/transactions", null, request, HttpMethod.Post), token);
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/transactions", null, request, HttpMethod.Post), token);
return Transaction.Parse(await HandleResponse<string>(response), network);
}
}

View File

@ -0,0 +1,37 @@
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
namespace BTCPayServer.Client
{
public partial class BTCPayServerClient
{
public virtual async Task<EmailSettingsData> GetStoreEmailSettings(string storeId,
CancellationToken token = default)
{
using var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/email", method: HttpMethod.Get),
token);
return await HandleResponse<EmailSettingsData>(response);
}
public virtual async Task<EmailSettingsData> UpdateStoreEmailSettings(string storeId, EmailSettingsData request,
CancellationToken token = default)
{
using var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/email", bodyPayload: request, method: HttpMethod.Put),
token);
return await HandleResponse<EmailSettingsData>(response);
}
public virtual async Task SendEmail(string storeId, SendEmailRequest request,
CancellationToken token = default)
{
using var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/email/send", bodyPayload: request, method: HttpMethod.Post),
token);
await HandleResponse(response);
}
}
}

View File

@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
namespace BTCPayServer.Client
{
public partial class BTCPayServerClient
{
public virtual async Task<IEnumerable<StoreUserData>> GetStoreUsers(string storeId,
CancellationToken token = default)
{
using var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/users"), token);
return await HandleResponse<IEnumerable<StoreUserData>>(response);
}
public virtual async Task RemoveStoreUser(string storeId, string userId, CancellationToken token = default)
{
using var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/users/{userId}", method: HttpMethod.Delete), token);
await HandleResponse(response);
}
public virtual async Task AddStoreUser(string storeId, StoreUserData request,
CancellationToken token = default)
{
if (request == null)
throw new ArgumentNullException(nameof(request));
using var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/users", bodyPayload: request, method: HttpMethod.Post),
token);
await HandleResponse(response);
}
}
}

View File

@ -27,6 +27,27 @@ namespace BTCPayServer.Client
await HandleResponse(response);
}
public virtual async Task<ApplicationUserData> GetUserByIdOrEmail(string idOrEmail, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/users/{idOrEmail}", null, HttpMethod.Get), token);
return await HandleResponse<ApplicationUserData>(response);
}
public virtual async Task ToggleUser(string idOrEmail, bool enabled, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/users/{idOrEmail}/toggle", null,new
{
enabled
} , HttpMethod.Post), token);
await HandleResponse(response);
}
public virtual async Task<ApplicationUserData[]> GetUsers( CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/users/", null, HttpMethod.Get), token);
return await HandleResponse<ApplicationUserData[]>(response);
}
public virtual async Task DeleteCurrentUser(CancellationToken token = default)
{
await DeleteUser("me", token);

View File

@ -35,5 +35,7 @@ namespace BTCPayServer.Client.Models
/// </summary>
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
public DateTimeOffset? Created { get; set; }
public bool Disabled { get; set; }
}
}

View File

@ -1,6 +1,6 @@
using System;
using BTCPayServer.Lightning;
using BTCPayServer.Lightning.JsonConverters;
using NBitcoin;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models
@ -11,15 +11,19 @@ namespace BTCPayServer.Client.Models
{
}
public CreateLightningInvoiceRequest(LightMoney amount, string description, TimeSpan expiry)
{
Amount = amount;
Description = description;
Expiry = expiry;
}
[JsonConverter(typeof(BTCPayServer.Client.JsonConverters.LightMoneyJsonConverter))]
[JsonConverter(typeof(JsonConverters.LightMoneyJsonConverter))]
public LightMoney Amount { get; set; }
public string Description { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.UInt256JsonConverter))]
public uint256 DescriptionHash { get; set; }
[JsonConverter(typeof(JsonConverters.TimeSpanJsonConverter.Seconds))]
public TimeSpan Expiry { get; set; }
public bool PrivateRouteHints { get; set; }

View File

@ -8,6 +8,7 @@ namespace BTCPayServer.Client.Models
public class CreatePullPaymentRequest
{
public string Name { get; set; }
public string Description { get; set; }
[JsonProperty(ItemConverterType = typeof(NumericStringJsonConverter))]
public decimal Amount { get; set; }
public string Currency { get; set; }

View File

@ -0,0 +1,33 @@
namespace BTCPayServer.Client.Models;
public class EmailSettingsData
{
public string Server
{
get; set;
}
public int? Port
{
get; set;
}
public string Login
{
get; set;
}
public string Password
{
get; set;
}
public string FromDisplay
{
get; set;
}
public string From
{
get; set;
}
}

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using BTCPayServer.JsonConverters;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client.Models
{
@ -34,6 +35,7 @@ namespace BTCPayServer.Client.Models
public string PaymentMethod { get; set; }
public string CryptoCode { get; set; }
public JObject AdditionalData { get; set; }
public class Payment
{

View File

@ -0,0 +1,32 @@
using System;
using BTCPayServer.Client.JsonConverters;
using BTCPayServer.Lightning;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace BTCPayServer.Client.Models
{
public class LightningPaymentData
{
public string Id { get; set; }
public string PaymentHash { get; set; }
[JsonConverter(typeof(StringEnumConverter))]
public LightningPaymentStatus Status { get; set; }
[JsonProperty("BOLT11")]
public string BOLT11 { get; set; }
public string Preimage { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
public DateTimeOffset? CreatedAt { get; set; }
[JsonConverter(typeof(LightMoneyJsonConverter))]
public LightMoney TotalAmount { get; set; }
[JsonConverter(typeof(LightMoneyJsonConverter))]
public LightMoney FeeAmount { get; set; }
}
}

View File

@ -21,9 +21,9 @@ namespace BTCPayServer.Client.Models
[JsonConverter(typeof(UInt256JsonConverter))]
public uint256 BlockHash { get; set; }
public int? BlockHeight { get; set; }
public long? BlockHeight { get; set; }
public int Confirmations { get; set; }
public long Confirmations { get; set; }
[JsonConverter(typeof(DateTimeToUnixTimeConverter))]
public DateTimeOffset Timestamp { get; set; }

View File

@ -21,6 +21,6 @@ namespace BTCPayServer.Client.Models
[JsonConverter(typeof(KeyPathJsonConverter))]
public KeyPath KeyPath { get; set; }
public string Address { get; set; }
public int Confirmations { get; set; }
public long Confirmations { get; set; }
}
}

View File

@ -0,0 +1,12 @@
#nullable enable
using System.Collections.Generic;
namespace BTCPayServer.Client.Models
{
public class PatchOnChainTransactionRequest
{
public string? Comment { get; set; } = null;
public List<string>? Labels { get; set; } = null;
}
}

View File

@ -1,8 +1,20 @@
#nullable enable
using BTCPayServer.Client.JsonConverters;
using BTCPayServer.JsonConverters;
using NBitcoin;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models
{
public class PayLightningInvoiceRequest
{
[Newtonsoft.Json.JsonProperty("BOLT11")]
[JsonProperty("BOLT11")]
public string BOLT11 { get; set; }
[JsonProperty(ItemConverterType = typeof(NumericStringJsonConverter))]
public float? MaxFeePercent { get; set; }
[JsonConverter(typeof(MoneyJsonConverter))]
public Money? MaxFeeFlat { get; set; }
}
}

View File

@ -5,6 +5,13 @@ using Newtonsoft.Json;
namespace BTCPayServer.Client.Models
{
public enum PullPaymentState
{
Active,
Expired,
Archived,
Future
}
public class PullPaymentData
{
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
@ -13,6 +20,7 @@ namespace BTCPayServer.Client.Models
public DateTimeOffset? ExpiresAt { get; set; }
public string Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string Currency { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal Amount { get; set; }

View File

@ -0,0 +1,10 @@
namespace BTCPayServer.Client.Models
{
public class SendEmailRequest
{
public string Email;
public string Subject;
public string Body;
}
}

View File

@ -7,4 +7,14 @@ namespace BTCPayServer.Client.Models
/// </summary>
public string Id { get; set; }
}
public class StoreUserData
{
/// <summary>
/// the id of the user
/// </summary>
public string UserId { get; set; }
public string Role { get; set; }
}
}

View File

@ -24,6 +24,7 @@ namespace BTCPayServer.Client
public const string CanViewProfile = "btcpay.user.canviewprofile";
public const string CanManageNotificationsForUser = "btcpay.user.canmanagenotificationsforuser";
public const string CanViewNotificationsForUser = "btcpay.user.canviewnotificationsforuser";
public const string CanViewUsers = "btcpay.server.canviewusers";
public const string CanCreateUser = "btcpay.server.cancreateuser";
public const string CanDeleteUser = "btcpay.user.candeleteuser";
public const string CanManagePullPayments = "btcpay.store.canmanagepullpayments";
@ -43,6 +44,7 @@ namespace BTCPayServer.Client
yield return CanModifyPaymentRequests;
yield return CanModifyProfile;
yield return CanViewProfile;
yield return CanViewUsers;
yield return CanCreateUser;
yield return CanDeleteUser;
yield return CanManageNotificationsForUser;
@ -175,7 +177,9 @@ namespace BTCPayServer.Client
case Policies.CanViewProfile when this.Policy == Policies.CanModifyProfile:
case Policies.CanModifyPaymentRequests when this.Policy == Policies.CanModifyStoreSettings:
case Policies.CanViewPaymentRequests when this.Policy == Policies.CanModifyStoreSettings:
case Policies.CanManagePullPayments when this.Policy == Policies.CanModifyStoreSettings:
case Policies.CanViewPaymentRequests when this.Policy == Policies.CanViewStoreSettings:
case Policies.CanViewPaymentRequests when this.Policy == Policies.CanModifyPaymentRequests:
case Policies.CanCreateLightningInvoiceInternalNode when this.Policy == Policies.CanUseInternalLightningNode:
case Policies.CanCreateLightningInvoiceInStore when this.Policy == Policies.CanUseLightningNodeInStore:
case Policies.CanViewNotificationsForUser when this.Policy == Policies.CanManageNotificationsForUser:

View File

@ -11,7 +11,7 @@ namespace BTCPayServer
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Althash",
DisplayName = "Htmlcoin",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://explorer.htmlcoin.com/api/tx/{0}" : "https://explorer.htmlcoin.com/api/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
DefaultRateRules = new[]
@ -21,7 +21,7 @@ namespace BTCPayServer
},
CryptoImagePath = "imlegacy/althash.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("88'") : new KeyPath("1'")
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("172'") : new KeyPath("1'")
});
}
}

View File

@ -0,0 +1,29 @@
using NBitcoin;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
// Change this if you want another zcash coin
public void InitZcash()
{
Add(new ZcashLikeSpecificBtcPayNetwork()
{
CryptoCode = "ZEC",
DisplayName = "Zcash",
Divisibility = 8,
BlockExplorerLink =
NetworkType == ChainName.Mainnet
? "https://www.exploreZcash.com/transaction/{0}"
: "https://testnet.xmrchain.net/tx/{0}",
DefaultRateRules = new[]
{
"ZEC_X = ZEC_BTC * BTC_X",
"ZEC_BTC = kraken(ZEC_BTC)"
},
CryptoImagePath = "/imlegacy/zcash.png",
UriScheme = "zcash"
});
}
}
}

View File

@ -0,0 +1,102 @@
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
namespace BTCPayServer.Services.Altcoins.Zcash.RPC
{
public class JsonRpcClient
{
private readonly Uri _address;
private readonly string _username;
private readonly string _password;
private readonly HttpClient _httpClient;
public JsonRpcClient(Uri address, string username, string password, HttpClient client = null)
{
_address = address;
_username = username;
_password = password;
_httpClient = client ?? new HttpClient();
}
public async Task<TResponse> SendCommandAsync<TRequest, TResponse>(string method, TRequest data,
CancellationToken cts = default(CancellationToken))
{
var jsonSerializer = new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
};
var httpRequest = new HttpRequestMessage()
{
Method = HttpMethod.Post,
RequestUri = new Uri(_address, method),
Content = new StringContent(
JsonConvert.SerializeObject(data, jsonSerializer),
Encoding.UTF8, "application/json")
};
// httpRequest.Headers.Accept.Clear();
// httpRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
// httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Basic",
// Convert.ToBase64String(Encoding.Default.GetBytes($"{_username}:{_password}")));
var rawResult = await _httpClient.SendAsync(httpRequest, cts);
var rawJson = await rawResult.Content.ReadAsStringAsync();
rawResult.EnsureSuccessStatusCode();
var response = JsonConvert.DeserializeObject<TResponse>(rawJson, jsonSerializer);
return response;
}
public class NoRequestModel
{
public static NoRequestModel Instance = new NoRequestModel();
}
internal class JsonRpcApiException : Exception
{
public JsonRpcResultError Error { get; set; }
public override string Message => Error?.Message;
}
public class JsonRpcResultError
{
[JsonProperty("code")] public int Code { get; set; }
[JsonProperty("message")] public string Message { get; set; }
[JsonProperty("data")] dynamic Data { get; set; }
}
internal class JsonRpcResult<T>
{
[JsonProperty("result")] public T Result { get; set; }
[JsonProperty("error")] public JsonRpcResultError Error { get; set; }
[JsonProperty("id")] public string Id { get; set; }
}
internal class JsonRpcCommand<T>
{
[JsonProperty("jsonRpc")] public string JsonRpc { get; set; } = "2.0";
[JsonProperty("id")] public string Id { get; set; } = Guid.NewGuid().ToString();
[JsonProperty("method")] public string Method { get; set; }
[JsonProperty("params")] public T Parameters { get; set; }
public JsonRpcCommand()
{
}
public JsonRpcCommand(string method, T parameters)
{
Method = method;
Parameters = parameters;
}
}
}
}

View File

@ -0,0 +1,9 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
{
public partial class CreateAccountRequest
{
[JsonProperty("label")] public string Label { get; set; }
}
}

View File

@ -0,0 +1,10 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
{
public partial class CreateAccountResponse
{
[JsonProperty("account_index")] public long AccountIndex { get; set; }
[JsonProperty("address")] public string Address { get; set; }
}
}

View File

@ -0,0 +1,10 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
{
public partial class CreateAddressRequest
{
[JsonProperty("account_index")] public long AccountIndex { get; set; }
[JsonProperty("label")] public string Label { get; set; }
}
}

View File

@ -0,0 +1,10 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
{
public partial class CreateAddressResponse
{
[JsonProperty("address")] public string Address { get; set; }
[JsonProperty("address_index")] public long AddressIndex { get; set; }
}
}

View File

@ -0,0 +1,9 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
{
public partial class GetAccountsRequest
{
[JsonProperty("tag")] public string Tag { get; set; }
}
}

View File

@ -0,0 +1,14 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
{
public partial class GetAccountsResponse
{
[JsonProperty("subaddress_accounts")] public List<SubaddressAccount> SubaddressAccounts { get; set; }
[JsonProperty("total_balance")] public decimal TotalBalance { get; set; }
[JsonProperty("total_unlocked_balance")]
public decimal TotalUnlockedBalance { get; set; }
}
}

View File

@ -0,0 +1,9 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
{
public class GetFeeEstimateRequest
{
[JsonProperty("grace_blocks")] public int? GraceBlocks { get; set; }
}
}

View File

@ -0,0 +1,11 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
{
public class GetFeeEstimateResponse
{
[JsonProperty("fee")] public long Fee { get; set; }
[JsonProperty("status")] public string Status { get; set; }
[JsonProperty("untrusted")] public bool Untrusted { get; set; }
}
}

View File

@ -0,0 +1,9 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
{
public partial class GetHeightResponse
{
[JsonProperty("height")] public long Height { get; set; }
}
}

View File

@ -0,0 +1,11 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
{
public class GetTransferByTransactionIdRequest
{
[JsonProperty("txid")] public string TransactionId { get; set; }
[JsonProperty("account_index")] public long AccountIndex { get; set; }
}
}

View File

@ -0,0 +1,31 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
{
public partial class GetTransferByTransactionIdResponse
{
[JsonProperty("transfer")] public TransferItem Transfer { get; set; }
[JsonProperty("transfers")] public IEnumerable<TransferItem> Transfers { get; set; }
public partial class TransferItem
{
[JsonProperty("address")] public string Address { get; set; }
[JsonProperty("amount")] public long Amount { get; set; }
[JsonProperty("confirmations")] public long Confirmations { get; set; }
[JsonProperty("double_spend_seen")] public bool DoubleSpendSeen { get; set; }
[JsonProperty("height")] public long Height { get; set; }
[JsonProperty("note")] public string Note { get; set; }
[JsonProperty("payment_id")] public string PaymentId { get; set; }
[JsonProperty("subaddr_index")] public SubaddrIndex SubaddrIndex { get; set; }
[JsonProperty("suggested_confirmations_threshold")]
public long SuggestedConfirmationsThreshold { get; set; }
[JsonProperty("timestamp")] public long Timestamp { get; set; }
[JsonProperty("txid")] public string Txid { get; set; }
[JsonProperty("type")] public string Type { get; set; }
[JsonProperty("unlock_time")] public long UnlockTime { get; set; }
}
}
}

View File

@ -0,0 +1,19 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
{
public partial class GetTransfersRequest
{
[JsonProperty("in")] public bool In { get; set; }
[JsonProperty("out")] public bool Out { get; set; }
[JsonProperty("pending")] public bool Pending { get; set; }
[JsonProperty("failed")] public bool Failed { get; set; }
[JsonProperty("pool")] public bool Pool { get; set; }
[JsonProperty("filter_by_height ")] public bool FilterByHeight { get; set; }
[JsonProperty("min_height")] public long MinHeight { get; set; }
[JsonProperty("max_height")] public long MaxHeight { get; set; }
[JsonProperty("account_index")] public long AccountIndex { get; set; }
[JsonProperty("subaddr_indices")] public List<long> SubaddrIndices { get; set; }
}
}

View File

@ -0,0 +1,35 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
{
public partial class GetTransfersResponse
{
[JsonProperty("in")] public List<GetTransfersResponseItem> In { get; set; }
[JsonProperty("out")] public List<GetTransfersResponseItem> Out { get; set; }
[JsonProperty("pending")] public List<GetTransfersResponseItem> Pending { get; set; }
[JsonProperty("failed")] public List<GetTransfersResponseItem> Failed { get; set; }
[JsonProperty("pool")] public List<GetTransfersResponseItem> Pool { get; set; }
public partial class GetTransfersResponseItem
{
[JsonProperty("address")] public string Address { get; set; }
[JsonProperty("amount")] public long Amount { get; set; }
[JsonProperty("confirmations")] public long Confirmations { get; set; }
[JsonProperty("double_spend_seen")] public bool DoubleSpendSeen { get; set; }
[JsonProperty("height")] public long Height { get; set; }
[JsonProperty("note")] public string Note { get; set; }
[JsonProperty("payment_id")] public string PaymentId { get; set; }
[JsonProperty("subaddr_index")] public SubaddrIndex SubaddrIndex { get; set; }
[JsonProperty("suggested_confirmations_threshold")]
public long SuggestedConfirmationsThreshold { get; set; }
[JsonProperty("timestamp")] public long Timestamp { get; set; }
[JsonProperty("txid")] public string Txid { get; set; }
[JsonProperty("type")] public string Type { get; set; }
[JsonProperty("unlock_time")] public long UnlockTime { get; set; }
}
}
}

View File

@ -0,0 +1,33 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
{
public partial class Info
{
[JsonProperty("address")] public string Address { get; set; }
[JsonProperty("avg_download")] public long AvgDownload { get; set; }
[JsonProperty("avg_upload")] public long AvgUpload { get; set; }
[JsonProperty("connection_id")] public string ConnectionId { get; set; }
[JsonProperty("current_download")] public long CurrentDownload { get; set; }
[JsonProperty("current_upload")] public long CurrentUpload { get; set; }
[JsonProperty("height")] public long Height { get; set; }
[JsonProperty("host")] public string Host { get; set; }
[JsonProperty("incoming")] public bool Incoming { get; set; }
[JsonProperty("ip")] public string Ip { get; set; }
[JsonProperty("live_time")] public long LiveTime { get; set; }
[JsonProperty("local_ip")] public bool LocalIp { get; set; }
[JsonProperty("localhost")] public bool Localhost { get; set; }
[JsonProperty("peer_id")] public string PeerId { get; set; }
[JsonProperty("port")]
[JsonConverter(typeof(ParseStringConverter))]
public long Port { get; set; }
[JsonProperty("recv_count")] public long RecvCount { get; set; }
[JsonProperty("recv_idle_time")] public long RecvIdleTime { get; set; }
[JsonProperty("send_count")] public long SendCount { get; set; }
[JsonProperty("send_idle_time")] public long SendIdleTime { get; set; }
[JsonProperty("state")] public string State { get; set; }
[JsonProperty("support_flags")] public long SupportFlags { get; set; }
}
}

View File

@ -0,0 +1,13 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
{
public partial class MakeUriRequest
{
[JsonProperty("address")] public string Address { get; set; }
[JsonProperty("amount")] public long Amount { get; set; }
[JsonProperty("payment_id")] public string PaymentId { get; set; }
[JsonProperty("tx_description")] public string TxDescription { get; set; }
[JsonProperty("recipient_name")] public string RecipientName { get; set; }
}
}

View File

@ -0,0 +1,9 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
{
public partial class MakeUriResponse
{
[JsonProperty("uri")] public string Uri { get; set; }
}
}

View File

@ -0,0 +1,40 @@
using System;
using System.Globalization;
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
{
internal class ParseStringConverter : JsonConverter
{
public override bool CanConvert(Type t) => t == typeof(long) || t == typeof(long?);
public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
var value = serializer.Deserialize<string>(reader);
long l;
if (Int64.TryParse(value, out l))
{
return l;
}
throw new Exception("Cannot unmarshal type long");
}
public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer)
{
if (untypedValue == null)
{
serializer.Serialize(writer, null);
return;
}
var value = (long)untypedValue;
serializer.Serialize(writer, value.ToString(CultureInfo.InvariantCulture));
return;
}
public static readonly ParseStringConverter Singleton = new ParseStringConverter();
}
}

View File

@ -0,0 +1,9 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
{
public partial class Peer
{
[JsonProperty("info")] public Info Info { get; set; }
}
}

View File

@ -0,0 +1,10 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
{
public partial class SubaddrIndex
{
[JsonProperty("major")] public long Major { get; set; }
[JsonProperty("minor")] public long Minor { get; set; }
}
}

View File

@ -0,0 +1,14 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
{
public partial class SubaddressAccount
{
[JsonProperty("account_index")] public long AccountIndex { get; set; }
[JsonProperty("balance")] public decimal Balance { get; set; }
[JsonProperty("base_address")] public string BaseAddress { get; set; }
[JsonProperty("label")] public string Label { get; set; }
[JsonProperty("tag")] public string Tag { get; set; }
[JsonProperty("unlocked_balance")] public decimal UnlockedBalance { get; set; }
}
}

View File

@ -0,0 +1,13 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
{
public partial class SyncInfoResponse
{
[JsonProperty("height")] public long Height { get; set; }
[JsonProperty("peers")] public List<Peer> Peers { get; set; }
[JsonProperty("status")] public string Status { get; set; }
[JsonProperty("target_height")] public long? TargetHeight { get; set; }
}
}

View File

@ -0,0 +1,20 @@
using System.Globalization;
namespace BTCPayServer.Services.Altcoins.Zcash.Utils
{
public class ZcashMoney
{
public static decimal Convert(long zat)
{
var amt = zat.ToString(CultureInfo.InvariantCulture).PadLeft(8, '0');
amt = amt.Length == 8 ? $"0.{amt}" : amt.Insert(amt.Length - 8, ".");
return decimal.Parse(amt, CultureInfo.InvariantCulture);
}
public static long Convert(decimal Zcash)
{
return System.Convert.ToInt64(Zcash * 100000000);
}
}
}

View File

@ -0,0 +1,8 @@
namespace BTCPayServer
{
public class ZcashLikeSpecificBtcPayNetwork : BTCPayNetworkBase
{
public int MaxTrackedConfirmation = 10;
public string UriScheme { get; set; }
}
}

View File

@ -127,7 +127,6 @@ namespace BTCPayServer
public string BlockExplorerLinkDefault { get; set; }
public string DisplayName { get; set; }
public int Divisibility { get; set; } = 8;
[Obsolete("Should not be needed")]
public bool IsBTC
{
get

View File

@ -55,7 +55,7 @@ namespace BTCPayServer
InitGroestlcoin();
InitViacoin();
InitMonero();
InitPolis();
InitZcash();
InitChaincoin();
// InitArgoneum();//their rate source is down 9/15/20.
InitMonetaryUnit();
@ -92,8 +92,8 @@ namespace BTCPayServer
return new BTCPayNetworkProvider(this, cryptoCodes);
}
[Obsolete("To use only for legacy stuff")]
public BTCPayNetwork BTC => GetNetwork<BTCPayNetwork>("BTC");
public BTCPayNetworkBase DefaultNetwork => BTC ?? GetAll().First();
public void Add(BTCPayNetwork network)
{

View File

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

View File

@ -1,9 +1,10 @@
using System;
using System.ComponentModel.DataAnnotations.Schema;
using BTCPayServer.Abstractions.Contracts;
namespace BTCPayServer.Data
{
public class StoredFile
public class StoredFile : IStoredFile
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public string Id { get; set; }

View File

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

View File

@ -1260,14 +1260,21 @@
"crypto":true
},
{
"name":"POLIS",
"code":"POLIS",
"name":"YEC",
"code":"YEC",
"divisibility":8,
"symbol":null,
"crypto":true
},
{
"name":"Althash",
"name":"ZEC",
"code":"ZEC",
"divisibility":8,
"symbol":null,
"crypto":true
},
{
"name":"Htmlcoin",
"code":"HTML",
"divisibility":8,
"symbol":null,

File diff suppressed because one or more lines are too long

View File

@ -1,25 +0,0 @@
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Rating;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.Rates
{
public class PolisRateProvider : IRateProvider
{
private readonly HttpClient _httpClient;
public PolisRateProvider(HttpClient httpClient)
{
_httpClient = httpClient ?? new HttpClient();
}
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{
var response = await _httpClient.GetAsync("https://obol.polispay.com/complex/btc/polis", cancellationToken); //Returns complex rate from BTC to POLIS
var jobj = await response.Content.ReadAsAsync<JObject>(cancellationToken);
var value = jobj["data"].Value<decimal>();
return new[] { new PairRate(new CurrencyPair("BTC", "POLIS"), new BidAsk(value)) };
}
}
}

View File

@ -78,8 +78,6 @@ namespace BTCPayServer.Services.Rates
yield return new AvailableRateProvider("ripio", "Ripio", "https://api.exchange.ripio.com/api/v1/rate/all/");
yield return new AvailableRateProvider("cryptomarket", "CryptoMarket", "https://api.exchange.cryptomkt.com/api/3/public/ticker/");
yield return new AvailableRateProvider("polispay", "PolisPay", "https://obol.polispay.com/complex/btc/polis");
yield return new AvailableRateProvider("bitfinex", "Bitfinex", "https://api.bitfinex.com/v2/tickers?symbols=tBTCUSD,tLTCUSD,tLTCBTC,tETHUSD,tETHBTC,tETCBTC,tETCUSD,tRRTUSD,tRRTBTC,tZECUSD,tZECBTC,tXMRUSD,tXMRBTC,tDSHUSD,tDSHBTC,tBTCEUR,tBTCJPY,tXRPUSD,tXRPBTC,tIOTUSD,tIOTBTC,tIOTETH,tEOSUSD,tEOSBTC,tEOSETH,tSANUSD,tSANBTC,tSANETH,tOMGUSD,tOMGBTC,tOMGETH,tNEOUSD,tNEOBTC,tNEOETH,tETPUSD,tETPBTC,tETPETH,tQTMUSD,tQTMBTC,tQTMETH,tAVTUSD,tAVTBTC,tAVTETH,tEDOUSD,tEDOBTC,tEDOETH,tBTGUSD,tBTGBTC,tDATUSD,tDATBTC,tDATETH,tQSHUSD,tQSHBTC,tQSHETH,tYYWUSD,tYYWBTC,tYYWETH,tGNTUSD,tGNTBTC,tGNTETH,tSNTUSD,tSNTBTC,tSNTETH,tIOTEUR,tBATUSD,tBATBTC,tBATETH,tMNAUSD,tMNABTC,tMNAETH,tFUNUSD,tFUNBTC,tFUNETH,tZRXUSD,tZRXBTC,tZRXETH,tTNBUSD,tTNBBTC,tTNBETH,tSPKUSD,tSPKBTC,tSPKETH,tTRXUSD,tTRXBTC,tTRXETH,tRCNUSD,tRCNBTC,tRCNETH,tRLCUSD,tRLCBTC,tRLCETH,tAIDUSD,tAIDBTC,tAIDETH,tSNGUSD,tSNGBTC,tSNGETH,tREPUSD,tREPBTC,tREPETH,tELFUSD,tELFBTC,tELFETH,tNECUSD,tNECBTC,tNECETH,tBTCGBP,tETHEUR,tETHJPY,tETHGBP,tNEOEUR,tNEOJPY,tNEOGBP,tEOSEUR,tEOSJPY,tEOSGBP,tIOTJPY,tIOTGBP,tIOSUSD,tIOSBTC,tIOSETH,tAIOUSD,tAIOBTC,tAIOETH,tREQUSD,tREQBTC,tREQETH,tRDNUSD,tRDNBTC,tRDNETH,tLRCUSD,tLRCBTC,tLRCETH,tWAXUSD,tWAXBTC,tWAXETH,tDAIUSD,tDAIBTC,tDAIETH,tAGIUSD,tAGIBTC,tAGIETH,tBFTUSD,tBFTBTC,tBFTETH,tMTNUSD,tMTNBTC,tMTNETH,tODEUSD,tODEBTC,tODEETH,tANTUSD,tANTBTC,tANTETH,tDTHUSD,tDTHBTC,tDTHETH,tMITUSD,tMITBTC,tMITETH,tSTJUSD,tSTJBTC,tSTJETH,tXLMUSD,tXLMEUR,tXLMJPY,tXLMGBP,tXLMBTC,tXLMETH,tXVGUSD,tXVGEUR,tXVGJPY,tXVGGBP,tXVGBTC,tXVGETH,tBCIUSD,tBCIBTC,tMKRUSD,tMKRBTC,tMKRETH,tKNCUSD,tKNCBTC,tKNCETH,tPOAUSD,tPOABTC,tPOAETH,tEVTUSD,tLYMUSD,tLYMBTC,tLYMETH,tUTKUSD,tUTKBTC,tUTKETH,tVEEUSD,tVEEBTC,tVEEETH,tDADUSD,tDADBTC,tDADETH,tORSUSD,tORSBTC,tORSETH,tAUCUSD,tAUCBTC,tAUCETH,tPOYUSD,tPOYBTC,tPOYETH,tFSNUSD,tFSNBTC,tFSNETH,tCBTUSD,tCBTBTC,tCBTETH,tZCNUSD,tZCNBTC,tZCNETH,tSENUSD,tSENBTC,tSENETH,tNCAUSD,tNCABTC,tNCAETH,tCNDUSD,tCNDBTC,tCNDETH,tCTXUSD,tCTXBTC,tCTXETH,tPAIUSD,tPAIBTC,tSEEUSD,tSEEBTC,tSEEETH,tESSUSD,tESSBTC,tESSETH,tATMUSD,tATMBTC,tATMETH,tHOTUSD,tHOTBTC,tHOTETH,tDTAUSD,tDTABTC,tDTAETH,tIQXUSD,tIQXBTC,tIQXEOS,tWPRUSD,tWPRBTC,tWPRETH,tZILUSD,tZILBTC,tZILETH,tBNTUSD,tBNTBTC,tBNTETH,tABSUSD,tABSETH,tXRAUSD,tXRAETH,tMANUSD,tMANETH,tBBNUSD,tBBNETH,tNIOUSD,tNIOETH,tDGXUSD,tDGXETH,tVETUSD,tVETBTC,tVETETH,tUTNUSD,tUTNETH,tTKNUSD,tTKNETH,tGOTUSD,tGOTEUR,tGOTETH,tXTZUSD,tXTZBTC,tCNNUSD,tCNNETH,tBOXUSD,tBOXETH,tTRXEUR,tTRXGBP,tTRXJPY,tMGOUSD,tMGOETH,tRTEUSD,tRTEETH,tYGGUSD,tYGGETH,tMLNUSD,tMLNETH,tWTCUSD,tWTCETH,tCSXUSD,tCSXETH,tOMNUSD,tOMNBTC,tINTUSD,tINTETH,tDRNUSD,tDRNETH,tPNKUSD,tPNKETH,tDGBUSD,tDGBBTC,tBSVUSD,tBSVBTC,tBABUSD,tBABBTC,tWLOUSD,tWLOXLM,tVLDUSD,tVLDETH,tENJUSD,tENJETH,tONLUSD,tONLETH,tRBTUSD,tRBTBTC,tUSTUSD,tEUTEUR,tEUTUSD,tGSDUSD,tUDCUSD,tTSDUSD,tPAXUSD,tRIFUSD,tRIFBTC,tPASUSD,tPASETH,tVSYUSD,tVSYBTC,tZRXDAI,tMKRDAI,tOMGDAI,tBTTUSD,tBTTBTC,tBTCUST,tETHUST,tCLOUSD,tCLOBTC,tIMPUSD,tIMPETH,tLTCUST,tEOSUST,tBABUST,tSCRUSD,tSCRETH,tGNOUSD,tGNOETH,tGENUSD,tGENETH,tATOUSD,tATOBTC,tATOETH,tWBTUSD,tXCHUSD,tEUSUSD,tWBTETH,tXCHETH,tEUSETH,tLEOUSD,tLEOBTC,tLEOUST,tLEOEOS,tLEOETH,tASTUSD,tASTETH,tFOAUSD,tFOAETH,tUFRUSD,tUFRETH,tZBTUSD,tZBTUST,tOKBUSD,tUSKUSD,tGTXUSD,tKANUSD,tOKBUST,tOKBETH,tOKBBTC,tUSKUST,tUSKETH,tUSKBTC,tUSKEOS,tGTXUST,tKANUST,tAMPUSD,tALGUSD,tALGBTC,tALGUST,tBTCXCH,tSWMUSD,tSWMETH,tTRIUSD,tTRIETH,tLOOUSD,tLOOETH,tAMPUST,tDUSK:USD,tDUSK:BTC,tUOSUSD,tUOSBTC,tRRBUSD,tRRBUST,tDTXUSD,tDTXUST,tAMPBTC,tFTTUSD,tFTTUST,tPAXUST,tUDCUST,tTSDUST,tBTC:CNHT,tUST:CNHT,tCNH:CNHT,tCHZUSD,tCHZUST,tBTCF0:USTF0,tETHF0:USTF0");
yield return new AvailableRateProvider("okex", "OKEx", "https://www.okex.com/api/futures/v3/instruments/ticker");
yield return new AvailableRateProvider("coinbasepro", "Coinbase Pro", "https://api.pro.coinbase.com/products");
@ -104,7 +102,6 @@ namespace BTCPayServer.Services.Rates
Providers.Add("ripio", new RipioExchangeProvider(_httpClientFactory?.CreateClient("EXCHANGE_RIPIO")));
Providers.Add("cryptomarket", new CryptoMarketExchangeRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_CRYPTOMARKET")));
Providers.Add("bitflyer", new BitflyerRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BITFLYER")));
Providers.Add("polispay", new PolisRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_POLIS")));
// Providers.Add("argoneum", new ArgoneumRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_ARGONEUM")));

View File

@ -350,7 +350,7 @@ namespace BTCPayServer.Tests
var user = s.Server.NewAccount();
await user.GrantAccessAsync();
s.GoToLogin();
s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password);
s.LogIn(user.RegisterDetails.Email, user.RegisterDetails.Password);
user.RegisterDerivationScheme("BTC");
await s.Server.ExplorerNode.GenerateAsync(1);
@ -358,7 +358,7 @@ namespace BTCPayServer.Tests
{
if (multiCurrency)
user.RegisterDerivationScheme("LTC");
foreach (var rateSelection in new[] { "FiatText", "CurrentRateText", "RateThenText" })
foreach (var rateSelection in new[] { "FiatTextRadio", "CurrentRateTextRadio", "RateThenTextRadio" })
await CanCreateRefundsCore(s, user, multiCurrency, rateSelection);
}
}
@ -402,11 +402,11 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id(rateSelection)).Click();
s.Driver.FindElement(By.Id("ok")).Click();
Assert.Contains("pull-payments", s.Driver.Url);
if (rateSelection == "FiatText")
if (rateSelection == "FiatTextRadio")
Assert.Contains("$5,500.00", s.Driver.PageSource);
if (rateSelection == "CurrentRateText")
if (rateSelection == "CurrentRateTextRadio")
Assert.Contains("2.20000000 ₿", s.Driver.PageSource);
if (rateSelection == "RateThenText")
if (rateSelection == "RateThenTextRadio")
Assert.Contains("1.10000000 ₿", s.Driver.PageSource);
s.GoToInvoice(invoice.Id);
s.Driver.FindElement(By.Id("refundlink")).Click();
@ -475,11 +475,11 @@ namespace BTCPayServer.Tests
tester.ActivateLTC();
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess();
await user.GrantAccessAsync();
user.RegisterDerivationScheme("BTC");
// First we try payment with a merchant having only BTC
var invoice = user.BitPay.CreateInvoice(
new Invoice()
var invoice = await user.BitPay.CreateInvoiceAsync(
new Invoice
{
Price = 5000.0m,
Currency = "USD",
@ -490,10 +490,10 @@ namespace BTCPayServer.Tests
}, Facade.Merchant);
var cashCow = tester.ExplorerNode;
cashCow.Generate(2); // get some money in case
await cashCow.GenerateAsync(2); // get some money in case
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
var firstPayment = Money.Coins(0.04m);
cashCow.SendToAddress(invoiceAddress, firstPayment);
await cashCow.SendToAddressAsync(invoiceAddress, firstPayment);
TestUtils.Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
@ -523,8 +523,8 @@ namespace BTCPayServer.Tests
// Retry now with LTC enabled
user.RegisterDerivationScheme("LTC");
invoice = user.BitPay.CreateInvoice(
new Invoice()
invoice = await user.BitPay.CreateInvoiceAsync(
new Invoice
{
Price = 5000.0m,
Currency = "USD",
@ -537,7 +537,7 @@ namespace BTCPayServer.Tests
cashCow = tester.ExplorerNode;
invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
firstPayment = Money.Coins(0.04m);
cashCow.SendToAddress(invoiceAddress, firstPayment);
await cashCow.SendToAddressAsync(invoiceAddress, firstPayment);
TestLogs.LogInformation("First payment sent to " + invoiceAddress);
TestUtils.Eventually(() =>
{
@ -550,8 +550,8 @@ namespace BTCPayServer.Tests
Assert.NotNull(ltcCryptoInfo);
invoiceAddress = BitcoinAddress.Create(ltcCryptoInfo.Address, cashCow.Network);
var secondPayment = Money.Coins(decimal.Parse(ltcCryptoInfo.Due, CultureInfo.InvariantCulture));
cashCow.Generate(4); // LTC is not worth a lot, so just to make sure we have money...
cashCow.SendToAddress(invoiceAddress, secondPayment);
await cashCow.GenerateAsync(4); // LTC is not worth a lot, so just to make sure we have money...
await cashCow.SendToAddressAsync(invoiceAddress, secondPayment);
TestLogs.LogInformation("Second payment sent to " + invoiceAddress);
TestUtils.Eventually(() =>
{
@ -570,7 +570,6 @@ namespace BTCPayServer.Tests
Assert.Equal(2, checkout.AvailableCryptos.Count);
Assert.Equal("LTC", checkout.CryptoCode);
Assert.Equal(2, invoice.PaymentCodes.Count());
Assert.Equal(2, invoice.SupportedTransactionCurrencies.Count());
Assert.Equal(2, invoice.SupportedTransactionCurrencies.Count());
@ -581,11 +580,10 @@ namespace BTCPayServer.Tests
Assert.True(invoice.SupportedTransactionCurrencies["LTC"].Enabled);
Assert.True(invoice.PaymentSubtotals.ContainsKey("LTC"));
Assert.True(invoice.PaymentTotals.ContainsKey("LTC"));
// Check if we can disable LTC
invoice = user.BitPay.CreateInvoice(
new Invoice()
invoice = await user.BitPay.CreateInvoiceAsync(
new Invoice
{
Price = 5000.0m,
Currency = "USD",

View File

@ -7,11 +7,11 @@ using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Security.Greenfield;
using BTCPayServer.Tests.Logging;
using BTCPayServer.Views.Manage;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OpenQA.Selenium;
using OpenQA.Selenium.Support.Extensions;
using Xunit;
using Xunit.Abstractions;
using StoreData = BTCPayServer.Data.StoreData;
@ -20,7 +20,7 @@ namespace BTCPayServer.Tests
{
public class ApiKeysTests : UnitTestBase
{
public const int TestTimeout = TestUtils.TestTimeout;
public const int TestTimeout = 120_000;
public const string TestApiPath = "api/test/apikey";
public ApiKeysTests(ITestOutputHelper helper) : base(helper)
@ -43,50 +43,54 @@ namespace BTCPayServer.Tests
await user.GrantAccessAsync();
await user.MakeAdmin(false);
s.GoToLogin();
s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password);
s.LogIn(user.RegisterDetails.Email, user.RegisterDetails.Password);
s.GoToProfile(ManageNavPages.APIKeys);
s.Driver.FindElement(By.Id("AddApiKey")).Click();
TestLogs.LogInformation("Checking admin permissions");
//not an admin, so this permission should not show
Assert.DoesNotContain("btcpay.server.canmodifyserversettings", s.Driver.PageSource);
await user.MakeAdmin();
s.Logout();
s.GoToLogin();
s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password);
s.LogIn(user.RegisterDetails.Email, user.RegisterDetails.Password);
s.GoToProfile(ManageNavPages.APIKeys);
s.Driver.FindElement(By.Id("AddApiKey")).Click();
Assert.Contains("btcpay.server.canmodifyserversettings", s.Driver.PageSource);
//server management should show now
s.Driver.SetCheckbox(By.Id("btcpay.server.canmodifyserversettings"), true);
s.Driver.SetCheckbox(By.Id("btcpay.store.canmodifystoresettings"), true);
s.Driver.SetCheckbox(By.Id("btcpay.user.canviewprofile"), true);
s.Driver.FindElement(By.Id("Generate")).Click();
var superApiKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text;
TestLogs.LogInformation("Checking super admin key");
//this api key has access to everything
await TestApiAgainstAccessToken(superApiKey, tester, user, Policies.CanModifyServerSettings, Policies.CanModifyStoreSettings, Policies.CanViewProfile);
s.Driver.FindElement(By.Id("AddApiKey")).Click();
s.Driver.SetCheckbox(By.Id("btcpay.server.canmodifyserversettings"), true);
s.Driver.FindElement(By.Id("Generate")).Click();
var serverOnlyApiKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text;
TestLogs.LogInformation("Checking CanModifyServerSettings permissions");
await TestApiAgainstAccessToken(serverOnlyApiKey, tester, user,
Policies.CanModifyServerSettings);
s.Driver.FindElement(By.Id("AddApiKey")).Click();
s.Driver.SetCheckbox(By.Id("btcpay.store.canmodifystoresettings"), true);
s.Driver.FindElement(By.Id("Generate")).Click();
var allStoreOnlyApiKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text;
TestLogs.LogInformation("Checking CanModifyStoreSettings permissions");
await TestApiAgainstAccessToken(allStoreOnlyApiKey, tester, user,
Policies.CanModifyStoreSettings);
s.Driver.FindElement(By.Id("AddApiKey")).Click();
s.Driver.FindElement(By.CssSelector("button[value='btcpay.store.canmodifystoresettings:change-store-mode']")).Click();
//there should be a store already by default in the dropdown
var src = s.Driver.PageSource;
var getPermissionValueIndex =
s.Driver.FindElement(By.CssSelector("input[value='btcpay.store.canmodifystoresettings']"))
.GetAttribute("name")
@ -95,14 +99,21 @@ namespace BTCPayServer.Tests
var option = dropdown.FindElement(By.TagName("option"));
var storeId = option.GetAttribute("value");
option.Click();
s.Driver.FindElement(By.Id("Generate")).Click();
s.Driver.WaitForAndClick(By.Id("Generate"));
var selectiveStoreApiKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text;
TestLogs.LogInformation("Checking CanModifyStoreSettings with StoreId permissions");
await TestApiAgainstAccessToken(selectiveStoreApiKey, tester, user,
Permission.Create(Policies.CanModifyStoreSettings, storeId).ToString());
s.Driver.FindElement(By.Id("AddApiKey")).Click();
s.Driver.FindElement(By.Id("Generate")).Click();
TestLogs.LogInformation("Adding API key for no permissions");
s.Driver.WaitForAndClick(By.Id("AddApiKey"));
TestLogs.LogInformation("Generating API key for no permissions");
s.Driver.WaitForAndClick(By.Id("Generate"));
var noPermissionsApiKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text;
TestLogs.LogInformation($"Checking no permissions: {noPermissionsApiKey}");
await TestApiAgainstAccessToken(noPermissionsApiKey, tester, user);
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
@ -110,6 +121,8 @@ namespace BTCPayServer.Tests
await TestApiAgainstAccessToken<bool>("incorrect key", $"{TestApiPath}/me/id",
tester.PayTester.HttpClient);
});
TestLogs.LogInformation("Checking authorize screen");
//let's test the authorized screen now
//options for authorize are:
@ -124,26 +137,35 @@ namespace BTCPayServer.Tests
var callbackUrl = s.ServerUri + "postredirect-callback-test";
var authUrl = BTCPayServerClient.GenerateAuthorizeUri(s.ServerUri,
new[] { Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings }, applicationDetails: (appidentifier, new Uri(callbackUrl))).ToString();
s.Driver.Navigate().GoToUrl(authUrl);
TestLogs.LogInformation($"Going to auth URL {authUrl}");
s.GoToUrl(authUrl);
Assert.Contains(appidentifier, s.Driver.PageSource);
Assert.Equal("hidden", s.Driver.FindElement(By.Id("btcpay.store.canmodifystoresettings")).GetAttribute("type").ToLowerInvariant());
Assert.Equal("true", s.Driver.FindElement(By.Id("btcpay.store.canmodifystoresettings")).GetAttribute("value").ToLowerInvariant());
Assert.Equal("hidden", s.Driver.FindElement(By.Id("btcpay.server.canmodifyserversettings")).GetAttribute("type").ToLowerInvariant());
Assert.Equal("true", s.Driver.FindElement(By.Id("btcpay.server.canmodifyserversettings")).GetAttribute("value").ToLowerInvariant());
Assert.DoesNotContain("change-store-mode", s.Driver.PageSource);
s.Driver.FindElement(By.Id("consent-yes")).Click();
TestLogs.LogInformation("Going to callback URL");
s.Driver.WaitForAndClick(By.Id("consent-yes"));
Assert.Equal(callbackUrl, s.Driver.Url);
TestLogs.LogInformation("On callback URL");
var apiKeyRepo = s.Server.PayTester.GetService<APIKeyRepository>();
var accessToken = GetAccessTokenFromCallbackResult(s.Driver);
TestLogs.LogInformation($"Access token: {accessToken}");
await TestApiAgainstAccessToken(accessToken, tester, user,
(await apiKeyRepo.GetKey(accessToken)).GetBlob().Permissions);
authUrl = BTCPayServerClient.GenerateAuthorizeUri(s.ServerUri,
new[] { Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings }, false, true, applicationDetails: (null, new Uri(callbackUrl))).ToString();
s.Driver.Navigate().GoToUrl(authUrl);
TestLogs.LogInformation($"Going to auth URL 2 {authUrl}");
s.GoToUrl(authUrl);
TestLogs.LogInformation("On auth URL 2");
Assert.DoesNotContain("kukksappname", s.Driver.PageSource);
Assert.Equal("checkbox", s.Driver.FindElement(By.Id("btcpay.store.canmodifystoresettings")).GetAttribute("type").ToLowerInvariant());
@ -153,45 +175,63 @@ namespace BTCPayServer.Tests
s.Driver.SetCheckbox(By.Id("btcpay.server.canmodifyserversettings"), false);
Assert.Contains("change-store-mode", s.Driver.PageSource);
s.Driver.FindElement(By.Id("consent-yes")).Click();
TestLogs.LogInformation("Going to callback URL 2");
s.Driver.WaitForAndClick(By.Id("consent-yes"));
Assert.Equal(callbackUrl, s.Driver.Url);
TestLogs.LogInformation("On callback URL 2");
accessToken = GetAccessTokenFromCallbackResult(s.Driver);
TestLogs.LogInformation($"Access token: {accessToken}");
TestLogs.LogInformation("Checking authorized permissions");
await TestApiAgainstAccessToken(accessToken, tester, user,
(await apiKeyRepo.GetKey(accessToken)).GetBlob().Permissions);
//let's test the app identifier system
TestLogs.LogInformation("Checking app identifier system");
authUrl = BTCPayServerClient.GenerateAuthorizeUri(s.ServerUri,
new[] { Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings }, false, true, (appidentifier, new Uri(callbackUrl))).ToString();
//if it's the same, go to the confirm page
s.Driver.Navigate().GoToUrl(authUrl);
s.Driver.FindElement(By.Id("continue")).Click();
TestLogs.LogInformation($"Going to auth URL 3 {authUrl}");
s.GoToUrl(authUrl);
TestLogs.LogInformation("On auth URL 3");
s.Driver.WaitForAndClick(By.Id("continue"));
TestLogs.LogInformation("Going to callback URL 3");
Assert.Equal(callbackUrl, s.Driver.Url);
TestLogs.LogInformation("On callback URL 3");
//same app but different redirect = nono
authUrl = BTCPayServerClient.GenerateAuthorizeUri(s.ServerUri,
new[] { Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings }, false, true, (appidentifier, new Uri("https://international.local/callback"))).ToString();
s.Driver.Navigate().GoToUrl(authUrl);
TestLogs.LogInformation($"Going to auth URL 4 {authUrl}");
s.GoToUrl(authUrl);
TestLogs.LogInformation("On auth URL 4");
Assert.False(s.Driver.Url.StartsWith("https://international.com/callback"));
// Make sure we can check all permissions when not an admin
TestLogs.LogInformation("Make sure we can check all permissions when not an admin");
await user.MakeAdmin(false);
s.Logout();
s.GoToLogin();
s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password);
s.GoToProfile(ManageNavPages.APIKeys);
s.Driver.FindElement(By.Id("AddApiKey")).Click();
int checkedPermissionCount = 0;
foreach (var checkbox in s.Driver.FindElements(By.ClassName("form-check-input")))
{
checkedPermissionCount++;
checkbox.Click();
}
s.Driver.FindElement(By.Id("Generate")).Click();
s.LogIn(user.RegisterDetails.Email, user.RegisterDetails.Password);
TestLogs.LogInformation("Go to API Keys page");
s.GoToUrl("/account/apikeys");
TestLogs.LogInformation("On API Keys page");
s.Driver.WaitForAndClick(By.Id("AddApiKey"));
int checkedPermissionCount = s.Driver.FindElements(By.ClassName("form-check-input")).Count;
TestLogs.LogInformation($"Adding API key: {checkedPermissionCount} permissions");
s.Driver.ExecuteJavaScript("document.querySelectorAll('#Permissions .form-check-input').forEach(i => i.click())");
TestLogs.LogInformation($"Clicked {checkedPermissionCount}");
TestLogs.LogInformation("Generating API key");
s.Driver.WaitForAndClick(By.Id("Generate"));
var allAPIKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text;
var apikeydata = await TestApiAgainstAccessToken<ApiKeyData>(allAPIKey, $"api/v1/api-keys/current", tester.PayTester.HttpClient);
TestLogs.LogInformation($"Checking API key permissions: {allAPIKey}");
var apikeydata = await TestApiAgainstAccessToken<ApiKeyData>(allAPIKey, "api/v1/api-keys/current", tester.PayTester.HttpClient);
Assert.Equal(checkedPermissionCount, apikeydata.Permissions.Length);
}
@ -202,12 +242,14 @@ namespace BTCPayServer.Tests
expectedPermissions ??= new Permission[0];
var apikeydata = await TestApiAgainstAccessToken<ApiKeyData>(accessToken, $"api/v1/api-keys/current", tester.PayTester.HttpClient);
var permissions = apikeydata.Permissions;
TestLogs.LogInformation($"TestApiAgainstAccessToken: Permissions {permissions.Length}");
Assert.Equal(expectedPermissions.Length, permissions.Length);
foreach (var expectPermission in expectedPermissions)
{
Assert.True(permissions.Any(p => p == expectPermission), $"Missing expected permission {expectPermission}");
}
TestLogs.LogInformation("Testing CanViewProfile");
if (permissions.Contains(Permission.Create(Policies.CanViewProfile)))
{
var resultUser = await TestApiAgainstAccessToken<string>(accessToken, $"{TestApiPath}/me/id", tester.PayTester.HttpClient);
@ -220,14 +262,17 @@ namespace BTCPayServer.Tests
await TestApiAgainstAccessToken<string>(accessToken, $"{TestApiPath}/me/id", tester.PayTester.HttpClient);
});
}
//create a second user to see if any of its data gets messed upin our results.
//create a second user to see if any of its data gets messed up in our results.
TestLogs.LogInformation("Testing second user");
var secondUser = tester.NewAccount();
secondUser.GrantAccess();
await secondUser.GrantAccessAsync();
var canModifyAllStores = Permission.Create(Policies.CanModifyStoreSettings, null);
var canModifyServer = Permission.Create(Policies.CanModifyServerSettings, null);
var unrestricted = Permission.Create(Policies.Unrestricted, null);
var selectiveStorePermissions = permissions.Where(p => p.Scope != null && p.Policy == Policies.CanModifyStoreSettings);
TestLogs.LogInformation("Testing can edit store for first user");
if (permissions.Contains(canModifyAllStores) || selectiveStorePermissions.Any())
{
var resultStores =
@ -287,7 +332,6 @@ namespace BTCPayServer.Tests
}
else if (!permissions.Contains(unrestricted))
{
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
{
await TestApiAgainstAccessToken<bool>(accessToken,
@ -302,6 +346,7 @@ namespace BTCPayServer.Tests
tester.PayTester.HttpClient);
}
TestLogs.LogInformation("Testing can edit store for second user");
if (!permissions.Contains(unrestricted))
{
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
@ -315,7 +360,9 @@ namespace BTCPayServer.Tests
await TestApiAgainstAccessToken<bool>(accessToken, $"{TestApiPath}/me/stores/{secondUser.StoreId}/can-edit",
tester.PayTester.HttpClient);
}
TestLogs.LogInformation("Testing can edit store for second user expectation met");
TestLogs.LogInformation($"Testing CanModifyServer with {permissions.Contains(canModifyServer)}");
if (permissions.Contains(canModifyServer))
{
Assert.True(await TestApiAgainstAccessToken<bool>(accessToken,
@ -331,17 +378,21 @@ namespace BTCPayServer.Tests
tester.PayTester.HttpClient);
});
}
TestLogs.LogInformation("Testing CanModifyServer expectation met");
}
public async Task<T> TestApiAgainstAccessToken<T>(string apikey, string url, HttpClient client)
{
var httpRequest = new HttpRequestMessage(HttpMethod.Get,
new Uri(client.BaseAddress, url));
var uri = new Uri(client.BaseAddress, url);
var httpRequest = new HttpRequestMessage(HttpMethod.Get, uri);
httpRequest.Headers.Authorization = new AuthenticationHeaderValue("token", apikey);
TestLogs.LogInformation($"Testing {uri}");
var result = await client.SendAsync(httpRequest);
TestLogs.LogInformation($"Testing {uri} status: {result.StatusCode}");
result.EnsureSuccessStatusCode();
var rawJson = await result.Content.ReadAsStringAsync();
TestLogs.LogInformation($"Testing {uri} result: {rawJson}");
if (typeof(T).IsPrimitive || typeof(T) == typeof(string))
{
return (T)Convert.ChangeType(rawJson, typeof(T));

View File

@ -23,7 +23,7 @@
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.14" />
<PackageReference Include="Selenium.Support" Version="3.141.0" />
<PackageReference Include="Selenium.WebDriver" Version="3.141.0" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="96.0.4664.4500" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="98.0.4758.10200" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PrivateAssets>all</PrivateAssets>

View File

@ -106,8 +106,6 @@ retry:
driver.ExecuteJavaScript($"document.getElementById('{collapseId}').classList.add('show')");
}
public static void SetAttribute(this IWebDriver driver, string element, string attribute, string value)
{
driver.ExecuteJavaScript($"document.getElementById('{element}').setAttribute('{attribute}', '{value}')");
@ -127,14 +125,28 @@ retry:
return el;
}
public static void ScrollTo(this IWebDriver driver, IWebElement element)
{
driver.ExecuteJavaScript("arguments[0].scrollIntoView();", element);
}
public static void ScrollTo(this IWebDriver driver, By selector)
{
var element = driver.FindElement(selector);
driver.ExecuteJavaScript("arguments[0].scrollIntoView();", element);
ScrollTo(driver, driver.FindElement(selector));
}
public static void WaitForAndClick(this IWebDriver driver, By selector)
{
// Try fast path
try
{
driver.FindElement(selector).Click();
return;
}
catch { }
// Sometimes, selenium complain, so we enter hack territory
var wait = new WebDriverWait(driver, SeleniumTester.ImplicitWait);
wait.UntilJsIsReady();
@ -158,22 +170,8 @@ retry:
public static void SetCheckbox(this IWebDriver driver, By selector, bool value)
{
var element = driver.FindElement(selector);
if ((value && !element.Selected) || (!value && element.Selected))
{
try
{
driver.WaitForAndClick(selector);
}
catch (ElementClickInterceptedException)
{
element.SendKeys(" ");
}
}
if (value != element.Selected)
{
driver.SetCheckbox(selector, value);
}
driver.WaitForAndClick(selector);
}
}
}

View File

@ -3,14 +3,13 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Security;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Configuration;
@ -40,7 +39,6 @@ using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace BTCPayServer.Tests
{
@ -204,8 +202,7 @@ namespace BTCPayServer.Tests
{
// Local link, this is fine
}
else if (attributeValue.StartsWith("http://") || attributeValue.StartsWith("https://") ||
attributeValue.StartsWith("@"))
else if (attributeValue.StartsWith("http://") || attributeValue.StartsWith("https://"))
{
// This can be an external link. Treating it as such.
var rel = GetAttributeValue(node, "rel");
@ -1766,5 +1763,18 @@ namespace BTCPayServer.Tests
Assert.Equal(btc.CryptoCode, paymentMethod.CryptoCode);
#pragma warning restore CS0618
}
[Fact]
public void AllPoliciesShowInUI()
{
foreach (var policy in Policies.AllPolicies)
{
Assert.True( UIManageController.AddApiKeyViewModel.PermissionValueItem.PermissionDescriptions.ContainsKey(policy));
if (Policies.IsStorePolicy(policy))
{
Assert.True( UIManageController.AddApiKeyViewModel.PermissionValueItem.PermissionDescriptions.ContainsKey($"{policy}:"));
}
}
}
}
}

View File

@ -212,6 +212,100 @@ namespace BTCPayServer.Tests
tester.Stores.Remove(user.StoreId);
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanViewUsersViaApi()
{
using var tester = CreateServerTester(newDb: true);
await tester.StartAsync();
var unauthClient = new BTCPayServerClient(tester.PayTester.ServerUri);
// Should be 401 for all calls because we don't have permission
await AssertHttpError(401, async () => await unauthClient.GetUsers());
await AssertHttpError(401, async () => await unauthClient.GetUserByIdOrEmail("non_existing_id"));
await AssertHttpError(401, async () => await unauthClient.GetUserByIdOrEmail("someone@example.com"));
var adminUser = tester.NewAccount();
await adminUser.GrantAccessAsync();
await adminUser.MakeAdmin();
var adminClient = await adminUser.CreateClient(Policies.Unrestricted);
// Should be 404 if user doesn't exist
await AssertHttpError(404,async () => await adminClient.GetUserByIdOrEmail("non_existing_id"));
await AssertHttpError(404,async () => await adminClient.GetUserByIdOrEmail("doesnotexist@example.com"));
// Try listing all users, should be fine
await adminClient.GetUsers();
// Try loading 1 user by ID. Loading myself.
await adminClient.GetUserByIdOrEmail(adminUser.UserId);
// Try loading 1 user by email. Loading myself.
await adminClient.GetUserByIdOrEmail(adminUser.Email);
// var badClient = await user.CreateClient(Policies.CanCreateInvoice);
// await AssertHttpError(403,
// async () => await badClient.DeleteCurrentUser());
var goodUser = tester.NewAccount();
await goodUser.GrantAccessAsync();
await goodUser.MakeAdmin();
var goodClient = await goodUser.CreateClient(Policies.CanViewUsers);
// Try listing all users, should be fine
await goodClient.GetUsers();
// Should be 404 if user doesn't exist
await AssertHttpError(404,async () => await goodClient.GetUserByIdOrEmail("non_existing_id"));
await AssertHttpError(404,async () => await goodClient.GetUserByIdOrEmail("doesnotexist@example.com"));
// Try listing all users, should be fine
await goodClient.GetUsers();
// Try loading 1 user by ID. Loading myself.
await goodClient.GetUserByIdOrEmail(goodUser.UserId);
// Try loading 1 user by email. Loading myself.
await goodClient.GetUserByIdOrEmail(goodUser.Email);
var badUser = tester.NewAccount();
await badUser.GrantAccessAsync();
await badUser.MakeAdmin();
// Bad user has a permission, but it's the wrong one.
var badClient = await goodUser.CreateClient(Policies.CanCreateInvoice);
// Try listing all users, should be fine
await AssertHttpError(403,async () => await badClient.GetUsers());
// Should be 404 if user doesn't exist
await AssertHttpError(403,async () => await badClient.GetUserByIdOrEmail("non_existing_id"));
await AssertHttpError(403,async () => await badClient.GetUserByIdOrEmail("doesnotexist@example.com"));
// Try listing all users, should be fine
await AssertHttpError(403,async () => await badClient.GetUsers());
// Try loading 1 user by ID. Loading myself.
await AssertHttpError(403,async () => await badClient.GetUserByIdOrEmail(badUser.UserId));
// 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")]
@ -361,6 +455,7 @@ namespace BTCPayServer.Tests
var result = await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest()
{
Name = "Test",
Description = "Test description",
Amount = 12.3m,
Currency = "BTC",
PaymentMethods = new[] { "BTC" }
@ -369,6 +464,7 @@ namespace BTCPayServer.Tests
void VerifyResult()
{
Assert.Equal("Test", result.Name);
Assert.Equal("Test description", result.Description);
Assert.Null(result.Period);
// If it contains ? it means that we are resolving an unknown route with the link generator
Assert.DoesNotContain("?", result.ViewLink);
@ -1097,12 +1193,13 @@ namespace BTCPayServer.Tests
new CreateInvoiceRequest() { Currency = "helloinvalid", Amount = 1 });
});
await user.RegisterDerivationSchemeAsync("BTC");
string origOrderId = "testOrder";
var newInvoice = await client.CreateInvoice(user.StoreId,
new CreateInvoiceRequest()
{
Currency = "USD",
Amount = 1,
Metadata = JObject.Parse("{\"itemCode\": \"testitem\", \"orderId\": \"testOrder\"}"),
Metadata = JObject.Parse($"{{\"itemCode\": \"testitem\", \"orderId\": \"{origOrderId}\"}}"),
Checkout = new CreateInvoiceRequest.CheckoutOptions()
{
RedirectAutomatically = true,
@ -1223,6 +1320,8 @@ namespace BTCPayServer.Tests
newInvoice = await client.GetInvoice(user.StoreId, newInvoice.Id);
const string newOrderId = "UPDATED-ORDER-ID";
JObject metadataForUpdate = JObject.Parse($"{{\"orderId\": \"{newOrderId}\", \"itemCode\": \"updated\", newstuff: [1,2,3,4,5]}}");
Assert.Contains(InvoiceStatus.Settled, newInvoice.AvailableStatusesForManualMarking);
Assert.DoesNotContain(InvoiceStatus.Invalid, newInvoice.AvailableStatusesForManualMarking);
await AssertHttpError(403, async () =>
@ -1230,23 +1329,36 @@ namespace BTCPayServer.Tests
await viewOnly.UpdateInvoice(user.StoreId, invoice.Id,
new UpdateInvoiceRequest()
{
Metadata = JObject.Parse("{\"itemCode\": \"updated\", newstuff: [1,2,3,4,5]}")
Metadata = metadataForUpdate
});
});
invoice = await client.UpdateInvoice(user.StoreId, invoice.Id,
new UpdateInvoiceRequest()
{
Metadata = JObject.Parse("{\"itemCode\": \"updated\", newstuff: [1,2,3,4,5]}")
Metadata = metadataForUpdate
});
Assert.Equal(newOrderId, invoice.Metadata["orderId"].Value<string>());
Assert.Equal("updated", invoice.Metadata["itemCode"].Value<string>());
Assert.Equal(15, ((JArray)invoice.Metadata["newstuff"]).Values<int>().Sum());
//also test the the metadata actually got saved
invoice = await client.GetInvoice(user.StoreId, invoice.Id);
Assert.Equal(newOrderId, invoice.Metadata["orderId"].Value<string>());
Assert.Equal("updated", invoice.Metadata["itemCode"].Value<string>());
Assert.Equal(15, ((JArray)invoice.Metadata["newstuff"]).Values<int>().Sum());
// test if we can find the updated invoice using the new orderId
var invoicesWithOrderId = await client.GetInvoices(user.StoreId, new[] { newOrderId });
Assert.NotNull(invoicesWithOrderId);
Assert.Single(invoicesWithOrderId);
Assert.Equal(invoice.Id, invoicesWithOrderId.First().Id);
// test if the old orderId does not yield any results anymore
var invoicesWithOldOrderId = await client.GetInvoices(user.StoreId, new[] { origOrderId });
Assert.NotNull(invoicesWithOldOrderId);
Assert.Empty(invoicesWithOldOrderId);
//archive
await AssertHttpError(403, async () =>
{
@ -1435,7 +1547,7 @@ namespace BTCPayServer.Tests
Assert.Single(info.NodeURIs);
Assert.NotEqual(0, info.BlockHeight);
await AssertAPIError("ligthning-node-unavailable", () => client.GetLightningNodeChannels("BTC"));
await AssertAPIError("lightning-node-unavailable", () => client.GetLightningNodeChannels("BTC"));
// Not permission for the store!
await AssertAPIError("missing-permission", () => client.GetLightningNodeChannels(user.StoreId, "BTC"));
var invoiceData = await client.CreateLightningInvoice("BTC", new CreateLightningInvoiceRequest()
@ -1591,7 +1703,7 @@ namespace BTCPayServer.Tests
Assert.Equal(firstAddress, (await viewOnlyClient.PreviewProposedStoreOnChainPaymentMethodAddresses(store.Id, "BTC",
new UpdateOnChainPaymentMethodRequest() { Enabled = true, DerivationScheme = xpub })).Addresses.First().Address);
await AssertValidationError(new[] { "accountKeyPath" }, () => viewOnlyClient.SendHttpRequest<GreenfieldValidationError[]>(path: $"api/v1/stores/{store.Id}/payment-methods/Onchain/BTC/preview", method: HttpMethod.Post,
await AssertValidationError(new[] { "accountKeyPath" }, () => viewOnlyClient.SendHttpRequest<GreenfieldValidationError[]>(path: $"api/v1/stores/{store.Id}/payment-methods/onchain/BTC/preview", method: HttpMethod.Post,
bodyPayload: JObject.Parse("{\"accountKeyPath\": \"0/1\"}")));
var method = await client.UpdateStoreOnChainPaymentMethod(store.Id, "BTC",
@ -2014,7 +2126,30 @@ namespace BTCPayServer.Tests
{
await viewOnlyClient.GetOnChainWalletTransaction(walletId.StoreId, walletId.CryptoCode, txdata.TransactionHash.ToString());
});
await client.GetOnChainWalletTransaction(walletId.StoreId, walletId.CryptoCode, txdata.TransactionHash.ToString());
var transaction = await client.GetOnChainWalletTransaction(walletId.StoreId, walletId.CryptoCode, txdata.TransactionHash.ToString());
Assert.Equal(transaction.TransactionHash, txdata.TransactionHash);
Assert.Equal(String.Empty, transaction.Comment);
Assert.Equal(new Dictionary<string, LabelData>(), transaction.Labels);
// transaction patch tests
var patchedTransaction = await client.PatchOnChainWalletTransaction(
walletId.StoreId, walletId.CryptoCode, txdata.TransactionHash.ToString(),
new PatchOnChainTransactionRequest() {
Comment = "test comment",
Labels = new List<string>
{
"test label"
}
});
Assert.Equal("test comment", patchedTransaction.Comment);
Assert.Equal(
new Dictionary<string, LabelData>()
{
{ "test label", new LabelData(){ Type = "raw", Text = "test label" } }
}.ToJson(),
patchedTransaction.Labels.ToJson()
);
await AssertHttpError(403, async () =>
{
@ -2111,5 +2246,96 @@ namespace BTCPayServer.Tests
}
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")]
public async Task StoreUsersAPITest()
{
using var tester = CreateServerTester();
await tester.StartAsync();
var user = tester.NewAccount();
await user.GrantAccessAsync(true);
var client = await user.CreateClient(Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings);
var users = await client.GetStoreUsers(user.StoreId);
var storeuser = Assert.Single(users);
Assert.Equal(user.UserId,storeuser.UserId);
Assert.Equal(StoreRoles.Owner,storeuser.Role);
var user2= tester.NewAccount();
await user2.GrantAccessAsync(false);
var user2Client =await user2.CreateClient(Policies.CanModifyStoreSettings);
//test no access to api when unrelated to store at all
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await user2Client.GetStoreUsers(user.StoreId));
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await user2Client.AddStoreUser(user.StoreId, new StoreUserData()));
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await user2Client.RemoveStoreUser(user.StoreId, user.UserId));
await client.AddStoreUser(user.StoreId, new StoreUserData() { Role = StoreRoles.Guest, UserId = user2.UserId });
//test no access to api when only a guest
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await user2Client.GetStoreUsers(user.StoreId));
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await user2Client.AddStoreUser(user.StoreId, new StoreUserData()));
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await user2Client.RemoveStoreUser(user.StoreId, user.UserId));
await user2Client.GetStore(user.StoreId);
await client.RemoveStoreUser(user.StoreId, user2.UserId);
await AssertHttpError(403, async () =>
await user2Client.GetStore(user.StoreId));
await client.AddStoreUser(user.StoreId, new StoreUserData() { Role = StoreRoles.Owner, UserId = user2.UserId });
await AssertAPIError("duplicate-store-user-role",async ()=>
await client.AddStoreUser(user.StoreId,
new StoreUserData() { Role = StoreRoles.Owner, UserId = user2.UserId }));
await user2Client.RemoveStoreUser(user.StoreId, user.UserId);
//test no access to api when unrelated to store at all
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await client.GetStoreUsers(user.StoreId));
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await client.AddStoreUser(user.StoreId, new StoreUserData()));
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await client.RemoveStoreUser(user.StoreId, user.UserId));
await AssertAPIError("store-user-role-orphaned", async () => await user2Client.RemoveStoreUser(user.StoreId, user2.UserId));
}
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")]
public async Task StoreEmailTests()
{
using var tester = CreateServerTester();
await tester.StartAsync();
var admin = tester.NewAccount();
await admin.GrantAccessAsync(true);
var adminClient = await admin.CreateClient(Policies.Unrestricted);
await adminClient.UpdateStoreEmailSettings(admin.StoreId,
new EmailSettingsData());
var data = new EmailSettingsData()
{
From = "admin@admin.com",
Login = "admin@admin.com",
Password = "admin@admin.com",
Port = 1234,
Server = "admin.com",
};
await adminClient.UpdateStoreEmailSettings(admin.StoreId, data);
var s = await adminClient.GetStoreEmailSettings(admin.StoreId);
Assert.Equal(JsonConvert.SerializeObject(s), JsonConvert.SerializeObject(data));
await AssertValidationError(new[] { nameof(EmailSettingsData.From) },
async () => await adminClient.UpdateStoreEmailSettings(admin.StoreId,
new EmailSettingsData() { From = "ass" }));
await adminClient.SendEmail(admin.StoreId,
new SendEmailRequest() { Body = "lol", Subject = "subj", Email = "sdasdas" });
}
}
}

View File

@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Controllers;
using BTCPayServer.Models;
@ -8,140 +9,125 @@ using BTCPayServer.Tests.Logging;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBitpayClient;
using OpenQA.Selenium;
using OpenQA.Selenium.Support.Extensions;
using Xunit;
using Xunit.Abstractions;
namespace BTCPayServer.Tests
{
[Collection(nameof(NonParallelizableCollectionDefinition))]
public class PSBTTests : UnitTestBase
{
public PSBTTests(ITestOutputHelper helper) : base(helper)
{
}
[Fact]
[Trait("Integration", "Integration")]
[Trait("Selenium", "Selenium")]
public async Task CanPlayWithPSBT()
{
using var tester = CreateServerTester();
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 10,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some \", description",
FullNotifications = true
}, Facade.Merchant);
var cashCow = tester.ExplorerNode;
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
cashCow.SendToAddress(invoiceAddress, Money.Coins(1.5m));
TestUtils.Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal("paid", invoice.Status);
});
using var s = CreateSeleniumTester(newDb: true);
await s.StartAsync();
var walletController = user.GetController<UIWalletsController>();
var walletId = new WalletId(user.StoreId, "BTC");
var sendDestination = new Key().PubKey.Hash.GetAddress(user.SupportedNetwork.NBitcoinNetwork).ToString();
var sendModel = new WalletSendModel()
{
Outputs = new List<WalletSendModel.TransactionOutput>()
{
new WalletSendModel.TransactionOutput()
{
DestinationAddress = sendDestination,
Amount = 0.1m,
}
},
FeeSatoshiPerByte = 1,
CurrentBalance = 1.5m
};
var u1 = s.RegisterNewUser(true);
var hot = s.CreateNewStore();
var seed = s.GenerateWallet(isHotWallet: true);
var cold = s.CreateNewStore();
s.GenerateWallet(isHotWallet: false, seed: seed.ToString());
string redirectedPSBT = AssertRedirectedPSBT(await walletController.WalletSend(walletId, sendModel, command: "analyze-psbt"), nameof(walletController.WalletPSBT));
var vmPSBT = await walletController.WalletPSBT(walletId, new WalletPSBTViewModel() { PSBT = redirectedPSBT }).AssertViewModelAsync<WalletPSBTViewModel>();
var unsignedPSBT = PSBT.Parse(vmPSBT.PSBT, user.SupportedNetwork.NBitcoinNetwork);
Assert.NotNull(vmPSBT.Decoded);
// Scenario 1: one user has two stores sharing same seed
// one store is hot wallet, the other not.
var filePSBT = (FileContentResult)(await walletController.WalletPSBT(walletId, vmPSBT, "save-psbt"));
PSBT.Load(filePSBT.FileContents, user.SupportedNetwork.NBitcoinNetwork);
// Here, the cold wallet create a PSBT, then we switch to hot wallet to sign
// the PSBT and broadcast
s.GoToStore(cold.storeId);
var address = await s.FundStoreWallet();
Thread.Sleep(1000);
s.GoToWallet(navPages: Views.Wallets.WalletsNavPages.Send);
SendAllTo(s, address);
s.Driver.FindElement(By.Id("SignWithPSBT")).Click();
var vmPSBT2 = await walletController.WalletPSBT(walletId, new WalletPSBTViewModel
{
SigningContext = new SigningContextModel
{
PSBT = AssertRedirectedPSBT(await walletController.WalletPSBT(walletId, vmPSBT, "broadcast"), nameof(walletController.WalletPSBTReady))
}
}).AssertViewModelAsync<WalletPSBTViewModel>();
Assert.NotEmpty(vmPSBT2.Inputs.Where(i => i.Error != null));
Assert.Equal(vmPSBT.PSBT, vmPSBT2.SigningContext.PSBT);
var psbt = ExtractPSBT(s);
var signedPSBT = unsignedPSBT.Clone();
signedPSBT.SignAll(user.DerivationScheme, user.GenerateWalletResponseV.AccountHDKey, user.GenerateWalletResponseV.AccountKeyPath);
vmPSBT.PSBT = signedPSBT.ToBase64();
var psbtReady = await walletController.WalletPSBT(walletId, new WalletPSBTViewModel
{
SigningContext = new SigningContextModel
{
PSBT = AssertRedirectedPSBT(await walletController.WalletPSBT(walletId, vmPSBT, "broadcast"), nameof(walletController.WalletPSBTReady))
}
}).AssertViewModelAsync<WalletPSBTViewModel>();
Assert.Equal(2 + 1, psbtReady.Destinations.Count); // The fee is a destination
Assert.Contains(psbtReady.Destinations, d => d.Destination == sendDestination && !d.Positive);
Assert.Contains(psbtReady.Destinations, d => d.Positive);
s.GoToStore(hot.storeId);
s.GoToWallet(navPages: Views.Wallets.WalletsNavPages.PSBT);
s.Driver.FindElement(By.Name("PSBT")).SendKeys(psbt);
s.Driver.FindElement(By.Id("Decode")).Click();
s.Driver.FindElement(By.Id("SignTransaction")).Click();
s.Driver.FindElement(By.Id("BroadcastTransaction")).Click();
s.FindAlertMessage();
vmPSBT.PSBT = unsignedPSBT.ToBase64();
var combineVM = await walletController.WalletPSBT(walletId, vmPSBT, "combine").AssertViewModelAsync<WalletPSBTCombineViewModel>();
Assert.Equal(vmPSBT.PSBT, combineVM.OtherPSBT);
combineVM.PSBT = signedPSBT.ToBase64();
var psbt = AssertRedirectedPSBT(await walletController.WalletPSBTCombine(walletId, combineVM), nameof(walletController.WalletPSBT));
// Scenario 2: Same as scenario 1, except we create a PSBT from hot wallet, then sign by manually
// entering the seed on the cold wallet.
s.GoToWallet(navPages: Views.Wallets.WalletsNavPages.Send);
SendAllTo(s, address);
psbt = ExtractPSBT(s);
var signedPSBT2 = PSBT.Parse(psbt, user.SupportedNetwork.NBitcoinNetwork);
Assert.True(signedPSBT.TryFinalize(out _));
Assert.True(signedPSBT2.TryFinalize(out _));
Assert.Equal(signedPSBT, signedPSBT2);
// Let's check it has been signed, then remove the signature.
// Also remove the hdkeys so we can test the update later
var psbtParsed = PSBT.Parse(psbt, s.Server.NetworkProvider.BTC.NBitcoinNetwork);
var signedPSBT = psbtParsed.Clone();
Assert.True(psbtParsed.Clone().TryFinalize(out _));
Assert.Single(psbtParsed.Inputs[0].PartialSigs);
psbtParsed.Inputs[0].PartialSigs.Clear();
Assert.Single(psbtParsed.Inputs[0].HDKeyPaths);
psbtParsed.Inputs[0].HDKeyPaths.Clear();
var skeletonPSBT = psbtParsed;
// Can use uploaded file?
combineVM.PSBT = null;
combineVM.UploadedPSBTFile = TestUtils.GetFormFile("signedPSBT", signedPSBT.ToBytes());
psbt = AssertRedirectedPSBT(await walletController.WalletPSBTCombine(walletId, combineVM), nameof(walletController.WalletPSBT));
signedPSBT2 = PSBT.Parse(psbt, user.SupportedNetwork.NBitcoinNetwork);
Assert.True(signedPSBT.TryFinalize(out _));
Assert.True(signedPSBT2.TryFinalize(out _));
Assert.Equal(signedPSBT, signedPSBT2);
s.GoToStore(cold.storeId);
s.GoToWallet(navPages: Views.Wallets.WalletsNavPages.PSBT);
s.Driver.FindElement(By.Name("PSBT")).SendKeys(skeletonPSBT.ToBase64());
s.Driver.FindElement(By.Id("Decode")).Click();
s.Driver.FindElement(By.Id("SignTransaction")).Click();
s.Driver.FindElement(By.Id("SignWithSeed")).Click();
s.Driver.FindElement(By.Name("SeedOrKey")).SendKeys(seed.ToString());
s.Driver.FindElement(By.Id("Submit")).Click();
s.Driver.FindElement(By.Id("BroadcastTransaction")).Click();
s.FindAlertMessage();
var ready = (await walletController.WalletPSBT(walletId, new WalletPSBTViewModel
{
SigningContext = new SigningContextModel(signedPSBT)
})).AssertViewModel<WalletPSBTViewModel>();
Assert.Equal(signedPSBT.ToBase64(), ready.SigningContext.PSBT);
psbt = AssertRedirectedPSBT(await walletController.WalletPSBTReady(walletId, ready, command: "analyze-psbt"), nameof(walletController.WalletPSBT));
Assert.Equal(signedPSBT.ToBase64(), psbt);
var redirect = Assert.IsType<RedirectToActionResult>(await walletController.WalletPSBTReady(walletId, ready, command: "broadcast"));
Assert.Equal(nameof(walletController.WalletTransactions), redirect.ActionName);
// Let's check if the update feature works
s.GoToWallet(navPages: Views.Wallets.WalletsNavPages.PSBT);
s.Driver.FindElement(By.Name("PSBT")).SendKeys(skeletonPSBT.ToBase64());
s.Driver.FindElement(By.Id("Decode")).Click();
s.Driver.FindElement(By.Id("PSBTOptionsAdvancedHeader")).Click();
s.Driver.WaitForElement(By.Id("update-psbt")).Click();
//test base64 psbt file
Assert.False(string.IsNullOrEmpty(Assert.IsType<WalletPSBTViewModel>(
Assert.IsType<ViewResult>(
await walletController.WalletPSBT(walletId,
new WalletPSBTViewModel
{
UploadedPSBTFile = TestUtils.GetFormFile("base64", signedPSBT.ToBase64())
})).Model).PSBT));
psbt = ExtractPSBT(s);
psbtParsed = PSBT.Parse(psbt, s.Server.NetworkProvider.BTC.NBitcoinNetwork);
Assert.Single(psbtParsed.Inputs[0].HDKeyPaths);
Assert.Empty(psbtParsed.Inputs[0].PartialSigs);
// Let's if we can combine the updated psbt (which has hdkeys, but no sig)
// with the signed psbt (which has sig, but no hdkeys)
s.GoToWallet(navPages: Views.Wallets.WalletsNavPages.PSBT);
s.Driver.FindElement(By.Name("PSBT")).SendKeys(psbtParsed.ToBase64());
s.Driver.FindElement(By.Id("Decode")).Click();
s.Driver.FindElement(By.Id("PSBTOptionsAdvancedHeader")).Click();
s.Driver.WaitForElement(By.Id("combine-psbt")).Click();
signedPSBT.Inputs[0].HDKeyPaths.Clear();
s.Driver.FindElement(By.Name("PSBT")).SendKeys(signedPSBT.ToBase64());
s.Driver.WaitForElement(By.Id("Submit")).Click();
psbt = ExtractPSBT(s);
psbtParsed = PSBT.Parse(psbt, s.Server.NetworkProvider.BTC.NBitcoinNetwork);
Assert.Single(psbtParsed.Inputs[0].HDKeyPaths);
Assert.Single(psbtParsed.Inputs[0].PartialSigs);
}
private static string AssertRedirectedPSBT(IActionResult view, string actionName)
private static void SendAllTo(SeleniumTester s, string address)
{
var postRedirectView = Assert.IsType<ViewResult>(view);
var postRedirectViewModel = Assert.IsType<PostRedirectViewModel>(postRedirectView.Model);
Assert.Equal(actionName, postRedirectViewModel.AspAction);
var redirectedPSBT = postRedirectViewModel.Parameters.Single(p => p.Key == "psbt" || p.Key == "SigningContext.PSBT").Value;
return redirectedPSBT;
s.Driver.FindElement(By.Name("Outputs[0].DestinationAddress")).SendKeys(address);
s.Driver.FindElement(By.ClassName("crypto-balance-link")).Click();
s.Driver.FindElement(By.Id("SignTransaction")).Click();
}
private static string ExtractPSBT(SeleniumTester s)
{
var pageSource = s.Driver.PageSource;
var start = pageSource.IndexOf("id=\"psbt-base64\">");
start += "id=\"psbt-base64\">".Length;
var end = pageSource.IndexOf("<", start);
return pageSource[start..end];
}
}
}

View File

@ -27,6 +27,7 @@ using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
using Newtonsoft.Json.Linq;
using OpenQA.Selenium;
using OpenQA.Selenium.Support.Extensions;
using OpenQA.Selenium.Support.UI;
using Xunit;
using Xunit.Abstractions;

View File

@ -1,7 +1,7 @@
# Tooling
This README describe some useful tooling that you may need during development and testing.
To learn how to get started with your local development environment, read [our documentation](https://docs.btcpayserver.org/LocalDevelopment/).
To learn how to get started with your local development environment, read [our documentation](https://docs.btcpayserver.org/Development/LocalDevelopment/).
## How to manually test payments

View File

@ -1,12 +1,10 @@
using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.BIP78.Sender;
using BTCPayServer.Lightning;
using BTCPayServer.Lightning.CLightning;
using BTCPayServer.Views.Manage;
@ -15,6 +13,7 @@ using BTCPayServer.Views.Stores;
using BTCPayServer.Views.Wallets;
using Microsoft.Extensions.Configuration;
using NBitcoin;
using NBitcoin.RPC;
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Support.UI;
@ -85,6 +84,11 @@ namespace BTCPayServer.Tests
Driver.AssertNoError();
}
public void PayInvoice()
{
Driver.FindElement(By.Id("FakePayment")).Click();
}
/// <summary>
/// Use this ServerUri when trying to browse with selenium
/// Because for some reason, the selenium container can't resolve the tests container domain name
@ -139,8 +143,15 @@ namespace BTCPayServer.Tests
Driver.FindElement(By.Id("IsAdmin")).Click();
Driver.FindElement(By.Id("RegisterButton")).Click();
Driver.AssertNoError();
CreatedUser = usr;
return usr;
}
string CreatedUser;
public TestAccount AsTestAccount()
{
return new TestAccount(Server) { RegisterDetails = new Models.AccountViewModels.RegisterViewModel() { Password = "123456", Email = CreatedUser } };
}
public (string storeName, string storeId) CreateNewStore(bool keepId = true)
{
@ -151,6 +162,7 @@ namespace BTCPayServer.Tests
}
Driver.WaitForElement(By.Id("StoreSelectorCreate")).Click();
var name = "Store" + RandomUtils.GetUInt64();
TestLogs.LogInformation($"Created store {name}");
Driver.WaitForElement(By.Id("Name")).SendKeys(name);
Driver.WaitForElement(By.Id("Create")).Click();
Driver.FindElement(By.Id("StoreNav-StoreSettings")).Click();
@ -161,7 +173,7 @@ namespace BTCPayServer.Tests
return (name, storeId);
}
public Mnemonic GenerateWallet(string cryptoCode = "BTC", string seed = "", bool importkeys = false, bool privkeys = false, ScriptPubKeyType format = ScriptPubKeyType.Segwit)
public Mnemonic GenerateWallet(string cryptoCode = "BTC", string seed = "", bool? importkeys = null, bool isHotWallet = false, ScriptPubKeyType format = ScriptPubKeyType.Segwit)
{
var isImport = !string.IsNullOrEmpty(seed);
GoToWalletSettings(cryptoCode);
@ -181,11 +193,11 @@ namespace BTCPayServer.Tests
Driver.FindElement(By.Id("ImportWalletOptionsLink")).Click();
Driver.FindElement(By.Id("ImportSeedLink")).Click();
Driver.FindElement(By.Id("ExistingMnemonic")).SendKeys(seed);
Driver.SetCheckbox(By.Id("SavePrivateKeys"), privkeys);
Driver.SetCheckbox(By.Id("SavePrivateKeys"), isHotWallet);
}
else
{
var option = privkeys ? "Hotwallet" : "Watchonly";
var option = isHotWallet ? "Hotwallet" : "Watchonly";
TestLogs.LogInformation($"Generating new seed ({option})");
Driver.FindElement(By.Id("GenerateWalletLink")).Click();
Driver.FindElement(By.Id($"Generate{option}Link")).Click();
@ -195,7 +207,8 @@ namespace BTCPayServer.Tests
Driver.FindElement(By.CssSelector($"#ScriptPubKeyType option[value={format}]")).Click();
Driver.ToggleCollapse("AdvancedSettings");
Driver.SetCheckbox(By.Id("ImportKeysToRPC"), importkeys);
if (importkeys is bool v)
Driver.SetCheckbox(By.Id("ImportKeysToRPC"), v);
Driver.FindElement(By.Id("Continue")).Click();
if (isImport)
@ -228,7 +241,10 @@ namespace BTCPayServer.Tests
/// <param name="derivationScheme"></param>
public void AddDerivationScheme(string cryptoCode = "BTC", string derivationScheme = "xpub661MyMwAqRbcGABgHMUXDzPzH1tU7eZaAaJQXhDXsSxsqyQzQeU6kznNfSuAyqAK9UaWSaZaMFdNiY5BCF4zBPAzSnwfUAwUhwttuAKwfRX-[legacy]")
{
GoToWalletSettings(cryptoCode);
if (!Driver.PageSource.Contains($"Setup {cryptoCode} Wallet"))
{
GoToWalletSettings(cryptoCode);
}
Driver.FindElement(By.Id("ImportWalletOptionsLink")).Click();
Driver.FindElement(By.Id("ImportXpubLink")).Click();
@ -251,10 +267,9 @@ namespace BTCPayServer.Tests
public void AddLightningNode(string cryptoCode = null, LightningConnectionType? connectionType = null, bool test = true)
{
cryptoCode ??= "BTC";
Driver.FindElement(By.Id($"StoreNav-Lightning{cryptoCode}")).Click();
if (Driver.PageSource.Contains("id=\"SetupLightningNodeLink\""))
if (!Driver.PageSource.Contains("Connect to a Lightning node"))
{
Driver.FindElement(By.Id("SetupLightningNodeLink")).Click();
GoToLightningSettings();
}
var connectionString = connectionType switch
@ -349,7 +364,7 @@ namespace BTCPayServer.Tests
Driver.FindElement(By.Id("Nav-Logout")).Click();
}
public void Login(string user, string password)
public void LogIn(string user, string password)
{
Driver.FindElement(By.Id("Email")).SendKeys(user);
Driver.FindElement(By.Id("Password")).SendKeys(password);
@ -364,7 +379,12 @@ namespace BTCPayServer.Tests
public void GoToStore(string storeId, StoreNavPages storeNavPage = StoreNavPages.General)
{
if (storeId is not null)
{
GoToUrl($"/stores/{storeId}/");
StoreId = storeId;
if (WalletId != null)
WalletId = new WalletId(storeId, WalletId.CryptoCode);
}
Driver.FindElement(By.Id("StoreNav-StoreSettings")).Click();
@ -388,9 +408,9 @@ namespace BTCPayServer.Tests
public void GoToWalletSettings(string cryptoCode = "BTC")
{
Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click();
if (Driver.PageSource.Contains("id=\"SectionNav-Settings\""))
if (Driver.PageSource.Contains("id=\"WalletNav-Settings\""))
{
Driver.FindElement(By.Id("SectionNav-Settings")).Click();
Driver.FindElement(By.Id("WalletNav-Settings")).Click();
}
}
@ -410,8 +430,9 @@ namespace BTCPayServer.Tests
Driver.FindElement(By.Id($"StoreSelectorMenuItem-{storeId}")).Click();
}
public void GoToInvoiceCheckout(string invoiceId)
public void GoToInvoiceCheckout(string invoiceId = null)
{
invoiceId ??= InvoiceId;
Driver.FindElement(By.Id("StoreNav-Invoices")).Click();
Driver.FindElement(By.Id($"invoice-checkout-{invoiceId}")).Click();
CheckForJSErrors();
@ -431,16 +452,17 @@ namespace BTCPayServer.Tests
else
{
GoToUrl(storeId == null ? "/invoices/" : $"/stores/{storeId}/invoices/");
StoreId = storeId;
}
}
public void GoToProfile(ManageNavPages navPages = ManageNavPages.Index)
{
Driver.FindElement(By.Id("Nav-Account")).Click();
Driver.FindElement(By.Id("Nav-ManageAccount")).Click();
Driver.WaitForAndClick(By.Id("Nav-Account"));
Driver.WaitForAndClick(By.Id("Nav-ManageAccount"));
if (navPages != ManageNavPages.Index)
{
Driver.FindElement(By.Id($"SectionNav-{navPages.ToString()}")).Click();
Driver.WaitForAndClick(By.Id($"SectionNav-{navPages.ToString()}"));
}
}
@ -471,6 +493,7 @@ namespace BTCPayServer.Tests
)
{
GoToInvoices(storeId);
Driver.FindElement(By.Id("CreateNewInvoice")).Click();
if (amount is decimal v)
Driver.FindElement(By.Id("Amount")).SendKeys(v.ToString(CultureInfo.InvariantCulture));
@ -485,10 +508,14 @@ namespace BTCPayServer.Tests
Driver.FindElement(By.Id("Create")).Click();
var statusElement = FindAlertMessage(expectedSeverity);
return expectedSeverity == StatusMessageModel.StatusSeverity.Success ? statusElement.Text.Split(" ")[1] : null;
var inv = expectedSeverity == StatusMessageModel.StatusSeverity.Success ? statusElement.Text.Split(" ")[1] : null;
InvoiceId = inv;
TestLogs.LogInformation($"Created invoice {inv}");
return inv;
}
string InvoiceId;
public async Task FundStoreWallet(WalletId walletId = null, int coins = 1, decimal denomination = 1m)
public async Task<string> FundStoreWallet(WalletId walletId = null, int coins = 1, decimal denomination = 1m)
{
walletId ??= WalletId;
GoToWallet(walletId, WalletsNavPages.Receive);
@ -497,24 +524,21 @@ namespace BTCPayServer.Tests
var address = BitcoinAddress.Create(addressStr, ((BTCPayNetwork)Server.NetworkProvider.GetNetwork(walletId.CryptoCode)).NBitcoinNetwork);
for (var i = 0; i < coins; i++)
{
await Server.ExplorerNode.SendToAddressAsync(address, Money.Coins(denomination));
bool mined = false;
retry:
try
{
await Server.ExplorerNode.SendToAddressAsync(address, Money.Coins(denomination));
}
catch (RPCException) when (!mined)
{
mined = true;
await Server.ExplorerNode.GenerateAsync(1);
goto retry;
}
}
}
public void PayInvoice(WalletId walletId, string invoiceId)
{
GoToInvoiceCheckout(invoiceId);
var bip21 = Driver.FindElement(By.ClassName("payment__details__instruction__open-wallet__btn"))
.GetAttribute("href");
Assert.Contains($"{PayjoinClient.BIP21EndpointKey}", bip21);
GoToWallet(walletId);
Driver.FindElement(By.Id("bip21parse")).Click();
Driver.SwitchTo().Alert().SendKeys(bip21);
Driver.SwitchTo().Alert().Accept();
Driver.FindElement(By.Id("SignTransaction")).Click();
Driver.FindElement(By.Id("SignWithSeed")).Click();
Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click();
Driver.Navigate().Refresh();
return addressStr;
}
private void CheckForJSErrors()
@ -544,9 +568,14 @@ namespace BTCPayServer.Tests
{
walletId ??= WalletId;
Driver.Navigate().GoToUrl(new Uri(ServerUri, $"wallets/{walletId}"));
if (navPages != WalletsNavPages.Transactions)
if (navPages == WalletsNavPages.PSBT)
{
Driver.FindElement(By.Id($"SectionNav-{navPages}")).Click();
Driver.FindElement(By.Id("WalletNav-Send")).Click();
Driver.FindElement(By.Id("PSBT")).Click();
}
else if (navPages != WalletsNavPages.Transactions)
{
Driver.FindElement(By.Id($"WalletNav-{navPages}")).Click();
}
}

View File

@ -64,6 +64,48 @@ namespace BTCPayServer.Tests
Assert.Contains("Starting listening NBXplorer", s.Driver.PageSource);
s.Driver.Quit();
}
[Fact(Timeout = TestTimeout)]
public async Task CanUseCPFP()
{
using var s = CreateSeleniumTester();
await s.StartAsync();
s.RegisterNewUser(true);
s.CreateNewStore();
s.GenerateWallet(isHotWallet: true);
await s.FundStoreWallet();
for (int i = 0; i < 3; i++)
{
s.CreateInvoice();
s.GoToInvoiceCheckout();
s.PayInvoice();
s.GoToInvoices(s.StoreId);
}
// Let's CPFP from the invoices page
s.Driver.SetCheckbox(By.Id("selectAllCheckbox"), true);
s.Driver.FindElement(By.Id("ActionsDropdownToggle")).Click();
s.Driver.FindElement(By.Id("BumpFee")).Click();
s.Driver.FindElement(By.Id("BroadcastTransaction")).Click();
s.FindAlertMessage();
Assert.Contains($"/stores/{s.StoreId}/invoices", s.Driver.Url);
// CPFP again should fail because all invoices got bumped
s.GoToInvoices();
s.Driver.SetCheckbox(By.Id("selectAllCheckbox"), true);
s.Driver.FindElement(By.Id("ActionsDropdownToggle")).Click();
s.Driver.FindElement(By.Id("BumpFee")).Click();
var err = s.FindAlertMessage(StatusMessageModel.StatusSeverity.Error);
Assert.Contains("any UTXO available", err.Text);
Assert.Contains($"/stores/{s.StoreId}/invoices", s.Driver.Url);
// But we should be able to bump from the wallet's page
s.GoToWallet(navPages: WalletsNavPages.Transactions);
s.Driver.SetCheckbox(By.Id("selectAllCheckbox"), true);
s.Driver.FindElement(By.Id("ActionsDropdownToggle")).Click();
s.Driver.FindElement(By.Id("BumpFee")).Click();
s.Driver.FindElement(By.Id("BroadcastTransaction")).Click();
s.FindAlertMessage();
Assert.Contains($"/wallets/{s.WalletId}", s.Driver.Url);
}
[Fact(Timeout = TestTimeout)]
[Trait("Lightning", "Lightning")]
@ -112,7 +154,7 @@ namespace BTCPayServer.Tests
await u2.MakeAdmin(false);
s.GoToLogin();
s.Login(u1.RegisterDetails.Email, u1.RegisterDetails.Password);
s.LogIn(u1.RegisterDetails.Email, u1.RegisterDetails.Password);
s.GoToProfile();
s.Driver.FindElement(By.Id("Email")).Clear();
s.Driver.FindElement(By.Id("Email")).SendKeys(u2.RegisterDetails.Email);
@ -344,6 +386,10 @@ namespace BTCPayServer.Tests
await s.StartAsync();
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
Assert.Contains("To create an invoice, you need to", s.Driver.PageSource);
s.AddDerivationScheme();
s.GoToInvoices();
s.CreateInvoice();
@ -407,6 +453,7 @@ namespace BTCPayServer.Tests
s.GoToStore();
Assert.Contains(storeName, s.Driver.PageSource);
Assert.DoesNotContain("id=\"Dashboard\"", s.Driver.PageSource);
// verify steps for wallet setup are displayed correctly
s.GoToStore(StoreNavPages.Dashboard);
@ -420,10 +467,11 @@ namespace BTCPayServer.Tests
s.Driver.AssertNoError();
s.GoToStore(StoreNavPages.Dashboard);
Assert.True(s.Driver.FindElement(By.Id("SetupGuide-WalletDone")).Displayed);
Assert.DoesNotContain("id=\"SetupGuide\"", s.Driver.PageSource);
Assert.True(s.Driver.FindElement(By.Id("Dashboard")).Displayed);
// setup offchain wallet
s.Driver.FindElement(By.Id("SetupGuide-Lightning")).Click();
s.Driver.FindElement(By.Id("StoreNav-LightningBTC")).Click();
s.AddLightningNode();
s.Driver.AssertNoError();
var successAlert = s.FindAlertMessage();
@ -431,9 +479,6 @@ namespace BTCPayServer.Tests
s.ClickOnAllSectionLinks();
s.GoToStore(StoreNavPages.Dashboard);
Assert.True(s.Driver.FindElement(By.Id("SetupGuide-LightningDone")).Displayed);
s.GoToInvoices();
Assert.Contains("There are no invoices matching your criteria.", s.Driver.PageSource);
var invoiceId = s.CreateInvoice();
@ -576,15 +621,16 @@ namespace BTCPayServer.Tests
[Fact(Timeout = TestTimeout)]
public async Task CanCreateAppPoS()
{
using var s = CreateSeleniumTester();
using var s = CreateSeleniumTester(newDb: true);
await s.StartAsync();
s.RegisterNewUser();
var userId = s.RegisterNewUser(true);
s.CreateNewStore();
s.Driver.FindElement(By.Id("StoreNav-CreateApp")).Click();
s.Driver.FindElement(By.Name("AppName")).SendKeys("PoS" + Guid.NewGuid());
s.Driver.FindElement(By.Id("SelectedAppType")).SendKeys("Point of Sale");
s.Driver.FindElement(By.Id("Create")).Click();
Assert.Contains("App successfully created", s.FindAlertMessage().Text);
s.Driver.FindElement(By.CssSelector(".template-item:nth-of-type(1) .btn-primary")).Click();
s.Driver.FindElement(By.Id("BuyButtonText")).SendKeys("Take my money");
s.Driver.FindElement(By.Id("SaveItemChanges")).Click();
@ -595,9 +641,14 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("DefaultView")).SendKeys("Item list and cart");
s.Driver.FindElement(By.Id("SaveSettings")).Click();
s.Driver.FindElement(By.Id("ViewApp")).Click();
Assert.Contains("App updated", s.FindAlertMessage().Text);
var posBaseUrl = s.Driver.Url.Replace("/Cart", "");
s.Driver.FindElement(By.Id("ViewApp")).Click();
var windows = s.Driver.WindowHandles;
Assert.Equal(2, windows.Count);
s.Driver.SwitchTo().Window(windows[1]);
var posBaseUrl = s.Driver.Url.Replace("/cart", "");
Assert.True(s.Driver.PageSource.Contains("Tea shop"), "Unable to create PoS");
Assert.True(s.Driver.PageSource.Contains("Cart"), "PoS not showing correct default view");
Assert.True(s.Driver.PageSource.Contains("Take my money"), "PoS not showing correct default view");
@ -607,6 +658,48 @@ namespace BTCPayServer.Tests
s.Driver.Url = posBaseUrl + "/cart";
Assert.True(s.Driver.PageSource.Contains("Cart"), "Cart PoS not showing correct view");
// Let's set change the root app
s.GoToHome();
s.GoToServer(ServerNavPages.Policies);
s.Driver.ScrollTo(By.Id("RootAppId"));
var select = new SelectElement(s.Driver.FindElement(By.Id("RootAppId")));
select.SelectByText("Point of", true);
s.Driver.FindElement(By.Id("SaveButton")).Click();
s.FindAlertMessage();
s.Logout();
s.GoToLogin();
s.LogIn(userId);
// Make sure after login, we are not redirected to the PoS
Assert.DoesNotContain("Tea shop", s.Driver.PageSource);
// We are only if explicitly going to /
s.GoToUrl("/");
Assert.Contains("Tea shop", s.Driver.PageSource);
s.Driver.Navigate().Back();
// Let's check with domain mapping as well.
s.GoToServer(ServerNavPages.Policies);
s.Driver.ScrollTo(By.Id("RootAppId"));
select = new SelectElement(s.Driver.FindElement(By.Id("RootAppId")));
select.SelectByText("None", true);
s.Driver.FindElement(By.Id("SaveButton")).Click();
s.Driver.ScrollTo(By.Id("RootAppId"));
s.Driver.FindElement(By.Id("AddDomainButton")).Click();
s.Driver.FindElement(By.Id("DomainToAppMapping_0__Domain")).SendKeys(new Uri(s.Driver.Url, UriKind.Absolute).DnsSafeHost);
select = new SelectElement(s.Driver.FindElement(By.Id("DomainToAppMapping_0__AppId")));
select.SelectByText("Point of", true);
s.Driver.FindElement(By.Id("SaveButton")).Click();
s.Logout();
s.LogIn(userId);
// Make sure after login, we are not redirected to the PoS
Assert.DoesNotContain("Tea shop", s.Driver.PageSource);
// We are only if explicitly going to /
s.GoToUrl("/");
Assert.Contains("Tea shop", s.Driver.PageSource);
}
[Fact(Timeout = TestTimeout)]
@ -622,14 +715,26 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Name("AppName")).SendKeys("CF" + Guid.NewGuid());
s.Driver.FindElement(By.Id("SelectedAppType")).SendKeys("Crowdfund");
s.Driver.FindElement(By.Id("Create")).Click();
Assert.Contains("App successfully created", s.FindAlertMessage().Text);
s.Driver.FindElement(By.Id("Title")).SendKeys("Kukkstarter");
s.Driver.FindElement(By.CssSelector("div.note-editable.card-block")).SendKeys("1BTC = 1BTC");
s.Driver.FindElement(By.Id("TargetCurrency")).Clear();
s.Driver.FindElement(By.Id("TargetCurrency")).SendKeys("JPY");
s.Driver.FindElement(By.Id("TargetAmount")).SendKeys("700");
s.Driver.FindElement(By.Id("SaveSettings")).Click();
Assert.Contains("App updated", s.FindAlertMessage().Text);
s.Driver.FindElement(By.Id("ViewApp")).Click();
var windows = s.Driver.WindowHandles;
Assert.Equal(2, windows.Count);
s.Driver.SwitchTo().Window(windows[1]);
Assert.Equal("currently active!",
s.Driver.FindElement(By.CssSelector("[data-test='time-state']")).Text);
s.Driver.Close();
s.Driver.SwitchTo().Window(windows[0]);
}
[Fact(Timeout = TestTimeout)]
@ -645,11 +750,14 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("CreatePaymentRequest")).Click();
s.Driver.FindElement(By.Id("Title")).SendKeys("Pay123");
s.Driver.FindElement(By.Id("Amount")).SendKeys("700");
s.Driver.FindElement(By.Id("Currency")).SendKeys("BTC");
var currencyInput = s.Driver.FindElement(By.Id("Currency"));
Assert.Equal("USD", currencyInput.GetAttribute("value"));
currencyInput.Clear();
currencyInput.SendKeys("BTC");
s.Driver.FindElement(By.Id("SaveButton")).Click();
var aaa = s.Driver.PageSource;
var url = s.Driver.Url;
s.Driver.FindElement(By.Id("ViewAppButton")).Click();
s.Driver.FindElement(By.Id("ViewPaymentRequest")).Click();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
Assert.Equal("Amount due", s.Driver.FindElement(By.CssSelector("[data-test='amount-due-title']")).Text);
Assert.Equal("Pay Invoice",
@ -672,6 +780,21 @@ namespace BTCPayServer.Tests
s.Driver.AssertElementNotFound(By.CssSelector("[data-test='status']"));
Assert.Equal("Pay Invoice",
s.Driver.FindElement(By.CssSelector("[data-test='pay-button']")).Text.Trim());
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
// archive (from details page)
var payReqId = s.Driver.Url.Split('/').Last();
s.Driver.FindElement(By.Id("ArchivePaymentRequest")).Click();
Assert.Contains("The payment request has been archived", s.FindAlertMessage().Text);
Assert.DoesNotContain("Pay123", s.Driver.PageSource);
s.Driver.FindElement(By.Id("SearchDropdownToggle")).Click();
s.Driver.FindElement(By.Id("SearchIncludeArchived")).Click();
Assert.Contains("Pay123", s.Driver.PageSource);
// unarchive (from list)
s.Driver.FindElement(By.Id($"ToggleArchival-{payReqId}")).Click();
Assert.Contains("The payment request has been unarchived", s.FindAlertMessage().Text);
Assert.Contains("Pay123", s.Driver.PageSource);
}
[Fact(Timeout = TestTimeout)]
@ -861,7 +984,7 @@ namespace BTCPayServer.Tests
{
var cryptoCode = "BTC";
s.CreateNewStore();
s.GenerateWallet(cryptoCode, "melody lizard phrase voice unique car opinion merge degree evil swift cargo", privkeys: isHotwallet);
s.GenerateWallet(cryptoCode, "melody lizard phrase voice unique car opinion merge degree evil swift cargo", isHotWallet: isHotwallet);
s.GoToWalletSettings(cryptoCode);
if (isHotwallet)
Assert.Contains("View seed", s.Driver.PageSource);
@ -885,22 +1008,22 @@ namespace BTCPayServer.Tests
//let's test quickly the receive wallet page
s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click();
s.Driver.FindElement(By.Id("SectionNav-Send")).Click();
s.Driver.FindElement(By.Id("WalletNav-Send")).Click();
s.Driver.FindElement(By.Id("SignTransaction")).Click();
//you cannot use the Sign with NBX option without saving private keys when generating the wallet.
Assert.DoesNotContain("nbx-seed", s.Driver.PageSource);
s.Driver.FindElement(By.Id("SectionNav-Receive")).Click();
s.Driver.FindElement(By.Id("WalletNav-Receive")).Click();
//generate a receiving address
s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click();
Assert.True(s.Driver.FindElement(By.ClassName("qr-container")).Displayed);
Assert.True(s.Driver.FindElement(By.CssSelector("#address-tab .qr-container")).Displayed);
var receiveAddr = s.Driver.FindElement(By.Id("address")).GetAttribute("value");
//unreserve
s.Driver.FindElement(By.CssSelector("button[value=unreserve-current-address]")).Click();
//generate it again, should be the same one as before as nothing got used in the meantime
s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click();
Assert.True(s.Driver.FindElement(By.ClassName("qr-container")).Displayed);
Assert.True(s.Driver.FindElement(By.CssSelector("#address-tab .qr-container")).Displayed);
Assert.Equal(receiveAddr, s.Driver.FindElement(By.Id("address")).GetAttribute("value"));
//send money to addr and ensure it changed
@ -958,20 +1081,19 @@ namespace BTCPayServer.Tests
Assert.Contains("m/84'/1'/0'",
s.Driver.FindElement(By.Id("AccountKeys_0__AccountKeyPath")).GetAttribute("value"));
s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click();
// Make sure we can rescan, because we are admin!
s.Driver.FindElement(By.Id("SectionNav-Rescan")).Click();
s.Driver.FindElement(By.Id("ActionsDropdownToggle")).Click();
s.Driver.FindElement(By.Id("Rescan")).Click();
Assert.Contains("The batch size make sure", s.Driver.PageSource);
// Check the tx sent earlier arrived
s.Driver.FindElement(By.Id("SectionNav-Transactions")).Click();
s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click();
var walletTransactionLink = s.Driver.Url;
Assert.Contains(tx.ToString(), s.Driver.PageSource);
// Send to bob
s.Driver.FindElement(By.Id("SectionNav-Send")).Click();
s.Driver.FindElement(By.Id("WalletNav-Send")).Click();
var bob = new Key().PubKey.Hash.GetAddress(Network.RegTest);
SetTransactionOutput(s, 0, bob, 1);
s.Driver.FindElement(By.Id("SignTransaction")).Click();
@ -983,7 +1105,7 @@ namespace BTCPayServer.Tests
Assert.Equal(walletTransactionLink, s.Driver.Url);
s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click();
s.Driver.FindElement(By.Id("SectionNav-Send")).Click();
s.Driver.FindElement(By.Id("WalletNav-Send")).Click();
var jack = new Key().PubKey.Hash.GetAddress(Network.RegTest);
SetTransactionOutput(s, 0, jack, 0.01m);
@ -1000,7 +1122,7 @@ namespace BTCPayServer.Tests
bip21 += "&label=Solid Snake&message=Snake? Snake? SNAAAAKE!";
var parsedBip21 = new BitcoinUrlBuilder(bip21, Network.RegTest);
s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click();
s.Driver.FindElement(By.Id("SectionNav-Send")).Click();
s.Driver.FindElement(By.Id("WalletNav-Send")).Click();
s.Driver.FindElement(By.Id("bip21parse")).Click();
s.Driver.SwitchTo().Alert().SendKeys(bip21);
s.Driver.SwitchTo().Alert().Accept();
@ -1351,7 +1473,9 @@ namespace BTCPayServer.Tests
s.GoToLightningSettings();
// LNURL is true by default
Assert.True(s.Driver.FindElement(By.Id("LNURLEnabled")).Selected);
s.Driver.SetCheckbox(By.Name("LUD12Enabled"), true);
s.Driver.FindElement(By.Id("save")).Click();
// Topup Invoice test
var i = s.CreateInvoice(storeId, null, cryptoCode);
s.GoToInvoiceCheckout(i);
@ -1363,13 +1487,13 @@ namespace BTCPayServer.Tests
Assert.Equal(1m, fetchedReuqest.MinSendable.ToDecimal(LightMoneyUnit.Satoshi));
Assert.NotEqual(1m, fetchedReuqest.MaxSendable.ToDecimal(LightMoneyUnit.Satoshi));
var lnurlResponse = await fetchedReuqest.SendRequest(new LightMoney(0.000001m, LightMoneyUnit.BTC),
network, new HttpClient());
network, new HttpClient(), comment: "lol");
Assert.Equal(new LightMoney(0.000001m, LightMoneyUnit.BTC),
lnurlResponse.GetPaymentRequest(network).MinimumAmount);
var lnurlResponse2 = await fetchedReuqest.SendRequest(new LightMoney(0.000002m, LightMoneyUnit.BTC),
network, new HttpClient());
network, new HttpClient(), comment: "lol2");
Assert.Equal(new LightMoney(0.000002m, LightMoneyUnit.BTC), lnurlResponse2.GetPaymentRequest(network).MinimumAmount);
await Assert.ThrowsAnyAsync<LightningRPCException>(async () =>
{
@ -1383,7 +1507,11 @@ namespace BTCPayServer.Tests
var inv = await s.Server.PayTester.InvoiceRepository.GetInvoice(i);
Assert.Equal(InvoiceStatusLegacy.Complete, inv.Status);
});
var greenfield = await s.AsTestAccount().CreateClient();
var paymentMethods = await greenfield.GetInvoicePaymentMethods(s.StoreId, i);
Assert.Single(paymentMethods, p => {
return p.AdditionalData["providedComment"].Value<string>() == "lol2";
});
// Standard invoice test
s.GoToStore(storeId);
s.GoToLightningSettings();
@ -1491,6 +1619,12 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("Name")).SendKeys("PP1");
s.Driver.FindElement(By.Id("Amount")).Clear();
s.Driver.FindElement(By.Id("Amount")).SendKeys("0.0000001");
var currencyInput = s.Driver.FindElement(By.Id("Currency"));
Assert.Equal("USD", currencyInput.GetAttribute("value"));
currencyInput.Clear();
currencyInput.SendKeys("BTC");
s.Driver.FindElement(By.Id("Create")).Click();
s.Driver.FindElement(By.LinkText("View")).Click();
s.Driver.FindElement(By.Id("Destination")).SendKeys(lnurl);
@ -1529,10 +1663,19 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Name("AppName")).SendKeys("CF" + Guid.NewGuid());
s.Driver.FindElement(By.Id("SelectedAppType")).SendKeys("Crowdfund");
s.Driver.FindElement(By.Id("Create")).Click();
Assert.Contains("App successfully created", s.FindAlertMessage().Text);
s.Driver.FindElement(By.Id("Title")).SendKeys("Kukkstarter");
s.Driver.FindElement(By.CssSelector("div.note-editable.card-block")).SendKeys("1BTC = 1BTC");
s.Driver.FindElement(By.Id("SaveSettings")).Click();
Assert.Contains("App updated", s.FindAlertMessage().Text);
s.Driver.FindElement(By.Id("ViewApp")).Click();
var windows = s.Driver.WindowHandles;
Assert.Equal(2, windows.Count);
s.Driver.SwitchTo().Window(windows[1]);
s.Driver.FindElement(By.CssSelector("#crowdfund-body-contribution-container .perk")).Click();
s.Driver.FindElement(By.PartialLinkText("LNURL")).Click();
lnurl = s.Driver.FindElement(By.ClassName("lnurl"))
@ -1540,7 +1683,8 @@ namespace BTCPayServer.Tests
LNURL.LNURL.Parse(lnurl, out tag);
s.Driver.Close();
s.Driver.SwitchTo().Window(windows[0]);
}
[Fact]
@ -1552,7 +1696,6 @@ namespace BTCPayServer.Tests
s.Server.ActivateLightning();
await s.StartAsync();
await s.Server.EnsureChannelsSetup();
var cryptoCode = "BTC";
s.RegisterNewUser(true);
//ln address tests
s.CreateNewStore();
@ -1687,7 +1830,7 @@ retry:
TestUtils.Eventually(() => s.FindAlertMessage());
s.Logout();
s.Login(user, "123456");
s.LogIn(user, "123456");
var section = s.Driver.FindElement(By.Id("lnurlauth-section"));
links = section.FindElements(By.CssSelector(".tab-content a")).Select(element => element.GetAttribute("href"));
Assert.Equal(2,links.Count());

View File

@ -5,6 +5,7 @@ using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.BIP78.Sender;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
@ -236,6 +237,7 @@ namespace BTCPayServer.Tests
}
UserId = account.RegisteredUserId;
Email = RegisterDetails.Email;
IsAdmin = account.RegisteredAdmin;
}
@ -252,6 +254,12 @@ namespace BTCPayServer.Tests
get;
set;
}
public string Email
{
get;
set;
}
public string StoreId
{

View File

@ -74,6 +74,8 @@ namespace BTCPayServer.Tests
[Fact]
public void CanQueryDirectProviders()
{
// TODO: Check once in a while whether or not they are working again
string[] brokenShitcoinCasinos = { "okex" };
var factory = FastTests.CreateBTCPayRateFactory();
var directlySupported = factory.GetSupportedExchanges().Where(s => s.Source == RateSource.Direct)
.Select(s => s.Id).ToHashSet();
@ -86,47 +88,48 @@ namespace BTCPayServer.Tests
Fetcher: (BackgroundFetcherRateProvider)p.Value))
.ToList())
{
TestLogs.LogInformation($"Testing {result.ExpectedName}");
var name = result.ExpectedName;
if (brokenShitcoinCasinos.Contains(name))
{
TestLogs.LogInformation($"Skipping {name}");
continue;
}
TestLogs.LogInformation($"Testing {name}");
result.Fetcher.InvalidateCache();
var exchangeRates = new ExchangeRates(result.ExpectedName, result.ResultAsync.Result);
var exchangeRates = new ExchangeRates(name, result.ResultAsync.Result);
result.Fetcher.InvalidateCache();
Assert.NotNull(exchangeRates);
Assert.NotEmpty(exchangeRates);
Assert.NotEmpty(exchangeRates.ByExchange[result.ExpectedName]);
if (result.ExpectedName == "bitbank" || result.ExpectedName == "bitflyer")
Assert.NotEmpty(exchangeRates.ByExchange[name]);
if (name == "bitbank" || name == "bitflyer")
{
Assert.Contains(exchangeRates.ByExchange[result.ExpectedName],
Assert.Contains(exchangeRates.ByExchange[name],
e => e.CurrencyPair == new CurrencyPair("BTC", "JPY") &&
e.BidAsk.Bid > 100m); // 1BTC will always be more than 100JPY
}
else if (result.ExpectedName == "polispay")
else if (name == "argoneum")
{
Assert.Contains(exchangeRates.ByExchange[result.ExpectedName],
e => e.CurrencyPair == new CurrencyPair("BTC", "POLIS") &&
e.BidAsk.Bid > 1.0m); // 1BTC will always be more than 1 POLIS
}
else if (result.ExpectedName == "argoneum")
{
Assert.Contains(exchangeRates.ByExchange[result.ExpectedName],
Assert.Contains(exchangeRates.ByExchange[name],
e => e.CurrencyPair == new CurrencyPair("BTC", "AGM") &&
e.BidAsk.Bid > 1.0m); // 1 BTC will always be more than 1 AGM
}
else if (result.ExpectedName == "ripio")
else if (name == "ripio")
{
// Ripio keeps changing their pair, so anything is fine...
Assert.NotEmpty(exchangeRates.ByExchange[result.ExpectedName]);
Assert.NotEmpty(exchangeRates.ByExchange[name]);
}
else if (result.ExpectedName == "cryptomarket")
else if (name == "cryptomarket")
{
Assert.Contains(exchangeRates.ByExchange[result.ExpectedName],
Assert.Contains(exchangeRates.ByExchange[name],
e => e.CurrencyPair == new CurrencyPair("BTC", "CLP") &&
e.BidAsk.Bid > 1.0m); // 1 BTC will always be more than 1 CLP
}
else
{
// This check if the currency pair is using right currency pair
Assert.Contains(exchangeRates.ByExchange[result.ExpectedName],
Assert.Contains(exchangeRates.ByExchange[name],
e => (e.CurrencyPair == new CurrencyPair("BTC", "USD") ||
e.CurrencyPair == new CurrencyPair("BTC", "EUR") ||
e.CurrencyPair == new CurrencyPair("BTC", "USDT") ||
@ -139,8 +142,8 @@ namespace BTCPayServer.Tests
// we need to modify the AvailableRateProvider
// There are some exception we stopped supporting but don't want to break backward compat
if (result.ExpectedName != "coinaverage" && result.ExpectedName != "gdax")
Assert.Contains(result.ExpectedName, directlySupported);
if (name != "coinaverage" && name != "gdax")
Assert.Contains(name, directlySupported);
}
// Kraken emit one request only after first GetRates
@ -191,7 +194,8 @@ namespace BTCPayServer.Tests
private async Task AssertLinkNotDead(HttpClient httpClient, string url, string file)
{
var uri = new Uri(url);
int retryLeft = 3;
retry:
try
{
using var request = new HttpRequestMessage(HttpMethod.Get, uri);
@ -200,11 +204,6 @@ namespace BTCPayServer.Tests
request.Headers.TryAddWithoutValidation("User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:75.0) Gecko/20100101 Firefox/75.0");
var response = await httpClient.SendAsync(request);
if (response.StatusCode == HttpStatusCode.ServiceUnavailable) // Temporary issue
{
TestLogs.LogInformation($"Unavailable: {url} ({file})");
return;
}
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
if (uri.Fragment.Length != 0)
{
@ -222,8 +221,13 @@ namespace BTCPayServer.Tests
throw;
}
catch (Exception) when (retryLeft > 0)
{
goto retry;
}
catch (Exception ex)
{
retryLeft--;
var details = ex is EqualException ? (ex as EqualException).Actual : ex.Message;
TestLogs.LogInformation($"FAILED: {url} ({file}) {details}");
@ -250,6 +254,7 @@ namespace BTCPayServer.Tests
[Fact]
public void CanGetRateCryptoCurrenciesByDefault()
{
string[] brokenShitcoins = { "BTX_USD", "CHC_USD" };
var provider = new BTCPayNetworkProvider(ChainName.Mainnet);
var factory = FastTests.CreateBTCPayRateFactory();
var fetcher = new RateFetcher(factory);
@ -260,11 +265,11 @@ namespace BTCPayServer.Tests
var rules = new StoreBlob().GetDefaultRateRules(provider);
var result = fetcher.FetchRates(pairs, rules, default);
foreach (var value in result)
foreach ((CurrencyPair key, Task<RateResult> value) in result)
{
var rateResult = value.Value.GetAwaiter().GetResult();
TestLogs.LogInformation($"Testing {value.Key.ToString()}");
if (value.Key.ToString() == "BTX_USD") // Broken shitcoin
var rateResult = value.GetAwaiter().GetResult();
TestLogs.LogInformation($"Testing {key}");
if (brokenShitcoins.Contains(key.ToString()))
continue;
Assert.True(rateResult.BidAsk != null, $"Impossible to get the rate {rateResult.EvaluatedRule}");
}

View File

@ -11,7 +11,9 @@ using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
@ -140,7 +142,7 @@ namespace BTCPayServer.Tests
var sresp = Assert
.IsType<JsonResult>(await tester.PayTester.GetController<UIHomeController>(acc.UserId, acc.StoreId)
.Swagger()).Value.ToJson();
.Swagger(tester.PayTester.GetService<IEnumerable<ISwaggerProvider>>())).Value.ToJson();
JObject swagger = JObject.Parse(sresp);
var schema = JSchema.Parse(File.ReadAllText(TestUtils.GetTestDataFullPath("OpenAPI-Specification-schema.json")));
IList<ValidationError> errors;
@ -189,7 +191,7 @@ namespace BTCPayServer.Tests
var sresp = Assert
.IsType<JsonResult>(await tester.PayTester.GetController<UIHomeController>(acc.UserId, acc.StoreId)
.Swagger()).Value.ToJson();
.Swagger(tester.PayTester.GetService<IEnumerable<ISwaggerProvider>>())).Value.ToJson();
JObject json = JObject.Parse(sresp);
@ -925,7 +927,7 @@ namespace BTCPayServer.Tests
private void AssertSearchInvoice(TestAccount acc, bool expected, string invoiceId, string filter, string storeId = null)
{
var result =
(InvoicesModel)((ViewResult)acc.GetController<UIInvoiceController>()
(InvoicesModel)((ViewResult)acc.GetController<UIInvoiceController>(storeId is not null)
.ListInvoices(new InvoicesModel { SearchTerm = filter, StoreId = storeId }).Result).Model;
Assert.Equal(expected, result.Invoices.Any(i => i.InvoiceId == invoiceId));
}

View File

@ -1,4 +1,4 @@
version: "3"
version: "3"
# Run `docker-compose up dev` for bootstrapping your development environment
# Doing so will expose NBXplorer, Bitcoind RPC and postgres port to the host so that tests can Run,
@ -35,7 +35,7 @@ services:
links:
- dev
- selenium
extra_hosts:
extra_hosts:
- "tests:127.0.0.1"
volumes:
- "sshd_datadir:/root/.ssh"
@ -43,7 +43,7 @@ services:
- "merchant_lightningd_datadir:/etc/merchant_lightningd_datadir"
# The dev container is not actually used, it is just handy to run `docker-compose up dev` to start all services
dev:
dev:
image: alpine:3.7
command: [ "/bin/sh", "-c", "trap : TERM INT; while :; do echo Ready to code and debug like a rockstar!!!; sleep 2073600; done & wait" ]
links:
@ -69,7 +69,7 @@ services:
volumes:
- "sshd_datadir:/root/.ssh"
devlnd:
devlnd:
image: btcpayserver/bitcoin:22.0
environment:
BITCOIN_NETWORK: regtest
@ -89,11 +89,11 @@ services:
expose:
- "4444"
nbxplorer:
image: nicolasdorier/nbxplorer:2.2.18
image: nicolasdorier/nbxplorer:2.3.14
restart: unless-stopped
ports:
- "32838:32838"
expose:
expose:
- "32838"
environment:
NBXPLORER_NETWORK: regtest
@ -114,6 +114,7 @@ services:
NBXPLORER_MINGAPSIZE: 5
NBXPLORER_MAXGAPSIZE: 10
NBXPLORER_VERBOSE: 1
NBXPLORER_POSTGRES: User ID=postgres;Include Error Detail=true;Host=postgres;Port=5432;Database=nbxplorer
NBXPLORER_NOAUTH: 1
NBXPLORER_EXPOSERPC: 1
links:
@ -140,7 +141,7 @@ services:
zmqpubrawtx=tcp://0.0.0.0:28333
deprecatedrpc=signrawtransaction
fallbackfee=0.0002
ports:
ports:
- "43782:43782"
- "39388:39388"
expose:
@ -155,7 +156,7 @@ services:
image: btcpayserver/lightning:v0.10.1-1-dev
stop_signal: SIGKILL
restart: unless-stopped
environment:
environment:
EXPOSE_TCP: "true"
LIGHTNINGD_CHAIN: "btc"
LIGHTNINGD_NETWORK: "regtest"
@ -203,7 +204,7 @@ services:
merchant_lightningd:
image: btcpayserver/lightning:v0.10.1-1-dev
stop_signal: SIGKILL
environment:
environment:
EXPOSE_TCP: "true"
LIGHTNINGD_CHAIN: "btc"
LIGHTNINGD_NETWORK: "regtest"
@ -235,7 +236,7 @@ services:
- "5432"
merchant_lnd:
image: btcpayserver/lnd:v0.14.1-beta
image: btcpayserver/lnd:v0.14.2-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -269,7 +270,7 @@ services:
- bitcoind
customer_lnd:
image: btcpayserver/lnd:v0.14.1-beta
image: btcpayserver/lnd:v0.14.2-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -336,24 +337,26 @@ services:
volumes:
- "./monero_wallet:/wallet"
depends_on:
- monerod
- monerod
litecoind:
restart: unless-stopped
image: nicolasdorier/docker-litecoin:0.16.3
image: btcpayserver/litecoin:0.18.1
environment:
BITCOIN_NETWORK: regtest
BITCOIN_EXTRA_ARGS: |-
rpcuser=ceiwHEbqWI83
rpcpassword=DwubwWsoo3
regtest=1
rpcport=43782
rpcbind=0.0.0.0:43782
port=39388
whitelist=0.0.0.0/0
ports:
ports:
- "43783:43782"
expose:
- "43782" # RPC
- "39388" # P2P
elementsd-liquid:
restart: always
container_name: btcpayserver_elementsd_liquid
@ -385,6 +388,7 @@ services:
- "19444:19444"
volumes:
- "elementsd_liquid_datadir:/data"
volumes:
sshd_datadir:
bitcoin_datadir:

View File

@ -1,4 +1,4 @@
version: "3"
version: "3"
# Run `docker-compose up dev` for bootstrapping your development environment
# Doing so will expose NBXplorer, Bitcoind RPC and postgres port to the host so that tests can Run,
@ -33,7 +33,7 @@ services:
links:
- dev
- selenium
extra_hosts:
extra_hosts:
- "tests:127.0.0.1"
volumes:
- "sshd_datadir:/root/.ssh"
@ -41,7 +41,7 @@ services:
- "merchant_lightningd_datadir:/etc/merchant_lightningd_datadir"
# The dev container is not actually used, it is just handy to run `docker-compose up dev` to start all services
dev:
dev:
image: alpine:3.7
command: [ "/bin/sh", "-c", "trap : TERM INT; while :; do echo Ready to code and debug like a rockstar!!!; sleep 2073600; done & wait" ]
links:
@ -66,7 +66,7 @@ services:
volumes:
- "sshd_datadir:/root/.ssh"
devlnd:
devlnd:
image: btcpayserver/bitcoin:22.0
environment:
BITCOIN_NETWORK: regtest
@ -86,11 +86,11 @@ services:
expose:
- "4444"
nbxplorer:
image: nicolasdorier/nbxplorer:2.2.18
image: nicolasdorier/nbxplorer:2.3.14
restart: unless-stopped
ports:
- "32838:32838"
expose:
expose:
- "32838"
environment:
NBXPLORER_NETWORK: regtest
@ -103,6 +103,7 @@ services:
NBXPLORER_MINGAPSIZE: 5
NBXPLORER_MAXGAPSIZE: 10
NBXPLORER_VERBOSE: 1
NBXPLORER_POSTGRES: User ID=postgres;Include Error Detail=true;Host=postgres;Port=5432;Database=nbxplorer
NBXPLORER_EXPOSERPC: 1
NBXPLORER_NOAUTH: 1
links:
@ -127,7 +128,7 @@ services:
zmqpubrawtx=tcp://0.0.0.0:28333
deprecatedrpc=signrawtransaction
fallbackfee=0.0002
ports:
ports:
- "43782:43782"
- "39388:39388"
expose:
@ -142,7 +143,7 @@ services:
image: btcpayserver/lightning:v0.10.1-1-dev
stop_signal: SIGKILL
restart: unless-stopped
environment:
environment:
EXPOSE_TCP: "true"
LIGHTNINGD_CHAIN: "btc"
LIGHTNINGD_NETWORK: "regtest"
@ -190,7 +191,7 @@ services:
merchant_lightningd:
image: btcpayserver/lightning:v0.10.1-1-dev
stop_signal: SIGKILL
environment:
environment:
EXPOSE_TCP: "true"
LIGHTNINGD_CHAIN: "btc"
LIGHTNINGD_NETWORK: "regtest"
@ -223,7 +224,7 @@ services:
- "5432"
merchant_lnd:
image: btcpayserver/lnd:v0.14.1-beta
image: btcpayserver/lnd:v0.14.2-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -249,7 +250,9 @@ services:
ports:
- "35531:8080"
expose:
- "8080"
- "9735"
- "10009"
volumes:
- "merchant_lnd_datadir:/data"
- "bitcoin_datadir:/deps/.bitcoin"
@ -257,7 +260,7 @@ services:
- bitcoind
customer_lnd:
image: btcpayserver/lnd:v0.14.1-beta
image: btcpayserver/lnd:v0.14.2-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -274,7 +277,7 @@ services:
bitcoind.rpcpass=DwubwWsoo3
bitcoind.zmqpubrawblock=tcp://bitcoind:28332
bitcoind.zmqpubrawtx=tcp://bitcoind:28333
externalip=customer_lnd:10009
externalip=customer_lnd:9735
bitcoin.defaultchanconfs=1
no-macaroons=1
debuglevel=debug
@ -284,6 +287,7 @@ services:
- "35532:8080"
expose:
- "8080"
- "9735"
- "10009"
volumes:
- "customer_lnd_datadir:/root/.lnd"

View File

@ -0,0 +1,4 @@
#!/bin/bash
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 "$@"

View File

@ -3,67 +3,102 @@
# Commands
BCMD=./docker-bitcoin-cli.sh
GCMD=./docker-bitcoin-generate.sh
CCMD=./docker-customer-lightning-cli.sh
MCMD=./docker-merchant-lightning-cli.sh
C_LN=./docker-customer-lncli.sh
M_LN=./docker-merchant-lncli.sh
C_CL=./docker-customer-lightning-cli.sh
M_CL=./docker-merchant-lightning-cli.sh
function channel_count () {
local cmd=$1; local id=$2;
local count=$($cmd listchannels | jq -r ".channels | map(select(.destination == \"$id\")) | length | tonumber") 2>/dev/null
local cmd="$1"; local id="$2";
if [[ $cmd =~ "lightning-cli" ]]; then
local count=$($cmd listchannels | jq -r ".channels | map(select(.destination == \"$id\" and .active == true)) | length | tonumber") 2>/dev/null
elif [[ $cmd =~ "lncli" ]]; then
local count=$($cmd listchannels | jq -r ".channels | map(select(.remote_pubkey == \"$id\" and .active == true)) | length | tonumber") 2>/dev/null
fi
return $count
}
function connect () {
local cmd="$1"; local uri="$2"; local desc="$3";
local connid=`$cmd connect $uri` 2>/dev/null
if [[ $connid =~ "already connected" ]]; then
printf "%s %s\n\r" "✅" "$desc"
else
printf "%s %s\n\r" $([[ $uri =~ ^$(echo $connid | jq -r '.id')* ]] && echo "✅" || echo "❌") "$desc"
fi
}
function create_channel () {
local cmd=$1; local id=$2;
local btcaddr=$($cmd newaddr | jq -r '.address')
$BCMD sendtoaddress $btcaddr 0.15 >/dev/null
$GCMD 10 >/dev/null
local fundres=$($cmd fundchannel $id 14500000 5000 | jq -r '.channel_id')
$GCMD 20 >/dev/null
sleep 2
channel_count $cmd $id
local cmd="$1"; local id="$2"; local desc="$3"; local opts="$4";
channel_count "$cmd" "$id"
local count=$?
return $count
if [[ $count -eq 0 ]]; then
# fund onchain wallet
if [[ $cmd =~ "lightning-cli" ]]; then
local btcaddr=$($cmd newaddr | jq -r '.bech32')
elif [[ $cmd =~ "lncli" ]]; then
local btcaddr=$($cmd newaddress p2wkh | jq -r '.address')
fi
$BCMD sendtoaddress $btcaddr 0.615 >/dev/null
$GCMD 10 >/dev/null
# open channel
if [[ $cmd =~ "lightning-cli" ]]; then
$cmd -k fundchannel id=$id amount=5000000 push_msat=2450000 $opts >/dev/null
elif [[ $cmd =~ "lncli" ]]; then
$cmd openchannel $id 5000000 2450000 $opts >/dev/null
fi
$GCMD 20 >/dev/null
sleep 1
channel_count "$cmd" "$id"
local count=$?
fi
printf "%s %s\n\r" $([[ $count -gt 0 ]] && echo "✅" || echo "❌") "$desc"
}
# General information
cinfo=$($CCMD getinfo | jq '.' 2>/dev/null)
minfo=$($MCMD getinfo | jq '.' 2>/dev/null)
cid=$(echo $cinfo | jq -r '.id')
mid=$(echo $minfo | jq -r '.id')
caddr=$(echo $cinfo | jq -r '.address[] | "\(.address):\(.port)"')
maddr=$(echo $minfo | jq -r '.address[] | "\(.address):\(.port)"')
# Nodes
c_cl_info=$($C_CL getinfo | jq '.' 2>/dev/null)
m_cl_info=$($M_CL getinfo | jq '.' 2>/dev/null)
c_cl_id=$(echo $c_cl_info | jq -r '.id')
m_cl_id=$(echo $m_cl_info | jq -r '.id')
c_cl_addr=$(echo $c_cl_info | jq -r '.address[] | "\(.address):\(.port)"')
m_cl_addr=$(echo $m_cl_info | jq -r '.address[] | "\(.address):\(.port)"')
c_cl_uri=$(echo "$c_cl_id@$c_cl_addr")
m_cl_uri=$(echo "$m_cl_id@$m_cl_addr")
printf "Customer ID: %s@%s\n\r" $cid $caddr
printf "Merchant ID: %s@%s\n\r" $mid $maddr
c_ln_info=$($C_LN getinfo | jq '.' 2>/dev/null)
m_ln_info=$($M_LN getinfo | jq '.' 2>/dev/null)
c_ln_id=$(echo $c_ln_info | jq -r '.identity_pubkey' 2>/dev/null)
m_ln_id=$(echo $m_ln_info | jq -r '.identity_pubkey' 2>/dev/null)
c_ln_uri=$(echo $c_ln_info | jq -r '.uris[]' 2>/dev/null)
m_ln_uri=$(echo $m_ln_info | jq -r '.uris[]' 2>/dev/null)
printf "\n\rNodes\n\r-----\n\r"
printf "Merchant c-lightning: %s\n\r" $m_cl_uri
printf "Merchant LND: %s\n\r" $m_ln_uri
printf "Customer c-lightning: %s\n\r" $c_cl_uri
printf "Customer LND: %s\n\r" $c_ln_uri
# Connections
printf "\n\rConnecting both parties\n\r"
printf "\n\rConnecting all parties\n\r----------------------\n\r"
cconnid=$($CCMD connect "$mid@$maddr" | jq -r '.id' 2>/dev/null)
mconnid=$($MCMD connect "$cid@$caddr" | jq -r '.id' 2>/dev/null)
printf "Customer to merchant %s\n\r" $([[ $cconnid == $mid ]] && echo "succeeded" || echo "failed")
printf "Merchant to customer %s\n\r" $([[ $mconnid == $cid ]] && echo "succeeded" || echo "failed")
connect $M_CL $c_cl_uri "Merchant (c-lightning) to Customer (c-lightning)"
connect $M_CL $c_ln_uri "Merchant (c-lightning) to Customer (LND)"
connect $M_CL $m_ln_uri "Merchant (c-lightning) to Merchant (LND)"
connect $C_CL $m_cl_uri "Customer (c-lightning) to Merchant (c-lightning)"
connect $C_CL $m_ln_uri "Customer (c-lightning) to Merchant (LND)"
connect $C_CL $c_ln_uri "Customer (c-lightning) to Customer (LND)"
connect $M_LN $c_cl_uri "Merchant (LND) to Customer (c-lightning)"
connect $M_LN $c_cl_uri "Merchant (LND) to Customer (c-lightning)"
connect $M_LN $c_ln_uri "Merchant (LND) to Customer (LND)"
connect $C_LN $m_cl_uri "Customer (LND) to Merchant (c-lightning)"
connect $C_LN $c_cl_uri "Customer (LND) to Customer (c-lightning)"
connect $C_LN $m_ln_uri "Customer (LND) to Merchant (LND)"
# Channels
printf "\n\rChecking channels\n\r"
channel_count $CCMD $mid
cchanscount=$?
channel_count $MCMD $cid
mchanscount=$?
printf "\n\rEstablishing channels\n\r----------------------\n\r"
printf "Customer channel count to merchant: %d\n\r" $cchanscount
printf "Merchant channel count to customer: %d\n\r" $mchanscount
# Open channels if there are none, details: https://github.com/ElementsProject/lightning#opening-a-channel
if [[ $cchanscount -eq 0 ]]; then
create_channel $CCMD $mid
cchanres=$?
printf "Establishing channel from customer to merchant %s\n\r" $([[ $cchanres -gt 0 ]] && echo "succeeded" || echo "failed")
fi
if [[ $mchanscount -eq 0 ]]; then
create_channel $MCMD $cid
mchanres=$?
printf "Establishing channel from merchant to customer %s\n\r" $([[ $mchanres -gt 0 ]] && echo "succeeded" || echo "failed")
fi
create_channel $C_CL $m_cl_id "Customer (c-lightning) to Merchant (c-lightning)"
create_channel $C_CL $m_ln_id "Customer (c-lightning) to Merchant (LND)"
create_channel $C_LN $c_cl_id "Customer (LND) to Customer (c-lightning)"
create_channel $M_CL $m_ln_id "Merchant (c-lightning) to Merchant (LND)" "announce=false"
create_channel $C_LN $m_ln_id "Customer (LND) to Merchant (LND)" --private

View File

@ -1,12 +1,15 @@
#!/bin/bash
set -e
./docker-customer-lncli.sh closeallchannels > /dev/null
./docker-merchant-lncli.sh closeallchannels > /dev/null
./docker-bitcoin-generate.sh 10 > /dev/null
channels=$(./docker-merchant-lightning-cli.sh listchannels | jq -cr '.channels | map(.short_channel_id) | unique')
printf "Channels: %s\n\r" $channels
for chanid in $(echo "${channels}" | jq -cr '.[]')
do
printf "Closing channel ID: %s\n\r" $chanid
./docker-merchant-lightning-cli.sh close $chanid
./docker-bitcoin-generate.sh 20 > /dev/null
./docker-merchant-lightning-cli.sh close $chanid > /dev/null
done
./docker-bitcoin-generate.sh 10 > /dev/null
printf "All channels closed!\r\n"

View File

@ -0,0 +1,4 @@
#!/bin/bash
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 "$@"

View File

@ -40,17 +40,20 @@
<ItemGroup Condition="'$(Altcoins)' != 'true'">
<Content Remove="Services\Altcoins\**\*" />
<Content Remove="Views\UIMoneroLikeStore\**\*" />
<Content Remove="Views\UIZcashLikeStore\**\*" />
<Content Remove="Views\Shared\Monero\**\*" />
<Content Remove="Views\Shared\Zcash\**\*" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BIP78.Sender" Version="0.2.2" />
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.2" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.2.19" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.3.4" />
<PackageReference Include="BuildBundlerMinifier" Version="3.2.449" />
<PackageReference Include="BundlerMinifier.Core" Version="3.2.435" />
<PackageReference Include="BundlerMinifier.TagHelpers" Version="3.2.435" />
<PackageReference Include="CsvHelper" Version="15.0.5" />
<PackageReference Include="Dapper" Version="2.0.123" />
<PackageReference Include="Fido2" Version="2.0.1" />
<PackageReference Include="Fido2.AspNet" Version="2.0.1" />
<PackageReference Include="HtmlSanitizer" Version="5.0.372" />
@ -229,10 +232,5 @@
</Content>
</ItemGroup>
<ItemGroup>
<_ContentIncludedByDefault Remove="Views\Authorization\Authorize.cshtml" />
<_ContentIncludedByDefault Remove="Views\Components\NotificationsDropdown\Default.cshtml" />
</ItemGroup>
<ProjectExtensions><VisualStudio><UserProperties wwwroot_4swagger_4v1_4swagger_1template_1invoices_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1misc_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1pull-payments_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1serverinfo_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1stores_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1users_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1webhooks_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" /></VisualStudio></ProjectExtensions>
</Project>

View File

@ -0,0 +1,34 @@
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Components.AppSales;
public class AppSales : ViewComponent
{
private readonly AppService _appService;
private readonly StoreRepository _storeRepo;
public AppSales(AppService appService, StoreRepository storeRepo)
{
_appService = appService;
_storeRepo = storeRepo;
}
public async Task<IViewComponentResult> InvokeAsync(AppData app)
{
var stats = await _appService.GetSalesStats(app);
var vm = new AppSalesViewModel
{
App = app,
SalesCount = stats.SalesCount,
Series = stats.Series
};
return View(vm);
}
}

View File

@ -0,0 +1,13 @@
using System.Collections;
using System.Collections.Generic;
using BTCPayServer.Data;
using BTCPayServer.Services.Apps;
namespace BTCPayServer.Components.AppSales;
public class AppSalesViewModel
{
public AppData App { get; set; }
public int SalesCount { get; set; }
public IEnumerable<SalesStatsItem> Series { get; set; }
}

View File

@ -0,0 +1,30 @@
@model BTCPayServer.Components.AppSales.AppSalesViewModel
@{
var action = $"Update{Model.App.AppType}";
}
<div id="AppSales-@Model.App.Id" class="widget app-sales">
<header class="mb-3">
<h3>@Model.App.Name Contributions</h3>
<a asp-controller="UIApps" asp-action="@action" asp-route-appId="@Model.App.Id">Manage</a>
</header>
<p>@Model.SalesCount Total Contributions</p>
<div class="ct-chart ct-major-octave"></div>
<script>
(function () {
const id = 'AppSales-@Model.App.Id';
const labels = @Safe.Json(Model.Series.Select(i => i.Label));
const series = @Safe.Json(Model.Series.Select(i => i.SalesCount));
const min = Math.min(...series);
const max = Math.max(...series);
const low = min === max ? 0 : Math.max(min - ((max - min) / 5), 0);
new Chartist.Bar(`#${id} .ct-chart`, {
labels,
series: [series]
}, {
low,
});
})();
</script>
</div>

View File

@ -0,0 +1,32 @@
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Components.AppTopItems;
public class AppTopItems : ViewComponent
{
private readonly AppService _appService;
private readonly StoreRepository _storeRepo;
public AppTopItems(AppService appService, StoreRepository storeRepo)
{
_appService = appService;
_storeRepo = storeRepo;
}
public async Task<IViewComponentResult> InvokeAsync(AppData app)
{
var entries = await _appService.GetPerkStats(app);
var vm = new AppTopItemsViewModel
{
App = app,
Entries = entries
};
return View(vm);
}
}

View File

@ -0,0 +1,11 @@
using System.Collections.Generic;
using BTCPayServer.Data;
using BTCPayServer.Services.Apps;
namespace BTCPayServer.Components.AppTopItems;
public class AppTopItemsViewModel
{
public AppData App { get; set; }
public IEnumerable<ItemStats> Entries { get; set; }
}

View File

@ -0,0 +1,33 @@
@model BTCPayServer.Components.AppTopItems.AppTopItemsViewModel
@{
var action = $"Update{Model.App.AppType}";
}
<div class="widget app-top-items">
<header class="mb-3">
<h3>Top Perks</h3>
<a asp-controller="UIApps" asp-action="@action" asp-route-appId="@Model.App.Id">View All</a>
</header>
@if (Model.Entries.Any())
{
<div class="app-items">
@foreach (var entry in Model.Entries)
{
<div class="app-item">
<span class="app-item-name">@entry.Title</span>
<span class="app-item-value">
@entry.SalesCount sale@(entry.SalesCount == 1 ? "" : "s"),
@entry.TotalFormatted total
</span>
</div>
}
</div>
}
else
{
<p class="text-secondary mt-3">
No contributions have been made yet.
</p>
}
</div>

View File

@ -60,14 +60,14 @@
{
<a asp-area="" asp-controller="UIWallets" asp-action="WalletTransactions" asp-route-walletId="@scheme.WalletId" class="nav-link js-scroll-trigger @ViewData.IsActiveCategory(typeof(WalletsNavPages), scheme.WalletId.ToString()) @ViewData.IsActivePage(StoreNavPages.OnchainSettings)" id="@($"StoreNav-Wallet{scheme.Crypto}")">
<span class="me-2 btcpay-status btcpay-status--@(scheme.Enabled ? "enabled" : "pending")"></span>
<span>@(Model.AltcoinsBuild ? $"{scheme.Crypto} " : "")Wallet</span>
<span>@(Model.AltcoinsBuild ? $"{scheme.Crypto} Wallet" : "Bitcoin")</span>
</a>
}
else
{
<a asp-area="" asp-controller="UIStores" asp-action="SetupWallet" asp-route-cryptoCode="@scheme.Crypto" asp-route-storeId="@Model.Store.Id" class="nav-link @ViewData.IsActivePage(StoreNavPages.OnchainSettings)" id="@($"StoreNav-Wallet{scheme.Crypto}")">
<span class="me-2 btcpay-status btcpay-status--@(scheme.Enabled ? "enabled" : "pending")"></span>
<span>@(Model.AltcoinsBuild ? $"{scheme.Crypto} " : "")Wallet</span>
<span>@(Model.AltcoinsBuild ? $"{scheme.Crypto} Wallet" : "Bitcoin")</span>
</a>
}
</li>
@ -187,9 +187,9 @@
<vc:ui-extension-point location="store-integrations-nav" model="@Model"/>
}
<li class="nav-item" permission="@Policies.CanModifyServerSettings">
<a asp-area="" asp-controller="UIServer" asp-action="ListPlugins" class="nav-link js-scroll-trigger @ViewData.IsActivePage(ServerNavPages.Plugins)" id="Nav-AddPlugin">
<a asp-area="" asp-controller="UIServer" asp-action="ListPlugins" class="nav-link js-scroll-trigger @ViewData.IsActivePage(ServerNavPages.Plugins)" id="Nav-ManagePlugins">
<vc:icon symbol="new"/>
<span>Add Plugin</span>
<span>Manage Plugins</span>
</a>
</li>
</ul>

View File

@ -19,7 +19,6 @@ namespace BTCPayServer.Components.MainNav
{
public class MainNav : ViewComponent
{
private const string RootName = "Global";
private readonly AppService _appService;
private readonly StoreRepository _storeRepo;
private readonly UIStoresController _storesController;

View File

@ -6,7 +6,7 @@
@if (Model.Total > pageSizeOptions.Min())
{
<nav aria-label="..." class="w-100">
<nav aria-label="..." class="w-100 clearfix">
@if (Model.Total > Model.Count)
{
<ul class="pagination float-start">

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