Compare commits

...

281 Commits

Author SHA1 Message Date
d6e3fb46ee bump 2021-10-29 23:41:02 +09:00
efc05edca3 Update Changelog.md (#2993)
* Update Changelog.md

* Apply suggestions from code review

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

* Apply suggestions from code review

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

* Update

* Update

* Update Changelog.md

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

* Update

* Update

* Update

Co-authored-by: d11n <mail@dennisreimann.de>
Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2021-10-29 23:25:44 +09:00
aa3d384f47 Add status message partial (#3030)
Along with #3029, this closes #2991.
2021-10-29 23:13:04 +09:00
a5aa5cf059 Fix tests 2021-10-29 23:09:19 +09:00
ccd8859d7f Add missing attribute for textSearch in get invoice 2021-10-29 22:27:21 +09:00
a28399e31d Add warning on ln address if install using rootpath 2021-10-29 22:18:33 +09:00
61f63a9996 POS Light: Add Bootstrap bundle (#3029)
So that the alert can be closed. Brought up by @satwo in this [Mattermost discussion](https://chat.btcpayserver.org/btcpayserver/pl/3f46i9yociydmkk4apncxp7shw).
2021-10-29 21:57:37 +09:00
de93c5c9d6 Fix test warning 2021-10-29 21:51:53 +09:00
94865815c7 Quick fixes before release 2021-10-29 14:50:18 +02:00
33754933d5 Add Azerbaijan language 2021-10-29 20:54:09 +09:00
560b6db480 fix refund button part 2 2021-10-29 13:46:24 +02:00
4c71167535 Fix refund button 2021-10-29 13:20:54 +02:00
707484709a Display and update App Name in settings (#3027)
* Edit and view app name in app settings

Currently the "name" property is not exposed at all in an app's settings/update page, which can result in confusion about which app is being updated, and also a general confusion between the `Title` property and the `Name` property.
This PR gives visibility to the app name in settings, and allows updating of the same.
I also changed the display label for `title` and `name` to make them more distinct and specific.

* Fix tests

* Update AltcoinTests.cs

* Update SeleniumTests.cs

* fix tests

Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com>
2021-10-29 19:29:02 +09:00
274be7c1bc Disable build warning in layout (#3028) 2021-10-29 19:06:15 +09:00
3f176a6b6b fix lnurl setter bug 2021-10-29 11:27:36 +02:00
fc8a5ff95f Lightning address support (#2804)
Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2021-10-29 11:01:16 +02:00
25f84d000b LNURL POS Support (#3019) 2021-10-29 10:27:33 +02:00
31b7826dce make input readonly for fixed cart items 2021-10-29 09:58:59 +02:00
2d4aa52fa5 Restructure store and payment settings (#2995)
Co-authored-by: Kukks <evilkukka@gmail.com>
2021-10-29 08:25:43 +02:00
eee8008bb2 Make sure migration for apps does not crash 2021-10-28 12:23:21 +02:00
a5ae509f9f Make sure invoice show lnurl instead of bolt11 labels 2021-10-28 10:39:49 +02:00
8f117b5079 Add ability to require refund email from app level (#3013)
* Add ability to require refund email from app level

* Add ability request refund email when creating invoice manually

* Adjust labels

* Add UI tests

* Add Greenfield API support

* Rename RequiresRefundEmailType to RequiresRefundEmail

* Fix build

Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com>
2021-10-27 23:32:56 +09:00
0b5d0349d4 Do not fire InvoiceExpired twice if invoice partially paid (Fix #3004) 2021-10-27 19:27:19 +09:00
8a0660cbd6 Add support for CryptoMarket exchange rates (#3012)
* Add support for CryptoMarket exchange rates

* Add unit test for CryptoMarket
2021-10-27 15:19:34 +09:00
f7a0b91ec1 Add boolean overPaid to the invoice settled webhook 2021-10-27 14:51:42 +09:00
1ecd1c1e54 LNURL: Add missing logic from lnurl merges and rebases 2021-10-26 14:08:09 +02:00
fccbbb6fb7 LNURL tests and small fixes 2021-10-26 13:55:59 +02:00
d1886b039e Wallet: Make account number an input instead of select (#3018) 2021-10-26 18:00:01 +09:00
79c61f01c8 Make supporter logos work across browsers (#3017) 2021-10-26 11:15:01 +09:00
e50c9266b4 [Fix] If the local culture of the server was not english, numeric values greenfield were not properly interpreted 2021-10-26 00:46:28 +09:00
fd27bd94e2 Add ability to accept tips in POS terminal (#2983)
* Add ability to accept tips in POS terminal

* Add logic for showing and hiding sections specific to a POS app type

* Fix issue with floating point error
2021-10-25 19:06:32 +09:00
05f99f3855 Merge pull request #3007 from bolatovumar/fix/typo-inlude
Fix typos
2021-10-25 18:46:23 +09:00
31ef763c05 Merge pull request #3011 from bolatovumar/add-number-formatting-crowdfund-app
Add number formatting in crowdfund app
2021-10-25 18:45:45 +09:00
c0abcbea24 Merge pull request #3015 from NicolasDorier/ewfoufeoiu
Fixes of bugs happening when using rootpath
2021-10-25 16:55:00 +09:00
a73383cd87 When creating a new apps, the default currency of the store should be used 2021-10-25 16:54:36 +09:00
951bfeefb1 LNURL Payment Method Support (#2897)
* LNURL Payment Method Support

* Merge recent Lightning controller related changes

* Fix build

* Create separate payment settings section for stores

* Improve LNURL configuration

* Prevent duplicate array entries when merging Swagger JSON

* Fix CanSetPaymentMethodLimitsLightning

* Fix CanUsePayjoinViaUI

* Adapt test for new cancel bolt invoice feature

* rebase fixes

* Fixes after rebase

* Test fixes

* Do not turn LNURL on by default, Off-Chain payment criteria should affects both BOLT11 and LNURL, Payment criteria of unset payment method shouldn't be shown

* Send better error if payment method not found

* Revert "Prevent duplicate array entries when merging Swagger JSON"

This reverts commit 5783db9eda17c29908a60fdef2c3ebe130a8b059.

* Fix LNUrl doc

* Fix some warnings

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2021-10-25 15:18:02 +09:00
fc7125b8cd Fix: Fonts and Home background not loading properly when using rootpath 2021-10-25 15:14:05 +09:00
26bcdbc766 Fix: Many SVG assets were not showing properly if rootpath is used 2021-10-25 15:14:05 +09:00
fb1fcbe0b9 Fix: The redirect url of crowdfund invoices wasn't set correctly if rootpath is used (Fix #2992) 2021-10-25 15:13:52 +09:00
2c45f803e4 Fix: favicon wasn't shown if using rootpath 2021-10-25 15:13:52 +09:00
fbdd2fc470 Fix flaky test for ripio exchange 2021-10-25 15:10:44 +09:00
0558631982 Improve store/selectlist labeling (#3014) 2021-10-25 13:15:08 +09:00
63e1c5807e Add number formatting in crowdfund app 2021-10-24 14:33:47 -07:00
a3cc1f2ef0 Fix typos 2021-10-23 23:14:36 -07:00
5318684e5c fix build 2021-10-23 22:10:54 +09:00
b3b9651cd8 Use buster rather than bulleyes for arm32 2021-10-23 22:09:13 +09:00
86e528e5df Use NBitcoin's Network.UriScheme rather than our own (#3005) 2021-10-23 14:47:15 +09:00
c46a69e1bd Add Paging to Pull Payments (#2997) 2021-10-22 15:10:59 +09:00
9b0d1a23dc Decouple Pull payment from wallets (#2987)
* Decouple Pull payments from wallet

* Update _Nav.cshtml

* Fixes
2021-10-22 11:17:40 +09:00
db038723f4 Payout Destination Handling (#2985)
* Payout Destination Handling

fixes #2765
This PR:
* reactivates the BIP21 support for payouts.
* allows LNUrl destinations to be reusable.
* allows addresses to be reused in claims as long as the other claims are in a final state

* Ensure bolt amount matches the payout amount

* fixes

* reduce duplicate parsing of bolt

* make hash the id of bolt

* better bolt11 tostring

* use cached payment request from lnurl
2021-10-22 00:43:02 +09:00
a193e1cbf3 Scanner: Improve QR data display 2021-10-21 17:05:44 +02:00
493f1b98c2 Upgrade vue-qr-code-reader and fix scanning issue 2021-10-21 17:05:44 +02:00
926b60df3d Fix camera scanner CSP issue 2021-10-21 17:05:44 +02:00
d8a162fb6e [Fix] CurrencyValue parsing shouldn't depend on locale 2021-10-21 19:15:02 +09:00
4cf3249e0b Add ability to set default currency for a store (#2998) 2021-10-20 23:17:40 +09:00
407f26b1dc fix netehreum warnings 2021-10-20 13:08:50 +02:00
0159588eed Merge pull request #3001 from dennisreimann/supporters
Supporters: Fix SVGs that are referenced in READMEs
2021-10-20 12:27:57 +02:00
6f7be7eb09 Supporters: Fix SVGs that are referenced in READMEs 2021-10-20 12:14:30 +02:00
09a53718bb Abort payjoin request after 30 sec of timeout 2021-10-20 17:06:27 +09:00
5655f22397 Fix default payment bug (#2975)
* Fix default payment bug

This attempts to fix the default payment bug described in #2963.

Update to complete #2986

This takes into account #2986 and @NicolasDorier 's suggestion to add a default payment type as an empty (valueless) option that is selected by default.

* Fix tests

Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2021-10-20 14:34:04 +09:00
791d0abb34 Clarify payment method criteria error message (#2989)
As requested by @BTCBellyButton, this clarifies the error message shown resulting from an invalid payment method criteria.
https://github.com/btcpayserver/btcpayserver/pull/2975#issuecomment-945218280
2021-10-20 14:02:20 +09:00
13d9930955 Improve supporters display (#2994) 2021-10-20 14:01:55 +09:00
c4f40d68e9 Fix cryptic error message issue (#2978) 2021-10-18 10:42:53 +02:00
9951370321 Fix logo issue (#2977) 2021-10-18 10:41:34 +02:00
0119ad452b Order language dropdown in CheckoutExperience
Solves the same issue as described in the first part of #2971 and solved in #2972, except in the Settings instead of the checkout UI.
2021-10-18 10:40:17 +02:00
3d3016fdca Fix fallback logic for default payment method (#2986) 2021-10-18 16:56:47 +09:00
262798d577 Refactor by adding extension FindPayoutHandler (#2984) 2021-10-18 15:00:38 +09:00
cf206e64a7 Add Lightning payout support (#2517)
* Add Lightning payout support

* Adjust Greenfield API to allow other payment types for Payouts

* Pull payment view: Improve payment method select

* Pull payments view: Update JS

* Pull payments view: Table improvements

* Pull payment form: Remove duplicate name field

* Cleanup Lightning branch after rebasing

* Update swagger documnetation for Lightning support

* Remove required requirement for amount in pull payments

* Adapt Refund endpoint to support multiple playment methods

* Support LNURL Pay for Pull Payments

* Revert "Remove required requirement for amount in pull payments"

This reverts commit 96cb78939d43b7be61ee2d257800ccd1cce45c4c.

* Support Lightning address payout claims

* Fix lightning claim handling and provide better error messages

* Fix tests

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2021-10-18 12:37:59 +09:00
5ac4135a13 fix crowdfund js 2021-10-15 12:53:45 +02:00
5176eaf4ba fix test 2021-10-15 12:53:45 +02:00
514417e888 Rename to custom and fix small css 2021-10-15 12:53:45 +02:00
f83e85dc36 addd test 2021-10-15 12:53:45 +02:00
9592a77cff use more concrete types for price type in app items 2021-10-15 12:53:45 +02:00
33a893ba31 Replace addLoadEvent with better practice 2021-10-15 12:53:45 +02:00
f89cdadde8 fix btn text 2021-10-15 12:53:45 +02:00
7d2aa28e1f Support Topup Invoices in Apps 2021-10-15 12:53:45 +02:00
9df4429fc2 Remove unreachable code (#2961) 2021-10-15 16:34:40 +09:00
843a2491ef Improve language dropdown UX/Update jquery-prettydropdowns to 4.17.0 (#2972)
* Improve language dropdown UX

Several improvements to the language selector dropdown list.

* Apply suggestions from code review

Incorporate styling suggestion from @dennisreimann

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

Co-authored-by: d11n <mail@dennisreimann.de>
2021-10-15 14:27:52 +09:00
d64fb15ac2 Greenfield: Provide negative undue when overpaid. (#2936)
* Greenfield: Provide negative undue when overpaid.

closes #2935

* Invoice's due can be negative, fix Amount field of invoice

* Update swagger.template.invoices.json

Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com>
2021-10-15 14:23:34 +09:00
3671e7f18c Fix test 2021-10-15 14:19:40 +09:00
899bf98f45 Fix tests 2021-10-15 12:50:33 +09:00
4230ba513f Add support for rpio exchange rate (close #2960) 2021-10-15 12:18:02 +09:00
18f1b4d8c1 Bump NBitcoin (Fix perf problem on signing of big transaction) 2021-10-14 01:01:32 +09:00
75776687bc Fix bug: Importing seed with Is hot wallet checked was not working (#2966) 2021-10-12 18:37:13 +09:00
b5ebd14589 Fix NRE in BaseService.StopAsync 2021-10-12 17:39:48 +09:00
67ba64b0a1 Fix build 2021-10-12 15:45:55 +09:00
b26e8311c1 Bump NBitcoin 2021-10-12 15:42:20 +09:00
0033aab03e Remove types from BTCPayServer.Client 2021-10-11 18:01:32 +09:00
1037fe0b14 Remove unused ExpireInvoiceResponse type 2021-10-11 17:58:01 +09:00
6c688b9684 Make sure cheater scan rpc capabilities 2021-10-11 17:49:04 +09:00
601e17ed0f Fix altcoins docker-compose 2021-10-11 17:39:12 +09:00
ad86c16bc9 Bump Tor 2021-10-11 12:35:26 +09:00
791db983c7 Bumping LND to 0.13.3-beta (#2964) 2021-10-11 12:32:58 +09:00
d7a7382d00 Introduce cheat mode (#2965) 2021-10-11 12:32:09 +09:00
e842a00402 Easier payment testing (#2672)
* Easier payment testing

* WIP, more TODOs and some cleanup. Help is appreciated.

* Added dummy button to expire monitoring (doesn't work yet)

* Added TODO

* Make fake tab default if present

* Split controller and change wording from fake to testing

* Extract and simplify checkout testing UI

* Restrict testing access to regtest

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2021-10-11 11:11:02 +09:00
78a8c8c1be Add Passport hardware wallet option to the wallet import screens (#2962)
* Add option for Passport

* Add Passport option to QR import

* Add Passport import option
2021-10-10 15:54:46 +09:00
a7c093a0eb Allow email notifications when creating invoices from Web UI (#2959)
Currently invoice email notifications are only sent when the invoice is created via the API. This commit adds an option to set an email address for notifications when an invoice is created from the Web UI.
2021-10-10 15:54:25 +09:00
86956c1e7b More CSP fixes (#2955)
* Fix CSP issue with time format switch on wallet transactions page

* Fix CSP issue with invoice modal link on invoices list page

* Fix CSP issue on FIDO2 auth page

* Fix JS error on FIDO2 auth page

* Minor UI code improvements
2021-10-10 15:52:39 +09:00
54539001f1 Allow User to delete own account (#2949)
* Allow User to delete own account

* Add User delete e2e test

* fix test

* Apply suggestions from code review

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

Co-authored-by: d11n <mail@dennisreimann.de>
2021-10-09 12:18:37 +09:00
4321cbf41a Coin selection improvements (#2956)
* Improve coin selection toggling

* Improve coin selection display

Fixes #2948.

* Improve remove destination button

* Display hide unconfirmed only if there are any unconfirmed UTXOs

* Improve label styles

* Test fix

* Add top margin for non-JS coin selection
2021-10-09 10:39:34 +09:00
8d9941bfd2 Update toggle styling to have pointer cursor (#2957) 2021-10-08 14:26:17 +09:00
f9e38deee7 Fix warnings 2021-10-07 19:08:53 +09:00
89fd044b00 Replace pruning wallet by wipe transactions (#2857) 2021-10-07 16:54:22 +09:00
039f88d14c Match Lightning payment based on payment hash if BOLT11 is not the same. (#2773)
* Match Lightning payment based on payment hash if BOLT11 is not the same.

* Fixup

* Fixup

Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com>
2021-10-07 16:53:27 +09:00
d3f9eb38a9 Fix annoying err message in logs when a websocket is cancelled 2021-10-07 15:35:19 +09:00
768d97ac6c Fix remaining Safari inline JS CSP issues (#2954)
* fix modfiy webhook inline JS issue

* fix uncofirmed warning issue in Safari

* fix inline JS Safari issues in checkout
2021-10-07 12:27:48 +09:00
154078d46f Fix docker images 2021-10-07 11:20:50 +09:00
d74c6a30c8 Improve perk card styles 2021-10-06 19:32:26 +02:00
1407d5be8d Decrease font size and weight in selection overlay 2021-10-06 19:32:26 +02:00
721c06e157 Crowdfund: Hide theme switch when custom theme is set 2021-10-06 19:32:26 +02:00
a0265f18d9 Crowdfund: Better customization input grouping 2021-10-06 19:32:26 +02:00
8948475cad Crowdfund: Add theme switch to footer 2021-10-06 19:32:26 +02:00
faed5349fb Make theme switch a view component 2021-10-06 19:32:26 +02:00
7bcaf956e7 Grerenfield: Add availableStatusesForManualMarking to Invoice Data (#2934)
closes ##2933
2021-10-06 18:19:34 +09:00
31c2a80758 Add ability to set invoice status from details page (#2923)
* Add ability to set invoice status from details page

* Remove unnecessary "using" statements

* Add print styles

* Fix Safari issues

* Simplify JS

* Update status badge class names

* Update dropdown toggle padding

* Adjust dropdown menu padding
2021-10-06 14:49:57 +09:00
150e4b842c Make sure the process doesn't crash if exception raised in Subscribe 2021-10-06 13:22:55 +09:00
b970f64639 Remove build warnings 2021-10-06 12:53:41 +09:00
08bd13b2cd Make CanUseWebhooks more resilient 2021-10-06 11:25:21 +09:00
6e3d6125c2 Payment Settled Webhook event (#2944)
* Payment Settled Webhook event

resolves #2691

* Move payment methods to payment services
2021-10-05 18:10:41 +09:00
143d5f69c1 Fix CSP for inline handlers in Safari (#2946)
* Fix CSP for inline handlers on LND seed backup page

* Fix CSP for inline handlers on checkout page

* Fix CSP for inline handlers on wallet sign pages

* Fix CSP for inline handlers on invoices list page

* Fix CSP for inline handlers on payouts page

* Fix CSP for inline handlers on confirm API key page

* Fix CSP for inline handlers on store rates page

* Fix CSP for inline handlers on notifications page

* Fix CSP for inline handlers on dynamic DNS page

* Fix CSP for inline handlers on checkout experience page
2021-10-05 15:52:14 +09:00
156ddd24fa Make CanUsePayjoinForTopUp more resilient 2021-10-05 15:47:57 +09:00
cf2c147f4f Bumping versions of libraries and dockerfile 2021-10-05 15:29:26 +09:00
ce1903b2fa Bumping versions of libraries and dockerfile 2021-10-05 15:26:40 +09:00
ccdfe5ac86 Detect websocket connection dropping in vault 2021-10-05 14:41:03 +09:00
b0ef98dd63 Keep websocket connection alive on notifications and vault 2021-10-05 14:30:01 +09:00
5c8e62bd90 Websocket ping message for notifications and vault 2021-10-05 14:14:22 +09:00
6674d76d6d Add missing file 2021-10-05 13:58:40 +09:00
dea747a9a9 Keep connection alive on checkout page by sending ping messages 2021-10-05 13:37:30 +09:00
a32ace1dcb Shorten balance assignment 2021-10-04 17:10:07 +02:00
4785f0a4dd Remove unused Network property 2021-10-04 17:10:07 +02:00
8249d8a0f5 Fix indentation in view 2021-10-04 17:10:07 +02:00
234bc30369 Update in adherence to the design 2021-10-04 17:10:07 +02:00
b7a081b9a4 Show total balances for each currency on List Wallets Page 2021-10-04 17:10:07 +02:00
88c925017d Apply suggestions from code review 2021-10-04 17:06:11 +02:00
a3c2a9ac61 Censor based on permissions 2021-10-04 17:06:11 +02:00
7cad6302b7 Fixed wrong API permission
Viewing store payment methods needs CanViewStoreSettings instead of CanModifyStoreSettings as per the docs
2021-10-04 17:06:11 +02:00
4b34090376 Adjust invoice details header layout 2021-10-04 17:02:57 +02:00
ac554a27b6 Merge pull request #2947 from dennisreimann/faq-links
Fix FAQ links
2021-10-04 14:03:47 +02:00
e1ea7200cf Fix FAQ links
Once more (and for the last time ;)) those linsk changed in btcpayserver/btcpayserver-doc#967
2021-10-04 13:42:31 +02:00
802fec6bf3 Move payment related properties 2021-10-04 09:50:36 +02:00
67aee9bdc6 Improve display and structure of payment related configuration 2021-10-04 09:50:36 +02:00
975ad2f8ab Crowdfund: Display contributions value (#2938) 2021-10-04 07:54:06 +02:00
b0814166f5 Merge pull request #2941 from git-sgmoore/patch-1
Update README.md
2021-10-02 13:23:54 +02:00
8f397865fd Update README.md
Remove one apostrophe
2021-10-01 23:10:43 -07:00
8ffa7525c5 Merge pull request #2939 from dennisreimann/docs-links
Fix documentation links
2021-10-01 18:34:41 +02:00
6048a26511 Fix documentation links
Some links changed with the recent docs restructuring. I've also added recirects for these on the docs side now.
2021-10-01 18:18:54 +02:00
1a8a7fc27f Crowdfund: Limit max width of embedded content like video
Closes #2782.
2021-09-29 07:58:50 +02:00
64901dfc22 Bump postgres 2021-09-29 13:16:53 +09:00
6a28497d33 Fix: Impossible to see relative time of transaction in wallet list 2021-09-29 13:10:00 +09:00
ed1ec2300d Remove Coinswitch entirely 2021-09-28 10:33:11 +02:00
edfde494fa bump bitcoin core 2021-09-28 12:47:34 +09:00
a3cc573e4d Do not test hot reload in CanCreatePayRequest (unreliable) 2021-09-28 12:20:40 +09:00
44fe70ae44 Make CanCreatePayRequest more resilient 2021-09-28 12:05:46 +09:00
d05a9295b3 Make a test more resilient 2021-09-28 11:22:35 +09:00
7aa4cd8104 Plugins: Pass the current Model to the Ui extension points 2021-09-27 14:16:38 +02:00
4a6088b6b9 Bump NBX Client and Plugins packages 2021-09-27 10:26:42 +02:00
d949680d5a fix: Plugins disabled message never dissappers even after re-enabling it. 2021-09-27 09:03:59 +02:00
0cd7380af0 Make CSP accessible to plugins 2021-09-27 08:45:55 +02:00
a3afcd2a6e Crowdfund public UI re-design (#2918)
* Patch bootstrap-vue modal close button

For Bootstrap v5 compatibility, which bootstrap-vue does not have currently.

* Crowdfund: Use common global layout head (and themes)

* Crowdfund: Display disabled alert also for simple variant

* Crowdfund: Unify non-JS/simple and JS-enabled view

* Improve fireworks animation

* Improve layout

Inspired by the Bitcoin Smiles compaign, see  #2783

* Cleanup and remove views

* Fix typo

* Fix test
2021-09-27 11:46:56 +09:00
bd6c7a8c3d Properly handle InvoiceMetadata string properties (Fix #2906) 2021-09-27 11:44:55 +09:00
920955657d Fix camera not working on wallet send (Fix #2922) 2021-09-27 10:32:03 +09:00
d975ea1509 bump 2021-09-26 14:33:36 +09:00
ba5c49a8df Update Changelog 2021-09-26 14:33:36 +09:00
9f6c7180b2 Greenfield quality of life improvements from feedback (#2880)
* Greenfield quality of life improvements from feedback

fix #2854

* Greenfield quality of life improvements from feedback

fix #2855
2021-09-25 14:04:34 +09:00
ef70f4d547 Simplify code with PaymentMethodId.TryParse 2021-09-24 14:21:30 +09:00
8c061b1f07 Attempt cover scenarios of switching back to Bitcoin only after taint (#2881) 2021-09-24 14:16:25 +09:00
0aed8fdb5b Improve app update buttons (#2915)
Better responsive behaviour and clearer distinction between primary and secondary actions. Also improves the wording ("View Crowdfund/Point of Sale" vs. "View app") and gets rid of the superfluous "Back to apps list" button.
2021-09-24 14:13:38 +09:00
6125a99381 Should not be able to activate a payment method on an invoice which is not new 2021-09-24 00:12:12 +09:00
83b54b7be1 Fix infinite loop happening if payment method unavailable with on invoices with lazy activation (#2914) 2021-09-24 00:00:55 +09:00
047222b3ee Fix offcanvas on desktop 2021-09-23 13:45:05 +02:00
2fe1ab83f5 Improve offcanvas nav animation 2021-09-23 13:45:05 +02:00
5372058c33 Use offcanvas nav for mobile 2021-09-23 13:45:05 +02:00
a08261a798 Update dark mode footer colors 2021-09-23 13:44:48 +02:00
869985f005 Finetune footer; remove obsolete images 2021-09-23 13:44:48 +02:00
97b5c473c1 Removes the "Let's Get In Touch!" block from the homepage 2021-09-23 13:44:48 +02:00
337db3daab Align social links to appear above copyright 2021-09-23 13:44:48 +02:00
10da82607d Adds social links to Layout
This removes the logo credits in the base Layout and adds the
links to Github, Mattermost and Twitter.
2021-09-23 13:44:48 +02:00
7f49824783 Improve public LN node info (#2876)
* Add store name to LN info unavailable pae

* Display multiple node info items if available

Allows to view clearnet and Tor connection info side by side.

* Re-add preferOnion for certain cases

* HTML compatible node ids

* Display more node connection failure details

* Fix syntax error

* Update BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs

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

* View updates

* Revert previous variable change

* Keep logic out of the view

Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com>
2021-09-23 20:36:42 +09:00
5d8bc73063 Reset "all stores" values to default 2021-09-23 10:11:37 +02:00
82675384c4 Don't adjust store mode if user is performing an action 2021-09-23 10:11:37 +02:00
3dc663f4d8 Adjust view 2021-09-23 10:11:37 +02:00
2b0fcdf95c [WIP] Fix issues with Authorization Request page
closes #2858
2021-09-23 10:11:37 +02:00
d0120f1427 Fix Selenium flakyness on Circle CI (#2904) 2021-09-22 21:31:44 +09:00
ac34109da3 Support preview/codegen for custom currencies in pay button (#2896) 2021-09-22 13:10:52 +09:00
d5c96eee32 Add test 2021-09-21 09:45:41 +02:00
7b252369e9 Add GetPOSItems method 2021-09-21 09:45:41 +02:00
9d48358f2a Rename Unavailable to Disabled 2021-09-21 09:45:41 +02:00
2dcd7db797 Filter unavailable items in controller 2021-09-21 09:45:41 +02:00
c267cf0e9c Rename "hidden" to "unavailable" 2021-09-21 09:45:41 +02:00
403820cf14 Add ability to hide a product from POS app
See discussion here: https://github.com/btcpayserver/btcpayserver/discussions/2827
2021-09-21 09:45:41 +02:00
6d667e2d78 bump redoc 2021-09-21 12:52:31 +09:00
258a19fdf5 Make CSP more specific for docs 2021-09-21 12:47:46 +09:00
1a593a1ced Fix documentation page broken by CSP 2021-09-21 12:43:50 +09:00
b8ac57a5b3 Merge pull request #2893 from bolatovumar/fix/show-qr-padding
Remove padding from "Show QR" button
2021-09-20 11:37:48 +02:00
b93f3ff445 Remove padding from "Show QR" button 2021-09-18 21:31:23 -07:00
5984edb7f4 Disallow cancelling payment request when "Allow payee to create invoices in their own denomination" is not enabled (#2843)
* Disallow cancelling payment request when "Allow payee to create invoices in their own denomination" is not enabled

close #2802

* Disallow cancelling pending invoice on BE

* Update non-JS version of payment request view to disallow cancelling

* Update CanCancelPaymentWhenPossible

* Fix typo
2021-09-17 10:24:48 +09:00
603bd2692e Summernote: Fix source map reference; remove unused files 2021-09-16 07:30:33 +02:00
2e61bdf88f Update swagger example 2021-09-16 07:29:18 +02:00
e6aa73197a Move GetDefaultPaymentId out of InvoiceExtensions 2021-09-16 07:29:18 +02:00
b2f9353be1 Add tests 2021-09-16 07:29:18 +02:00
5cba59932b Add ability to specify default payment method through Greenfield API 2021-09-16 07:29:18 +02:00
809340e629 Add ability to select default payment method for invoice through UI 2021-09-16 07:29:18 +02:00
bb6a188883 bump nbx in both docker compose envs 2021-09-16 07:19:54 +02:00
24674d354c removes slack + updates columns 2021-09-16 07:18:31 +02:00
e93562b1db Do not publish NewTransactionEvent if GetValidOutputs did not match with the network's. 2021-09-15 07:28:17 +02:00
f7099cf6aa Bump NBX and elements 2021-09-15 07:28:17 +02:00
d88f012d82 Improve warning when creating invoice without wallet (#2844)
* Improve warning when creating invoice without wallet

close #2834

* Update link class name
2021-09-13 14:24:10 +09:00
f96767d3dc Bootstrap update (#2870)
* Update Bootstrap

* Fix notification checkbox styling
2021-09-13 14:23:10 +09:00
6666786b7a Unify Fido2 authentication under two-factor tab (#2866)
* Unify Fido2 authentication under two-factor tab

Closes #2754.

* Improve UI and wording

* Improve register FIDO2 device page
2021-09-13 10:16:52 +09:00
eccbe8e018 Sanitize UrlRoot in PayButton 2021-09-12 20:33:51 +09:00
aac87539ae Fix pay button CSP issue when using modal (#2872)
* Fix pay button CSP issue when using modal

Fixes #2864.

* Use event handler, refactor csp tags

* Fix script indentation

* Fix onsubmit event handler integration

Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2021-09-12 20:31:35 +09:00
a8995d2bed Fix unit test fail bc shitcoin 2021-09-10 15:35:51 +09:00
7f40698bba Fix interactive XSS when entering javascript: in store's website. 2021-09-10 10:56:48 +09:00
c4f4c3138c Update Changelog 2021-09-10 00:27:27 +09:00
3b9c7db481 The page could crash if the user clicks too many time on Notificate 'Mark as Seen' 2021-09-10 00:22:33 +09:00
649f650da6 Fix build 2021-09-10 00:16:28 +09:00
4c83265de4 Update Changelog 2021-09-10 00:14:45 +09:00
ad7b62fa3d Fix CSP when there is a theme 2021-09-10 00:14:26 +09:00
650df97e50 Fix Summernote XSS possibility (#2859) 2021-09-09 22:49:30 +09:00
6f75125cf5 bump 2021-09-09 22:17:46 +09:00
26c05a8d5f Changelog 2021-09-09 22:17:22 +09:00
fc4e47cec6 Add CSP at the website level (#2863) 2021-09-09 21:51:28 +09:00
c39f1341aa Fix warnings 2021-09-09 20:31:35 +09:00
a39d1e0886 fix broken plugin page 2021-09-09 13:12:44 +02:00
435f51a777 Fix for crowdfund perk editor
With no title entered, the editor got stuck in an endless loop, because it recursively invoked the save function without checking for the ID fallback being present.

Fixes #2862.
2021-09-09 10:59:03 +02:00
de2c12c1df Fix warning 2021-09-08 18:45:12 +09:00
ae43af999b update HWI lib 2021-09-08 18:27:36 +09:00
14a10eeef8 Remove useless code (#2852) 2021-09-07 19:18:56 +09:00
180be49824 Add "#nullable enable" 2021-09-07 10:39:36 +02:00
5faa756f1c Add check for empty theme URI before saving theme settings 2021-09-07 10:39:36 +02:00
06db29dd43 Delete confirmation modals (#2614)
* Refactor confirm view: separate modal

* Add delete confirmation modals for apps and FIDO2

* Add delete confirmation modals for 2FA actions

* Add delete confirmation modals for api keys and webhooks

* Add delete confirmation modals for stores and store users

* Add delete confirmation modals for LND seed and SSH

* Add delete confirmation modals for rate rule scripting

* Test fixes and improvements

* Add delete confirmation modals for dynamic DNS

* Add delete confirmation modals for store access tokens

* Add confirmation modals for pull payment archiving

* Refactor confirm modal code

* Add confirmation input, update wording

* Update modal styles

* Upgrade ChromeDriver

* Simplify and unify confirmation input

* Test fixes

* Fix wording

* Add modals for wallet replace and removal
2021-09-07 11:55:53 +09:00
6d317937c7 Do not generate payment methods when 0 amount invoice (#2776)
* Do not generate payment methods when 0 amount invoice

* Add test for 0 amoutn invoices
2021-09-07 00:23:41 +09:00
522c990278 Allow creating top-up invoice when min/max payment amount criteria is active
closes #2831
2021-09-06 10:44:10 +02:00
e584ebe7de Decouple taproot activation from support in generate wallet (#2837) 2021-09-04 22:07:09 +09:00
1422bd8540 Decrease notification badge padding (#2841)
Fixes #2840.
2021-09-04 22:06:56 +09:00
fe18e71538 Refactor themes (#2794)
* Remove Bootstrap/Creative CSS file customizability

Customizations should be done using themes

* Remove deprecated Casa and Classic themes

They are still available in the design system repo and should be added as custom theme CSS file

* Use either standard or custom theme

* Remove deprecated themes

* Improve theme select UI

* Finish and refactor theme switching

* updates theme copy

* Update BTCPayServer/Views/Server/Theme.cshtml

Co-authored-by: Zaxounette <51208677+Zaxounette@users.noreply.github.com>

* Combine creative.css and site.css

Co-authored-by: dstrukt <gfxdsign@gmail.com>
Co-authored-by: Zaxounette <51208677+Zaxounette@users.noreply.github.com>
2021-09-03 16:16:36 +09:00
e5699f674b Taproot support for wallets (#2830)
* Support taproot for HotWallet

* Support taproot for hardware wallets

* Fix NBX version

* Undo formatting

* Do not show Taproot when not supported

* Create taproot wallet from xpub

* Bug Fix
2021-09-03 15:37:12 +09:00
203db44b4e Patch summernote to fix issues in Chrome/Chromium
As we are using a customized version for the Bootstrap 5 compatibility anyways, with this we are also including the changes proposed in summernote/summernote#4034 to fix #2816.
2021-09-02 15:35:32 +02:00
69c5f5b9e5 Reduce navbar padding 2021-09-02 12:36:02 +02:00
5a16dfced3 Cleanup site.css 2021-09-02 12:36:02 +02:00
cb46ef9e6c Revert semibold links 2021-09-02 12:36:02 +02:00
3b810e9b8d revert left sidebar font size 2021-09-02 12:36:02 +02:00
663dcecb8e Cleanup home styles 2021-09-02 12:36:02 +02:00
fbb5671f89 Cleanup styles 2021-09-02 12:36:02 +02:00
47d348359f Use util classes for navbar padding 2021-09-02 12:36:02 +02:00
ab2f460f35 converts to rem 2021-09-02 12:36:02 +02:00
c77e3a9396 navbar queries added 2021-09-02 12:36:02 +02:00
014c43f79b updates section 2021-09-02 12:36:02 +02:00
21fb3813dd adds section + media query 2021-09-02 12:36:02 +02:00
9a039a747a Move site customizations to design system 2021-09-02 12:36:02 +02:00
17578a4615 Bootstrap updates
- Increase vertical navbar padding
- Update alert close button styles
- Use semibold font for links
2021-09-02 12:36:02 +02:00
abb68d6f59 Improve fee rate component 2021-09-02 12:36:02 +02:00
3c443344a5 Ensure round notification badge 2021-09-02 12:36:02 +02:00
2887017d12 updates line-height, removes sm-table hack 2021-09-02 12:36:02 +02:00
f2286fb1be updates copy to reduce button size 2021-09-02 12:36:02 +02:00
bdd11a14ff adjusts logo size for future size update 2021-09-02 12:36:02 +02:00
18f6e5af4d left sidebar navigation updates 2021-09-02 12:36:02 +02:00
83b07e0caf Refactor tables, remove old customizations 2021-09-02 12:36:02 +02:00
840a5ac4b4 Refactor Bootstrap colors 2021-09-02 12:36:02 +02:00
4923311168 Update Bootstrap and theme variables 2021-09-02 12:36:02 +02:00
748423dfe8 Add UpdateOnChainPaymentMethodRequest 2021-09-02 10:42:41 +02:00
2f4e610900 Add UpdateLightningNetworkPaymentMethodRequest 2021-09-02 10:42:41 +02:00
d39ec97b9a Remove "enabled" option from address preview endpoint 2021-09-02 10:42:41 +02:00
0d08bd3ad1 Minor syntax changes 2021-09-02 10:39:35 +02:00
3343738bb3 Remove back links to POSTed pages 2021-09-02 10:39:35 +02:00
373b0b7850 Remove superfluous export command 2021-09-02 10:39:35 +02:00
aa24da72b9 Improve raw export options 2021-09-02 10:39:35 +02:00
aecc5f3c6e Update wording 2021-09-02 10:39:35 +02:00
3d4ef48ceb View cleanup 2021-09-02 10:39:35 +02:00
9bb74a17d8 Refine views and use cases 2021-09-02 10:39:35 +02:00
b654dfb237 Cleanup initial PSBT input view 2021-09-02 10:39:35 +02:00
1bca8c81a4 Fix Selenium test 2021-09-02 10:39:35 +02:00
4d35fd4ab3 Fix test 2021-09-02 10:39:35 +02:00
bf5ea23bc6 Streamline views 2021-09-02 10:39:35 +02:00
b4c1f695a8 Remove psbt ready view
The transaction info is now shown on the decoded PSBT page.
2021-09-02 10:39:35 +02:00
de3d966835 WIP 2021-09-02 10:39:35 +02:00
bcdb1ab1d8 Improve review/broadcast flow and fix test 2021-09-02 10:39:35 +02:00
63944792b0 Move signing views into wizard 2021-09-02 10:39:35 +02:00
3895b133a3 Move PSBT flow into wizard 2021-09-02 10:39:35 +02:00
4371b81ef3 Adjust existing PSBT views for consistency 2021-09-02 10:39:35 +02:00
cd93a5ab6b Add separate signing flow for PSBT 2021-09-02 10:39:35 +02:00
448 changed files with 13441 additions and 8209 deletions

View File

@ -31,10 +31,10 @@
<None Include="icon.png" Pack="true" PackagePath="\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.4" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="3.1.4" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="3.1.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.19" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.19" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="3.1.18" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="3.2.7" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BTCPayServer.Client\BTCPayServer.Client.csproj" />

View File

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NBitcoin.Crypto;
namespace BTCPayServer.Security
{
@ -9,6 +10,8 @@ namespace BTCPayServer.Security
{
public ConsentSecurityPolicy(string name, string value)
{
if (value.Contains(';', StringComparison.OrdinalIgnoreCase))
throw new FormatException();
_Value = value;
_Name = name;
}
@ -67,10 +70,48 @@ namespace BTCPayServer.Security
}
readonly HashSet<ConsentSecurityPolicy> _Policies = new HashSet<ConsentSecurityPolicy>();
/// <summary>
/// Allow a specific script as event handler
/// </summary>
/// <param name="script"></param>
public void AllowUnsafeHashes(string script = null)
{
if (!allowUnsafeHashes)
{
Add("script-src", $"'unsafe-hashes'");
allowUnsafeHashes = true;
}
if (script != null)
{
var sha = GetSha256(script);
Add("script-src", $"'sha256-{sha}'");
}
}
bool allowUnsafeHashes = false;
/// <summary>
/// Allow the injection of script tag with the following script
/// </summary>
/// <param name="script"></param>
public void AllowInline(string script)
{
if (script is null)
throw new ArgumentNullException(nameof(script));
var sha = GetSha256(script);
Add("script-src", $"'sha256-{sha}'");
}
static string GetSha256(string script)
{
return Convert.ToBase64String(Hashes.SHA256(Encoding.UTF8.GetBytes(script.Replace("\r\n", "\n", StringComparison.Ordinal))));
}
public void Add(string name, string value)
{
Add(new ConsentSecurityPolicy(name, value));
}
public void Add(ConsentSecurityPolicy policy)
{
if (_Policies.Any(p => p.Name == policy.Name && p.Value == policy.Name))
return;
_Policies.Add(policy);
}
@ -87,34 +128,19 @@ namespace BTCPayServer.Security
{
value.Append(';');
}
List<string> values = new List<string>();
HashSet<string> values = new HashSet<string>();
List<string> valuesList = new List<string>();
values.Add(group.Key);
valuesList.Add(group.Key);
foreach (var v in group)
{
values.Add(v.Value);
if (values.Add(v.Value))
valuesList.Add(v.Value);
}
foreach (var i in authorized)
{
values.Add(i);
}
value.Append(String.Join(" ", values.OfType<object>().ToArray()));
value.Append(String.Join(" ", valuesList.OfType<object>().ToArray()));
firstGroup = false;
}
return value.ToString();
}
internal void Clear()
{
authorized.Clear();
_Policies.Clear();
}
readonly HashSet<string> authorized = new HashSet<string>();
internal void AddAllAuthorized(string v)
{
authorized.Add(v);
}
public IEnumerable<string> Authorized => authorized;
}
}

View File

@ -13,7 +13,7 @@
<RepositoryType>git</RepositoryType>
</PropertyGroup>
<PropertyGroup>
<Version Condition=" '$(Version)' == '' ">1.4.0</Version>
<Version Condition=" '$(Version)' == '' ">1.5.0</Version>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<PublishRepositoryUrl>true</PublishRepositoryUrl>
@ -27,9 +27,9 @@
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="NBitcoin" Version="6.0.8" />
<PackageReference Include="NBitcoin" Version="6.0.15" />
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>
<ItemGroup>
<None Include="icon.png" Pack="true" PackagePath="\" />

View File

@ -0,0 +1,59 @@
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<LNURLPayPaymentMethodData>>
GetStoreLNURLPayPaymentMethods(string storeId, bool? enabled = null,
CancellationToken token = default)
{
var query = new Dictionary<string, object>();
if (enabled != null)
{
query.Add(nameof(enabled), enabled);
}
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/LNURLPay",
query), token);
return await HandleResponse<IEnumerable<LNURLPayPaymentMethodData>>(response);
}
public virtual async Task<LNURLPayPaymentMethodData> GetStoreLNURLPayPaymentMethod(
string storeId,
string cryptoCode, CancellationToken token = default)
{
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/LNURLPay/{cryptoCode}"), token);
return await HandleResponse<LNURLPayPaymentMethodData>(response);
}
public virtual async Task RemoveStoreLNURLPayPaymentMethod(string storeId,
string cryptoCode, CancellationToken token = default)
{
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/LNURLPay/{cryptoCode}",
method: HttpMethod.Delete), token);
await HandleResponse(response);
}
public virtual async Task<LNURLPayPaymentMethodData> UpdateStoreLNURLPayPaymentMethod(
string storeId,
string cryptoCode, LNURLPayPaymentMethodData paymentMethod,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/LNURLPay/{cryptoCode}",
bodyPayload: paymentMethod, method: HttpMethod.Put), token);
return await HandleResponse<LNURLPayPaymentMethodData>(response);
}
}
}

View File

@ -47,7 +47,7 @@ namespace BTCPayServer.Client
public virtual async Task<LightningNetworkPaymentMethodData> UpdateStoreLightningNetworkPaymentMethod(
string storeId,
string cryptoCode, LightningNetworkPaymentMethodData paymentMethod,
string cryptoCode, UpdateLightningNetworkPaymentMethodRequest paymentMethod,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
@ -55,10 +55,5 @@ namespace BTCPayServer.Client
bodyPayload: paymentMethod, method: HttpMethod.Put), token);
return await HandleResponse<LightningNetworkPaymentMethodData>(response);
}
public virtual Task<LightningNetworkPaymentMethodData>
UpdateStoreLightningNetworkPaymentMethodToInternalNode(string storeId,
string cryptoCode, CancellationToken token = default) => UpdateStoreLightningNetworkPaymentMethod(
storeId, cryptoCode, new LightningNetworkPaymentMethodData(cryptoCode, "Internal Node", true), token);
}
}

View File

@ -45,7 +45,7 @@ namespace BTCPayServer.Client
}
public virtual async Task<OnChainPaymentMethodData> UpdateStoreOnChainPaymentMethod(string storeId,
string cryptoCode, OnChainPaymentMethodData paymentMethod,
string cryptoCode, UpdateOnChainPaymentMethodRequest paymentMethod,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
@ -56,7 +56,7 @@ namespace BTCPayServer.Client
public virtual async Task<OnChainPaymentMethodPreviewResultData>
PreviewProposedStoreOnChainPaymentMethodAddresses(
string storeId, string cryptoCode, OnChainPaymentMethodData paymentMethod, int offset = 0,
string storeId, string cryptoCode, UpdateOnChainPaymentMethodRequest paymentMethod, int offset = 0,
int amount = 10,
CancellationToken token = default)
{

View File

@ -42,6 +42,8 @@ namespace BTCPayServer.Client
public virtual async Task<WebhookDeliveryData> GetWebhookDelivery(string storeId, string webhookId, string deliveryId, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/webhooks/{webhookId}/deliveries/{deliveryId}"), token);
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
return null;
return await HandleResponse<WebhookDeliveryData>(response);
}
public virtual async Task<string> RedeliverWebhook(string storeId, string webhookId, string deliveryId, CancellationToken token = default)

View File

@ -22,13 +22,17 @@ namespace BTCPayServer.JsonConverters
switch (token.Type)
{
case JTokenType.Float:
if (objectType == typeof(decimal) || objectType == typeof(decimal?))
return token.Value<decimal>();
if (objectType == typeof(double) || objectType == typeof(double?))
return token.Value<double>();
throw new JsonSerializationException("Unexpected object type: " + objectType);
case JTokenType.Integer:
case JTokenType.String:
if (objectType == typeof(decimal) || objectType == typeof(decimal?) )
return decimal.Parse(token.ToString(), CultureInfo.InvariantCulture);
if (objectType == typeof(double) || objectType == typeof(double?))
return double.Parse(token.ToString(), CultureInfo.InvariantCulture);
throw new JsonSerializationException("Unexpected object type: " + objectType);
case JTokenType.Null when objectType == typeof(decimal?) || objectType == typeof(double?):
return null;

View File

@ -1,9 +1,5 @@
using System;
using BTCPayServer.Client.JsonConverters;
using BTCPayServer.JsonConverters;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client.Models
{

View File

@ -4,5 +4,6 @@
{
public bool Enabled { get; set; }
public object Data { get; set; }
public string CryptoCode { get; set; }
}
}

View File

@ -26,6 +26,7 @@ namespace BTCPayServer.Client.Models
public SpeedPolicy? SpeedPolicy { get; set; }
public string[] PaymentMethods { get; set; }
public string DefaultPaymentMethod { get; set; }
[JsonConverter(typeof(TimeSpanJsonConverter.Minutes))]
[JsonProperty("expirationMinutes")]
@ -39,6 +40,7 @@ namespace BTCPayServer.Client.Models
public string RedirectURL { get; set; }
public bool? RedirectAutomatically { get; set; }
public bool? RequiresRefundEmail { get; set; } = null;
public string DefaultLanguage { get; set; }
}
}
@ -59,6 +61,8 @@ namespace BTCPayServer.Client.Models
public DateTimeOffset ExpirationTime { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
public DateTimeOffset CreatedTime { get; set; }
[JsonProperty(ItemConverterType = typeof(StringEnumConverter))]
public InvoiceStatus[] AvailableStatusesForManualMarking { get; set; }
}
public enum InvoiceStatus
{

View File

@ -0,0 +1,14 @@
namespace BTCPayServer.Client.Models
{
public class LNURLPayPaymentMethodBaseData
{
public bool UseBech32Scheme { get; set; }
public bool EnableForStandardInvoices { get; set; }
public bool LUD12Enabled { get; set; }
public LNURLPayPaymentMethodBaseData()
{
}
}
}

View File

@ -0,0 +1,27 @@
namespace BTCPayServer.Client.Models
{
public class LNURLPayPaymentMethodData: LNURLPayPaymentMethodBaseData
{
/// <summary>
/// Whether the payment method is enabled
/// </summary>
public bool Enabled { get; set; }
/// <summary>
/// Crypto code of the payment method
/// </summary>
public string CryptoCode { get; set; }
public LNURLPayPaymentMethodData()
{
}
public LNURLPayPaymentMethodData(string cryptoCode, bool enabled, bool useBech32Scheme, bool enableForStandardInvoices)
{
Enabled = enabled;
CryptoCode = cryptoCode;
UseBech32Scheme = useBech32Scheme;
EnableForStandardInvoices = enableForStandardInvoices;
}
}
}

View File

@ -4,6 +4,7 @@
{
public string ConnectionString { get; set; }
public bool DisableBOLT11PaymentOption { get; set; }
public LightningNetworkPaymentMethodBaseData()
{

View File

@ -16,11 +16,15 @@ namespace BTCPayServer.Client.Models
{
}
public LightningNetworkPaymentMethodData(string cryptoCode, string connectionString, bool enabled)
public LightningNetworkPaymentMethodData(string cryptoCode, string connectionString, bool enabled, string paymentMethod, bool disableBOLT11PaymentOption)
{
Enabled = enabled;
CryptoCode = cryptoCode;
ConnectionString = connectionString;
PaymentMethod = paymentMethod;
DisableBOLT11PaymentOption = disableBOLT11PaymentOption;
}
public string PaymentMethod { get; set; }
}
}

View File

@ -2,30 +2,46 @@ using NBitcoin;
namespace BTCPayServer.Client.Models
{
public class OnChainPaymentMethodData : OnChainPaymentMethodBaseData
public class OnChainPaymentMethodDataPreview : OnChainPaymentMethodBaseData
{
/// <summary>
/// Whether the payment method is enabled
/// </summary>
public bool Enabled { get; set; }
/// <summary>
/// Crypto code of the payment method
/// </summary>
public string CryptoCode { get; set; }
public OnChainPaymentMethodData()
public OnChainPaymentMethodDataPreview()
{
}
public OnChainPaymentMethodData(string cryptoCode, string derivationScheme, bool enabled, string label, RootedKeyPath accountKeyPath)
public OnChainPaymentMethodDataPreview(string cryptoCode, string derivationScheme, string label, RootedKeyPath accountKeyPath)
{
Enabled = enabled;
Label = label;
AccountKeyPath = accountKeyPath;
CryptoCode = cryptoCode;
DerivationScheme = derivationScheme;
}
}
public class OnChainPaymentMethodData : OnChainPaymentMethodDataPreview
{
/// <summary>
/// Whether the payment method is enabled
/// </summary>
public bool Enabled { get; set; }
public string PaymentMethod { get; set; }
public OnChainPaymentMethodData()
{
}
public OnChainPaymentMethodData(string cryptoCode, string derivationScheme, bool enabled, string label, RootedKeyPath accountKeyPath, string paymentMethod) :
base(cryptoCode, derivationScheme, label, accountKeyPath)
{
Enabled = enabled;
PaymentMethod = paymentMethod;
}
}
}

View File

@ -11,8 +11,8 @@ namespace BTCPayServer.Client.Models
}
public OnChainPaymentMethodDataWithSensitiveData(string cryptoCode, string derivationScheme, bool enabled,
string label, RootedKeyPath accountKeyPath, Mnemonic mnemonic) : base(cryptoCode, derivationScheme, enabled,
label, accountKeyPath)
string label, RootedKeyPath accountKeyPath, Mnemonic mnemonic, string paymentMethod) : base(cryptoCode, derivationScheme, enabled,
label, accountKeyPath, paymentMethod)
{
Mnemonic = mnemonic;
}

View File

@ -29,8 +29,7 @@ namespace BTCPayServer.Client.Models
public string LightningDescriptionTemplate { get; set; }
public double PaymentTolerance { get; set; } = 0;
public bool AnyoneCanCreateInvoice { get; set; }
public string DefaultCurrency { get; set; }
public bool RequiresRefundEmail { get; set; }
public bool LightningAmountInSatoshi { get; set; }
public bool LightningPrivateRouteHints { get; set; }

View File

@ -0,0 +1,20 @@
namespace BTCPayServer.Client.Models
{
public class UpdateLightningNetworkPaymentMethodRequest: LightningNetworkPaymentMethodBaseData
{
/// <summary>
/// Whether the payment method is enabled
/// </summary>
public bool Enabled { get; set; }
public UpdateLightningNetworkPaymentMethodRequest()
{
}
public UpdateLightningNetworkPaymentMethodRequest(string connectionString, bool enabled)
{
Enabled = enabled;
ConnectionString = connectionString;
}
}
}

View File

@ -0,0 +1,25 @@
using NBitcoin;
namespace BTCPayServer.Client.Models
{
public class UpdateOnChainPaymentMethodRequest : OnChainPaymentMethodBaseData
{
/// <summary>
/// Whether the payment method is enabled
/// </summary>
public bool Enabled { get; set; }
public UpdateOnChainPaymentMethodRequest()
{
}
public UpdateOnChainPaymentMethodRequest(bool enabled, string derivationScheme, string label, RootedKeyPath accountKeyPath)
{
Enabled = enabled;
Label = label;
AccountKeyPath = accountKeyPath;
DerivationScheme = derivationScheme;
}
}
}

View File

@ -11,6 +11,7 @@ namespace BTCPayServer.Client.Models
InvoiceProcessing,
InvoiceExpired,
InvoiceSettled,
InvoiceInvalid
InvoiceInvalid,
InvoicePaymentSettled,
}
}

View File

@ -10,72 +10,89 @@ namespace BTCPayServer.Client.Models
{
public WebhookInvoiceEvent()
{
}
public WebhookInvoiceEvent(WebhookEventType evtType)
{
this.Type = evtType;
}
[JsonProperty(Order = 1)]
public string StoreId { get; set; }
[JsonProperty(Order = 2)]
public string InvoiceId { get; set; }
[JsonProperty(Order = 1)] public string StoreId { get; set; }
[JsonProperty(Order = 2)] public string InvoiceId { get; set; }
}
public class WebhookInvoiceSettledEvent : WebhookInvoiceEvent
{
public WebhookInvoiceSettledEvent()
{
}
public WebhookInvoiceSettledEvent(WebhookEventType evtType) : base(evtType)
{
}
public bool ManuallyMarked { get; set; }
}
public class WebhookInvoiceInvalidEvent : WebhookInvoiceEvent
{
public WebhookInvoiceInvalidEvent()
{
}
public WebhookInvoiceInvalidEvent(WebhookEventType evtType) : base(evtType)
{
}
public bool ManuallyMarked { get; set; }
}
public class WebhookInvoiceProcessingEvent : WebhookInvoiceEvent
{
public WebhookInvoiceProcessingEvent()
{
}
public WebhookInvoiceProcessingEvent(WebhookEventType evtType) : base(evtType)
{
}
public bool OverPaid { get; set; }
}
public class WebhookInvoiceReceivedPaymentEvent : WebhookInvoiceEvent
{
public WebhookInvoiceReceivedPaymentEvent()
{
}
public WebhookInvoiceReceivedPaymentEvent(WebhookEventType evtType) : base(evtType)
{
}
public bool AfterExpiration { get; set; }
public string PaymentMethod { get; set; }
public InvoicePaymentMethodDataModel.Payment Payment { get; set; }
public bool OverPaid { get; set; }
}
public class WebhookInvoicePaymentSettledEvent : WebhookInvoiceReceivedPaymentEvent
{
public WebhookInvoicePaymentSettledEvent()
{
}
public WebhookInvoicePaymentSettledEvent(WebhookEventType evtType) : base(evtType)
{
}
}
public class WebhookInvoiceExpiredEvent : WebhookInvoiceEvent
{
public WebhookInvoiceExpiredEvent()
{
}
public WebhookInvoiceExpiredEvent(WebhookEventType evtType) : base(evtType)
{
}

View File

@ -14,7 +14,6 @@ namespace BTCPayServer
DisplayName = "Althash",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://explorer.htmlcoin.com/api/tx/{0}" : "https://explorer.htmlcoin.com/api/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "htmlcoin",
DefaultRateRules = new[]
{
"HTML_X = HTML_USD",

View File

@ -15,7 +15,6 @@ namespace BTCPayServer
? "https://chainz.cryptoid.info/agm/tx.dws?{0}"
: "https://chainz.cryptoid.info/agm-test/tx.dws?{0}",
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "argoneum",
DefaultRateRules = new[]
{
"AGM_X = AGM_BTC * BTC_X",

View File

@ -13,7 +13,6 @@ namespace BTCPayServer
DisplayName = "BGold",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://btgexplorer.com/tx/{0}" : "https://testnet.btgexplorer.com/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "bitcoingold",
DefaultRateRules = new[]
{
"BTG_X = BTG_BTC * BTC_X",

View File

@ -14,7 +14,6 @@ namespace BTCPayServer
DisplayName = "BPlus",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://chainz.cryptoid.info/xbc/tx.dws?{0}" : "https://chainz.cryptoid.info/xbc/tx.dws?{0}",
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "bplus-fix-it",
DefaultRateRules = new[]
{
"XBC_X = XBC_BTC * BTC_X",

View File

@ -14,7 +14,6 @@ namespace BTCPayServer
DisplayName = "BitCore",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://explorer.bitcore.cc/tx/{0}" : "https://explorer.bitcore.cc/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "bitcore",
DefaultRateRules = new[]
{
"BTX_X = BTX_BTC * BTC_X",

View File

@ -15,7 +15,6 @@ namespace BTCPayServer
? "https://explorer.chaincoin.org/Explorer/Transaction/{0}"
: "https://test.explorer.chaincoin.org/Explorer/Transaction/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "chaincoin",
DefaultRateRules = new[]
{
"CHC_X = CHC_BTC * BTC_X",

View File

@ -16,7 +16,6 @@ namespace BTCPayServer
? "https://insight.dash.org/insight/tx/{0}"
: "https://testnet-insight.dashevo.org/insight/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "dash",
DefaultRateRules = new[]
{
"DASH_X = DASH_BTC * BTC_X",

View File

@ -14,7 +14,6 @@ namespace BTCPayServer
DisplayName = "Dogecoin",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://dogechain.info/tx/{0}" : "https://dogechain.info/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "dogecoin",
DefaultRateRules = new[]
{
"DOGE_X = DOGE_BTC * BTC_X",

View File

@ -14,7 +14,6 @@ namespace BTCPayServer
DisplayName = "Feathercoin",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://explorer.feathercoin.com/tx/{0}" : "https://explorer.feathercoin.com/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "feathercoin",
DefaultRateRules = new[]
{
"FTC_X = FTC_BTC * BTC_X",

View File

@ -15,7 +15,6 @@ namespace BTCPayServer
? "https://chainz.cryptoid.info/grs/tx.dws?{0}.htm"
: "https://chainz.cryptoid.info/grs-test/tx.dws?{0}.htm",
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "groestlcoin",
DefaultRateRules = new[]
{
"GRS_X = GRS_BTC * BTC_X",

View File

@ -17,7 +17,6 @@ namespace BTCPayServer
? "https://live.blockcypher.com/ltc/tx/{0}/"
: "http://explorer.litecointools.com/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "litecoin",
DefaultRateRules = new[]
{
"LTC_X = LTC_BTC * BTC_X",

View File

@ -14,7 +14,6 @@ namespace BTCPayServer
DisplayName = "Monacoin",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://mona.insight.monaco-ex.org/insight/tx/{0}" : "https://testnet-mona.insight.monaco-ex.org/insight/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "monacoin",
DefaultRateRules = new[]
{
"MONA_X = MONA_BTC * BTC_X",

View File

@ -14,7 +14,6 @@ namespace BTCPayServer
DisplayName = "MonetaryUnit",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://explorer.monetaryunit.org/#/MUE/mainnet/tx/{0}" : "https://explorer.monetaryunit.org/#/MUE/mainnet/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "monetaryunit",
DefaultRateRules = new[]
{
"MUE_X = MUE_BTC * BTC_X",

View File

@ -14,7 +14,6 @@ namespace BTCPayServer
DisplayName = "Polis",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://blockbook.polispay.org/tx/{0}" : "https://blockbook.polispay.org/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "polis",
DefaultRateRules = new[]
{
"POLIS_X = POLIS_BTC * BTC_X",

View File

@ -14,7 +14,6 @@ namespace BTCPayServer
DisplayName = "Ufo",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://chainz.cryptoid.info/ufo/tx.dws?{0}" : "https://chainz.cryptoid.info/ufo/tx.dws?{0}",
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "ufo",
DefaultRateRules = new[]
{
"UFO_X = UFO_BTC * BTC_X",

View File

@ -14,7 +14,6 @@ namespace BTCPayServer
DisplayName = "Viacoin",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://explorer.viacoin.org/tx/{0}" : "https://explorer.viacoin.org/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "viacoin",
DefaultRateRules = new[]
{
"VIA_X = VIA_BTC * BTC_X",

View File

@ -24,7 +24,6 @@ namespace BTCPayServer
},
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://blockstream.info/liquid/tx/{0}" : "https://blockstream.info/testnet/liquid/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "liquidnetwork",
CryptoImagePath = "imlegacy/liquid.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("1776'") : new KeyPath("1'"),

View File

@ -23,7 +23,6 @@ namespace BTCPayServer
DisplayName = "Liquid Tether",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://blockstream.info/liquid/tx/{0}" : "https://blockstream.info/testnet/liquid/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "liquidnetwork",
CryptoImagePath = "imlegacy/liquid-tether.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("1776'") : new KeyPath("1'"),
@ -47,7 +46,6 @@ namespace BTCPayServer
DisplayName = "Ethiopian Birr",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://blockstream.info/liquid/tx/{0}" : "https://blockstream.info/testnet/liquid/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "liquidnetwork",
CryptoImagePath = "imlegacy/etb.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("1776'") : new KeyPath("1'"),
@ -70,7 +68,6 @@ namespace BTCPayServer
DisplayName = "Liquid CAD",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://blockstream.info/liquid/tx/{0}" : "https://blockstream.info/testnet/liquid/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "liquidnetwork",
CryptoImagePath = "imlegacy/lcad.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("1776'") : new KeyPath("1'"),

View File

@ -66,44 +66,9 @@ namespace BTCPayServer
public virtual bool ReadonlyWallet { get; set; } = false;
public virtual bool VaultSupported { get; set; } = false;
public int MaxTrackedConfirmation { get; set; } = 6;
public string UriScheme { get; set; }
public bool SupportPayJoin { get; set; } = false;
public bool SupportLightning { get; set; } = true;
public KeyPath GetRootKeyPath(DerivationType type)
{
KeyPath baseKey;
if (!NBitcoinNetwork.Consensus.SupportSegwit)
{
baseKey = new KeyPath("44'");
}
else
{
switch (type)
{
case DerivationType.Legacy:
baseKey = new KeyPath("44'");
break;
case DerivationType.SegwitP2SH:
baseKey = new KeyPath("49'");
break;
case DerivationType.Segwit:
baseKey = new KeyPath("84'");
break;
default:
throw new ArgumentOutOfRangeException(nameof(type), type, null);
}
}
return baseKey
.Derive(CoinType);
}
public KeyPath GetRootKeyPath()
{
return new KeyPath(NBitcoinNetwork.Consensus.SupportSegwit ? "49'" : "44'")
.Derive(CoinType);
}
public override T ToObject<T>(string json)
{
return NBXplorerNetwork.Serializer.ToObject<T>(json);
@ -124,7 +89,7 @@ namespace BTCPayServer
public virtual PaymentUrlBuilder GenerateBIP21(string cryptoInfoAddress, Money cryptoInfoDue)
{
var builder = new PaymentUrlBuilder(UriScheme);
var builder = new PaymentUrlBuilder(this.NBitcoinNetwork.UriScheme);
builder.Host = cryptoInfoAddress;
if (cryptoInfoDue != null && cryptoInfoDue != Money.Zero)
{

View File

@ -17,7 +17,6 @@ namespace BTCPayServer
NetworkType == Bitcoin.Instance.Signet.ChainName ? "https://explorer.bc-2.jp/tx/{0}"
: "https://blockstream.info/testnet/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "bitcoin",
CryptoImagePath = "imlegacy/bitcoin.svg",
LightningImagePath = "imlegacy/bitcoin-lightning.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),

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.0" />
<PackageReference Include="NBXplorer.Client" Version="4.1.3" />
</ItemGroup>
<ItemGroup Condition="'$(Altcoins)' != 'true'">
<Compile Remove="Altcoins\**\*.cs"></Compile>

View File

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

View File

@ -24,7 +24,6 @@ namespace BTCPayServer.Data
.HasOne(o => o.ApplicationUser)
.WithMany(i => i.Fido2Credentials)
.HasForeignKey(i => i.ApplicationUserId).OnDelete(DeleteBehavior.Cascade);
}
public ApplicationUser ApplicationUser { get; set; }

View File

@ -1,5 +1,6 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using BTCPayServer.Client.Models;
using Microsoft.EntityFrameworkCore;
using NBitcoin;
@ -19,25 +20,24 @@ namespace BTCPayServer.Data
[MaxLength(20)]
[Required]
public string PaymentMethodId { get; set; }
public string Destination { get; set; }
public byte[] Blob { get; set; }
public byte[] Proof { get; set; }
#nullable enable
public string? Destination { get; set; }
#nullable restore
internal static void OnModelCreating(ModelBuilder builder)
{
builder.Entity<PayoutData>()
.HasOne(o => o.PullPaymentData)
.WithMany(o => o.Payouts).OnDelete(DeleteBehavior.Cascade);
builder.Entity<PayoutData>()
.Property(o => o.State)
.HasConversion<string>();
builder.Entity<PayoutData>()
.HasIndex(o => o.Destination)
.IsUnique();
builder.Entity<PayoutData>()
.HasIndex(o => o.State);
builder.Entity<PayoutData>()
.HasIndex(x => new { DestinationId = x.Destination, x.State});
}
// utility methods

View File

@ -0,0 +1,41 @@
// <auto-generated />
using System;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20211021085011_RemovePayoutDestinationConstraint")]
public partial class RemovePayoutDestinationConstraint : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_Payouts_Destination",
table: "Payouts");
migrationBuilder.CreateIndex(
name: "IX_Payouts_Destination_State",
table: "Payouts",
columns: new[] { "Destination", "State" });
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_Payouts_Destination_State",
table: "Payouts");
migrationBuilder.CreateIndex(
name: "IX_Payouts_Destination",
table: "Payouts",
column: "Destination",
unique: true);
}
}
}

View File

@ -15,7 +15,7 @@ namespace BTCPayServer.Migrations
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "3.1.4");
.HasAnnotation("ProductVersion", "3.1.19");
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
{
@ -527,13 +527,12 @@ namespace BTCPayServer.Migrations
b.HasKey("Id");
b.HasIndex("Destination")
.IsUnique();
b.HasIndex("PullPaymentDataId");
b.HasIndex("State");
b.HasIndex("Destination", "State");
b.ToTable("Payouts");
});

View File

@ -13,7 +13,7 @@
<EmbeddedResource Include="Resources\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.4">
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.19">
</PackageReference>
</ItemGroup>
</Project>

View File

@ -4,10 +4,10 @@
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.6.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.11.0" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.7" />
<PackageReference Include="NBitcoin" Version="6.0.8" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="NBitcoin" Version="6.0.15" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.6.3" />
</ItemGroup>

View File

@ -0,0 +1,48 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Rating;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.Rates
{
public class CryptoMarketExchangeRateProvider : IRateProvider
{
private readonly HttpClient _httpClient;
public CryptoMarketExchangeRateProvider(HttpClient httpClient)
{
_httpClient = httpClient ?? new HttpClient();
}
readonly List<string> SupportedPairs = new List<string>()
{
"BTCARS",
"BTCCLP",
"BTCBRL"
};
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{
var response = await _httpClient.GetAsync("https://api.exchange.cryptomkt.com/api/3/public/ticker/", cancellationToken);
var jobj = await response.Content.ReadAsAsync<JObject>(cancellationToken);
return ((jobj as JObject) ?? new JObject())
.Properties()
.Where(p => SupportedPairs.Contains(p.Name))
.Select(p => new PairRate(CurrencyPair.Parse(p.Name), CreateBidAsk(p)))
.ToArray();
}
private static BidAsk CreateBidAsk(JProperty p)
{
var bid = decimal.Parse(p.Value["bid"].Value<string>(), System.Globalization.NumberStyles.Any, CultureInfo.InvariantCulture);
var ask = decimal.Parse(p.Value["ask"].Value<string>(), System.Globalization.NumberStyles.Any, CultureInfo.InvariantCulture);
if (bid > ask)
return null;
return new BidAsk(bid, ask);
}
}
}

View File

@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Rating;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.Rates
{
public class RipioExchangeProvider : IRateProvider
{
private readonly HttpClient _httpClient;
public RipioExchangeProvider(HttpClient httpClient)
{
_httpClient = httpClient ?? new HttpClient();
}
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{
var response = await _httpClient.GetAsync("https://api.exchange.ripio.com/api/v1/rate/all/", cancellationToken);
var jarray = (JArray)(await response.Content.ReadAsAsync<JArray>(cancellationToken));
return jarray
.Children<JObject>()
.Select(jobj => ParsePair(jobj))
.Where(p => p != null)
.ToArray();
}
private PairRate ParsePair(JObject jobj)
{
var pair = CurrencyPair.Parse(jobj["pair"].Value<string>());
var bid = decimal.Parse(jobj["bid"].Value<string>(), System.Globalization.NumberStyles.Any, CultureInfo.InvariantCulture);
var ask = decimal.Parse(jobj["ask"].Value<string>(), System.Globalization.NumberStyles.Any, CultureInfo.InvariantCulture);
if (bid > ask)
return null;
return new PairRate(pair, new BidAsk(bid, ask));
}
}
}

View File

@ -75,6 +75,8 @@ namespace BTCPayServer.Services.Rates
yield return new AvailableRateProvider("bitbank", "Bitbank", "https://public.bitbank.cc/prices");
yield return new AvailableRateProvider("bitflyer", "Bitflyer", "https://api.bitflyer.com/v1/ticker");
yield return new AvailableRateProvider("bitpay", "Bitpay", "https://bitpay.com/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");
@ -99,6 +101,8 @@ namespace BTCPayServer.Services.Rates
Providers.Add("bylls", new ByllsRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BYLLS")));
Providers.Add("bitbank", new BitbankRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BITBANK")));
Providers.Add("bitpay", new BitpayRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BITPAY")));
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

@ -26,6 +26,7 @@ using Newtonsoft.Json.Linq;
using OpenQA.Selenium;
using Xunit;
using Xunit.Abstractions;
using WalletSettingsViewModel = BTCPayServer.Models.StoreViewModels.WalletSettingsViewModel;
namespace BTCPayServer.Tests
{
@ -76,10 +77,10 @@ namespace BTCPayServer.Tests
Assert.IsType<RedirectToActionResult>(response);
// Get enabled state from overview action
StoreViewModel storeModel;
response = await controller.UpdateStore();
storeModel = (StoreViewModel)Assert.IsType<ViewResult>(response).Model;
var lnNode = storeModel.LightningNodes.Find(node => node.CryptoCode == cryptoCode);
PaymentMethodsViewModel paymentMethodsModel;
response = controller.PaymentMethods();
paymentMethodsModel = (PaymentMethodsViewModel)Assert.IsType<ViewResult>(response).Model;
var lnNode = paymentMethodsModel.LightningNodes.Find(node => node.CryptoCode == cryptoCode);
Assert.NotNull(lnNode);
Assert.False(lnNode.Enabled);
@ -89,18 +90,18 @@ namespace BTCPayServer.Tests
Assert.IsType<ViewResult>(response);
// Get enabled state from overview action
response = await controller.UpdateStore();
storeModel = (StoreViewModel)Assert.IsType<ViewResult>(response).Model;
var derivationScheme = storeModel.DerivationSchemes.Find(scheme => scheme.Crypto == cryptoCode);
response = controller.PaymentMethods();
paymentMethodsModel = (PaymentMethodsViewModel)Assert.IsType<ViewResult>(response).Model;
var derivationScheme = paymentMethodsModel.DerivationSchemes.Find(scheme => scheme.Crypto == cryptoCode);
Assert.NotNull(derivationScheme);
Assert.True(derivationScheme.Enabled);
// Disable wallet
response = controller.SetWalletEnabled(storeId, cryptoCode, false).GetAwaiter().GetResult();
Assert.IsType<RedirectToActionResult>(response);
response = await controller.UpdateStore();
storeModel = (StoreViewModel)Assert.IsType<ViewResult>(response).Model;
derivationScheme = storeModel.DerivationSchemes.Find(scheme => scheme.Crypto == cryptoCode);
response = controller.PaymentMethods();
paymentMethodsModel = (PaymentMethodsViewModel)Assert.IsType<ViewResult>(response).Model;
derivationScheme = paymentMethodsModel.DerivationSchemes.Find(scheme => scheme.Crypto == cryptoCode);
Assert.NotNull(derivationScheme);
Assert.False(derivationScheme.Enabled);
@ -138,9 +139,9 @@ namespace BTCPayServer.Tests
Assert.True(setupVm.Confirmation);
response = await controller.UpdateWallet(setupVm);
Assert.IsType<RedirectToActionResult>(response);
response = await controller.ModifyWallet(new WalletSetupViewModel { StoreId = storeId, CryptoCode = cryptoCode });
setupVm = (WalletSetupViewModel)Assert.IsType<ViewResult>(response).Model;
Assert.Equal("CoboVault", setupVm.Source);
response = await controller.WalletSettings(storeId, cryptoCode);
var settingsVm = (WalletSettingsViewModel)Assert.IsType<ViewResult>(response).Model;
Assert.Equal("CoboVault", settingsVm.Source);
// wasabi wallet file
content = "{\r\n \"EncryptedSecret\": \"6PYWBQ1zsukowsnTNA57UUx791aBuJusm7E4egXUmF5WGw3tcdG3cmTL57\",\r\n \"ChainCode\": \"waSIVbn8HaoovoQg/0t8IS1+ZCxGsJRGFT21i06nWnc=\",\r\n \"MasterFingerprint\": \"7a7563b5\",\r\n \"ExtPubKey\": \"xpub6CEqRFZ7yZxCFXuEWZBAdnC8bdvu9SRHevaoU2SsW9ZmKhrCShmbpGZWwaR15hdLURf8hg47g4TpPGaqEU8hw5LEJCE35AUhne67XNyFGBk\",\r\n \"PasswordVerified\": false,\r\n \"MinGapLimit\": 21,\r\n \"AccountKeyPath\": \"84'/0'/0'\",\r\n \"BlockchainState\": {\r\n \"Network\": \"RegTest\",\r\n \"Height\": \"0\"\r\n },\r\n \"HdPubKeys\": []\r\n}";
@ -149,9 +150,9 @@ namespace BTCPayServer.Tests
Assert.True(setupVm.Confirmation);
response = await controller.UpdateWallet(setupVm);
Assert.IsType<RedirectToActionResult>(response);
response = await controller.ModifyWallet(new WalletSetupViewModel { StoreId = storeId, CryptoCode = cryptoCode });
setupVm = (WalletSetupViewModel)Assert.IsType<ViewResult>(response).Model;
Assert.Equal("WasabiFile", setupVm.Source);
response = await controller.WalletSettings(storeId, cryptoCode);
settingsVm = (WalletSettingsViewModel)Assert.IsType<ViewResult>(response).Model;
Assert.Equal("WasabiFile", settingsVm.Source);
// Can we upload coldcard settings? (Should fail, we are giving a mainnet file to a testnet network)
content = "{\"keystore\": {\"ckcc_xpub\": \"xpub661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw\", \"xpub\": \"ypub6WWc2gWwHbdnAAyJDnR4SPL1phRh7REqrPBfZeizaQ1EmTshieRXJC3Z5YoU4wkcdKHEjQGkh6AYEzCQC1Kz3DNaWSwdc1pc8416hAjzqyD\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/0'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}";
@ -166,9 +167,9 @@ namespace BTCPayServer.Tests
Assert.True(setupVm.Confirmation);
response = await controller.UpdateWallet(setupVm);
Assert.IsType<RedirectToActionResult>(response);
response = await controller.ModifyWallet(new WalletSetupViewModel { StoreId = storeId, CryptoCode = cryptoCode });
setupVm = (WalletSetupViewModel)Assert.IsType<ViewResult>(response).Model;
Assert.Equal("ElectrumFile", setupVm.Source);
response = await controller.WalletSettings(storeId, cryptoCode);
settingsVm = (WalletSettingsViewModel)Assert.IsType<ViewResult>(response).Model;
Assert.Equal("ElectrumFile", settingsVm.Source);
// Now let's check that no data has been lost in the process
var store = tester.PayTester.StoreRepository.FindStore(storeId).GetAwaiter().GetResult();
@ -612,7 +613,7 @@ namespace BTCPayServer.Tests
user.RegisterDerivationScheme("LTC");
var apps = user.GetController<AppsController>();
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model);
vm.Name = "test";
vm.AppName = "test";
vm.SelectedAppType = AppType.PointOfSale.ToString();
Assert.IsType<RedirectToActionResult>(apps.CreateApp(vm).Result);
var appId = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model)
@ -820,6 +821,51 @@ normal:
normalInvoice.CryptoInfo,
s => PaymentTypes.BTCLike.ToString() == s.PaymentType && new[] { "BTC", "LTC" }.Contains(
s.CryptoCode));
//test topup option
vmpos.Template = @"
a:
price: 1000.0
title: good apple
b:
price: 10.0
custom: false
c:
price: 1.02
custom: true
d:
price: 1.02
price_type: fixed
e:
price: 1.02
price_type: minimum
f:
price_type: topup
g:
custom: topup
";
Assert.IsType<RedirectToActionResult>(apps.UpdatePointOfSale(appId, vmpos).Result);
vmpos = Assert.IsType<UpdatePointOfSaleViewModel>(Assert
.IsType<ViewResult>(await apps.UpdatePointOfSale(appId)).Model);
Assert.DoesNotContain("custom", vmpos.Template);
var items = appService.Parse(vmpos.Template, vmpos.Currency);
Assert.Contains(items, item => item.Id == "a" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed);
Assert.Contains(items, item => item.Id == "b" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed);
Assert.Contains(items, item => item.Id == "c" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Minimum);
Assert.Contains(items, item => item.Id == "d" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed);
Assert.Contains(items, item => item.Id == "e" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Minimum);
Assert.Contains(items, item => item.Id == "f" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup);
Assert.Contains(items, item => item.Id == "g" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup);
Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(appId, PosViewType.Static, null, null, null, null, null, "g").Result);
invoices = user.BitPay.GetInvoices();
var topupInvoice = invoices.Single(invoice => invoice.ItemCode == "g");
Assert.Equal(0, topupInvoice.Price);
Assert.Equal("new", topupInvoice.Status);
}
}

View File

@ -125,11 +125,11 @@ namespace BTCPayServer.Tests
});
//test precision based on https://github.com/ElementsProject/elements/issues/805#issuecomment-601277606
var etbBip21 = new BitcoinUrlBuilder(invoice.CryptoInfo.Single(info => info.CryptoCode == "ETB").PaymentUrls.BIP21.Replace(etb.UriScheme, "bitcoin"), etb.NBitcoinNetwork);
var etbBip21 = new BitcoinUrlBuilder(invoice.CryptoInfo.Single(info => info.CryptoCode == "ETB").PaymentUrls.BIP21, etb.NBitcoinNetwork);
//precision = 2, 1ETB = 0.00000100
Assert.Equal(100, etbBip21.Amount.Satoshi);
var lbtcBip21 = new BitcoinUrlBuilder(invoice.CryptoInfo.Single(info => info.CryptoCode == "LBTC").PaymentUrls.BIP21.Replace(lbtc.UriScheme, "bitcoin"), lbtc.NBitcoinNetwork);
var lbtcBip21 = new BitcoinUrlBuilder(invoice.CryptoInfo.Single(info => info.CryptoCode == "LBTC").PaymentUrls.BIP21, lbtc.NBitcoinNetwork);
//precision = 8, 0.1 = 0.1
Assert.Equal(0.1m, lbtcBip21.Amount.ToDecimal(MoneyUnit.BTC));
}

View File

@ -124,8 +124,8 @@ namespace BTCPayServer.Tests
//redirect
//appidentifier
var appidentifier = "testapp";
var callbackUrl = tester.PayTester.ServerUri + "postredirect-callback-test";
var authUrl = BTCPayServerClient.GenerateAuthorizeUri(tester.PayTester.ServerUri,
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);
Assert.Contains(appidentifier, s.Driver.PageSource);
@ -143,7 +143,7 @@ namespace BTCPayServer.Tests
await TestApiAgainstAccessToken(accessToken, tester, user,
(await apiKeyRepo.GetKey(accessToken)).GetBlob().Permissions);
authUrl = BTCPayServerClient.GenerateAuthorizeUri(tester.PayTester.ServerUri,
authUrl = BTCPayServerClient.GenerateAuthorizeUri(s.ServerUri,
new[] { Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings }, false, true, applicationDetails: (null, new Uri(callbackUrl))).ToString();
s.Driver.Navigate().GoToUrl(authUrl);
@ -164,7 +164,7 @@ namespace BTCPayServer.Tests
(await apiKeyRepo.GetKey(accessToken)).GetBlob().Permissions);
//let's test the app identifier system
authUrl = BTCPayServerClient.GenerateAuthorizeUri(tester.PayTester.ServerUri,
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
@ -173,7 +173,7 @@ namespace BTCPayServer.Tests
Assert.Equal(callbackUrl, s.Driver.Url);
//same app but different redirect = nono
authUrl = BTCPayServerClient.GenerateAuthorizeUri(tester.PayTester.ServerUri,
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);

View File

@ -19,11 +19,11 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.13" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<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="90.0.4430.2400" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="94.0.4606.6100" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.2">
<PrivateAssets>all</PrivateAssets>

View File

@ -85,7 +85,7 @@ namespace BTCPayServer.Tests
public HashSet<string> Chains { get; set; } = new HashSet<string>() { "BTC" };
public bool UseLightning { get; set; }
public bool AllowAdminRegistration { get; set; } = true;
public bool CheatMode { get; set; } = true;
public bool DisableRegistration { get; set; } = false;
public async Task StartAsync()
{
@ -128,13 +128,13 @@ namespace BTCPayServer.Tests
config.AppendLine($"lbtc.explorer.url={LBTCNBXplorerUri.AbsoluteUri}");
config.AppendLine($"lbtc.explorer.cookiefile=0");
}
if (AllowAdminRegistration)
config.AppendLine("allow-admin-registration=1");
if (CheatMode)
config.AppendLine("cheatmode=1");
config.AppendLine($"torrcfile={TestUtils.GetTestDataFullPath("Tor/torrc")}");
config.AppendLine($"socksendpoint={SocksEndpoint}");
config.AppendLine($"debuglog=debug.log");
config.AppendLine($"nocsp={NoCSP.ToString().ToLowerInvariant()}");
if (!string.IsNullOrEmpty(SSHPassword) && string.IsNullOrEmpty(SSHKeyFile))
config.AppendLine($"sshpassword={SSHPassword}");
@ -198,6 +198,7 @@ namespace BTCPayServer.Tests
coinAverageMock = new MockRateProvider();
coinAverageMock.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("BTC_USD"), new BidAsk(5000m)));
coinAverageMock.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("BTC_EUR"), new BidAsk(4000m)));
coinAverageMock.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("BTC_CAD"), new BidAsk(4500m)));
coinAverageMock.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("BTC_LTC"), new BidAsk(162m)));
coinAverageMock.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("LTC_USD"), new BidAsk(500m)));
@ -283,6 +284,8 @@ namespace BTCPayServer.Tests
public string SSHPassword { get; internal set; }
public string SSHKeyFile { get; internal set; }
public string SSHConnection { get; set; }
public bool NoCSP { get; set; }
public T GetController<T>(string userId = null, string storeId = null, bool isAdmin = false) where T : Controller
{
var context = new DefaultHttpContext();

View File

@ -1,13 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Payments;
using BTCPayServer.Tests.Logging;
using BTCPayServer.Views.Stores;
using NBitcoin;
using OpenQA.Selenium;
using OpenQA.Selenium.Support.UI;
using Xunit;
using Xunit.Abstractions;
@ -34,7 +31,7 @@ namespace BTCPayServer.Tests
s.RegisterNewUser();
var store = s.CreateNewStore();
s.AddDerivationScheme("BTC");
s.GoToStore(store.storeId, StoreNavPages.Checkout);
s.GoToStore(store.storeId, StoreNavPages.CheckoutAppearance);
s.Driver.FindElement(By.Id("RequiresRefundEmail")).Click();
s.Driver.FindElement(By.Name("command")).Click();
@ -73,6 +70,68 @@ namespace BTCPayServer.Tests
}
}
[Fact(Timeout = TestTimeout)]
public async Task CanHandleRefundEmailForm2()
{
using (var s = SeleniumTester.Create())
{
// Prepare user account and store
await s.StartAsync();
s.GoToRegister();
s.RegisterNewUser();
var store = s.CreateNewStore();
s.AddDerivationScheme("BTC");
// Now create an invoice that requires a refund email
var invoice = s.CreateInvoice(store.storeName, 100, "USD", "", null, true);
s.GoToInvoiceCheckout(invoice);
var emailInput = s.Driver.FindElement(By.Id("emailAddressFormInput"));
Assert.True(emailInput.Displayed);
emailInput.SendKeys("a@g.com");
var actionButton = s.Driver.FindElement(By.Id("emailAddressForm")).FindElement(By.CssSelector("button.action-button"));
actionButton.Click();
try // Sometimes the click only take the focus, without actually really clicking on it...
{
actionButton.Click();
}
catch { }
s.Driver.AssertElementNotFound(By.Id("emailAddressFormInput"));
s.Driver.Navigate().Refresh();
s.Driver.AssertElementNotFound(By.Id("emailAddressFormInput"));
s.GoToHome();
// Now create an invoice that doesn't require a refund email
s.CreateInvoice(store.storeName, 100, "USD", "", null, false);
s.Driver.FindElement(By.ClassName("invoice-details-link")).Click();
s.Driver.AssertNoError();
s.Driver.Navigate().Back();
s.Driver.FindElement(By.ClassName("invoice-checkout-link")).Click();
Assert.NotEmpty(s.Driver.FindElements(By.Id("checkoutCtrl")));
s.Driver.AssertElementNotFound(By.Id("emailAddressFormInput"));
s.Driver.Navigate().Refresh();
s.Driver.AssertElementNotFound(By.Id("emailAddressFormInput"));
s.GoToHome();
// Now create an invoice that requires refund email but already has one set, email input shouldn't show up
s.CreateInvoice(store.storeName, 100, "USD", "a@g.com", null, true);
s.Driver.FindElement(By.ClassName("invoice-details-link")).Click();
s.Driver.AssertNoError();
s.Driver.Navigate().Back();
s.Driver.FindElement(By.ClassName("invoice-checkout-link")).Click();
Assert.NotEmpty(s.Driver.FindElements(By.Id("checkoutCtrl")));
s.Driver.AssertElementNotFound(By.Id("emailAddressFormInput"));
s.Driver.Navigate().Refresh();
s.Driver.AssertElementNotFound(By.Id("emailAddressFormInput"));
}
}
[Fact(Timeout = TestTimeout)]
public async Task CanUseLanguageDropdown()
{
@ -105,7 +164,7 @@ namespace BTCPayServer.Tests
[Fact(Timeout = TestTimeout)]
[Trait("Lightning", "Lightning")]
public async Task CanUseLightningSatsFeature()
public async Task CanSetDefaultPaymentMethod()
{
using (var s = SeleniumTester.Create())
{
@ -115,15 +174,37 @@ namespace BTCPayServer.Tests
s.RegisterNewUser(true);
var store = s.CreateNewStore();
s.AddLightningNode();
s.GoToStore(store.storeId, StoreNavPages.Checkout);
s.Driver.SetCheckbox(By.Id("LightningAmountInSatoshi"), true);
var command = s.Driver.FindElement(By.Name("command"));
s.AddDerivationScheme("BTC");
command.Click();
var invoiceId = s.CreateInvoice(store.storeName, 10, "USD", "a@g.com");
var invoiceId = s.CreateInvoice(store.storeName, defaultPaymentMethod: "BTC_LightningLike");
s.GoToInvoiceCheckout(invoiceId);
Assert.Equal("Bitcoin (Lightning) (BTC)", s.Driver.FindElement(By.ClassName("payment__currencies")).Text);
s.Driver.Quit();
}
}
[Fact(Timeout = TestTimeout)]
[Trait("Lightning", "Lightning")]
public async Task CanUseLightningSatsFeature()
{
using (var s = SeleniumTester.Create())
{
s.Server.ActivateLightning();
await s.StartAsync();
s.GoToRegister();
s.RegisterNewUser(true);
(string storeName, string storeId) = s.CreateNewStore();
s.AddLightningNode();
s.GoToStore(storeId);
s.Driver.FindElement(By.Id("Modify-LightningBTC")).Click();
s.Driver.SetCheckbox(By.Id("LightningAmountInSatoshi"), true);
s.Driver.FindElement(By.Id("save")).Click();
Assert.Contains("BTC Lightning settings successfully updated", s.FindAlertMessage().Text);
var invoiceId = s.CreateInvoice(storeName, 10, "USD", "a@g.com");
s.GoToInvoiceCheckout(invoiceId);
Assert.Contains("Sats", s.Driver.FindElement(By.ClassName("payment__currencies_noborder")).Text);
}
}
@ -141,7 +222,7 @@ namespace BTCPayServer.Tests
var invoiceId = s.CreateInvoice(store.storeId, 0.001m, "BTC", "a@x.com");
var invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice(invoiceId);
s.Driver.Navigate()
.GoToUrl(new Uri(s.Server.PayTester.ServerUri, $"tests/index.html?invoice={invoiceId}"));
.GoToUrl(new Uri(s.ServerUri, $"tests/index.html?invoice={invoiceId}"));
TestUtils.Eventually(() =>
{
Assert.True(s.Driver.FindElement(By.Name("btcpay")).Displayed);
@ -162,7 +243,7 @@ namespace BTCPayServer.Tests
closebutton.Click();
s.Driver.AssertElementNotFound(By.Name("btcpay"));
Assert.Equal(s.Driver.Url,
new Uri(s.Server.PayTester.ServerUri, $"tests/index.html?invoice={invoiceId}").ToString());
new Uri(s.ServerUri, $"tests/index.html?invoice={invoiceId}").ToString());
}
}
}

View File

@ -38,8 +38,8 @@ namespace BTCPayServer.Tests
var apps2 = user2.GetController<AppsController>();
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model);
Assert.NotNull(vm.SelectedAppType);
Assert.Null(vm.Name);
vm.Name = "test";
Assert.Null(vm.AppName);
vm.AppName = "test";
vm.SelectedAppType = AppType.Crowdfund.ToString();
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.CreateApp(vm).Result);
Assert.Equal(nameof(apps.UpdateCrowdfund), redirectToAction.ActionName);
@ -75,7 +75,7 @@ namespace BTCPayServer.Tests
user.RegisterDerivationScheme("BTC");
var apps = user.GetController<AppsController>();
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model);
vm.Name = "test";
vm.AppName = "test";
vm.SelectedAppType = AppType.Crowdfund.ToString();
Assert.IsType<RedirectToActionResult>(apps.CreateApp(vm).Result);
var appId = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model)
@ -163,12 +163,12 @@ namespace BTCPayServer.Tests
{
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess();
await user.GrantAccessAsync();
user.RegisterDerivationScheme("BTC");
await user.ModifyStore(s => s.NetworkFeeMode = NetworkFeeMode.Never);
await user.SetNetworkFeeMode(NetworkFeeMode.Never);
var apps = user.GetController<AppsController>();
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model);
vm.Name = "test";
vm.AppName = "test";
vm.SelectedAppType = AppType.Crowdfund.ToString();
Assert.IsType<RedirectToActionResult>(apps.CreateApp(vm).Result);
var appId = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model)

View File

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

View File

@ -48,6 +48,22 @@ namespace BTCPayServer.Tests
return Assert.IsType<T>(vr.Model);
}
// Sometimes, selenium is flaky...
public static IWebElement FindElementUntilNotStaled(this IWebDriver driver, By by, Action<IWebElement> act)
{
retry:
try
{
var el = driver.FindElement(by);
act(el);
return el;
}
catch (StaleElementReferenceException)
{
goto retry;
}
}
public static void AssertElementNotFound(this IWebDriver driver, By by)
{
DateTimeOffset now = DateTimeOffset.Now;
@ -115,7 +131,14 @@ namespace BTCPayServer.Tests
var element = driver.FindElement(selector);
if ((value && !element.Selected) || (!value && element.Selected))
{
driver.WaitForAndClick(selector);
try
{
driver.WaitForAndClick(selector);
}
catch (ElementClickInterceptedException)
{
element.SendKeys(" ");
}
}
if (value != element.Selected)

View File

@ -440,9 +440,11 @@ namespace BTCPayServer.Tests
Assert.Null(payout.PaymentMethodAmount);
Logs.Tester.LogInformation("Can't overdraft");
var destination2 = (await tester.ExplorerNode.GetNewAddressAsync()).ToString();
await this.AssertAPIError("overdraft", async () => await unauthenticated.CreatePayout(pps[0].Id, new CreatePayoutRequest()
{
Destination = destination,
Destination = destination2,
Amount = 0.00001m,
PaymentMethod = "BTC"
}));
@ -450,7 +452,7 @@ namespace BTCPayServer.Tests
Logs.Tester.LogInformation("Can't create too low payout");
await this.AssertAPIError("amount-too-low", async () => await unauthenticated.CreatePayout(pps[0].Id, new CreatePayoutRequest()
{
Destination = destination,
Destination = destination2,
PaymentMethod = "BTC"
}));
@ -781,9 +783,10 @@ namespace BTCPayServer.Tests
var newDeliveryId = await clientProfile.RedeliverWebhook(user.StoreId, hook.Id, delivery.Id);
req = await fakeServer.GetNextRequest();
req.Response.StatusCode = 404;
fakeServer.Done();
await TestUtils.EventuallyAsync(async () =>
{
// Releasing semaphore several times may help making this test less flaky
fakeServer.Done();
var newDelivery = await clientProfile.GetWebhookDelivery(user.StoreId, hook.Id, newDeliveryId);
Assert.NotNull(newDelivery);
Assert.Equal(404, newDelivery.HttpCode);
@ -1023,6 +1026,38 @@ namespace BTCPayServer.Tests
}
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanOverpayInvoice()
{
using (var tester = ServerTester.Create())
{
await tester.StartAsync();
var user = tester.NewAccount();
await user.RegisterDerivationSchemeAsync("BTC");
var client = await user.CreateClient();
var invoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest() { Amount = 5000.0m, Currency = "USD" });
var methods = await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id);
var method = methods.First();
var amount = method.Amount;
Assert.Equal(amount, method.Due);
#pragma warning disable CS0618 // Type or member is obsolete
var btc = tester.NetworkProvider.BTC.NBitcoinNetwork;
#pragma warning restore CS0618 // Type or member is obsolete
await tester.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(method.Destination, btc), Money.Coins(method.Due) + Money.Coins(1.0m));
await TestUtils.EventuallyAsync(async () =>
{
invoice = await client.GetInvoice(user.StoreId, invoice.Id);
Assert.True(invoice.Status == InvoiceStatus.Processing);
methods = await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id);
method = methods.First();
Assert.Equal(amount, method.Amount);
Assert.Equal(-1.0m, method.Due);
Assert.Equal(amount + 1.0m, method.TotalPaid);
});
}
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task InvoiceTests()
@ -1040,7 +1075,7 @@ namespace BTCPayServer.Tests
//create
//validation errors
await AssertValidationError(new[] { nameof(CreateInvoiceRequest.Currency), nameof(CreateInvoiceRequest.Amount), $"{nameof(CreateInvoiceRequest.Checkout)}.{nameof(CreateInvoiceRequest.Checkout.PaymentTolerance)}", $"{nameof(CreateInvoiceRequest.Checkout)}.{nameof(CreateInvoiceRequest.Checkout.PaymentMethods)}[0]" }, async () =>
await AssertValidationError(new[] { nameof(CreateInvoiceRequest.Amount), $"{nameof(CreateInvoiceRequest.Checkout)}.{nameof(CreateInvoiceRequest.Checkout.PaymentTolerance)}", $"{nameof(CreateInvoiceRequest.Checkout)}.{nameof(CreateInvoiceRequest.Checkout.PaymentMethods)}[0]" }, async () =>
{
await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest() { Amount = -1, Checkout = new CreateInvoiceRequest.CheckoutOptions() { PaymentTolerance = -2, PaymentMethods = new[] { "jasaas_sdsad" } } });
});
@ -1059,11 +1094,13 @@ namespace BTCPayServer.Tests
Metadata = JObject.Parse("{\"itemCode\": \"testitem\", \"orderId\": \"testOrder\"}"),
Checkout = new CreateInvoiceRequest.CheckoutOptions()
{
RedirectAutomatically = true
RedirectAutomatically = true,
RequiresRefundEmail = true
},
AdditionalSearchTerms = new string[] { "Banana" }
});
Assert.True(newInvoice.Checkout.RedirectAutomatically);
Assert.True(newInvoice.Checkout.RequiresRefundEmail);
Assert.Equal(user.StoreId, newInvoice.StoreId);
//list
var invoices = await viewOnly.GetInvoices(user.StoreId);
@ -1155,10 +1192,16 @@ namespace BTCPayServer.Tests
//update
newInvoice = await client.CreateInvoice(user.StoreId,
new CreateInvoiceRequest() { Currency = "USD", Amount = 1 });
Assert.Contains(InvoiceStatus.Settled, newInvoice.AvailableStatusesForManualMarking);
Assert.Contains(InvoiceStatus.Invalid, newInvoice.AvailableStatusesForManualMarking);
await client.MarkInvoiceStatus(user.StoreId, newInvoice.Id, new MarkInvoiceStatusRequest()
{
Status = InvoiceStatus.Settled
});
newInvoice = await client.GetInvoice(user.StoreId, newInvoice.Id);
Assert.DoesNotContain(InvoiceStatus.Settled, newInvoice.AvailableStatusesForManualMarking);
Assert.Contains(InvoiceStatus.Invalid, newInvoice.AvailableStatusesForManualMarking);
newInvoice = await client.CreateInvoice(user.StoreId,
new CreateInvoiceRequest() { Currency = "USD", Amount = 1 });
await client.MarkInvoiceStatus(user.StoreId, newInvoice.Id, new MarkInvoiceStatusRequest()
@ -1166,6 +1209,10 @@ namespace BTCPayServer.Tests
Status = InvoiceStatus.Invalid
});
newInvoice = await client.GetInvoice(user.StoreId, newInvoice.Id);
Assert.Contains(InvoiceStatus.Settled, newInvoice.AvailableStatusesForManualMarking);
Assert.DoesNotContain(InvoiceStatus.Invalid, newInvoice.AvailableStatusesForManualMarking);
await AssertHttpError(403, async () =>
{
await viewOnly.UpdateInvoice(user.StoreId, invoice.Id,
@ -1289,6 +1336,66 @@ namespace BTCPayServer.Tests
paymentMethods = await client.GetInvoicePaymentMethods(store.Id, invoice.Id);
Assert.Single(paymentMethods);
Assert.True(paymentMethods.First().Activated);
var invoiceWithdefaultPaymentMethodLN = await client.CreateInvoice(user.StoreId,
new CreateInvoiceRequest()
{
Currency = "USD",
Amount = 100,
Checkout = new CreateInvoiceRequest.CheckoutOptions()
{
PaymentMethods = new[] { "BTC", "BTC-LightningNetwork" },
DefaultPaymentMethod = "BTC_LightningLike"
}
});
Assert.Equal("BTC_LightningLike", invoiceWithdefaultPaymentMethodLN.Checkout.DefaultPaymentMethod);
var invoiceWithdefaultPaymentMethodOnChain = await client.CreateInvoice(user.StoreId,
new CreateInvoiceRequest()
{
Currency = "USD",
Amount = 100,
Checkout = new CreateInvoiceRequest.CheckoutOptions()
{
PaymentMethods = new[] { "BTC", "BTC-LightningNetwork" },
DefaultPaymentMethod = "BTC"
}
});
Assert.Equal("BTC", invoiceWithdefaultPaymentMethodOnChain.Checkout.DefaultPaymentMethod);
store = await client.GetStore(user.StoreId);
store.LazyPaymentMethods = false;
store = await client.UpdateStore(store.Id,
JObject.FromObject(store).ToObject<UpdateStoreRequest>());
//let's see the overdue amount
invoice = await client.CreateInvoice(user.StoreId,
new CreateInvoiceRequest()
{
Currency = "BTC",
Amount = 0.0001m,
Checkout = new CreateInvoiceRequest.CheckoutOptions()
{
PaymentMethods = new[] { "BTC" },
DefaultPaymentMethod = "BTC"
}
});
var pm = Assert.Single(await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id));
Assert.Equal(0.0001m, pm.Due);
await tester.WaitForEvent<NewOnChainTransactionEvent>(async () =>
{
await tester.ExplorerNode.SendToAddressAsync(
BitcoinAddress.Create(pm.Destination, tester.ExplorerClient.Network.NBitcoinNetwork),
new Money(0.0002m, MoneyUnit.BTC));
});
await TestUtils.EventuallyAsync(async () =>
{
var pm = Assert.Single(await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id));
Assert.Single(pm.Payments);
Assert.Equal(-0.0001m, pm.Due);
});
}
}
@ -1440,7 +1547,7 @@ namespace BTCPayServer.Tests
Assert.Empty(await client.GetStoreOnChainPaymentMethods(store.Id));
await AssertHttpError(403, async () =>
{
await viewOnlyClient.UpdateStoreOnChainPaymentMethod(store.Id, "BTC", new OnChainPaymentMethodData() { });
await viewOnlyClient.UpdateStoreOnChainPaymentMethod(store.Id, "BTC", new UpdateOnChainPaymentMethodRequest() { });
});
var xpriv = new Mnemonic("all all all all all all all all all all all all").DeriveExtKey()
@ -1453,15 +1560,15 @@ namespace BTCPayServer.Tests
});
Assert.Equal(firstAddress, (await viewOnlyClient.PreviewProposedStoreOnChainPaymentMethodAddresses(store.Id, "BTC",
new OnChainPaymentMethodData() { Enabled = true, DerivationScheme = xpub })).Addresses.First().Address);
new UpdateOnChainPaymentMethodRequest() { Enabled = true, DerivationScheme = xpub })).Addresses.First().Address);
var method = await client.UpdateStoreOnChainPaymentMethod(store.Id, "BTC",
new OnChainPaymentMethodData() { Enabled = true, DerivationScheme = xpub });
new UpdateOnChainPaymentMethodRequest() { Enabled = true, DerivationScheme = xpub });
Assert.Equal(xpub, method.DerivationScheme);
method = await client.UpdateStoreOnChainPaymentMethod(store.Id, "BTC",
new OnChainPaymentMethodData() { Enabled = true, DerivationScheme = xpub, Label = "lol", AccountKeyPath = RootedKeyPath.Parse("01020304/1/2/3") });
new UpdateOnChainPaymentMethodRequest() { Enabled = true, DerivationScheme = xpub, Label = "lol", AccountKeyPath = RootedKeyPath.Parse("01020304/1/2/3") });
method = await client.GetStoreOnChainPaymentMethod(store.Id, "BTC");
@ -1557,7 +1664,7 @@ namespace BTCPayServer.Tests
Assert.Empty(await adminClient.GetStoreLightningNetworkPaymentMethods(store.Id));
await AssertHttpError(403, async () =>
{
await viewOnlyClient.UpdateStoreLightningNetworkPaymentMethod(store.Id, "BTC", new LightningNetworkPaymentMethodData() { });
await viewOnlyClient.UpdateStoreLightningNetworkPaymentMethod(store.Id, "BTC", new UpdateLightningNetworkPaymentMethodRequest() { });
});
await AssertHttpError(404, async () =>
{
@ -1592,37 +1699,33 @@ namespace BTCPayServer.Tests
{
var ex = await AssertValidationError(new[] { "ConnectionString" }, async () =>
{
await adminClient.UpdateStoreLightningNetworkPaymentMethod(store.Id, "BTC", new LightningNetworkPaymentMethodData()
await adminClient.UpdateStoreLightningNetworkPaymentMethod(store.Id, "BTC", new UpdateLightningNetworkPaymentMethodRequest()
{
ConnectionString = forbidden,
CryptoCode = "BTC",
Enabled = true
});
});
Assert.Contains("btcpay.server.canmodifyserversettings", ex.Message);
// However, the other client should work because he has `btcpay.server.canmodifyserversettings`
await admin2Client.UpdateStoreLightningNetworkPaymentMethod(admin2.StoreId, "BTC", new LightningNetworkPaymentMethodData()
await admin2Client.UpdateStoreLightningNetworkPaymentMethod(admin2.StoreId, "BTC", new UpdateLightningNetworkPaymentMethodRequest()
{
ConnectionString = forbidden,
CryptoCode = "BTC",
Enabled = true
});
}
// Allowed ip should be ok
await adminClient.UpdateStoreLightningNetworkPaymentMethod(store.Id, "BTC", new LightningNetworkPaymentMethodData()
await adminClient.UpdateStoreLightningNetworkPaymentMethod(store.Id, "BTC", new UpdateLightningNetworkPaymentMethodRequest()
{
ConnectionString = "type=clightning;server=tcp://8.8.8.8",
CryptoCode = "BTC",
Enabled = true
});
// If we strip the admin's right, he should not be able to set unsafe anymore, even if the API key is still valid
await admin2.MakeAdmin(false);
await AssertValidationError(new[] { "ConnectionString" }, async () =>
{
await admin2Client.UpdateStoreLightningNetworkPaymentMethod(admin2.StoreId, "BTC", new LightningNetworkPaymentMethodData()
await admin2Client.UpdateStoreLightningNetworkPaymentMethod(admin2.StoreId, "BTC", new UpdateLightningNetworkPaymentMethodRequest()
{
ConnectionString = "type=clightning;server=tcp://127.0.0.1",
CryptoCode = "BTC",
Enabled = true
});
});
@ -1640,14 +1743,22 @@ namespace BTCPayServer.Tests
});
await Assert.ThrowsAsync<GreenFieldValidationException>(async () =>
{
await nonAdminUserClient.UpdateStoreLightningNetworkPaymentMethod(nonAdminUser.StoreId, "BTC", method);
await nonAdminUserClient.UpdateStoreLightningNetworkPaymentMethod(nonAdminUser.StoreId, "BTC", new UpdateLightningNetworkPaymentMethodRequest()
{
Enabled = method.Enabled,
ConnectionString = method.ConnectionString
});
});
settings = await tester.PayTester.GetService<SettingsRepository>().GetSettingAsync<PoliciesSettings>();
settings.AllowLightningInternalNodeForAll = true;
await tester.PayTester.GetService<SettingsRepository>().UpdateSetting(settings);
await nonAdminUserClient.UpdateStoreLightningNetworkPaymentMethod(nonAdminUser.StoreId, "BTC", method);
await nonAdminUserClient.UpdateStoreLightningNetworkPaymentMethod(nonAdminUser.StoreId, "BTC", new UpdateLightningNetworkPaymentMethodRequest()
{
Enabled = method.Enabled,
ConnectionString = method.ConnectionString
});
}
[Fact(Timeout = 60 * 2 * 1000)]
@ -1870,10 +1981,13 @@ namespace BTCPayServer.Tests
}
[Fact(Timeout = TestTimeout)]
[Theory(Timeout = TestTimeout)]
[InlineData("DE-de")]
[InlineData("")]
[Trait("Fast", "Fast")]
public void NumericJsonConverterTests()
public void NumericJsonConverterTests(string culture)
{
System.Globalization.CultureInfo.CurrentCulture = System.Globalization.CultureInfo.GetCultureInfo(culture);
JsonReader Get(string val)
{
return new JsonTextReader(new StringReader(val));
@ -1923,16 +2037,17 @@ namespace BTCPayServer.Tests
var admin = tester.NewAccount();
await admin.GrantAccessAsync(true);
var adminClient = await admin.CreateClient(Policies.Unrestricted);
var viewerOnlyClient = await admin.CreateClient(Policies.CanViewStoreSettings);
var store = await adminClient.GetStore(admin.StoreId);
Assert.Empty(await adminClient.GetStorePaymentMethods(store.Id));
await adminClient.UpdateStoreLightningNetworkPaymentMethodToInternalNode(admin.StoreId, "BTC");
await adminClient.UpdateStoreLightningNetworkPaymentMethod(admin.StoreId, "BTC", new UpdateLightningNetworkPaymentMethodRequest("Internal Node", true));
void VerifyLightning(Dictionary<string, GenericPaymentMethodData> dictionary)
{
Assert.True(dictionary.TryGetValue(new PaymentMethodId("BTC", PaymentTypes.LightningLike).ToStringNormalized(), out var item));
var lightningNetworkPaymentMethodBaseData =Assert.IsType<JObject>(item.Data).ToObject<LightningNetworkPaymentMethodBaseData>();
var lightningNetworkPaymentMethodBaseData = Assert.IsType<JObject>(item.Data).ToObject<LightningNetworkPaymentMethodBaseData>();
Assert.Equal("Internal Node", lightningNetworkPaymentMethodBaseData.ConnectionString);
}
@ -1942,12 +2057,12 @@ namespace BTCPayServer.Tests
var randK = new Mnemonic(Wordlist.English, WordCount.Twelve).DeriveExtKey().Neuter().ToString(Network.RegTest);
await adminClient.UpdateStoreOnChainPaymentMethod(admin.StoreId, "BTC",
new OnChainPaymentMethodData("BTC", randK, true, "testing", null));
new UpdateOnChainPaymentMethodRequest(true, randK, "testing", null));
void VerifyOnChain(Dictionary<string, GenericPaymentMethodData> dictionary)
{
Assert.True(dictionary.TryGetValue(new PaymentMethodId("BTC", PaymentTypes.BTCLike).ToStringNormalized(), out var item));
var paymentMethodBaseData =Assert.IsType<JObject>(item.Data).ToObject<OnChainPaymentMethodBaseData>();
var paymentMethodBaseData = Assert.IsType<JObject>(item.Data).ToObject<OnChainPaymentMethodBaseData>();
Assert.Equal(randK, paymentMethodBaseData.DerivationScheme);
}
@ -1956,8 +2071,30 @@ namespace BTCPayServer.Tests
VerifyLightning(methods);
VerifyOnChain(methods);
methods = await viewerOnlyClient.GetStorePaymentMethods(store.Id);
VerifyLightning(methods);
await adminClient.UpdateStoreLightningNetworkPaymentMethod(store.Id, "BTC",
new UpdateLightningNetworkPaymentMethodRequest(
tester.GetLightningConnectionString(LightningConnectionType.CLightning, true), true));
methods = await viewerOnlyClient.GetStorePaymentMethods(store.Id);
Assert.True(methods.TryGetValue(new PaymentMethodId("BTC", PaymentTypes.LightningLike).ToStringNormalized(), out var item));
var lightningNetworkPaymentMethodBaseData =Assert.IsType<JObject>(item.Data).ToObject<LightningNetworkPaymentMethodBaseData>();
Assert.Equal("*NEED CanModifyStoreSettings PERMISSION TO VIEW*", lightningNetworkPaymentMethodBaseData.ConnectionString);
methods = await adminClient.GetStorePaymentMethods(store.Id);
Assert.True(methods.TryGetValue(new PaymentMethodId("BTC", PaymentTypes.LightningLike).ToStringNormalized(), out item));
lightningNetworkPaymentMethodBaseData =Assert.IsType<JObject>(item.Data).ToObject<LightningNetworkPaymentMethodBaseData>();
Assert.NotEqual("*NEED CanModifyStoreSettings PERMISSION TO VIEW*", lightningNetworkPaymentMethodBaseData.ConnectionString);
}
}
}

View File

@ -0,0 +1,71 @@
using System.Threading.Tasks;
using BTCPayServer.Controllers;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Services.Apps;
using BTCPayServer.Tests.Logging;
using Microsoft.AspNetCore.Mvc;
using Xunit;
using Xunit.Abstractions;
using static BTCPayServer.Tests.UnitTest1;
namespace BTCPayServer.Tests
{
public class POSTests
{
public POSTests(ITestOutputHelper helper)
{
Logs.Tester = new XUnitLog(helper) { Name = "Tests" };
Logs.LogProvider = new XUnitLogProvider(helper);
}
[Fact(Timeout = LongRunningTestTimeout)]
public async Task CanUsePoSApp1()
{
using (var tester = ServerTester.Create())
{
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
var apps = user.GetController<AppsController>();
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model);
vm.AppName = "test";
vm.SelectedAppType = AppType.PointOfSale.ToString();
Assert.IsType<RedirectToActionResult>(apps.CreateApp(vm).Result);
var appId = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model)
.Apps[0].Id;
var vmpos = Assert.IsType<UpdatePointOfSaleViewModel>(Assert
.IsType<ViewResult>(apps.UpdatePointOfSale(appId).Result).Model);
vmpos.Template = @"
apple:
price: 5.0
title: good apple
disabled: true
orange:
price: 10.0
donation:
price: 1.02
custom: true
";
Assert.IsType<RedirectToActionResult>(apps.UpdatePointOfSale(appId, vmpos).Result);
vmpos = Assert.IsType<UpdatePointOfSaleViewModel>(Assert
.IsType<ViewResult>(apps.UpdatePointOfSale(appId).Result).Model);
var publicApps = user.GetController<AppsPublicController>();
var vmview =
Assert.IsType<ViewPointOfSaleViewModel>(Assert
.IsType<ViewResult>(publicApps.ViewPointOfSale(appId, PosViewType.Cart).Result).Model);
// apple shouldn't be available since we it's set to "disabled: true" above
Assert.Equal(2, vmview.Items.Length);
Assert.Equal("orange", vmview.Items[0].Title);
Assert.Equal("donation", vmview.Items[1].Title);
// orange is available
Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(appId, PosViewType.Cart, 0, null, null, null, null, "orange").Result);
// apple is not found
Assert.IsType<NotFoundResult>(publicApps
.ViewPointOfSale(appId, PosViewType.Cart, 0, null, null, null, null, "apple").Result);
}
}
}
}

View File

@ -73,26 +73,26 @@ namespace BTCPayServer.Tests
var filePSBT = (FileContentResult)(await walletController.WalletPSBT(walletId, vmPSBT, "save-psbt"));
PSBT.Load(filePSBT.FileContents, user.SupportedNetwork.NBitcoinNetwork);
var vmPSBT2 = await walletController.WalletPSBTReady(walletId, new WalletPSBTReadyViewModel()
var vmPSBT2 = await walletController.WalletPSBT(walletId, new WalletPSBTViewModel
{
SigningContext = new SigningContextModel()
SigningContext = new SigningContextModel
{
PSBT = AssertRedirectedPSBT(await walletController.WalletPSBT(walletId, vmPSBT, "broadcast"), nameof(walletController.WalletPSBTReady))
}
}).AssertViewModelAsync<WalletPSBTReadyViewModel>();
}).AssertViewModelAsync<WalletPSBTViewModel>();
Assert.NotEmpty(vmPSBT2.Inputs.Where(i => i.Error != null));
Assert.Equal(vmPSBT.PSBT, vmPSBT2.SigningContext.PSBT);
var signedPSBT = unsignedPSBT.Clone();
signedPSBT.SignAll(user.DerivationScheme, user.GenerateWalletResponseV.AccountHDKey, user.GenerateWalletResponseV.AccountKeyPath);
vmPSBT.PSBT = signedPSBT.ToBase64();
var psbtReady = await walletController.WalletPSBTReady(walletId, new WalletPSBTReadyViewModel
var psbtReady = await walletController.WalletPSBT(walletId, new WalletPSBTViewModel
{
SigningContext = new SigningContextModel
{
PSBT = AssertRedirectedPSBT(await walletController.WalletPSBT(walletId, vmPSBT, "broadcast"), nameof(walletController.WalletPSBTReady))
}
}).AssertViewModelAsync<WalletPSBTReadyViewModel>();
}).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);
@ -117,10 +117,10 @@ namespace BTCPayServer.Tests
Assert.True(signedPSBT2.TryFinalize(out _));
Assert.Equal(signedPSBT, signedPSBT2);
var ready = (await walletController.WalletPSBTReady(walletId, new WalletPSBTReadyViewModel
var ready = (await walletController.WalletPSBT(walletId, new WalletPSBTViewModel
{
SigningContext = new SigningContextModel(signedPSBT)
})).AssertViewModel<WalletPSBTReadyViewModel>();
})).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);
@ -131,7 +131,7 @@ namespace BTCPayServer.Tests
Assert.False(string.IsNullOrEmpty(Assert.IsType<WalletPSBTViewModel>(
Assert.IsType<ViewResult>(
await walletController.WalletPSBT(walletId,
new WalletPSBTViewModel()
new WalletPSBTViewModel
{
UploadedPSBTFile = TestUtils.GetFormFile("base64", signedPSBT.ToBase64())
})).Model).PSBT));

View File

@ -20,6 +20,7 @@ using BTCPayServer.Views.Wallets;
using Microsoft.AspNetCore.Http;
using NBitcoin;
using BTCPayServer.BIP78.Sender;
using BTCPayServer.Views.Stores;
using NBitcoin.Payment;
using NBitpayClient;
using NBXplorer.DerivationStrategy;
@ -200,7 +201,7 @@ namespace BTCPayServer.Tests
var receiverUser = tester.NewAccount();
receiverUser.GrantAccess(true);
receiverUser.RegisterDerivationScheme("BTC", receiverAddressType, true);
await receiverUser.EnablePayJoin();
await receiverUser.ModifyWalletSettings(p => p.PayJoinEnabled = true);
var receiverCoin = await receiverUser.ReceiveUTXO(Money.Satoshis(810), network);
string errorCode = receiverAddressType == senderAddressType ? null : "unavailable|any UTXO available";
@ -255,8 +256,8 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("bip21parse")).Click();
s.Driver.SwitchTo().Alert().SendKeys(bip21);
s.Driver.SwitchTo().Alert().Accept();
s.Driver.FindElement(By.Id("Outputs_0__Amount")).Clear();
s.Driver.FindElement(By.Id("Outputs_0__Amount")).SendKeys("0.023");
s.Driver.FindElementUntilNotStaled(By.Id("Outputs_0__Amount"), we => we.Clear());
s.Driver.FindElementUntilNotStaled(By.Id("Outputs_0__Amount"), we => we.SendKeys("0.023"));
s.Driver.FindElement(By.Id("SignTransaction")).Click();
@ -289,9 +290,10 @@ namespace BTCPayServer.Tests
foreach (var format in new []{ScriptPubKeyType.Segwit, ScriptPubKeyType.SegwitP2SH})
{
var cryptoCode = "BTC";
var receiver = s.CreateNewStore();
var receiverSeed = s.GenerateWallet("BTC", "", true, true, format);
var receiverWalletId = new WalletId(receiver.storeId, "BTC");
var receiverSeed = s.GenerateWallet(cryptoCode, "", true, true, format);
var receiverWalletId = new WalletId(receiver.storeId, cryptoCode);
//payjoin is enabled by default.
var invoiceId = s.CreateInvoice(receiver.storeName);
@ -302,11 +304,12 @@ namespace BTCPayServer.Tests
s.GoToHome();
s.GoToStore(receiver.storeId);
s.Driver.FindElement(By.Id($"Modify{cryptoCode}")).Click();
Assert.True(s.Driver.FindElement(By.Id("PayJoinEnabled")).Selected);
var sender = s.CreateNewStore();
var senderSeed = s.GenerateWallet("BTC", "", true, true, format);
var senderWalletId = new WalletId(sender.storeId, "BTC");
var senderSeed = s.GenerateWallet(cryptoCode, "", true, true, format);
var senderWalletId = new WalletId(sender.storeId, cryptoCode);
await s.Server.ExplorerNode.GenerateAsync(1);
await s.FundStoreWallet(senderWalletId);
@ -570,9 +573,9 @@ namespace BTCPayServer.Tests
address = (await nbx.GetUnusedAsync(bob.DerivationScheme, DerivationFeature.Deposit)).Address;
tester.ExplorerNode.SendToAddress(address, Money.Coins(1.1m));
await notifications.NextEventAsync();
await bob.ModifyStore(s => s.PayJoinEnabled = true);
await bob.ModifyWalletSettings(p => p.PayJoinEnabled = true);
var invoice = bob.BitPay.CreateInvoice(
new Invoice() { Price = 0.1m, Currency = "BTC", FullNotifications = true });
new Invoice { Price = 0.1m, Currency = "BTC", FullNotifications = true });
var invoiceBIP21 = new BitcoinUrlBuilder(invoice.CryptoInfo.First().PaymentUrls.BIP21,
tester.ExplorerClient.Network.NBitcoinNetwork);
@ -659,7 +662,7 @@ namespace BTCPayServer.Tests
var receiverUser = tester.NewAccount();
receiverUser.GrantAccess(true);
receiverUser.RegisterDerivationScheme("BTC", ScriptPubKeyType.Segwit, true);
await receiverUser.EnablePayJoin();
await receiverUser.ModifyWalletSettings(p => p.PayJoinEnabled = true);
var receiverCoin = await receiverUser.ReceiveUTXO(Money.Satoshis(810), network);
string lastInvoiceId = null;
@ -856,7 +859,7 @@ retry:
receiverUser.GrantAccess(true);
receiverUser.RegisterDerivationScheme("BTC", ScriptPubKeyType.Segwit, true);
await receiverUser.EnablePayJoin();
await receiverUser.ModifyWalletSettings(p => p.PayJoinEnabled = true);
// payjoin is enabled, with a segwit wallet, and the keys are available in nbxplorer
invoice = receiverUser.BitPay.CreateInvoice(
new Invoice() { Price = 0.02m, Currency = "BTC", FullNotifications = true });

View File

@ -189,10 +189,20 @@ namespace BTCPayServer.Tests
var response = Assert
.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(null, request).Result)
.RouteValues.First();
var invoiceId = response.Value.ToString();
await paymentRequestController.PayPaymentRequest(invoiceId, false);
Assert.IsType<BadRequestObjectResult>(await
paymentRequestController.CancelUnpaidPendingInvoice(invoiceId, false));
request.AllowCustomPaymentAmounts = true;
response = Assert
.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(null, request).Result)
.RouteValues.First();
var paymentRequestId = response.Value.ToString();
var invoiceId = Assert
invoiceId = Assert
.IsType<OkObjectResult>(await paymentRequestController.PayPaymentRequest(paymentRequestId, false))
.Value
.ToString();
@ -224,12 +234,12 @@ namespace BTCPayServer.Tests
invoice = user.BitPay.GetInvoice(invoiceId, Facade.Merchant);
//a hack to generate invoices for the payment request is to manually create an invocie with an order id that matches:
//a hack to generate invoices for the payment request is to manually create an invoice with an order id that matches:
user.BitPay.CreateInvoice(new Invoice(1, "USD")
{
OrderId = PaymentRequestRepository.GetOrderIdForPaymentRequest(paymentRequestId)
});
//shouldnt crash
//shouldn't crash
await paymentRequestController.ViewPaymentRequest(paymentRequestId);
await paymentRequestController.CancelUnpaidPendingInvoice(paymentRequestId);
}

View File

@ -3,11 +3,11 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Lightning;
using BTCPayServer.Lightning.CLightning;
using BTCPayServer.Services;
using BTCPayServer.Tests.Logging;
using BTCPayServer.Views.Manage;
using BTCPayServer.Views.Server;
@ -18,8 +18,8 @@ using NBitcoin;
using BTCPayServer.BIP78.Sender;
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Support.Extensions;
using Xunit;
using OpenQA.Selenium.Support.UI;
namespace BTCPayServer.Tests
{
@ -38,6 +38,7 @@ namespace BTCPayServer.Tests
public async Task StartAsync()
{
Server.PayTester.NoCSP = true;
await Server.StartAsync();
var windowSize = (Width: 1200, Height: 1000);
@ -52,11 +53,6 @@ namespace BTCPayServer.Tests
var chromeDriverPath = config["ChromeDriverDirectory"] ?? (Server.PayTester.InContainer ? "/usr/bin" : Directory.GetCurrentDirectory());
var options = new ChromeOptions();
if (Server.PayTester.InContainer)
{
// this must be first option https://stackoverflow.com/questions/53073411/selenium-webdriverexceptionchrome-failed-to-start-crashed-as-google-chrome-is#comment102570662_53073789
options.AddArgument("no-sandbox");
}
if (!runInBrowser)
{
options.AddArguments("headless");
@ -64,28 +60,48 @@ namespace BTCPayServer.Tests
options.AddArguments($"window-size={windowSize.Width}x{windowSize.Height}");
options.AddArgument("shm-size=2g");
options.AddArgument("start-maximized");
var cds = ChromeDriverService.CreateDefaultService(chromeDriverPath);
cds.EnableVerboseLogging = true;
cds.Port = Utils.FreeTcpPort();
cds.HostName = "127.0.0.1";
cds.Start();
Driver = new ChromeDriver(cds, options,
// A bit less than test timeout
TimeSpan.FromSeconds(50));
if (Server.PayTester.InContainer)
{
Driver = new OpenQA.Selenium.Remote.RemoteWebDriver(new Uri("http://selenium:4444/wd/hub"), new RemoteSessionSettings(options));
var containerIp = File.ReadAllText("/etc/hosts").Split('\n', StringSplitOptions.RemoveEmptyEntries).Last()
.Split('\t', StringSplitOptions.RemoveEmptyEntries)[0].Trim();
Logs.Tester.LogInformation($"Selenium: Container's IP {containerIp}");
ServerUri = new Uri(Server.PayTester.ServerUri.AbsoluteUri.Replace($"http://{Server.PayTester.HostName}", $"http://{containerIp}", StringComparison.OrdinalIgnoreCase), UriKind.Absolute);
}
else
{
var cds = ChromeDriverService.CreateDefaultService(chromeDriverPath);
cds.EnableVerboseLogging = true;
cds.Port = Utils.FreeTcpPort();
cds.HostName = "127.0.0.1";
cds.Start();
Driver = new ChromeDriver(cds, options,
// A bit less than test timeout
TimeSpan.FromSeconds(50));
ServerUri = Server.PayTester.ServerUri;
}
Driver.Manage().Window.Maximize();
Logs.Tester.LogInformation($"Selenium: Using {Driver.GetType()}");
Logs.Tester.LogInformation($"Selenium: Browsing to {Server.PayTester.ServerUri}");
Logs.Tester.LogInformation($"Selenium: Browsing to {ServerUri}");
Logs.Tester.LogInformation($"Selenium: Resolution {Driver.Manage().Window.Size}");
GoToRegister();
Driver.AssertNoError();
}
/// <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
/// </summary>
public Uri ServerUri;
internal IWebElement FindAlertMessage(StatusMessageModel.StatusSeverity severity = StatusMessageModel.StatusSeverity.Success)
{
var className = $"alert-{StatusMessageModel.ToString(severity)}";
var el = Driver.FindElement(By.ClassName(className)) ?? Driver.WaitForElement(By.ClassName(className));
return FindAlertMessage(new[] {severity});
}
internal IWebElement FindAlertMessage(params StatusMessageModel.StatusSeverity[] severity)
{
var className = string.Join(", ", severity.Select(statusSeverity => $".alert-{StatusMessageModel.ToString(statusSeverity)}"));
var el = Driver.FindElement(By.CssSelector(className)) ?? Driver.WaitForElement(By.CssSelector(className));
if (el is null)
throw new NoSuchElementException($"Unable to find {className}");
return el;
@ -93,7 +109,7 @@ namespace BTCPayServer.Tests
public string Link(string relativeLink)
{
return Server.PayTester.ServerUri.AbsoluteUri.WithoutEndingSlash() + relativeLink.WithStartingSlash();
return ServerUri.AbsoluteUri.WithoutEndingSlash() + relativeLink.WithStartingSlash();
}
public void GoToRegister()
@ -115,15 +131,19 @@ namespace BTCPayServer.Tests
return usr;
}
public (string storeName, string storeId) CreateNewStore()
public (string storeName, string storeId) CreateNewStore(bool keepId = true)
{
Driver.WaitForElement(By.Id("Stores")).Click();
Driver.WaitForElement(By.Id("CreateStore")).Click();
var name = "Store" + RandomUtils.GetUInt64();
Driver.WaitForElement(By.Id("Name")).SendKeys(name);
Driver.WaitForElement(By.Id("Create")).Click();
StoreId = Driver.WaitForElement(By.Id("Id")).GetAttribute("value");
return (name, StoreId);
Driver.FindElement(By.Id($"Nav-{StoreNavPages.GeneralSettings.ToString()}")).Click();
var storeId = Driver.WaitForElement(By.Id("Id")).GetAttribute("value");
Driver.FindElement(By.Id($"Nav-{StoreNavPages.PaymentMethods.ToString()}")).Click();
if (keepId)
StoreId = storeId;
return (name, storeId);
}
public Mnemonic GenerateWallet(string cryptoCode = "BTC", string seed = "", bool importkeys = false, bool privkeys = false, ScriptPubKeyType format = ScriptPubKeyType.Segwit)
@ -135,7 +155,8 @@ namespace BTCPayServer.Tests
if (Driver.PageSource.Contains("id=\"ChangeWalletLink\""))
{
Driver.FindElement(By.Id("ChangeWalletLink")).Click();
Driver.FindElement(By.Id("continue")).Click();
Driver.WaitForElement(By.Id("ConfirmInput")).SendKeys("REPLACE");
Driver.FindElement(By.Id("ConfirmContinue")).Click();
}
if (isImport)
@ -195,10 +216,15 @@ namespace BTCPayServer.Tests
FindAlertMessage();
}
public void AddLightningNode(string cryptoCode = "BTC", LightningConnectionType? connectionType = null)
public void AddLightningNode(string cryptoCode = "BTC", LightningConnectionType? connectionType = null, bool test = true)
{
Driver.FindElement(By.Id($"Modify-Lightning{cryptoCode}")).Click();
if (Driver.PageSource.Contains("id=\"SetupLightningNodeLink\""))
{
Driver.FindElement(By.Id($"SetupLightningNodeLink")).Click();
}
var connectionString = connectionType switch
{
LightningConnectionType.Charge =>
@ -218,10 +244,13 @@ namespace BTCPayServer.Tests
else
{
Driver.FindElement(By.CssSelector("label[for=\"LightningNodeType-Custom\"]")).Click();
Driver.FindElement(By.Id("ConnectionString")).Clear();
Driver.FindElement(By.Id("ConnectionString")).SendKeys(connectionString);
Driver.FindElement(By.Id("test")).Click();
Assert.Contains("Connection to the Lightning node successful.", FindAlertMessage().Text);
if (test)
{
Driver.FindElement(By.Id("test")).Click();
Assert.Contains("Connection to the Lightning node successful.", FindAlertMessage().Text);
}
}
Driver.FindElement(By.Id("save")).Click();
@ -239,7 +268,6 @@ namespace BTCPayServer.Tests
{
var links = Driver.FindElements(By.CssSelector(".nav .nav-link")).Select(c => c.GetAttribute("href")).ToList();
Driver.AssertNoError();
Assert.NotEmpty(links);
foreach (var l in links)
{
Logs.Tester.LogInformation($"Checking no error on {l}");
@ -274,7 +302,7 @@ namespace BTCPayServer.Tests
public void GoToHome()
{
Driver.Navigate().GoToUrl(Server.PayTester.ServerUri);
Driver.Navigate().GoToUrl(ServerUri);
}
public void Logout()
@ -288,20 +316,24 @@ namespace BTCPayServer.Tests
Driver.FindElement(By.Id("Password")).SendKeys(password);
Driver.FindElement(By.Id("LoginButton")).Click();
}
public void GoToApps()
{
Driver.FindElement(By.Id("Apps")).Click();
}
public void GoToStores()
{
Driver.FindElement(By.Id("Stores")).Click();
}
public void GoToStore(string storeId, StoreNavPages storeNavPage = StoreNavPages.Index)
public void GoToStore(string storeId, StoreNavPages storeNavPage = StoreNavPages.PaymentMethods)
{
Driver.FindElement(By.Id("Stores")).Click();
GoToHome();
Driver.WaitForAndClick(By.Id("Stores"));
Driver.FindElement(By.Id($"update-store-{storeId}")).Click();
if (storeNavPage != StoreNavPages.Index)
if (storeNavPage != StoreNavPages.PaymentMethods)
{
Driver.FindElement(By.Id(storeNavPage.ToString())).Click();
Driver.FindElement(By.Id($"Nav-{storeNavPage.ToString()}")).Click();
}
}
@ -328,10 +360,18 @@ namespace BTCPayServer.Tests
public void GoToLogin()
{
Driver.Navigate().GoToUrl(new Uri(Server.PayTester.ServerUri, "/login"));
Driver.Navigate().GoToUrl(new Uri(ServerUri, "/login"));
}
public string CreateInvoice(string storeName, decimal? amount = 100, string currency = "USD", string refundEmail = "")
public string CreateInvoice(
string storeName,
decimal? amount = 100,
string currency = "USD",
string refundEmail = "",
string defaultPaymentMethod = null,
bool? requiresRefundEmail = null,
StatusMessageModel.StatusSeverity expectedSeverity = StatusMessageModel.StatusSeverity.Success
)
{
GoToInvoices();
Driver.FindElement(By.Id("CreateNewInvoice")).Click();
@ -342,11 +382,14 @@ namespace BTCPayServer.Tests
currencyEl.SendKeys(currency);
Driver.FindElement(By.Id("BuyerEmail")).SendKeys(refundEmail);
Driver.FindElement(By.Name("StoreId")).SendKeys(storeName);
if (defaultPaymentMethod is string)
new SelectElement(Driver.FindElement(By.Name("DefaultPaymentMethod"))).SelectByValue(defaultPaymentMethod);
if (requiresRefundEmail is bool)
new SelectElement(Driver.FindElement(By.Name("RequiresRefundEmail"))).SelectByValue(requiresRefundEmail == true ? "1" : "2");
Driver.FindElement(By.Id("Create")).Click();
var statusElement = FindAlertMessage();
var id = statusElement.Text.Split(" ")[1];
return id;
var statusElement = FindAlertMessage(expectedSeverity);
return expectedSeverity == StatusMessageModel.StatusSeverity.Success ? statusElement.Text.Split(" ")[1] : null;
}
public async Task FundStoreWallet(WalletId walletId = null, int coins = 1, decimal denomination = 1m)
@ -404,7 +447,7 @@ namespace BTCPayServer.Tests
public void GoToWallet(WalletId walletId = null, WalletsNavPages navPages = WalletsNavPages.Send)
{
walletId ??= WalletId;
Driver.Navigate().GoToUrl(new Uri(Server.PayTester.ServerUri, $"wallets/{walletId}"));
Driver.Navigate().GoToUrl(new Uri(ServerUri, $"wallets/{walletId}"));
if (navPages != WalletsNavPages.Transactions)
{
Driver.FindElement(By.Id($"Wallet{navPages}")).Click();
@ -413,7 +456,7 @@ namespace BTCPayServer.Tests
public void GoToUrl(string relativeUrl)
{
Driver.Navigate().GoToUrl(new Uri(Server.PayTester.ServerUri, relativeUrl));
Driver.Navigate().GoToUrl(new Uri(ServerUri, relativeUrl));
}
public void GoToServer(ServerNavPages navPages = ServerNavPages.Index)

View File

@ -2,19 +2,30 @@ using System;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Net.Security;
using System.Security.Authentication;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Lightning;
using BTCPayServer.Lightning.Charge;
using BTCPayServer.Lightning.CLightning;
using BTCPayServer.Lightning.LND;
using BTCPayServer.Payments;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Wallets;
using BTCPayServer.Tests.Logging;
using BTCPayServer.Views.Manage;
using BTCPayServer.Views.Server;
using BTCPayServer.Views.Stores;
using BTCPayServer.Views.Wallets;
using LNURL;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using NBitcoin;
@ -27,6 +38,7 @@ using OpenQA.Selenium.Support.UI;
using Renci.SshNet.Security.Cryptography;
using Xunit;
using Xunit.Abstractions;
using CreateInvoiceRequest = BTCPayServer.Lightning.Charge.CreateInvoiceRequest;
namespace BTCPayServer.Tests
{
@ -85,7 +97,8 @@ namespace BTCPayServer.Tests
Assert.True(passEl.Displayed);
Assert.Contains(passEl.Text, "hellorockstar", StringComparison.OrdinalIgnoreCase);
s.Driver.FindElement(By.Id("delete")).Click();
s.Driver.FindElement(By.Id("continue")).Click();
s.Driver.WaitForElement(By.Id("ConfirmInput")).SendKeys("DELETE");
s.Driver.FindElement(By.Id("ConfirmContinue")).Click();
s.FindAlertMessage();
seedEl = s.Driver.FindElement(By.Id("Seed"));
Assert.Contains("Seed removed", seedEl.Text, StringComparison.OrdinalIgnoreCase);
@ -205,6 +218,15 @@ namespace BTCPayServer.Tests
// We should be logged in now
s.Driver.FindElement(By.Id("mainNav"));
//let's test delete user quickly while we're at it
s.GoToProfile();
s.Driver.FindElement(By.Id("danger-zone-expander")).Click();
s.Driver.FindElement(By.Id("delete-user")).Click();
s.Driver.WaitForElement(By.Id("ConfirmInput")).SendKeys("DELETE");
s.Driver.FindElement(By.Id("ConfirmContinue")).Click();
Assert.Contains("/login", s.Driver.Url);
}
}
@ -221,13 +243,15 @@ namespace BTCPayServer.Tests
s.RegisterNewUser(isAdmin: true);
s.Driver.Navigate().GoToUrl(s.Link("/server/services"));
Assert.Contains("server/services/ssh", s.Driver.PageSource);
using (var client = await s.Server.PayTester.GetService<Configuration.BTCPayServerOptions>().SSHSettings.ConnectAsync())
using (var client = await s.Server.PayTester.GetService<Configuration.BTCPayServerOptions>().SSHSettings
.ConnectAsync())
{
var result = await client.RunBash("echo hello");
Assert.Equal(string.Empty, result.Error);
Assert.Equal("hello\n", result.Output);
Assert.Equal(0, result.ExitStatus);
}
s.Driver.Navigate().GoToUrl(s.Link("/server/services/ssh"));
s.Driver.AssertNoError();
s.Driver.FindElement(By.Id("SSHKeyFileContent")).Clear();
@ -239,7 +263,8 @@ namespace BTCPayServer.Tests
// Browser replace \n to \r\n, so it is hard to compare exactly what we want
Assert.Contains("tes't", text);
Assert.Contains("test2", text);
Assert.True(s.Driver.PageSource.Contains("authorized_keys has been updated", StringComparison.OrdinalIgnoreCase));
Assert.True(s.Driver.PageSource.Contains("authorized_keys has been updated",
StringComparison.OrdinalIgnoreCase));
s.Driver.FindElement(By.Id("SSHKeyFileContent")).Clear();
s.Driver.FindElement(By.Id("submit")).Click();
@ -249,12 +274,14 @@ namespace BTCPayServer.Tests
// Let's try to disable it now
s.Driver.FindElement(By.Id("disable")).Click();
s.Driver.FindElement(By.Id("continue")).Click();
s.Driver.WaitForElement(By.Id("ConfirmInput")).SendKeys("DISABLE");
s.Driver.FindElement(By.Id("ConfirmContinue")).Click();
s.Driver.Navigate().GoToUrl(s.Link("/server/services/ssh"));
Assert.True(s.Driver.PageSource.Contains("404 - Page not found", StringComparison.OrdinalIgnoreCase));
policies = await settings.GetSettingAsync<PoliciesSettings>();
Assert.True(policies.DisableSSHService);
s.Driver.Navigate().GoToUrl(s.Link("/server/services/ssh"));
Assert.True(s.Driver.PageSource.Contains("404 - Page not found", StringComparison.OrdinalIgnoreCase));
policies.DisableSSHService = false;
await settings.UpdateSetting(policies);
}
@ -273,6 +300,7 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.CssSelector("button[value=\"ResetPassword\"]")).Submit();
s.FindAlertMessage();
}
CanSetupEmailCore(s);
s.CreateNewStore();
s.GoToUrl($"stores/{s.StoreId}/emails");
@ -296,8 +324,9 @@ namespace BTCPayServer.Tests
{
// Cleanup old test run
s.Driver.Navigate().GoToUrl(s.Link("/server/services/dynamic-dns/pouet.hello.com/delete"));
s.Driver.FindElement(By.Id("continue")).Click();
s.Driver.FindElement(By.Id("ConfirmContinue")).Click();
}
s.Driver.FindElement(By.Id("AddDynamicDNS")).Click();
s.Driver.AssertNoError();
// We will just cheat for test purposes by only querying the server
@ -323,7 +352,7 @@ namespace BTCPayServer.Tests
s.Driver.Navigate().GoToUrl(s.Link("/server/services/dynamic-dns"));
Assert.Contains("/server/services/dynamic-dns/pouet.hello.com/delete", s.Driver.PageSource);
s.Driver.Navigate().GoToUrl(s.Link("/server/services/dynamic-dns/pouet.hello.com/delete"));
s.Driver.FindElement(By.Id("continue")).Click();
s.Driver.FindElement(By.Id("ConfirmContinue")).Click();
s.Driver.AssertNoError();
Assert.DoesNotContain("/server/services/dynamic-dns/pouet.hello.com/delete", s.Driver.PageSource);
@ -353,13 +382,15 @@ namespace BTCPayServer.Tests
s.GoToStore(storeId);
Assert.Contains(storeName, s.Driver.PageSource);
Assert.True(s.Driver.PageSource.Contains(onchainHint), "Wallet hint should be present at this point");
Assert.True(s.Driver.PageSource.Contains(offchainHint), "Lightning hint should be present at this point");
Assert.True(s.Driver.PageSource.Contains(offchainHint),
"Lightning hint should be present at this point");
// setup onchain wallet
s.GoToStore(storeId);
s.AddDerivationScheme();
s.Driver.AssertNoError();
Assert.False(s.Driver.PageSource.Contains(onchainHint), "Wallet hint not dismissed on derivation scheme add");
Assert.False(s.Driver.PageSource.Contains(onchainHint),
"Wallet hint not dismissed on derivation scheme add");
// setup offchain wallet
s.GoToStore(storeId);
@ -367,7 +398,8 @@ namespace BTCPayServer.Tests
s.Driver.AssertNoError();
var successAlert = s.FindAlertMessage();
Assert.Contains("BTC Lightning node updated.", successAlert.Text);
Assert.False(s.Driver.PageSource.Contains(offchainHint), "Lightning hint should be dismissed at this point");
Assert.False(s.Driver.PageSource.Contains(offchainHint),
"Lightning hint should be dismissed at this point");
var storeUrl = s.Driver.Url;
s.ClickOnAllSideMenus();
@ -427,8 +459,9 @@ namespace BTCPayServer.Tests
s.Logout();
s.LogIn(alice);
s.Driver.FindElement(By.Id("Stores")).Click();
s.Driver.FindElement(By.LinkText("Remove")).Click();
s.Driver.FindElement(By.Id("continue")).Click();
s.Driver.FindElement(By.LinkText("Delete")).Click();
s.Driver.WaitForElement(By.Id("ConfirmInput")).SendKeys("DELETE");
s.Driver.FindElement(By.Id("ConfirmContinue")).Click();
s.Driver.FindElement(By.Id("Stores")).Click();
s.Driver.Navigate().GoToUrl(storeUrl);
Assert.Contains("ReturnUrl", s.Driver.Url);
@ -448,7 +481,7 @@ namespace BTCPayServer.Tests
s.CreateNewStore();
s.AddDerivationScheme();
s.Driver.FindElement(By.Id("Tokens")).Click();
s.Driver.FindElement(By.Id("Nav-Tokens")).Click();
s.Driver.FindElement(By.Id("CreateNewToken")).Click();
s.Driver.FindElement(By.Id("RequestPairing")).Click();
var pairingCode = AssertUrlHasPairingCode(s);
@ -457,27 +490,21 @@ namespace BTCPayServer.Tests
s.FindAlertMessage();
Assert.Contains(pairingCode, s.Driver.PageSource);
var client = new NBitpayClient.Bitpay(new Key(), s.Server.PayTester.ServerUri);
var client = new NBitpayClient.Bitpay(new Key(), s.ServerUri);
await client.AuthorizeClient(new NBitpayClient.PairingCode(pairingCode));
await client.CreateInvoiceAsync(new NBitpayClient.Invoice()
{
Price = 0.000000012m,
Currency = "USD",
FullNotifications = true
}, NBitpayClient.Facade.Merchant);
await client.CreateInvoiceAsync(
new NBitpayClient.Invoice() { Price = 0.000000012m, Currency = "USD", FullNotifications = true },
NBitpayClient.Facade.Merchant);
client = new NBitpayClient.Bitpay(new Key(), s.Server.PayTester.ServerUri);
client = new NBitpayClient.Bitpay(new Key(), s.ServerUri);
var code = await client.RequestClientAuthorizationAsync("hehe", NBitpayClient.Facade.Merchant);
s.Driver.Navigate().GoToUrl(code.CreateLink(s.Server.PayTester.ServerUri));
s.Driver.Navigate().GoToUrl(code.CreateLink(s.ServerUri));
s.Driver.FindElement(By.Id("ApprovePairing")).Click();
await client.CreateInvoiceAsync(new NBitpayClient.Invoice()
{
Price = 0.000000012m,
Currency = "USD",
FullNotifications = true
}, NBitpayClient.Facade.Merchant);
await client.CreateInvoiceAsync(
new NBitpayClient.Invoice() { Price = 0.000000012m, Currency = "USD", FullNotifications = true },
NBitpayClient.Facade.Merchant);
s.Driver.Navigate().GoToUrl(s.Link("/api-tokens"));
s.Driver.FindElement(By.Id("RequestPairing")).Click();
@ -497,7 +524,7 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("Apps")).Click();
s.Driver.FindElement(By.Id("CreateNewApp")).Click();
s.Driver.FindElement(By.Name("Name")).SendKeys("PoS" + Guid.NewGuid());
s.Driver.FindElement(By.Name("AppName")).SendKeys("PoS" + Guid.NewGuid());
s.Driver.FindElement(By.Id("SelectedAppType")).SendKeys("Point of Sale");
s.Driver.FindElement(By.Id("SelectedStore")).SendKeys(storeName);
s.Driver.FindElement(By.Id("Create")).Click();
@ -538,7 +565,7 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("Apps")).Click();
s.Driver.FindElement(By.Id("CreateNewApp")).Click();
s.Driver.FindElement(By.Name("Name")).SendKeys("CF" + Guid.NewGuid());
s.Driver.FindElement(By.Name("AppName")).SendKeys("CF" + Guid.NewGuid());
s.Driver.FindElement(By.Id("SelectedAppType")).SendKeys("Crowdfund");
s.Driver.FindElement(By.Id("SelectedStore")).SendKeys(storeName);
s.Driver.FindElement(By.Id("Create")).Click();
@ -548,7 +575,8 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("TargetAmount")).SendKeys("700");
s.Driver.FindElement(By.Id("SaveSettings")).Click();
s.Driver.FindElement(By.Id("ViewApp")).Click();
Assert.Equal("Currently Active!", s.Driver.FindElement(By.CssSelector(".h6.text-muted")).Text);
Assert.Equal("currently active!",
s.Driver.FindElement(By.CssSelector("[data-test='time-state']")).Text);
}
}
@ -571,22 +599,26 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Name("ViewAppButton")).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", s.Driver.FindElement(By.CssSelector("[data-test='pay-button']")).Text.Trim());
Assert.Equal("Pay Invoice",
s.Driver.FindElement(By.CssSelector("[data-test='pay-button']")).Text.Trim());
// expire
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
s.Driver.ExecuteJavaScript("document.getElementById('ExpiryDate').value = '2021-01-21T21:00:00.000Z'");
s.Driver.FindElement(By.Id("SaveButton")).Click();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
Assert.Equal("Expired", s.Driver.FindElement(By.CssSelector("[data-test='status']")).Text);
s.Driver.Navigate().Refresh();
Assert.Equal("Expired", s.Driver.WaitForElement(By.CssSelector("[data-test='status']")).Text);
// unexpire
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
s.Driver.FindElement(By.Id("ClearExpiryDate")).Click();
s.Driver.FindElement(By.Id("SaveButton")).Click();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
s.Driver.Navigate().Refresh();
s.Driver.AssertElementNotFound(By.CssSelector("[data-test='status']"));
Assert.Equal("Pay Invoice", s.Driver.FindElement(By.CssSelector("[data-test='pay-button']")).Text.Trim());
Assert.Equal("Pay Invoice",
s.Driver.FindElement(By.CssSelector("[data-test='pay-button']")).Text.Trim());
}
}
@ -603,15 +635,18 @@ namespace BTCPayServer.Tests
s.GoToWallet(walletId, WalletsNavPages.Receive);
s.Driver.FindElement(By.Id("generateButton")).Click();
var addressStr = s.Driver.FindElement(By.Id("address")).GetProperty("value");
var address = BitcoinAddress.Create(addressStr, ((BTCPayNetwork)s.Server.NetworkProvider.GetNetwork("BTC")).NBitcoinNetwork);
var address = BitcoinAddress.Create(addressStr,
((BTCPayNetwork)s.Server.NetworkProvider.GetNetwork("BTC")).NBitcoinNetwork);
await s.Server.ExplorerNode.GenerateAsync(1);
for (int i = 0; i < 6; i++)
{
await s.Server.ExplorerNode.SendToAddressAsync(address, Money.Coins(1.0m));
}
var targetTx = await s.Server.ExplorerNode.SendToAddressAsync(address, Money.Coins(1.2m));
var tx = await s.Server.ExplorerNode.GetRawTransactionAsync(targetTx);
var spentOutpoint = new OutPoint(targetTx, tx.Outputs.FindIndex(txout => txout.Value == Money.Coins(1.2m)));
var spentOutpoint = new OutPoint(targetTx,
tx.Outputs.FindIndex(txout => txout.Value == Money.Coins(1.2m)));
await TestUtils.EventuallyAsync(async () =>
{
var store = await s.Server.PayTester.StoreRepository.FindStore(storeId);
@ -626,10 +661,10 @@ namespace BTCPayServer.Tests
});
await s.Server.ExplorerNode.GenerateAsync(1);
s.GoToWallet(walletId);
s.Driver.ToggleCollapse("AdvancedSettings");
s.Driver.WaitForAndClick(By.Id("toggleInputSelection"));
s.Driver.FindElement(By.Id(spentOutpoint.ToString()));
Assert.Equal("true", s.Driver.FindElement(By.Name("InputSelection")).GetAttribute("value").ToLowerInvariant());
s.Driver.WaitForElement(By.Id(spentOutpoint.ToString()));
Assert.Equal("true",
s.Driver.FindElement(By.Name("InputSelection")).GetAttribute("value").ToLowerInvariant());
var el = s.Driver.FindElement(By.Id(spentOutpoint.ToString()));
s.Driver.FindElement(By.Id(spentOutpoint.ToString())).Click();
var inputSelectionSelect = s.Driver.FindElement(By.Name("SelectedInputs"));
@ -674,7 +709,8 @@ namespace BTCPayServer.Tests
var deletes = s.Driver.FindElements(By.LinkText("Delete"));
Assert.Equal(2, deletes.Count);
deletes[0].Click();
s.Driver.FindElement(By.Id("continue")).Click();
s.Driver.WaitForElement(By.Id("ConfirmInput")).SendKeys("DELETE");
s.Driver.FindElement(By.Id("ConfirmContinue")).Click();
deletes = s.Driver.FindElements(By.LinkText("Delete"));
Assert.Single(deletes);
s.FindAlertMessage();
@ -698,6 +734,7 @@ namespace BTCPayServer.Tests
// Fix as needed.
Assert.Contains($"value=\"{value}\"", s.Driver.PageSource);
}
// This one should be checked
Assert.Contains($"value=\"InvoiceProcessing\" checked", s.Driver.PageSource);
Assert.Contains($"value=\"InvoiceCreated\" checked", s.Driver.PageSource);
@ -716,7 +753,8 @@ namespace BTCPayServer.Tests
var headers = request.Request.Headers;
var actualSig = headers["BTCPay-Sig"].First();
var bytes = await request.Request.Body.ReadBytesAsync((int)headers.ContentLength.Value);
var expectedSig = $"sha256={Encoders.Hex.EncodeData(new HMACSHA256(Encoding.UTF8.GetBytes("HelloWorld")).ComputeHash(bytes))}";
var expectedSig =
$"sha256={Encoders.Hex.EncodeData(new HMACSHA256(Encoding.UTF8.GetBytes("HelloWorld")).ComputeHash(bytes))}";
Assert.Equal(expectedSig, actualSig);
request.Response.StatusCode = 200;
server.Done();
@ -757,15 +795,34 @@ namespace BTCPayServer.Tests
server.Done();
Logs.Tester.LogInformation("Let's see if we can delete store with some webhooks inside");
s.GoToStore(storeId);
s.Driver.ToggleCollapse("danger-zone");
s.GoToStore(storeId, StoreNavPages.GeneralSettings);
s.Driver.FindElement(By.Id("delete-store")).Click();
s.Driver.FindElement(By.Id("continue")).Click();
s.Driver.WaitForElement(By.Id("ConfirmContinue")).Click();
s.FindAlertMessage();
}
}
[Fact(Timeout = TestTimeout)]
public async Task CanImportMnemonic()
{
using (var s = SeleniumTester.Create())
{
await s.StartAsync();
s.RegisterNewUser(true);
foreach (var isHotwallet in new[] { false, true })
{
var (storeName, storeId) = s.CreateNewStore();
s.GenerateWallet(privkeys: isHotwallet,
seed: "melody lizard phrase voice unique car opinion merge degree evil swift cargo");
s.GoToWallet(s.WalletId, WalletsNavPages.Settings);
if (isHotwallet)
Assert.Contains("View seed", s.Driver.PageSource);
else
Assert.DoesNotContain("View seed", s.Driver.PageSource);
}
}
}
[Fact(Timeout = TestTimeout)]
public async Task CanManageWallet()
{
@ -804,7 +861,8 @@ namespace BTCPayServer.Tests
var sess = await s.Server.ExplorerClient.CreateWebsocketNotificationSessionAsync();
await sess.ListenAllTrackedSourceAsync();
var nextEvent = sess.NextEventAsync();
await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(receiveAddr, Network.RegTest), Money.Parse("0.1"));
await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(receiveAddr, Network.RegTest),
Money.Parse("0.1"));
await nextEvent;
await Task.Delay(200);
s.Driver.Navigate().Refresh();
@ -827,7 +885,8 @@ namespace BTCPayServer.Tests
var address = invoice.EntityToDTO().Addresses["BTC"];
//wallet should have been imported to bitcoin core wallet in watch only mode.
var result = await s.Server.ExplorerNode.GetAddressInfoAsync(BitcoinAddress.Create(address, Network.RegTest));
var result =
await s.Server.ExplorerNode.GetAddressInfoAsync(BitcoinAddress.Create(address, Network.RegTest));
Assert.True(result.IsWatchOnly);
s.GoToStore(storeId);
var mnemonic = s.GenerateWallet("BTC", "", true, true);
@ -837,21 +896,25 @@ namespace BTCPayServer.Tests
invoiceId = s.CreateInvoice(storeName);
invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice(invoiceId);
address = invoice.EntityToDTO().Addresses["BTC"];
result = await s.Server.ExplorerNode.GetAddressInfoAsync(BitcoinAddress.Create(address, Network.RegTest));
result = await s.Server.ExplorerNode.GetAddressInfoAsync(
BitcoinAddress.Create(address, Network.RegTest));
//spendable from bitcoin core wallet!
Assert.False(result.IsWatchOnly);
var tx = s.Server.ExplorerNode.SendToAddress(BitcoinAddress.Create(address, Network.RegTest), Money.Coins(3.0m));
var tx = s.Server.ExplorerNode.SendToAddress(BitcoinAddress.Create(address, Network.RegTest),
Money.Coins(3.0m));
await s.Server.ExplorerNode.GenerateAsync(1);
s.Driver.FindElement(By.Id("Wallets")).Click();
s.Driver.FindElement(By.LinkText("Manage")).Click();
s.ClickOnAllSideMenus();
// Make sure wallet info is correct
s.Driver.FindElement(By.Id("WalletSettings")).Click();
Assert.Contains(mnemonic.DeriveExtKey().GetPublicKey().GetHDFingerPrint().ToString(), s.Driver.FindElement(By.Id("AccountKeys_0__MasterFingerprint")).GetAttribute("value"));
Assert.Contains("m/84'/1'/0'", s.Driver.FindElement(By.Id("AccountKeys_0__AccountKeyPath")).GetAttribute("value"));
Assert.Contains(mnemonic.DeriveExtKey().GetPublicKey().GetHDFingerPrint().ToString(),
s.Driver.FindElement(By.Id("AccountKeys_0__MasterFingerprint")).GetAttribute("value"));
Assert.Contains("m/84'/1'/0'",
s.Driver.FindElement(By.Id("AccountKeys_0__AccountKeyPath")).GetAttribute("value"));
// Make sure we can rescan, because we are admin!
s.Driver.FindElement(By.Id("WalletRescan")).Click();
@ -865,7 +928,7 @@ namespace BTCPayServer.Tests
var walletTransactionLink = s.Driver.Url;
Assert.Contains(tx.ToString(), s.Driver.PageSource);
// Send to bob
s.Driver.FindElement(By.Id("WalletSend")).Click();
var bob = new Key().PubKey.Hash.GetAddress(Network.RegTest);
@ -888,12 +951,6 @@ namespace BTCPayServer.Tests
Assert.Contains(jack.ToString(), s.Driver.PageSource);
Assert.Contains("0.01000000", s.Driver.PageSource);
s.Driver.FindElement(By.CssSelector("button[value=analyze-psbt]")).Click();
Assert.EndsWith("psbt", s.Driver.Url);
s.Driver.FindElement(By.Id("OtherActionsDropdownToggle")).Click();
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click();
Assert.EndsWith("psbt/ready", s.Driver.Url);
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click();
Assert.Equal(walletTransactionLink, s.Driver.Url);
@ -909,8 +966,10 @@ namespace BTCPayServer.Tests
s.Driver.SwitchTo().Alert().SendKeys(bip21);
s.Driver.SwitchTo().Alert().Accept();
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Info);
Assert.Equal(parsedBip21.Amount.ToString(false), s.Driver.FindElement(By.Id("Outputs_0__Amount")).GetAttribute("value"));
Assert.Equal(parsedBip21.Address.ToString(), s.Driver.FindElement(By.Id("Outputs_0__DestinationAddress")).GetAttribute("value"));
Assert.Equal(parsedBip21.Amount.ToString(false),
s.Driver.FindElement(By.Id("Outputs_0__Amount")).GetAttribute("value"));
Assert.Equal(parsedBip21.Address.ToString(),
s.Driver.FindElement(By.Id("Outputs_0__DestinationAddress")).GetAttribute("value"));
s.GoToWallet(new WalletId(storeId, "BTC"), WalletsNavPages.Settings);
var walletUrl = s.Driver.Url;
@ -918,9 +977,11 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.CssSelector("button[value=view-seed]")).Click();
// Seed backup page
var recoveryPhrase = s.Driver.FindElements(By.Id("RecoveryPhrase")).First().GetAttribute("data-mnemonic");
var recoveryPhrase = s.Driver.FindElements(By.Id("RecoveryPhrase")).First()
.GetAttribute("data-mnemonic");
Assert.Equal(mnemonic.ToString(), recoveryPhrase);
Assert.Contains("The recovery phrase will also be stored on the server as a hot wallet.", s.Driver.PageSource);
Assert.Contains("The recovery phrase will also be stored on the server as a hot wallet.",
s.Driver.PageSource);
// No confirmation, just a link to return to the wallet
Assert.Empty(s.Driver.FindElements(By.Id("confirm")));
@ -937,20 +998,25 @@ namespace BTCPayServer.Tests
await s.StartAsync();
s.RegisterNewUser(true);
var (_, storeId) = s.CreateNewStore();
var mnemonic = s.GenerateWallet("BTC", "click chunk owner kingdom faint steak safe evidence bicycle repeat bulb wheel");
var mnemonic = s.GenerateWallet("BTC",
"click chunk owner kingdom faint steak safe evidence bicycle repeat bulb wheel");
// Make sure wallet info is correct
s.GoToWallet(new WalletId(storeId, "BTC"), WalletsNavPages.Settings);
Assert.Contains(mnemonic.DeriveExtKey().GetPublicKey().GetHDFingerPrint().ToString(), s.Driver.FindElement(By.Id("AccountKeys_0__MasterFingerprint")).GetAttribute("value"));
Assert.Contains( "m/84'/1'/0'", s.Driver.FindElement(By.Id("AccountKeys_0__AccountKeyPath")).GetAttribute("value"));
Assert.Contains(mnemonic.DeriveExtKey().GetPublicKey().GetHDFingerPrint().ToString(),
s.Driver.FindElement(By.Id("AccountKeys_0__MasterFingerprint")).GetAttribute("value"));
Assert.Contains("m/84'/1'/0'",
s.Driver.FindElement(By.Id("AccountKeys_0__AccountKeyPath")).GetAttribute("value"));
}
}
[Fact]
[Trait("Selenium", "Selenium")]
[Trait("Lightning", "Lightning")]
public async Task CanUsePullPaymentsViaUI()
{
using var s = SeleniumTester.Create();
s.Server.ActivateLightning();
await s.StartAsync();
s.RegisterNewUser(true);
s.CreateNewStore();
@ -958,15 +1024,16 @@ namespace BTCPayServer.Tests
await s.Server.ExplorerNode.GenerateAsync(1);
await s.FundStoreWallet(denomination: 50.0m);
s.GoToWallet(navPages: WalletsNavPages.PullPayments);
s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
s.Driver.FindElement(By.Id("NewPullPayment")).Click();
s.Driver.FindElement(By.Id("Name")).SendKeys("PP1");
s.Driver.FindElement(By.Id("Amount")).Clear();
s.Driver.FindElement(By.Id("Amount")).SendKeys("99.0");;
s.Driver.FindElement(By.Id("Amount")).SendKeys("99.0");
;
s.Driver.FindElement(By.Id("Create")).Click();
s.Driver.FindElement(By.LinkText("View")).Click();
s.GoToWallet(navPages: WalletsNavPages.PullPayments);
s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
s.Driver.FindElement(By.Id("NewPullPayment")).Click();
s.Driver.FindElement(By.Id("Name")).SendKeys("PP2");
@ -998,16 +1065,16 @@ namespace BTCPayServer.Tests
var viewPullPaymentUrl = s.Driver.Url;
// This one should have nothing
s.GoToWallet(navPages: WalletsNavPages.PullPayments);
s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
var payouts = s.Driver.FindElements(By.ClassName("pp-payout"));
Assert.Equal(2, payouts.Count);
payouts[1].Click();
Assert.Empty(s.Driver.FindElements(By.ClassName("payout")));
// PP2 should have payouts
s.GoToWallet(navPages: WalletsNavPages.PullPayments);
s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
payouts = s.Driver.FindElements(By.ClassName("pp-payout"));
payouts[0].Click();
Assert.NotEmpty(s.Driver.FindElements(By.ClassName("payout")));
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-selectAllCheckbox")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-actions")).Click();
@ -1024,14 +1091,14 @@ namespace BTCPayServer.Tests
});
Assert.Equal("payout", s.Driver.FindElement(By.ClassName("transactionLabel")).Text);
s.GoToWallet(navPages: WalletsNavPages.Payouts);
s.GoToStore(s.StoreId, StoreNavPages.Payouts);
var x = s.Driver.PageSource;
s.Driver.FindElement(By.Id($"{PayoutState.InProgress}-view")).Click();
ReadOnlyCollection<IWebElement> txs;
TestUtils.Eventually(() =>
{
s.Driver.Navigate().Refresh();
txs = s.Driver.FindElements(By.ClassName("transaction-link"));
Assert.Equal(2, txs.Count);
});
@ -1062,12 +1129,11 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("NotificationsDropdownToggle")).Click();
s.Driver.FindElement(By.CssSelector("#notificationsForm button")).Click();
var newStore = s.CreateNewStore();
s.GenerateWallet("BTC", "", true, true);
var newWalletId = new WalletId(newStore.storeId, "BTC");
s.GoToWallet(newWalletId, WalletsNavPages.PullPayments);
s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
s.Driver.FindElement(By.Id("NewPullPayment")).Click();
s.Driver.FindElement(By.Id("Name")).SendKeys("External Test");
s.Driver.FindElement(By.Id("Amount")).Clear();
@ -1076,34 +1142,439 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("Currency")).SendKeys("BTC");
s.Driver.FindElement(By.Id("Create")).Click();
s.Driver.FindElement(By.LinkText("View")).Click();
address = await s.Server.ExplorerNode.GetNewAddressAsync();
s.Driver.FindElement(By.Id("Destination")).SendKeys(address.ToString());
s.Driver.FindElement(By.Id("ClaimedAmount")).SendKeys(Keys.Enter);
s.FindAlertMessage();
Assert.Contains(PayoutState.AwaitingApproval.GetStateString(), s.Driver.PageSource);
s.GoToWallet(newWalletId, WalletsNavPages.Payouts);
s.GoToStore(s.StoreId, StoreNavPages.Payouts);
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-view")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-selectAllCheckbox")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-actions")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-approve")).Click();
s.FindAlertMessage();
var tx =await s.Server.ExplorerNode.SendToAddressAsync(address, Money.FromUnit(0.001m, MoneyUnit.BTC));
var tx = await s.Server.ExplorerNode.SendToAddressAsync(address, Money.FromUnit(0.001m, MoneyUnit.BTC));
s.GoToStore(s.StoreId, StoreNavPages.Payouts);
s.GoToWallet(newWalletId, WalletsNavPages.Payouts);
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-view")).Click();
Assert.Contains(PayoutState.AwaitingPayment.GetStateString(), s.Driver.PageSource);
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-selectAllCheckbox")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-actions")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-mark-paid")).Click();
s.FindAlertMessage();
s.Driver.FindElement(By.Id("InProgress-view")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.InProgress}-view")).Click();
Assert.Contains(tx.ToString(), s.Driver.PageSource);
//lightning tests
newStore = s.CreateNewStore();
s.AddLightningNode("BTC");
//Currently an onchain wallet is required to use the Lightning payouts feature..
s.GenerateWallet("BTC", "", true, true);
s.GoToStore(newStore.storeId, StoreNavPages.PullPayments);
s.Driver.FindElement(By.Id("NewPullPayment")).Click();
var paymentMethodOptions = s.Driver.FindElements(By.CssSelector("#PaymentMethods option"));
Assert.Equal(2, paymentMethodOptions.Count);
s.Driver.FindElement(By.Id("Name")).SendKeys("Lightning Test");
s.Driver.FindElement(By.Id("Amount")).Clear();
s.Driver.FindElement(By.Id("Amount")).SendKeys("0.00001");
s.Driver.FindElement(By.Id("Currency")).Clear();
s.Driver.FindElement(By.Id("Currency")).SendKeys("BTC");
s.Driver.FindElement(By.Id("Create")).Click();
s.Driver.FindElement(By.LinkText("View")).Click();
var bolt = (await s.Server.MerchantLnd.Client.CreateInvoice(
LightMoney.FromUnit(0.00001m, LightMoneyUnit.BTC),
$"LN payout test {DateTime.Now.Ticks}",
TimeSpan.FromHours(1), CancellationToken.None)).BOLT11;
s.Driver.FindElement(By.Id("Destination")).SendKeys(bolt);
s.Driver.FindElement(By.Id("SelectedPaymentMethod")).Click();
s.Driver.FindElement(By.CssSelector(
$"#SelectedPaymentMethod option[value={new PaymentMethodId("BTC", PaymentTypes.LightningLike)}]"))
.Click();
s.Driver.FindElement(By.Id("ClaimedAmount")).SendKeys(Keys.Enter);
//we do not allow short-life bolts.
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Error);
bolt = (await s.Server.MerchantLnd.Client.CreateInvoice(
LightMoney.FromUnit(0.00001m, LightMoneyUnit.BTC),
$"LN payout test {DateTime.Now.Ticks}",
TimeSpan.FromDays(31), CancellationToken.None)).BOLT11;
s.Driver.FindElement(By.Id("Destination")).Clear();
s.Driver.FindElement(By.Id("Destination")).SendKeys(bolt);
s.Driver.FindElement(By.Id("SelectedPaymentMethod")).Click();
s.Driver.FindElement(By.CssSelector(
$"#SelectedPaymentMethod option[value={new PaymentMethodId("BTC", PaymentTypes.LightningLike)}]"))
.Click();
s.Driver.FindElement(By.Id("ClaimedAmount")).SendKeys(Keys.Enter);
s.FindAlertMessage();
Assert.Contains(PayoutState.AwaitingApproval.GetStateString(), s.Driver.PageSource);
s.GoToStore(newStore.storeId, StoreNavPages.Payouts);
s.Driver.FindElement(By.Id($"{new PaymentMethodId("BTC", PaymentTypes.LightningLike)}-view")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-view")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-selectAllCheckbox")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-actions")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-approve-pay")).Click();
Assert.Contains(bolt, s.Driver.PageSource);
Assert.Contains("0.00001 BTC", s.Driver.PageSource);
s.Driver.FindElement(By.CssSelector("#pay-invoices-form")).Submit();
//lightning config in tests is very unstable so we can just go ahead and handle it as both
s.FindAlertMessage(new[]
{
StatusMessageModel.StatusSeverity.Error, StatusMessageModel.StatusSeverity.Success
});
s.GoToStore(newStore.storeId, StoreNavPages.Payouts);
s.Driver.FindElement(By.Id($"{new PaymentMethodId("BTC", PaymentTypes.LightningLike)}-view")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.Completed}-view")).Click();
if (!s.Driver.PageSource.Contains(bolt))
{
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-view")).Click();
Assert.Contains(bolt, s.Driver.PageSource);
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-selectAllCheckbox")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-actions")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-mark-paid")).Click();
s.Driver.FindElement(By.Id($"{new PaymentMethodId("BTC", PaymentTypes.LightningLike)}-view")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.Completed}-view")).Click();
Assert.Contains(bolt, s.Driver.PageSource);
}
}
[Fact]
[Trait("Selenium", "Selenium")]
[Trait("Lightning", "Lightning")]
public async Task CanUsePOSPrint()
{
using var s = SeleniumTester.Create();
s.Server.ActivateLightning();
await s.StartAsync();
await s.Server.EnsureChannelsSetup();
s.RegisterNewUser(true);
var store = s.CreateNewStore();
var network = s.Server.NetworkProvider.GetNetwork<BTCPayNetwork>("BTC").NBitcoinNetwork;
s.GoToStore(store.storeId);
s.AddLightningNode("BTC", LightningConnectionType.CLightning, false);
s.Driver.FindElement(By.Id($"Modify-LightningBTC")).Click();
s.Driver.SetCheckbox(By.Id("LNURLEnabled"), true);
s.GoToApps();
s.Driver.FindElement(By.Id("CreateNewApp")).Click();
s.Driver.FindElement(By.Id("SelectedAppType")).Click();
s.Driver.FindElement(By.CssSelector("option[value='PointOfSale']")).Click();
s.Driver.FindElement(By.Id("AppName")).SendKeys(Guid.NewGuid().ToString());
s.Driver.FindElement(By.Id("Create")).Click();
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success);
s.Driver.FindElement(By.Id("DefaultView")).Click();
s.Driver.FindElement(By.CssSelector("option[value='3']")).Click();
s.Driver.FindElement(By.Id("SaveSettings")).Click();
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success);
s.Driver.FindElement(By.Id("ViewApp")).Click();
var btns = s.Driver.FindElements(By.ClassName("lnurl"));
foreach (IWebElement webElement in btns)
{
var choice = webElement.GetAttribute("data-choice");
var lnurl = webElement.GetAttribute("href");
var parsed = LNURL.LNURL.Parse(lnurl, out _);
Assert.EndsWith(choice, parsed.ToString());
Assert.IsType<LNURLPayRequest>(await LNURL.LNURL.FetchInformation(parsed, new HttpClient()));
}
}
[Fact]
[Trait("Selenium", "Selenium")]
[Trait("Lightning", "Lightning")]
public async Task CanUseLNURL()
{
using var s = SeleniumTester.Create();
s.Server.ActivateLightning();
await s.StartAsync();
await s.Server.EnsureChannelsSetup();
var cryptoCode = "BTC";
await Lightning.Tests.ConnectChannels.ConnectAll(s.Server.ExplorerNode,
new[] { s.Server.MerchantLightningD },
new[] { s.Server.MerchantLnd.Client });
s.RegisterNewUser(true);
var store = s.CreateNewStore();
var network = s.Server.NetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode).NBitcoinNetwork;
s.GoToStore(store.storeId);
s.AddLightningNode(cryptoCode, LightningConnectionType.CLightning);
s.Driver.FindElement(By.Id($"Modify-Lightning{cryptoCode}")).Click();
// LNURL is false by default
Assert.False(s.Driver.FindElement(By.Id("LNURLEnabled")).Selected);
// LNURL settings are not expanded when LNURL is disabled
Assert.DoesNotContain("show", s.Driver.FindElement(By.Id("LNURLSettings")).GetAttribute("class"));
s.Driver.SetCheckbox(By.Id("LNURLEnabled"), true);
s.Driver.WaitForAndClick(By.Id("save"));
Assert.Contains($"{cryptoCode} Lightning settings successfully updated", s.FindAlertMessage().Text);
// Topup Invoice test
var i = s.CreateInvoice(store.storeName, null, cryptoCode);
s.GoToInvoiceCheckout(i);
s.Driver.FindElement(By.Id("copy-tab")).Click();
var lnurl = s.Driver.FindElement(By.CssSelector("input.checkoutTextbox")).GetAttribute("value");
var parsed = LNURL.LNURL.Parse(lnurl, out var tag);
var fetchedReuqest =
Assert.IsType<LNURL.LNURLPayRequest>(await LNURL.LNURL.FetchInformation(parsed, new HttpClient()));
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());
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());
Assert.Equal(new LightMoney(0.000002m, LightMoneyUnit.BTC), lnurlResponse2.GetPaymentRequest(network).MinimumAmount);
await Assert.ThrowsAnyAsync<LightningRPCException>(async () =>
{
// Initial bolt was cancelled
await s.Server.CustomerLightningD.Pay(lnurlResponse.Pr);
});
await s.Server.CustomerLightningD.Pay(lnurlResponse2.Pr);
await TestUtils.EventuallyAsync(async () =>
{
var inv = await s.Server.PayTester.InvoiceRepository.GetInvoice(i);
Assert.Equal(InvoiceStatusLegacy.Complete, inv.Status);
});
// Standard invoice test
s.GoToHome();
i = s.CreateInvoice(store.storeName, 0.0000001m, cryptoCode);
s.GoToInvoiceCheckout(i);
s.Driver.FindElement(By.ClassName("payment__currencies")).Click();
// BOLT11 is also available for standard invoices
Assert.Equal(2, s.Driver.FindElements(By.CssSelector(".vex.vex-theme-btcpay .vex-content .vexmenu li.vexmenuitem")).Count);
s.Driver.FindElement(By.CssSelector(".vex.vex-theme-btcpay .vex-content .vexmenu li.vexmenuitem")).Click();
s.Driver.FindElement(By.Id("copy-tab")).Click();
lnurl = s.Driver.FindElement(By.CssSelector("input.checkoutTextbox")).GetAttribute("value");
parsed = LNURL.LNURL.Parse(lnurl, out tag);
fetchedReuqest = Assert.IsType<LNURLPayRequest>(await LNURL.LNURL.FetchInformation(parsed, new HttpClient()));
Assert.Equal(0.0000001m, fetchedReuqest.MaxSendable.ToDecimal(LightMoneyUnit.BTC));
Assert.Equal(0.0000001m, fetchedReuqest.MinSendable.ToDecimal(LightMoneyUnit.BTC));
await Assert.ThrowsAsync<HttpRequestException>(async () =>
{
await fetchedReuqest.SendRequest(new LightMoney(0.0000002m, LightMoneyUnit.BTC),
network, new HttpClient());
});
await Assert.ThrowsAsync<HttpRequestException>(async () =>
{
await fetchedReuqest.SendRequest(new LightMoney(0.00000005m, LightMoneyUnit.BTC),
network, new HttpClient());
});
lnurlResponse = await fetchedReuqest.SendRequest(new LightMoney(0.0000001m, LightMoneyUnit.BTC),
network, new HttpClient());
lnurlResponse2 = await fetchedReuqest.SendRequest(new LightMoney(0.0000001m, LightMoneyUnit.BTC),
network, new HttpClient());
//invoice amounts do no change so the paymnet request is not regenerated
Assert.Equal(lnurlResponse.Pr, lnurlResponse2.Pr);
await s.Server.CustomerLightningD.Pay(lnurlResponse.Pr);
Assert.Equal(new LightMoney(0.0000001m, LightMoneyUnit.BTC),
lnurlResponse2.GetPaymentRequest(network).MinimumAmount);
s.GoToStore(s.StoreId);
s.Driver.FindElement(By.Id($"Modify-Lightning{cryptoCode}")).Click();
// LNURL is enabled and settings are expanded
Assert.True(s.Driver.FindElement(By.Id("LNURLEnabled")).Selected);
Assert.Contains("show", s.Driver.FindElement(By.Id("LNURLSettings")).GetAttribute("class"));
s.Driver.SetCheckbox(By.Id("LNURLStandardInvoiceEnabled"), false);
s.Driver.FindElement(By.Id("save")).Click();
Assert.Contains($"{cryptoCode} Lightning settings successfully updated", s.FindAlertMessage().Text);
i = s.CreateInvoice(store.storeName, 0.000001m, cryptoCode);
s.GoToInvoiceCheckout(i);
s.Driver.FindElement(By.ClassName("payment__currencies_noborder"));
s.GoToHome();
i = s.CreateInvoice(store.storeName, null, cryptoCode);
s.GoToInvoiceCheckout(i);
s.Driver.FindElement(By.ClassName("payment__currencies_noborder"));
s.GoToStore(s.StoreId);
s.Driver.FindElement(By.Id($"Modify-Lightning{cryptoCode}")).Click();
s.Driver.SetCheckbox(By.Id("LNURLBech32Mode"), false);
s.Driver.SetCheckbox(By.Id("LNURLStandardInvoiceEnabled"), false);
s.Driver.SetCheckbox(By.Id("DisableBolt11PaymentMethod"), true);
s.Driver.FindElement(By.Id("save")).Click();
Assert.Contains($"{cryptoCode} Lightning settings successfully updated", s.FindAlertMessage().Text);
// Ensure the toggles are set correctly
s.Driver.FindElement(By.Id($"Modify-Lightning{cryptoCode}")).Click();
//TODO: DisableBolt11PaymentMethod is actually disabled because LNURLStandardInvoiceEnabled is disabled
// checkboxes is not good choice here, in next release we should have multi choice instead
Assert.False(s.Driver.FindElement(By.Id("DisableBolt11PaymentMethod")).Selected);
Assert.False(s.Driver.FindElement(By.Id("LNURLStandardInvoiceEnabled")).Selected);
Assert.False(s.Driver.FindElement(By.Id("LNURLBech32Mode")).Selected);
s.CreateInvoice(store.storeName, 0.0000001m, cryptoCode,"",null, expectedSeverity: StatusMessageModel.StatusSeverity.Error);
i = s.CreateInvoice(store.storeName, null, cryptoCode);
s.GoToInvoiceCheckout(i);
s.Driver.FindElement(By.ClassName("payment__currencies_noborder"));
s.Driver.FindElement(By.Id("copy-tab")).Click();
lnurl = s.Driver.FindElement(By.CssSelector("input.checkoutTextbox")).GetAttribute("value");
Assert.StartsWith("lnurlp", lnurl);
LNURL.LNURL.Parse(lnurl, out tag);
s.GoToHome();
var newStore = s.CreateNewStore(false);
s.AddLightningNode(cryptoCode, LightningConnectionType.LndREST, false);
s.Driver.FindElement(By.Id($"Modify-Lightning{cryptoCode}")).Click();
s.Driver.SetCheckbox(By.Id("LNURLEnabled"), true);
s.Driver.SetCheckbox(By.Id("DisableBolt11PaymentMethod"), true);
s.Driver.FindElement(By.Id("save")).Click();
Assert.Contains($"{cryptoCode} Lightning settings successfully updated", s.FindAlertMessage().Text);
var invForPP = s.CreateInvoice(newStore.storeName, 0.0000001m, cryptoCode);
s.GoToInvoiceCheckout(invForPP);
s.Driver.FindElement(By.Id("copy-tab")).Click();
lnurl = s.Driver.FindElement(By.CssSelector("input.checkoutTextbox")).GetAttribute("value");
parsed = LNURL.LNURL.Parse(lnurl, out tag);
// Check that pull payment has lightning option
s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
s.Driver.FindElement(By.Id("NewPullPayment")).Click();
Assert.Equal(new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike),PaymentMethodId.Parse(Assert.Single(s.Driver.FindElement(By.Id("PaymentMethods")).FindElements(By.TagName("option"))).GetAttribute("value")));
s.Driver.FindElement(By.Id("Name")).SendKeys("PP1");
s.Driver.FindElement(By.Id("Amount")).Clear();
s.Driver.FindElement(By.Id("Amount")).SendKeys("0.0000001");
;
s.Driver.FindElement(By.Id("Create")).Click();
s.Driver.FindElement(By.LinkText("View")).Click();
s.Driver.FindElement(By.Id("Destination")).SendKeys(lnurl);
var pullPaymentId = s.Driver.Url.Split('/').Last();
s.Driver.FindElement(By.Id("ClaimedAmount")).Clear();
s.Driver.FindElement(By.Id("ClaimedAmount")).SendKeys("0.0000001" + Keys.Enter);
s.FindAlertMessage();
s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
var payouts = s.Driver.FindElements(By.ClassName("pp-payout"));
payouts[0].Click();
s.Driver.FindElement(By.Id("BTC_LightningLike-view")).Click();
Assert.NotEmpty(s.Driver.FindElements(By.ClassName("payout")));
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-selectAllCheckbox")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-actions")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-approve-pay")).Click();
Assert.Contains(lnurl, s.Driver.PageSource);
s.Driver.FindElement(By.Id("pay-invoices-form")).Submit();
await TestUtils.EventuallyAsync(async () =>
{
var inv = await s.Server.PayTester.InvoiceRepository.GetInvoice(invForPP);
Assert.Equal(InvoiceStatusLegacy.Complete, inv.Status);
await using var ctx = s.Server.PayTester.GetService<ApplicationDbContextFactory>().CreateContext();
var payoutsData = await ctx.Payouts.Where(p => p.PullPaymentDataId == pullPaymentId).ToListAsync();
Assert.True(payoutsData.All(p => p.State == PayoutState.Completed));
});
}
[Fact]
[Trait("Selenium", "Selenium")]
[Trait("Lightning", "Lightning")]
public async Task CanUseLNAddress()
{
using var s = SeleniumTester.Create();
s.Server.ActivateLightning();
await s.StartAsync();
await s.Server.EnsureChannelsSetup();
s.RegisterNewUser(true);
//ln address tests
var store = s.CreateNewStore();
s.GoToStore(s.StoreId, StoreNavPages.Integrations);
//ensure ln address is not available as lnurl is not configured
s.Driver.FindElement(By.Id("lightning-address-option"))
.FindElement(By.ClassName("btcpay-status--disabled"));
s.GoToStore(s.StoreId, StoreNavPages.PaymentMethods);
s.AddLightningNode("BTC", LightningConnectionType.LndREST, false);
s.Driver.FindElement(By.Id($"Modify-LightningBTC")).Click();
s.Driver.SetCheckbox(By.Id("LNURLEnabled"), true);
s.Driver.WaitForAndClick(By.Id("save"));
Assert.Contains($"BTC Lightning settings successfully updated", s.FindAlertMessage().Text);
s.GoToStore(s.StoreId, StoreNavPages.Integrations);
s.Driver.FindElement(By.Id("lightning-address-option"))
.FindElement(By.Id("lightning-address-setup-link")).Click();
s.Driver.ToggleCollapse("AddAddress");
var lnaddress1 = Guid.NewGuid().ToString();
s.Driver.FindElement(By.Id("Add_Username")).SendKeys(lnaddress1);
s.Driver.FindElement(By.CssSelector("button[value='add']")).Click();
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success);
s.Driver.ToggleCollapse("AddAddress");
s.Driver.FindElement(By.Id("Add_Username")).SendKeys(lnaddress1);
s.Driver.FindElement(By.CssSelector("button[value='add']")).Click();
s.Driver.FindElement(By.ClassName("text-danger"));
s.Driver.FindElement(By.Id("Add_Username")).Clear();
var lnaddress2 = "EUR" + Guid.NewGuid().ToString();
s.Driver.FindElement(By.Id("Add_Username")).SendKeys(lnaddress2);
s.Driver.ToggleCollapse("AdvancedSettings");
s.Driver.FindElement(By.Id("Add_CurrencyCode")).SendKeys("EUR");
s.Driver.FindElement(By.Id("Add_Min")).SendKeys("2");
s.Driver.FindElement(By.Id("Add_Max")).SendKeys("10");
s.Driver.FindElement(By.CssSelector("button[value='add']")).Click();
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success);
var addresses = s.Driver.FindElements(By.ClassName("lightning-address-value"));
Assert.Equal(2, addresses.Count);
foreach (IWebElement webElement in addresses)
{
var value = webElement.GetAttribute("value");
//cannot test this directly as https is not supported on our e2e tests
// var request = await LNURL.LNURL.FetchPayRequestViaInternetIdentifier(value, new HttpClient());
var lnurl = new Uri(LNURL.LNURL.ExtractUriFromInternetIdentifier(value).ToString()
.Replace("https", "http"));
var request =(LNURL.LNURLPayRequest) await LNURL.LNURL.FetchInformation(lnurl, new HttpClient());
switch (value)
{
case { } v when v.StartsWith(lnaddress2):
Assert.Equal(2, request.MinSendable.ToDecimal(LightMoneyUnit.Satoshi));
Assert.Equal(10, request.MaxSendable.ToDecimal(LightMoneyUnit.Satoshi));
break;
case { } v when v.StartsWith(lnaddress1):
Assert.Equal(1, request.MinSendable.ToDecimal(LightMoneyUnit.Satoshi));
Assert.Equal(6.12m, request.MaxSendable.ToDecimal(LightMoneyUnit.BTC));
break;
}
}
}
private static void CanBrowseContent(SeleniumTester s)
{
s.Driver.FindElement(By.ClassName("delivery-content")).Click();

View File

@ -109,7 +109,7 @@ namespace BTCPayServer.Tests
{
await RegisterAsync(isAdmin);
await CreateStoreAsync();
var store = this.GetController<StoresController>();
var store = GetController<StoresController>();
var pairingCode = BitPay.RequestClientAuthorization("test", Facade.Merchant);
Assert.IsType<ViewResult>(await store.RequestPairing(pairingCode.ToString()));
await store.Pair(pairingCode.ToString(), StoreId);
@ -127,19 +127,28 @@ namespace BTCPayServer.Tests
public async Task SetNetworkFeeMode(NetworkFeeMode mode)
{
await ModifyStore(store =>
await ModifyPayment(payment =>
{
store.NetworkFeeMode = mode;
payment.NetworkFeeMode = mode;
});
}
public async Task ModifyStore(Action<StoreViewModel> modify)
public async Task ModifyPayment(Action<PaymentMethodsViewModel> modify)
{
var storeController = GetController<StoresController>();
var response = await storeController.UpdateStore();
StoreViewModel store = (StoreViewModel)((ViewResult)response).Model;
modify(store);
storeController.UpdateStore(store).GetAwaiter().GetResult();
var response = storeController.PaymentMethods();
PaymentMethodsViewModel paymentMethods = (PaymentMethodsViewModel)((ViewResult)response).Model;
modify(paymentMethods);
await storeController.PaymentMethods(paymentMethods);
}
public async Task ModifyWalletSettings(Action<WalletSettingsViewModel> modify)
{
var storeController = GetController<StoresController>();
var response = await storeController.WalletSettings(StoreId, "BTC");
WalletSettingsViewModel walletSettings = (WalletSettingsViewModel)((ViewResult)response).Model;
modify(walletSettings);
storeController.UpdateWalletSettings(walletSettings).GetAwaiter().GetResult();
}
public T GetController<T>(bool setImplicitStore = true) where T : Controller
@ -154,8 +163,8 @@ namespace BTCPayServer.Tests
{
await RegisterAsync();
}
var store = this.GetController<UserStoresController>();
await store.CreateStore(new CreateStoreViewModel() { Name = "Test Store" });
var store = GetController<UserStoresController>();
await store.CreateStore(new CreateStoreViewModel { Name = "Test Store" });
StoreId = store.CreatedStoreId;
parent.Stores.Add(StoreId);
}
@ -188,19 +197,11 @@ namespace BTCPayServer.Tests
return new WalletId(StoreId, cryptoCode);
}
public Task EnablePayJoin()
{
return ModifyStore(s => s.PayJoinEnabled = true);
}
public GenerateWalletResponse GenerateWalletResponseV { get; set; }
public DerivationStrategyBase DerivationScheme
{
get
{
return GenerateWalletResponseV.DerivationScheme;
}
get => GenerateWalletResponseV.DerivationScheme;
}
private async Task RegisterAsync(bool isAdmin = false)
@ -255,8 +256,9 @@ namespace BTCPayServer.Tests
var connectionString = parent.GetLightningConnectionString(connectionType, isMerchant);
var nodeType = connectionString == LightningSupportedPaymentMethod.InternalNode ? LightningNodeType.Internal : LightningNodeType.Custom;
var vm = new LightningNodeViewModel { ConnectionString = connectionString, LightningNodeType = nodeType, SkipPortTest = true };
await storeController.SetupLightningNode(storeId ?? StoreId,
new LightningNodeViewModel { ConnectionString = connectionString, LightningNodeType = nodeType, SkipPortTest = true }, "save", cryptoCode);
vm, "save", cryptoCode);
if (storeController.ModelState.ErrorCount != 0)
Assert.False(true, storeController.ModelState.FirstOrDefault().Value.Errors[0].ErrorMessage);
}

View File

@ -24,10 +24,12 @@ using BTCPayServer.Fido2.Models;
using BTCPayServer.HostedServices;
using BTCPayServer.Hosting;
using BTCPayServer.Lightning;
using BTCPayServer.Lightning.CLightning;
using BTCPayServer.Models;
using BTCPayServer.Models.AccountViewModels;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Models.ManageViewModels;
using BTCPayServer.Models.ServerViewModels;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Models.WalletViewModels;
@ -49,8 +51,6 @@ using ExchangeSharp;
using Fido2NetLib;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using NBitcoin;
@ -362,15 +362,15 @@ namespace BTCPayServer.Tests
{
"https://www.btse.com", // not allowing to be hit from circleci
"https://www.bitpay.com", // not allowing to be hit from circleci
"https://support.bitpay.com",
"https://www.pnxbet.com" //has geo blocking
};
foreach (var match in regex.Matches(text).OfType<Match>())
{
var url = match.Groups[1].Value;
if (urlBlacklist.Any(a => a.StartsWith(url.ToLowerInvariant())))
if (urlBlacklist.Any(a => url.StartsWith(a.ToLowerInvariant())))
continue;
checkLinks.Add(AssertLinkNotDead(httpClient, url, file));
}
@ -817,11 +817,11 @@ namespace BTCPayServer.Tests
// Set tolerance to 50%
var stores = user.GetController<StoresController>();
var response = await stores.UpdateStore();
var vm = Assert.IsType<StoreViewModel>(Assert.IsType<ViewResult>(response).Model);
var response = stores.PaymentMethods();
var vm = Assert.IsType<PaymentMethodsViewModel>(Assert.IsType<ViewResult>(response).Model);
Assert.Equal(0.0, vm.PaymentTolerance);
vm.PaymentTolerance = 50.0;
Assert.IsType<RedirectToActionResult>(stores.UpdateStore(vm).Result);
Assert.IsType<RedirectToActionResult>(stores.PaymentMethods(vm).Result);
var invoice = user.BitPay.CreateInvoice(
new Invoice()
@ -995,8 +995,7 @@ namespace BTCPayServer.Tests
Assert.Equal(4, tor.Services.Length);
}
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")]
[Trait("Lightning", "Lightning")]
@ -1011,7 +1010,7 @@ namespace BTCPayServer.Tests
await user.RegisterDerivationSchemeAsync("BTC");
await user.RegisterLightningNodeAsync("BTC", LightningConnectionType.CLightning);
await user.SetNetworkFeeMode(NetworkFeeMode.Never);
await user.ModifyStore(model => model.SpeedPolicy = SpeedPolicy.HighSpeed);
await user.ModifyWalletSettings(p => p.SpeedPolicy = SpeedPolicy.HighSpeed);
var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(0.0001m, "BTC"));
await tester.WaitForEvent<InvoiceNewPaymentDetailsEvent>(async () =>
{
@ -1041,14 +1040,22 @@ namespace BTCPayServer.Tests
Assert.Contains(fetchedInvoice.Status, new[] { InvoiceStatusLegacy.Complete, InvoiceStatusLegacy.Confirmed });
Assert.Equal(InvoiceExceptionStatus.None, fetchedInvoice.ExceptionStatus);
Logs.Tester.LogInformation($"Paying invoice {invoice.Id} original full amount bolt11 invoice ");
evt = await tester.WaitForEvent<InvoiceDataChangedEvent>(async () =>
//BTCPay will attempt to cancel previous bolt11 invoices so that there are less weird edge case scenarios
Logs.Tester.LogInformation($"Attempting to pay invoice {invoice.Id} original full amount bolt11 invoice ");
await Assert.ThrowsAsync<LightningRPCException>(async () =>
{
await tester.SendLightningPaymentAsync(invoice);
}, evt => evt.InvoiceId == invoice.Id);
Assert.Equal(evt.InvoiceId, invoice.Id);
fetchedInvoice = await tester.PayTester.InvoiceRepository.GetInvoice(evt.InvoiceId);
Assert.Equal(3, fetchedInvoice.Payments.Count);
});
//NOTE: Eclair does not support cancelling invoice so the below test case would make sense for it
// Logs.Tester.LogInformation($"Paying invoice {invoice.Id} original full amount bolt11 invoice ");
// evt = await tester.WaitForEvent<InvoiceDataChangedEvent>(async () =>
// {
// await tester.SendLightningPaymentAsync(invoice);
// }, evt => evt.InvoiceId == invoice.Id);
// Assert.Equal(evt.InvoiceId, invoice.Id);
// fetchedInvoice = await tester.PayTester.InvoiceRepository.GetInvoice(evt.InvoiceId);
// Assert.Equal(3, fetchedInvoice.Payments.Count);
}
[Fact(Timeout = 60 * 2 * 1000)]
@ -1064,7 +1071,7 @@ namespace BTCPayServer.Tests
var user = tester.NewAccount();
user.GrantAccess(true);
var storeController = user.GetController<StoresController>();
var storeResponse = await storeController.UpdateStore();
var storeResponse = storeController.PaymentMethods();
Assert.IsType<ViewResult>(storeResponse);
Assert.IsType<ViewResult>(await storeController.SetupLightningNode(user.StoreId, "BTC"));
@ -1088,9 +1095,9 @@ namespace BTCPayServer.Tests
new LightningNodeViewModel { ConnectionString = tester.MerchantCharge.Client.Uri.AbsoluteUri },
"save", "BTC").GetAwaiter().GetResult());
storeResponse = await storeController.UpdateStore();
storeResponse = storeController.PaymentMethods();
var storeVm =
Assert.IsType<StoreViewModel>(Assert
Assert.IsType<PaymentMethodsViewModel>(Assert
.IsType<ViewResult>(storeResponse).Model);
Assert.Single(storeVm.LightningNodes.Where(l => !string.IsNullOrEmpty(l.Address)));
}
@ -1204,7 +1211,7 @@ namespace BTCPayServer.Tests
var acc = tester.NewAccount();
acc.GrantAccess();
acc.RegisterDerivationScheme("BTC");
await acc.ModifyStore(s => s.SpeedPolicy = SpeedPolicy.LowSpeed);
await acc.ModifyWalletSettings(p => p.SpeedPolicy = SpeedPolicy.LowSpeed);
var invoice = acc.BitPay.CreateInvoice(new Invoice
{
Price = 5.0m,
@ -1619,7 +1626,7 @@ namespace BTCPayServer.Tests
{
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess();
await user.GrantAccessAsync();
user.RegisterDerivationScheme("BTC");
await user.SetNetworkFeeMode(NetworkFeeMode.Always);
var invoice =
@ -1723,10 +1730,9 @@ namespace BTCPayServer.Tests
Assert.False(i.GetPayments(false).First().Accounted);
});
Logs.Tester.LogInformation(
$"Let's test if we can RBF a normal payment without adding fees to the invoice");
Logs.Tester.LogInformation("Let's test if we can RBF a normal payment without adding fees to the invoice");
await user.SetNetworkFeeMode(NetworkFeeMode.MultiplePaymentsOnly);
invoice = user.BitPay.CreateInvoice(new Invoice() { Price = 5000.0m, Currency = "USD" }, Facade.Merchant);
invoice = user.BitPay.CreateInvoice(new Invoice { Price = 5000.0m, Currency = "USD" }, Facade.Merchant);
payment1 = invoice.BtcDue;
tx1 = new uint256(tester.ExplorerNode.SendCommand("sendtoaddress", new object[]
{
@ -2031,7 +2037,7 @@ namespace BTCPayServer.Tests
});
Assert.Equal(404, (int)response.StatusCode);
await user.ModifyStore(s => s.AnyoneCanCreateInvoice = true);
await user.ModifyPayment(p => p.AnyoneCanCreateInvoice = true);
Logs.Tester.LogInformation("Bad store with anyone can create invoice = 403");
response = await tester.PayTester.HttpClient.SendAsync(
@ -2117,7 +2123,7 @@ namespace BTCPayServer.Tests
rng = new Random(seed);
Logs.Tester.LogInformation("Seed: " + seed);
foreach (var networkFeeMode in Enum.GetValues(typeof(NetworkFeeMode)).Cast<NetworkFeeMode>())
{
{
await user.SetNetworkFeeMode(networkFeeMode);
await AssertTopUpBtcPrice(tester, user, Money.Coins(1.0m), 5000.0m, networkFeeMode);
await AssertTopUpBtcPrice(tester, user, Money.Coins(1.23456789m), 5000.0m * 1.23456789m, networkFeeMode);
@ -2265,6 +2271,15 @@ namespace BTCPayServer.Tests
}
}
[Fact]
[Trait("Fast", "Fast")]
public void SetOrderIdMetadataDoesntConvertInOctal()
{
var m = new InvoiceMetadata();
m.OrderId = "000000161";
Assert.Equal("000000161", m.OrderId);
}
[Fact]
[Trait("Fast", "Fast")]
public void CanParseCurrencyValue()
@ -2288,28 +2303,77 @@ namespace BTCPayServer.Tests
[Fact]
[Trait("Integration", "Integration")]
public async Task CanSetPaymentMethodLimits()
public async Task CanUseDefaultCurrency()
{
using (var tester = ServerTester.Create())
{
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess();
await user.GrantAccessAsync(true);
user.RegisterDerivationScheme("BTC");
var vm = Assert.IsType<CheckoutExperienceViewModel>(Assert
.IsType<ViewResult>(user.GetController<StoresController>().CheckoutExperience()).Model);
Assert.Single(vm.PaymentMethodCriteria);
var criteria = vm.PaymentMethodCriteria.First();
await user.ModifyPayment(s =>
{
Assert.Equal("USD", s.DefaultCurrency);
s.DefaultCurrency = "EUR";
});
var client = await user.CreateClient();
// with greenfield
var invoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest());
Assert.Equal("EUR", invoice.Currency);
Assert.Equal(InvoiceType.TopUp, invoice.Type);
// with bitpay api
var invoice2 = await user.BitPay.CreateInvoiceAsync(new Invoice());
Assert.Equal("EUR", invoice2.Currency);
// via UI
var controller = user.GetController<InvoiceController>();
var model = await controller.CreateInvoice();
(await controller.CreateInvoice(new CreateInvoiceModel(), default)).AssertType<RedirectToActionResult>();
invoice = await client.GetInvoice(user.StoreId, controller.CreatedInvoiceId);
Assert.Equal("EUR", invoice.Currency);
Assert.Equal(InvoiceType.TopUp, invoice.Type);
// Check that the SendWallet use the default currency
var walletController = user.GetController<WalletsController>();
var walletSend = await walletController.WalletSend(new WalletId(user.StoreId, "BTC")).AssertViewModelAsync<WalletSendModel>();
Assert.Equal("EUR", walletSend.Fiat);
}
}
[Fact]
[Trait("Lightning", "Lightning")]
public async Task CanSetPaymentMethodLimits()
{
using (var tester = ServerTester.Create())
{
tester.ActivateLightning();
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess(true);
user.RegisterDerivationScheme("BTC");
await user.RegisterLightningNodeAsync("BTC");
var lnMethod = new PaymentMethodId("BTC", PaymentTypes.LightningLike).ToString();
var btcMethod = new PaymentMethodId("BTC", PaymentTypes.BTCLike).ToString();
// We allow BTC and LN, but not BTC under 5 USD, so only LN should be in the invoice
var vm = Assert.IsType<CheckoutAppearanceViewModel>(Assert
.IsType<ViewResult>(user.GetController<StoresController>().CheckoutAppearance()).Model);
Assert.Equal(2, vm.PaymentMethodCriteria.Count);
var criteria = Assert.Single(vm.PaymentMethodCriteria.Where(m => m.PaymentMethod == btcMethod.ToString()));
Assert.Equal(new PaymentMethodId("BTC", BitcoinPaymentType.Instance).ToString(), criteria.PaymentMethod);
criteria.Value = "5 USD";
criteria.Type = PaymentMethodCriteriaViewModel.CriteriaType.GreaterThan;
Assert.IsType<RedirectToActionResult>(user.GetController<StoresController>().CheckoutExperience(vm)
Assert.IsType<RedirectToActionResult>(user.GetController<StoresController>().CheckoutAppearance(vm)
.Result);
var invoice = user.BitPay.CreateInvoice(
new Invoice()
new Invoice
{
Price = 5.5m,
Price = 4.5m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
@ -2318,7 +2382,41 @@ namespace BTCPayServer.Tests
}, Facade.Merchant);
Assert.Single(invoice.CryptoInfo);
Assert.Equal(PaymentTypes.BTCLike.ToString(), invoice.CryptoInfo[0].PaymentType);
Assert.Equal(PaymentTypes.LightningLike.ToString(), invoice.CryptoInfo[0].PaymentType);
// Let's replicate https://github.com/btcpayserver/btcpayserver/issues/2963
// We allow BTC for more than 5 USD, and LN for less than 150. The default is LN, so the default
// payment method should be LN.
vm = Assert.IsType<CheckoutAppearanceViewModel>(Assert
.IsType<ViewResult>(user.GetController<StoresController>().CheckoutAppearance()).Model);
vm.DefaultPaymentMethod = lnMethod;
criteria = vm.PaymentMethodCriteria.First();
criteria.Value = "150 USD";
criteria.Type = PaymentMethodCriteriaViewModel.CriteriaType.LessThan;
criteria = vm.PaymentMethodCriteria.Skip(1).First();
criteria.Value = "5 USD";
criteria.Type = PaymentMethodCriteriaViewModel.CriteriaType.GreaterThan;
Assert.IsType<RedirectToActionResult>(user.GetController<StoresController>().CheckoutAppearance(vm)
.Result);
invoice = user.BitPay.CreateInvoice(
new Invoice()
{
Price = 50m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
var checkout = (await user.GetController<InvoiceController>().Checkout(invoice.Id)).AssertViewModel<PaymentModel>();
Assert.Equal(lnMethod, checkout.PaymentMethodId);
// If we change store's default, it should change the checkout's default
vm.DefaultPaymentMethod = btcMethod;
Assert.IsType<RedirectToActionResult>(user.GetController<StoresController>().CheckoutAppearance(vm)
.Result);
checkout = (await user.GetController<InvoiceController>().Checkout(invoice.Id)).AssertViewModel<PaymentModel>();
Assert.Equal(btcMethod, checkout.PaymentMethodId);
}
}
@ -2332,12 +2430,13 @@ namespace BTCPayServer.Tests
await tester.StartAsync();
await tester.EnsureChannelsSetup();
var user = tester.NewAccount();
user.GrantAccess(true);
user.RegisterDerivationScheme("BTC", ScriptPubKeyType.Segwit);
user.RegisterLightningNode("BTC", LightningConnectionType.CLightning);
var cryptoCode = "BTC";
await user.GrantAccessAsync(true);
user.RegisterDerivationScheme(cryptoCode, ScriptPubKeyType.Segwit);
user.RegisterLightningNode(cryptoCode, LightningConnectionType.CLightning);
var invoice = user.BitPay.CreateInvoice(
new Invoice()
new Invoice
{
Price = 5.5m,
Currency = "USD",
@ -2355,12 +2454,12 @@ namespace BTCPayServer.Tests
Assert.DoesNotContain("&lightning=", paymentMethodFirst.InvoiceBitcoinUrlQR);
// enable unified QR code in settings
var vm = Assert.IsType<CheckoutExperienceViewModel>(Assert
.IsType<ViewResult>(user.GetController<StoresController>().CheckoutExperience()).Model
var vm = Assert.IsType<LightningSettingsViewModel>(Assert
.IsType<ViewResult>(await user.GetController<StoresController>().LightningSettings(user.StoreId, cryptoCode)).Model
);
vm.OnChainWithLnInvoiceFallback = true;
Assert.IsType<RedirectToActionResult>(
user.GetController<StoresController>().CheckoutExperience(vm).Result
user.GetController<StoresController>().LightningSettings(vm).Result
);
// validate that QR code now has both onchain and offchain payment urls
@ -2377,7 +2476,7 @@ namespace BTCPayServer.Tests
Assert.True($"bitcoin:{paymentMethodSecond.BtcAddress.ToUpperInvariant()}" == split);
// Fallback lightning invoice should be uppercase inside the QR code.
var lightningFallback = paymentMethodSecond.InvoiceBitcoinUrlQR.Split(new string[] { "&lightning=" }, StringSplitOptions.None)[1];
var lightningFallback = paymentMethodSecond.InvoiceBitcoinUrlQR.Split(new [] { "&lightning=" }, StringSplitOptions.None)[1];
Assert.True(lightningFallback.ToUpperInvariant() == lightningFallback);
}
}
@ -2393,31 +2492,54 @@ namespace BTCPayServer.Tests
await tester.StartAsync();
await tester.EnsureChannelsSetup();
var user = tester.NewAccount();
var cryptoCode = "BTC";
user.GrantAccess(true);
user.RegisterLightningNode("BTC", LightningConnectionType.Charge);
var vm = Assert.IsType<CheckoutExperienceViewModel>(Assert
.IsType<ViewResult>(user.GetController<StoresController>().CheckoutExperience()).Model);
Assert.Single(vm.PaymentMethodCriteria);
var criteria = vm.PaymentMethodCriteria.First();
Assert.Equal(new PaymentMethodId("BTC", LightningPaymentType.Instance).ToString(), criteria.PaymentMethod);
user.RegisterLightningNode(cryptoCode, LightningConnectionType.Charge);
var vm = user.GetController<StoresController>().CheckoutAppearance().AssertViewModel<CheckoutAppearanceViewModel>();
var criteria = Assert.Single(vm.PaymentMethodCriteria);
Assert.Equal(new PaymentMethodId(cryptoCode, LightningPaymentType.Instance).ToString(), criteria.PaymentMethod);
criteria.Value = "2 USD";
criteria.Type = PaymentMethodCriteriaViewModel.CriteriaType.LessThan;
Assert.IsType<RedirectToActionResult>(user.GetController<StoresController>().CheckoutExperience(vm)
Assert.IsType<RedirectToActionResult>(user.GetController<StoresController>().CheckoutAppearance(vm)
.Result);
var invoice = user.BitPay.CreateInvoice(
new Invoice()
new Invoice
{
Price = 1.5m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
Currency = "USD"
}, Facade.Merchant);
Assert.Single(invoice.CryptoInfo);
Assert.Equal(PaymentTypes.LightningLike.ToString(), invoice.CryptoInfo[0].PaymentType);
// Activating LNUrl, we should still have only 1 payment criteria that can be set.
user.RegisterLightningNode(cryptoCode, LightningConnectionType.Charge);
var lnSettingsVm = await user.GetController<StoresController>().LightningSettings(user.StoreId, cryptoCode).AssertViewModelAsync<LightningSettingsViewModel>();
lnSettingsVm.LNURLEnabled = true;
lnSettingsVm.LNURLStandardInvoiceEnabled = true;
Assert.IsType<RedirectToActionResult>(user.GetController<StoresController>().LightningSettings(lnSettingsVm).Result);
vm = user.GetController<StoresController>().CheckoutAppearance().AssertViewModel<CheckoutAppearanceViewModel>();
criteria = Assert.Single(vm.PaymentMethodCriteria);
Assert.Equal(new PaymentMethodId(cryptoCode, LightningPaymentType.Instance).ToString(), criteria.PaymentMethod);
Assert.IsType<RedirectToActionResult>(user.GetController<StoresController>().CheckoutAppearance(vm).Result);
// However, creating an invoice should show LNURL
invoice = user.BitPay.CreateInvoice(
new Invoice
{
Price = 1.5m,
Currency = "USD"
}, Facade.Merchant);
Assert.Equal(2, invoice.CryptoInfo.Length);
// Make sure this throw: Since BOLT11 and LN Url share the same criteria, there should be no payment method available
Assert.Throws<BitPayException>(() => user.BitPay.CreateInvoice(
new Invoice
{
Price = 2.5m,
Currency = "USD"
}, Facade.Merchant));
}
}
@ -2610,11 +2732,11 @@ namespace BTCPayServer.Tests
{
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess();
await user.GrantAccessAsync();
user.RegisterDerivationScheme("BTC");
await user.SetNetworkFeeMode(NetworkFeeMode.Always);
var invoice = user.BitPay.CreateInvoice(
new Invoice()
new Invoice
{
Price = 10,
Currency = "USD",
@ -2690,7 +2812,7 @@ namespace BTCPayServer.Tests
Logs.Tester.LogInformation($"Trying with {nameof(networkFeeMode)}={networkFeeMode}");
await user.SetNetworkFeeMode(networkFeeMode);
var invoice = user.BitPay.CreateInvoice(
new Invoice()
new Invoice
{
Price = 10,
Currency = "USD",
@ -2773,11 +2895,11 @@ namespace BTCPayServer.Tests
{
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess();
await user.GrantAccessAsync();
user.RegisterDerivationScheme("BTC");
await user.SetNetworkFeeMode(NetworkFeeMode.Always);
var invoice = user.BitPay.CreateInvoice(
new Invoice()
new Invoice
{
Price = 500,
Currency = "USD",
@ -2822,8 +2944,8 @@ namespace BTCPayServer.Tests
var apps2 = user2.GetController<AppsController>();
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model);
Assert.NotNull(vm.SelectedAppType);
Assert.Null(vm.Name);
vm.Name = "test";
Assert.Null(vm.AppName);
vm.AppName = "test";
vm.SelectedAppType = AppType.PointOfSale.ToString();
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.CreateApp(vm).Result);
Assert.Equal(nameof(apps.UpdatePointOfSale), redirectToAction.ActionName);
@ -2900,10 +3022,26 @@ namespace BTCPayServer.Tests
{
Amount = 50.513m,
Currency = "USD",
Metadata = new JObject() { new JProperty("taxIncluded", 50.516m) }
Metadata = new JObject() { new JProperty("taxIncluded", 50.516m), new JProperty("orderId", "000000161") }
});
Assert.Equal(50.51m, invoice5g.Amount);
Assert.Equal(50.51m, (decimal)invoice5g.Metadata["taxIncluded"]);
Assert.Equal("000000161", (string)invoice5g.Metadata["orderId"]);
var zeroInvoice = await greenfield.CreateInvoice(user.StoreId, new CreateInvoiceRequest()
{
Amount = 0m,
Currency = "USD"
});
Assert.Equal(InvoiceStatus.New, zeroInvoice.Status);
await TestUtils.EventuallyAsync(async () =>
{
zeroInvoice = await greenfield.GetInvoice(user.StoreId, zeroInvoice.Id);
Assert.Equal(InvoiceStatus.Settled, zeroInvoice.Status);
});
var zeroInvoicePM = await greenfield.GetInvoicePaymentMethods(user.StoreId, zeroInvoice.Id);
Assert.Empty(zeroInvoicePM);
}
}
@ -3093,6 +3231,20 @@ namespace BTCPayServer.Tests
c =>
{
Assert.False(c.AfterExpiration);
Assert.Equal(new PaymentMethodId("BTC", PaymentTypes.BTCLike).ToStringNormalized(),c.PaymentMethod);
Assert.NotNull(c.Payment);
Assert.Equal(invoice.BitcoinAddress, c.Payment.Destination);
Assert.StartsWith(txId.ToString(), c.Payment.Id);
});
user.AssertHasWebhookEvent<WebhookInvoicePaymentSettledEvent>(WebhookEventType.InvoicePaymentSettled,
c =>
{
Assert.False(c.AfterExpiration);
Assert.Equal(new PaymentMethodId("BTC", PaymentTypes.BTCLike).ToStringNormalized(),c.PaymentMethod);
Assert.NotNull(c.Payment);
Assert.Equal(invoice.BitcoinAddress, c.Payment.Destination);
Assert.StartsWith(txId.ToString(), c.Payment.Id);
});
}
}
@ -3144,15 +3296,38 @@ namespace BTCPayServer.Tests
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")
{
// This test is strange because ripio sometimes change the pairs it supports
try
{
Assert.Contains(exchangeRates.ByExchange[result.ExpectedName],
e => e.CurrencyPair == new CurrencyPair("BTC", "ARS") &&
e.BidAsk.Bid > 1.0m); // 1 BTC will always be more than 1 ARS
}
catch (XunitException)
{
Assert.Contains(exchangeRates.ByExchange[result.ExpectedName],
e => (e.CurrencyPair == new CurrencyPair("BTC", "USDC")
&& e.BidAsk.Bid > 1.0m)); // 1BTC will always be more than 1USD
}
}
else if (result.ExpectedName == "cryptomarket")
{
Assert.Contains(exchangeRates.ByExchange[result.ExpectedName],
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],
e => (e.CurrencyPair == new CurrencyPair("BTC", "USD") ||
e.CurrencyPair == new CurrencyPair("BTC", "EUR") ||
e.CurrencyPair == new CurrencyPair("BTC", "USDT") ||
e.CurrencyPair == new CurrencyPair("BTC", "CAD"))
&& e.BidAsk.Bid > 1.0m // 1BTC will always be more than 1USD
e.CurrencyPair == new CurrencyPair("BTC", "EUR") ||
e.CurrencyPair == new CurrencyPair("BTC", "USDT") ||
e.CurrencyPair == new CurrencyPair("BTC", "USDC") ||
e.CurrencyPair == new CurrencyPair("BTC", "CAD"))
&& e.BidAsk.Bid > 1.0m // 1BTC will always be more than 1USD
);
}
// We are not showing a directly implemented exchange as directly implemented in the UI
@ -3228,6 +3403,8 @@ namespace BTCPayServer.Tests
{
var rateResult = value.Value.GetAwaiter().GetResult();
Logs.Tester.LogInformation($"Testing {value.Key.ToString()}");
if (value.Key.ToString() == "BTX_USD") // Broken shitcoin
continue;
Assert.True(rateResult.BidAsk != null, $"Impossible to get the rate {rateResult.EvaluatedRule}");
}
}
@ -3520,14 +3697,15 @@ namespace BTCPayServer.Tests
Password = user.RegisterDetails.Password
})).ActionName);
var listController = user.GetController<ManageController>();
var manageController = user.GetController<Fido2Controller>();
//by default no fido2 devices available
Assert.Empty(Assert
.IsType<Fido2AuthenticationViewModel>(Assert
.IsType<ViewResult>(await manageController.List()).Model).Credentials);
.IsType<TwoFactorAuthenticationViewModel>(Assert
.IsType<ViewResult>(await listController.TwoFactorAuthentication()).Model).Credentials);
Assert.IsType<CredentialCreateOptions>(Assert
.IsType<ViewResult>(await manageController.Create(new AddFido2CredentialViewModel()
.IsType<ViewResult>(await manageController.Create(new AddFido2CredentialViewModel
{
Name = "label"
})).Model);
@ -3555,8 +3733,8 @@ namespace BTCPayServer.Tests
Assert.NotNull(newDevice.Id);
Assert.NotEmpty(Assert
.IsType<Fido2AuthenticationViewModel>(Assert
.IsType<ViewResult>(await manageController.List()).Model).Credentials);
.IsType<TwoFactorAuthenticationViewModel>(Assert
.IsType<ViewResult>(await listController.TwoFactorAuthentication()).Model).Credentials);
}
//check if we are showing the fido2 login screen now

View File

@ -34,6 +34,7 @@ services:
- "80"
links:
- dev
- selenium
extra_hosts:
- "tests:127.0.0.1"
volumes:
@ -69,7 +70,7 @@ services:
- "sshd_datadir:/root/.ssh"
devlnd:
image: btcpayserver/bitcoin:0.21.1
image: btcpayserver/bitcoin:22.0
environment:
BITCOIN_NETWORK: regtest
BITCOIN_WALLETDIR: "/data/wallets"
@ -83,8 +84,12 @@ services:
- postgres
- customer_lnd
- merchant_lnd
selenium:
image: selenium/standalone-chrome:3
expose:
- "4444"
nbxplorer:
image: nicolasdorier/nbxplorer:2.2.0
image: nicolasdorier/nbxplorer:2.2.7
restart: unless-stopped
ports:
- "32838:32838"
@ -118,7 +123,7 @@ services:
bitcoind:
restart: unless-stopped
image: btcpayserver/bitcoin:0.21.1
image: btcpayserver/bitcoin:22.0
environment:
BITCOIN_NETWORK: regtest
BITCOIN_WALLETDIR: "/data/wallets"
@ -220,14 +225,16 @@ services:
links:
- bitcoind
postgres:
image: postgres:9.6.5
image: postgres:13.4
environment:
POSTGRES_HOST_AUTH_METHOD: trust
ports:
- "39372:5432"
expose:
- "5432"
merchant_lnd:
image: btcpayserver/lnd:v0.13.1-beta-withloop
image: btcpayserver/lnd:v0.13.3-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -261,7 +268,7 @@ services:
- bitcoind
customer_lnd:
image: btcpayserver/lnd:v0.13.1-beta-withloop
image: btcpayserver/lnd:v0.13.3-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -297,7 +304,7 @@ services:
tor:
restart: unless-stopped
image: btcpayserver/tor:0.4.1.5
image: btcpayserver/tor:0.4.6.5
container_name: tor
environment:
TOR_PASSWORD: btcpayserver
@ -349,7 +356,7 @@ services:
elementsd-liquid:
restart: always
container_name: btcpayserver_elementsd_liquid
image: btcpayserver/elements:0.18.1.7
image: btcpayserver/elements:0.18.1.12
environment:
ELEMENTS_CHAIN: elementsregtest
ELEMENTS_EXTRA_ARGS: |

View File

@ -32,6 +32,7 @@ services:
- "80"
links:
- dev
- selenium
extra_hosts:
- "tests:127.0.0.1"
volumes:
@ -66,7 +67,7 @@ services:
- "sshd_datadir:/root/.ssh"
devlnd:
image: btcpayserver/bitcoin:0.21.1
image: btcpayserver/bitcoin:22.0
environment:
BITCOIN_NETWORK: regtest
BITCOIN_WALLETDIR: "/data/wallets"
@ -80,8 +81,12 @@ services:
- postgres
- customer_lnd
- merchant_lnd
selenium:
image: selenium/standalone-chrome:3
expose:
- "4444"
nbxplorer:
image: nicolasdorier/nbxplorer:2.2.0
image: nicolasdorier/nbxplorer:2.2.7
restart: unless-stopped
ports:
- "32838:32838"
@ -105,7 +110,7 @@ services:
bitcoind:
restart: unless-stopped
image: btcpayserver/bitcoin:0.21.1
image: btcpayserver/bitcoin:22.0
environment:
BITCOIN_NETWORK: regtest
BITCOIN_WALLETDIR: "/data/wallets"
@ -208,14 +213,16 @@ services:
- bitcoind
postgres:
image: postgres:9.6.5
image: postgres:13.4
environment:
POSTGRES_HOST_AUTH_METHOD: trust
ports:
- "39372:5432"
expose:
- "5432"
merchant_lnd:
image: btcpayserver/lnd:v0.13.1-beta-withloop
image: btcpayserver/lnd:v0.13.3-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -249,7 +256,7 @@ services:
- bitcoind
customer_lnd:
image: btcpayserver/lnd:v0.13.1-beta-withloop
image: btcpayserver/lnd:v0.13.3-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -285,7 +292,7 @@ services:
tor:
restart: unless-stopped
image: btcpayserver/tor:0.4.1.5
image: btcpayserver/tor:0.4.6.5
container_name: tor
environment:
TOR_PASSWORD: btcpayserver

View File

@ -1,4 +1,4 @@
 <Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<Import Project="../Build/Version.csproj" Condition="Exists('../Build/Version.csproj')" />
<Import Project="../Build/Common.csproj" />
@ -21,6 +21,15 @@
<None Remove="Build\**" />
<None Remove="wwwroot\bundles\jqueryvalidate\**" />
<None Remove="wwwroot\vendor\jquery-nice-select\**" />
<Content Update="Views\StorePullPayments\NewPullPayment.cshtml">
<Pack>false</Pack>
</Content>
<Content Update="Views\StorePullPayments\PullPayments.cshtml">
<Pack>false</Pack>
</Content>
<Content Update="Views\StorePullPayments\Payouts.cshtml">
<Pack>false</Pack>
</Content>
</ItemGroup>
<ItemGroup>
<None Remove="Currencies.txt" />
@ -29,10 +38,10 @@
<EmbeddedResource Include="bundleconfig.json" />
</ItemGroup>
<ItemGroup Condition="'$(Altcoins)' == 'true'">
<PackageReference Include="Nethereum.ABI" Version="3.8.0" />
<PackageReference Include="Nethereum.HdWallet" Version="3.8.0" />
<PackageReference Include="Nethereum.StandardTokenEIP20" Version="3.8.0" />
<PackageReference Include="Nethereum.Web3" Version="3.8.0" />
<PackageReference Include="Nethereum.ABI" Version="4.1.0" />
<PackageReference Include="Nethereum.HdWallet" Version="4.1.0" />
<PackageReference Include="Nethereum.StandardTokenEIP20" Version="4.1.0" />
<PackageReference Include="Nethereum.Web3" Version="4.1.0" />
</ItemGroup>
<ItemGroup Condition="'$(Altcoins)' != 'true'">
@ -45,8 +54,8 @@
<ItemGroup>
<PackageReference Include="BIP78.Sender" Version="0.2.2" />
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.1" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.2.12" />
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.2" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.2.13" />
<PackageReference Include="BuildBundlerMinifier" Version="3.2.449" />
<PackageReference Include="BundlerMinifier.Core" Version="3.2.435" />
<PackageReference Include="BundlerMinifier.TagHelpers" Version="3.2.435" />
@ -54,16 +63,17 @@
<PackageReference Include="Fido2" Version="2.0.1" />
<PackageReference Include="Fido2.AspNet" Version="2.0.1" />
<PackageReference Include="HtmlSanitizer" Version="5.0.372" />
<PackageReference Include="McMaster.NETCore.Plugins.Mvc" Version="1.3.1" />
<PackageReference Include="LNURL" Version="0.0.14" />
<PackageReference Include="McMaster.NETCore.Plugins.Mvc" Version="1.4.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="2.9.8">
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="3.3.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="QRCoder" Version="1.4.1" />
<PackageReference Include="System.IO.Pipelines" Version="4.7.2" />
<PackageReference Include="System.IO.Pipelines" Version="4.7.4" />
<PackageReference Include="NBitpayClient" Version="1.0.0.39" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.2" />
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.3" />
<PackageReference Include="NicolasDorier.RateLimits" Version="1.1.0" />
@ -85,8 +95,8 @@
<PackageReference Include="TwentyTwenty.Storage.Google" Version="2.12.1" />
<PackageReference Include="TwentyTwenty.Storage.Local" Version="2.12.1" />
<PackageReference Include="YamlDotNet" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="3.1.1" Condition="'$(RazorCompileOnBuild)' != 'true'" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="3.1.19" Condition="'$(RazorCompileOnBuild)' != 'true'" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.19" />
</ItemGroup>
<ItemGroup>
@ -196,12 +206,6 @@
<Content Update="Views\Wallets\ListWallets.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\NewPullPayment.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\Payouts.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\WalletPSBTCombine.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
@ -217,9 +221,6 @@
<Content Update="Views\Wallets\WalletSendVault.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\PullPayments.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\WalletTransactions.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>

View File

@ -1,4 +1,3 @@
@inject LinkGenerator linkGenerator
@inject UserManager<ApplicationUser> UserManager
@inject ISettingsRepository SettingsRepository
@using BTCPayServer.HostedServices
@ -56,47 +55,3 @@ else
</a>
</li>
}
@{
var disabled = (await SettingsRepository.GetPolicies()).DisableInstantNotifications;
if (!disabled)
{
var user = await UserManager.GetUserAsync(User);
disabled = user?.DisabledNotifications == "all";
}
}
@if (!disabled)
{
<script type="text/javascript">
var supportsWebSockets = 'WebSocket' in window && window.WebSocket.CLOSING === 2;
if (supportsWebSockets) {
var loc = window.location, ws_uri;
if (loc.protocol === "https:") {
ws_uri = "wss:";
} else {
ws_uri = "ws:";
}
ws_uri += "//" + loc.host;
ws_uri += "@linkGenerator.GetPathByAction("SubscribeUpdates", "Notifications")";
var newDataEndpoint = "@linkGenerator.GetPathByAction("GetNotificationDropdownUI", "Notifications")";
try {
socket = new WebSocket(ws_uri);
socket.onmessage = function (e) {
$.get(newDataEndpoint, function(data){
$("#notifications-nav-item").replaceWith($(data));
});
};
socket.onerror = function (e) {
console.error("Error while connecting to websocket for notifications (callback)", e);
};
}
catch (e) {
console.error("Error while connecting to websocket for notifications", e);
}
}
</script>
}

View File

@ -0,0 +1,12 @@
@model BTCPayServer.Components.ThemeSwitch.ThemeSwitchViewModel
<button class="btcpay-theme-switch @Model.CssClass">
<svg class="@(string.IsNullOrEmpty(Model.Responsive) || Model.Responsive == "none" ? "d-inline-block" : $"d-{Model.Responsive}-inline-block d-none")" xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10">
<path class="btcpay-theme-switch-dark" transform="translate(1 1)" d="M2.72 0A3.988 3.988 0 000 3.78c0 2.21 1.79 4 4 4 1.76 0 3.25-1.14 3.78-2.72-.4.13-.83.22-1.28.22-2.21 0-4-1.79-4-4 0-.45.08-.88.22-1.28z"/>
<path class="btcpay-theme-switch-light" transform="translate(1 1)" d="M4 0c-.28 0-.5.22-.5.5s.22.5.5.5.5-.22.5-.5S4.28 0 4 0zM1.5 1c-.28 0-.5.22-.5.5s.22.5.5.5.5-.22.5-.5-.22-.5-.5-.5zm5 0c-.28 0-.5.22-.5.5s.22.5.5.5.5-.22.5-.5-.22-.5-.5-.5zM4 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zM.5 3.5c-.28 0-.5.22-.5.5s.22.5.5.5.5-.22.5-.5-.22-.5-.5-.5zm7 0c-.28 0-.5.22-.5.5s.22.5.5.5.5-.22.5-.5-.22-.5-.5-.5zM1.5 6c-.28 0-.5.22-.5.5s.22.5.5.5.5-.22.5-.5-.22-.5-.5-.5zm5 0c-.28 0-.5.22-.5.5s.22.5.5.5.5-.22.5-.5-.22-.5-.5-.5zM4 7c-.28 0-.5.22-.5.5s.22.5.5.5.5-.22.5-.5S4.28 7 4 7z"/>
</svg>
@if (!string.IsNullOrEmpty(Model.Responsive))
{
<span class="d-@Model.Responsive-none d-inline-block"><span class="btcpay-theme-switch-dark">Dark theme</span><span class="btcpay-theme-switch-light">Light theme</span></span>
}
</button>

View File

@ -0,0 +1,17 @@
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Components.ThemeSwitch
{
public class ThemeSwitch : ViewComponent
{
public IViewComponentResult Invoke(string cssClass = null, string responsive = null)
{
var vm = new ThemeSwitchViewModel
{
CssClass = cssClass,
Responsive = responsive
};
return View(vm);
}
}
}

View File

@ -0,0 +1,8 @@
namespace BTCPayServer.Components.ThemeSwitch
{
public class ThemeSwitchViewModel
{
public string CssClass { get; set; }
public string Responsive { get; set; }
}
}

View File

@ -1,6 +1,6 @@
@model IEnumerable<string>
@model BTCPayServer.Components.UIExtensionPoint.UiExtensionPointViewModel
@foreach (var partial in Model)
@foreach (var partial in Model.Partials)
{
await Html.RenderPartialAsync(partial);
await Html.RenderPartialAsync(partial, Model.Model);
}

View File

@ -15,9 +15,22 @@ namespace BTCPayServer.Components.UIExtensionPoint
_uiExtensions = uiExtensions;
}
public IViewComponentResult Invoke(string location)
public IViewComponentResult Invoke(string location, object model)
{
return View(_uiExtensions.Where(extension => extension.Location.Equals(location, StringComparison.InvariantCultureIgnoreCase)).Select(extension => extension.Partial));
return View(new UiExtensionPointViewModel()
{
Partials = _uiExtensions
.Where(extension =>
extension.Location.Equals(location, StringComparison.InvariantCultureIgnoreCase))
.Select(extension => extension.Partial).ToArray(),
Model = model
});
}
}
public class UiExtensionPointViewModel
{
public string[] Partials { get; set; }
public object Model { get; set; }
}
}

View File

@ -1,4 +1,5 @@
using System;
using BTCPayServer.Configuration;
using System.IO;
using System.Linq;
using System.Net;
@ -68,7 +69,6 @@ namespace BTCPayServer.Configuration
BundleJsCss = conf.GetOrDefault<bool>("bundlejscss", true);
DockerDeployment = conf.GetOrDefault<bool>("dockerdeployment", true);
AllowAdminRegistration = conf.GetOrDefault<bool>("allow-admin-registration", false);
TorrcFile = conf.GetOrDefault<string>("torrcfile", null);
TorServices = conf.GetOrDefault<string>("torservices", null)
@ -146,10 +146,14 @@ namespace BTCPayServer.Configuration
DisableRegistration = conf.GetOrDefault<bool>("disable-registration", true);
PluginRemote = conf.GetOrDefault("plugin-remote", "btcpayserver/btcpayserver-plugins");
RecommendedPlugins = conf.GetOrDefault("recommended-plugins", "").ToLowerInvariant().Split('\r','\n','\t',' ').Where(s => !string.IsNullOrEmpty(s)).Distinct().ToArray();
CheatMode = conf.GetOrDefault("cheatmode", false);
if (CheatMode && this.NetworkType == ChainName.Mainnet)
throw new ConfigException($"cheatmode can't be used on mainnet");
}
public string PluginRemote { get; set; }
public string[] RecommendedPlugins { get; set; }
public bool CheatMode { get; set; }
private SSHSettings ParseSSHConfiguration(IConfiguration conf)
{
@ -193,7 +197,6 @@ namespace BTCPayServer.Configuration
get;
set;
}
public bool AllowAdminRegistration { get; set; }
public SSHSettings SSHSettings
{
get;

View File

@ -24,10 +24,10 @@ namespace BTCPayServer.Configuration
app.Option("--testnet | -testnet", $"Use testnet (deprecated, use --network instead)", CommandOptionType.BoolValue);
app.Option("--regtest | -regtest", $"Use regtest (deprecated, use --network instead)", CommandOptionType.BoolValue);
app.Option("--signet | -signet", $"Use signet (deprecated, use --network instead)", CommandOptionType.BoolValue);
app.Option("--allow-admin-registration", $"For debug only, will show a checkbox when a new user register to add himself as admin. (default: false)", CommandOptionType.BoolValue);
app.Option("--chains | -c", $"Chains to support as a comma separated (default: btc; available: {chains})", CommandOptionType.SingleValue);
app.Option("--postgres", $"Connection string to a PostgreSQL database", CommandOptionType.SingleValue);
app.Option("--mysql", $"Connection string to a MySQL database", CommandOptionType.SingleValue);
app.Option("--nocsp", $"Disable CSP (default false)", CommandOptionType.BoolValue);
app.Option("--sqlitefile", $"File name to an SQLite database file inside the data directory", CommandOptionType.SingleValue);
app.Option("--externalservices", $"Links added to external services inside Server Settings / Services under the format service1:path2;service2:path2.(default: empty)", CommandOptionType.SingleValue);
app.Option("--bundlejscss", $"Bundle JavaScript and CSS files for better performance (default: true)", CommandOptionType.SingleValue);
@ -48,6 +48,7 @@ namespace BTCPayServer.Configuration
app.Option("--plugin-remote", "Which github repository to fetch the available plugins list (default:btcpayserver/btcpayserver-plugins)", CommandOptionType.SingleValue);
app.Option("--recommended-plugins", "Plugins which would be marked as recommended to be installed. Separated by newline or space", CommandOptionType.MultipleValue);
app.Option("--xforwardedproto", "If specified, set X-Forwarded-Proto to the specified value, this may be useful if your reverse proxy handle https but is not configured to add X-Forwarded-Proto (example: --xforwardedproto https)", CommandOptionType.SingleValue);
app.Option("--cheatmode", "Add elements in the UI to facilitate dev-time testing (Default false)", CommandOptionType.BoolValue);
foreach (var network in provider.GetAll().OfType<BTCPayNetwork>())
{
var crypto = network.CryptoCode.ToLowerInvariant();

View File

@ -411,7 +411,6 @@ namespace BTCPayServer.Controllers
if (policies.LockSubscription && !User.IsInRole(Roles.ServerAdmin))
return RedirectToAction(nameof(HomeController.Index), "Home");
ViewData["ReturnUrl"] = returnUrl;
ViewData["AllowIsAdmin"] = _Options.AllowAdminRegistration;
return View();
}
@ -429,7 +428,6 @@ namespace BTCPayServer.Controllers
ViewData["ReturnUrl"] = returnUrl;
ViewData["Logon"] = logon.ToString(CultureInfo.InvariantCulture).ToLowerInvariant();
ViewData["AllowIsAdmin"] = _Options.AllowAdminRegistration;
var policies = await _SettingsRepository.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
if (policies.LockSubscription && !User.IsInRole(Roles.ServerAdmin))
return RedirectToAction(nameof(HomeController.Index), "Home");
@ -441,7 +439,7 @@ namespace BTCPayServer.Controllers
if (result.Succeeded)
{
var admin = await _userManager.GetUsersInRoleAsync(Roles.ServerAdmin);
if (admin.Count == 0 || (model.IsAdmin && _Options.AllowAdminRegistration))
if (admin.Count == 0 || (model.IsAdmin && _Options.CheatMode))
{
await _RoleManager.CreateAsync(new IdentityRole(Roles.ServerAdmin));
await _userManager.AddToRoleAsync(user, Roles.ServerAdmin);

View File

@ -1,4 +1,5 @@
using System;
using BTCPayServer.Data;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Models.AppViewModels;
@ -32,6 +33,7 @@ namespace BTCPayServer.Controllers
Title = settings.Title,
StoreId = app.StoreDataId,
StoreName = app.StoreData?.StoreName,
AppName = app.Name,
Enabled = settings.Enabled,
EnforceTargetAmount = settings.EnforceTargetAmount,
StartDate = settings.StartDate,
@ -55,6 +57,7 @@ namespace BTCPayServer.Controllers
AppId = appId,
SearchTerm = app.TagAllInvoices ? $"storeid:{app.StoreDataId}" : $"orderid:{AppService.GetCrowdfundOrderId(appId)}",
DisplayPerksRanking = settings.DisplayPerksRanking,
DisplayPerksValue = settings.DisplayPerksValue,
SortPerksByPopularity = settings.SortPerksByPopularity,
Sounds = string.Join(Environment.NewLine, settings.Sounds),
AnimationColors = string.Join(Environment.NewLine, settings.AnimationColors)
@ -65,7 +68,11 @@ namespace BTCPayServer.Controllers
[Route("{appId}/settings/crowdfund")]
public async Task<IActionResult> UpdateCrowdfund(string appId, UpdateCrowdfundViewModel vm, string command)
{
if (!string.IsNullOrEmpty(vm.TargetCurrency) && _currencies.GetCurrencyData(vm.TargetCurrency, false) == null)
var app = await GetOwnedApp(appId, AppType.Crowdfund);
if (app == null)
return NotFound();
vm.TargetCurrency = await GetStoreDefaultCurrentIfEmpty(app.StoreDataId, vm.TargetCurrency);
if (_currencies.GetCurrencyData(vm.TargetCurrency, false) == null)
ModelState.AddModelError(nameof(vm.TargetCurrency), "Invalid currency");
try
@ -115,11 +122,7 @@ namespace BTCPayServer.Controllers
return View(vm);
}
var app = await GetOwnedApp(appId, AppType.Crowdfund);
if (app == null)
return NotFound();
app.Name = vm.AppName;
var newSettings = new CrowdfundSettings()
{
Title = vm.Title,
@ -142,6 +145,7 @@ namespace BTCPayServer.Controllers
AnimationsEnabled = vm.AnimationsEnabled,
ResetEveryAmount = vm.ResetEveryAmount,
ResetEvery = Enum.Parse<CrowdfundResetEvery>(vm.ResetEvery),
DisplayPerksValue = vm.DisplayPerksValue,
DisplayPerksRanking = vm.DisplayPerksRanking,
SortPerksByPopularity = vm.SortPerksByPopularity,
Sounds = parsedSounds,

View File

@ -1,5 +1,6 @@
using System;
using System.Linq;
using BTCPayServer.Data;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.RegularExpressions;
@ -17,7 +18,6 @@ namespace BTCPayServer.Controllers
public PointOfSaleSettings()
{
Title = "Tea shop";
Currency = "USD";
Template =
"green tea:\n" +
" price: 1\n" +
@ -56,6 +56,7 @@ namespace BTCPayServer.Controllers
ShowCustomAmount = true;
ShowDiscount = true;
EnableTips = true;
RequiresRefundEmail = RequiresRefundEmail.InheritFromStore;
}
public string Title { get; set; }
public string Currency { get; set; }
@ -65,6 +66,7 @@ namespace BTCPayServer.Controllers
public bool ShowCustomAmount { get; set; }
public bool ShowDiscount { get; set; }
public bool EnableTips { get; set; }
public RequiresRefundEmail RequiresRefundEmail { get; set; }
public const string BUTTON_TEXT_DEF = "Buy for {0}";
public string ButtonText { get; set; } = BUTTON_TEXT_DEF;
@ -96,12 +98,12 @@ namespace BTCPayServer.Controllers
var settings = app.GetSettings<PointOfSaleSettings>();
settings.DefaultView = settings.EnableShoppingCart ? PosViewType.Cart : settings.DefaultView;
settings.EnableShoppingCart = false;
var vm = new UpdatePointOfSaleViewModel
{
Id = appId,
StoreId = app.StoreDataId,
StoreName = app.StoreData?.StoreName,
AppName = app.Name,
Title = settings.Title,
DefaultView = settings.DefaultView,
ShowCustomAmount = settings.ShowCustomAmount,
@ -119,11 +121,12 @@ namespace BTCPayServer.Controllers
NotificationUrl = settings.NotificationUrl,
RedirectUrl = settings.RedirectUrl,
SearchTerm = $"storeid:{app.StoreDataId}",
RedirectAutomatically = settings.RedirectAutomatically.HasValue ? settings.RedirectAutomatically.Value ? "true" : "false" : ""
RedirectAutomatically = settings.RedirectAutomatically.HasValue ? settings.RedirectAutomatically.Value ? "true" : "false" : "",
RequiresRefundEmail = settings.RequiresRefundEmail
};
if (HttpContext?.Request != null)
{
var appUrl = HttpContext.Request.GetAbsoluteRoot().WithTrailingSlash() + $"apps/{appId}/pos";
var appUrl = HttpContext.Request.GetAbsoluteUri($"/apps/{appId}/pos");
var encoder = HtmlEncoder.Default;
if (settings.ShowCustomAmount)
{
@ -140,6 +143,7 @@ namespace BTCPayServer.Controllers
}
try
{
var items = _AppService.Parse(settings.Template, settings.Currency);
var builder = new StringBuilder();
builder.AppendLine($"<form method=\"POST\" action=\"{encoder.Encode(appUrl)}\">");
@ -162,11 +166,14 @@ namespace BTCPayServer.Controllers
[Route("{appId}/settings/pos")]
public async Task<IActionResult> UpdatePointOfSale(string appId, UpdatePointOfSaleViewModel vm)
{
var app = await GetOwnedApp(appId, AppType.PointOfSale);
if (app == null)
return NotFound();
if (!ModelState.IsValid)
{
return View(vm);
}
vm.Currency = await GetStoreDefaultCurrentIfEmpty(app.StoreDataId, vm.Currency);
if (_currencies.GetCurrencyData(vm.Currency, false) == null)
ModelState.AddModelError(nameof(vm.Currency), "Invalid currency");
try
@ -181,9 +188,8 @@ namespace BTCPayServer.Controllers
{
return View(vm);
}
var app = await GetOwnedApp(appId, AppType.PointOfSale);
if (app == null)
return NotFound();
app.Name = vm.AppName;
app.SetSettings(new PointOfSaleSettings
{
Title = vm.Title,
@ -191,7 +197,7 @@ namespace BTCPayServer.Controllers
ShowCustomAmount = vm.ShowCustomAmount,
ShowDiscount = vm.ShowDiscount,
EnableTips = vm.EnableTips,
Currency = vm.Currency.ToUpperInvariant(),
Currency = vm.Currency,
Template = vm.Template,
ButtonText = vm.ButtonText,
CustomButtonText = vm.CustomButtonText,
@ -202,7 +208,8 @@ namespace BTCPayServer.Controllers
RedirectUrl = vm.RedirectUrl,
Description = vm.Description,
EmbeddedCSS = vm.EmbeddedCSS,
RedirectAutomatically = string.IsNullOrEmpty(vm.RedirectAutomatically) ? (bool?)null : bool.Parse(vm.RedirectAutomatically)
RedirectAutomatically = string.IsNullOrEmpty(vm.RedirectAutomatically) ? (bool?)null : bool.Parse(vm.RedirectAutomatically),
RequiresRefundEmail = vm.RequiresRefundEmail,
});
await _AppService.UpdateOrCreateApp(app);
TempData[WellKnownTempData.SuccessMessage] = "App updated";

View File

@ -1,16 +1,17 @@
using System;
using BTCPayServer.Data;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Data;
using BTCPayServer.Models;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Security;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Mails;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
@ -29,6 +30,7 @@ namespace BTCPayServer.Controllers
BTCPayNetworkProvider networkProvider,
CurrencyNameTable currencies,
EmailSenderFactory emailSenderFactory,
Services.Stores.StoreRepository storeRepository,
AppService AppService)
{
_UserManager = userManager;
@ -37,6 +39,7 @@ namespace BTCPayServer.Controllers
_NetworkProvider = networkProvider;
_currencies = currencies;
_emailSenderFactory = emailSenderFactory;
_storeRepository = storeRepository;
_AppService = AppService;
}
@ -46,6 +49,7 @@ namespace BTCPayServer.Controllers
private readonly BTCPayNetworkProvider _NetworkProvider;
private readonly CurrencyNameTable _currencies;
private readonly EmailSenderFactory _emailSenderFactory;
private readonly StoreRepository _storeRepository;
private readonly AppService _AppService;
public string CreatedAppId { get; set; }
@ -100,7 +104,7 @@ namespace BTCPayServer.Controllers
if (appData == null)
return NotFound();
if (await _AppService.DeleteApp(appData))
TempData[WellKnownTempData.SuccessMessage] = "App removed successfully";
TempData[WellKnownTempData.SuccessMessage] = "App deleted successfully.";
return RedirectToAction(nameof(ListApps));
}
@ -159,9 +163,25 @@ namespace BTCPayServer.Controllers
var appData = new AppData
{
StoreDataId = selectedStore,
Name = vm.Name,
Name = vm.AppName,
AppType = appType.ToString()
};
var defaultCurrency = await GetStoreDefaultCurrentIfEmpty(appData.StoreDataId, null);
switch (appType)
{
case AppType.Crowdfund:
var emptyCrowdfund = new CrowdfundSettings();
emptyCrowdfund.TargetCurrency = defaultCurrency;
appData.SetSettings(emptyCrowdfund);
break;
case AppType.PointOfSale:
var empty = new PointOfSaleSettings();
empty.Currency = defaultCurrency;
appData.SetSettings(empty);
break;
}
await _AppService.UpdateOrCreateApp(appData);
TempData[WellKnownTempData.SuccessMessage] = "App successfully created";
CreatedAppId = appData.Id;
@ -177,19 +197,22 @@ namespace BTCPayServer.Controllers
}
}
[HttpGet]
[Route("{appId}/delete")]
async Task<string> GetStoreDefaultCurrentIfEmpty(string storeId, string currency)
{
if (String.IsNullOrWhiteSpace(currency))
{
currency = (await _storeRepository.FindStore(storeId)).GetStoreBlob().DefaultCurrency;
}
return currency.Trim().ToUpperInvariant();
}
[HttpGet("{appId}/delete")]
public async Task<IActionResult> DeleteApp(string appId)
{
var appData = await GetOwnedApp(appId);
if (appData == null)
return NotFound();
return View("Confirm", new ConfirmModel()
{
Title = $"Delete app {appData.Name} ({appData.AppType})",
Description = "This app will be removed from this store",
Action = "Delete"
});
return View("Confirm", new ConfirmModel("Delete app", $"The app <strong>{appData.Name}</strong> and its settings will be permanently deleted. Are you sure?", "Delete"));
}
private Task<AppData> GetOwnedApp(string appId, AppType? type = null)
@ -197,7 +220,6 @@ namespace BTCPayServer.Controllers
return _AppService.GetAppDataIfOwner(GetUserId(), appId, type);
}
private string GetUserId()
{
return _UserManager.GetUserId(User);

View File

@ -92,7 +92,7 @@ namespace BTCPayServer.Controllers
Prefixed = new[] { 0, 2 }.Contains(numberFormatInfo.CurrencyPositivePattern),
SymbolSpace = new[] { 2, 3 }.Contains(numberFormatInfo.CurrencyPositivePattern)
},
Items = _AppService.Parse(settings.Template, settings.Currency),
Items = _AppService.GetPOSItems(settings.Template, settings.Currency),
ButtonText = settings.ButtonText,
CustomButtonText = settings.CustomButtonText,
CustomTipText = settings.CustomTipText,
@ -100,8 +100,10 @@ namespace BTCPayServer.Controllers
CustomCSSLink = settings.CustomCSSLink,
CustomLogoLink = storeBlob.CustomLogo,
AppId = appId,
Store = store,
Description = settings.Description,
EmbeddedCSS = settings.EmbeddedCSS
EmbeddedCSS = settings.EmbeddedCSS,
RequiresRefundEmail = settings.RequiresRefundEmail
});
}
@ -114,13 +116,15 @@ namespace BTCPayServer.Controllers
[DomainMappingConstraint(AppType.PointOfSale)]
public async Task<IActionResult> ViewPointOfSale(string appId,
PosViewType viewType,
[ModelBinder(typeof(InvariantDecimalModelBinder))] decimal amount,
[ModelBinder(typeof(InvariantDecimalModelBinder))] decimal? amount,
string email,
string orderId,
string notificationUrl,
string redirectUrl,
string choiceKey,
string posData = null, CancellationToken cancellationToken = default)
string posData = null,
RequiresRefundEmail requiresRefundEmail = RequiresRefundEmail.InheritFromStore,
CancellationToken cancellationToken = default)
{
var app = await _AppService.GetApp(appId, AppType.PointOfSale);
if (string.IsNullOrEmpty(choiceKey) && amount <= 0)
@ -136,19 +140,27 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId, viewType = viewType });
}
string title = null;
var price = 0.0m;
decimal? price = null;
Dictionary<string, InvoiceSupportedTransactionCurrency> paymentMethods = null;
ViewPointOfSaleViewModel.Item choice = null;
if (!string.IsNullOrEmpty(choiceKey))
{
var choices = _AppService.Parse(settings.Template, settings.Currency);
var choices = _AppService.GetPOSItems(settings.Template, settings.Currency);
choice = choices.FirstOrDefault(c => c.Id == choiceKey);
if (choice == null)
return NotFound();
title = choice.Title;
price = choice.Price.Value;
if (amount > price)
price = amount;
if (choice.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup)
{
price = null;
}
else
{
price = choice.Price.Value;
if (amount > price)
price = amount;
}
if (choice.Inventory.HasValue)
{
@ -177,7 +189,7 @@ namespace BTCPayServer.Controllers
AppService.TryParsePosCartItems(posData, out var cartItems))
{
var choices = _AppService.Parse(settings.Template, settings.Currency);
var choices = _AppService.GetPOSItems(settings.Template, settings.Currency);
foreach (var cartItem in cartItems)
{
var itemChoice = choices.FirstOrDefault(c => c.Id == cartItem.Key);
@ -218,6 +230,9 @@ namespace BTCPayServer.Controllers
PosData = string.IsNullOrEmpty(posData) ? null : posData,
RedirectAutomatically = settings.RedirectAutomatically,
SupportedTransactionCurrencies = paymentMethods,
RequiresRefundEmail = requiresRefundEmail == RequiresRefundEmail.InheritFromStore
? store.GetStoreBlob().RequiresRefundEmail
: requiresRefundEmail == RequiresRefundEmail.On,
}, store, HttpContext.Request.GetAbsoluteRoot(),
new List<string>() { AppService.GetAppInternalTag(appId) },
cancellationToken);
@ -258,8 +273,8 @@ namespace BTCPayServer.Controllers
return NotFound("A Target Currency must be set for this app in order to be loadable.");
}
var appInfo = (ViewCrowdfundViewModel)(await _AppService.GetAppInfo(appId));
appInfo.HubPath = AppHub.GetHubPath(this.Request);
var appInfo = await GetAppInfo(appId);
if (settings.Enabled)
return View(appInfo);
if (!isAdmin)
@ -277,17 +292,13 @@ namespace BTCPayServer.Controllers
[DomainMappingConstraintAttribute(AppType.Crowdfund)]
public async Task<IActionResult> ContributeToCrowdfund(string appId, ContributeToCrowdfund request, CancellationToken cancellationToken)
{
if (request.Amount <= 0)
{
return NotFound("Please provide an amount greater than 0");
}
var app = await _AppService.GetApp(appId, AppType.Crowdfund, true);
if (app == null)
return NotFound();
var settings = app.GetSettings<CrowdfundSettings>();
var isAdmin = await _AppService.GetAppDataIfOwner(GetUserId(), appId, AppType.Crowdfund) != null;
if (!settings.Enabled && !isAdmin)
@ -295,8 +306,7 @@ namespace BTCPayServer.Controllers
return NotFound("Crowdfund is not currently active");
}
var info = (ViewCrowdfundViewModel)await _AppService.GetAppInfo(appId);
info.HubPath = AppHub.GetHubPath(this.Request);
var info = await GetAppInfo(appId);
if (!isAdmin &&
((settings.StartDate.HasValue && DateTime.Now < settings.StartDate) ||
(settings.EndDate.HasValue && DateTime.Now > settings.EndDate) ||
@ -309,21 +319,27 @@ namespace BTCPayServer.Controllers
var store = await _AppService.GetStore(app);
var title = settings.Title;
var price = request.Amount;
decimal? price = request.Amount;
Dictionary<string, InvoiceSupportedTransactionCurrency> paymentMethods = null;
ViewPointOfSaleViewModel.Item choice = null;
if (!string.IsNullOrEmpty(request.ChoiceKey))
{
var choices = _AppService.Parse(settings.PerksTemplate, settings.TargetCurrency);
var choices = _AppService.GetPOSItems(settings.PerksTemplate, settings.TargetCurrency);
choice = choices.FirstOrDefault(c => c.Id == request.ChoiceKey);
if (choice == null)
return NotFound("Incorrect option provided");
title = choice.Title;
price = choice.Price.Value;
if (request.Amount > price)
price = request.Amount;
if (choice.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup)
{
price = null;
}
else
{
price = choice.Price.Value;
if (request.Amount > price)
price = request.Amount;
}
if (choice.Inventory.HasValue)
{
if (choice.Inventory <= 0)
@ -331,14 +347,21 @@ namespace BTCPayServer.Controllers
return NotFound("Option was out of stock");
}
}
if (choice?.PaymentMethods?.Any() is true)
{
paymentMethods = choice?.PaymentMethods.ToDictionary(s => s,
s => new InvoiceSupportedTransactionCurrency() { Enabled = true });
}
}
else
{
if (request.Amount < 0)
{
return NotFound("Please provide an amount greater than 0");
}
price = request.Amount;
}
if (!isAdmin && (settings.EnforceTargetAmount && info.TargetAmount.HasValue && price >
(info.TargetAmount - (info.Info.CurrentAmount + info.Info.CurrentPendingAmount))))
@ -361,7 +384,7 @@ namespace BTCPayServer.Controllers
ExtendedNotifications = true,
SupportedTransactionCurrencies = paymentMethods,
RedirectURL = request.RedirectUrl ??
new Uri(new Uri(new Uri(HttpContext.Request.GetAbsoluteRoot()), _BtcPayServerOptions.RootPath), $"apps/{appId}/crowdfund").ToString()
HttpContext.Request.GetAbsoluteUri($"/apps/{appId}/crowdfund")
}, store, HttpContext.Request.GetAbsoluteRoot(),
new List<string> { AppService.GetAppInternalTag(appId) },
cancellationToken: cancellationToken);
@ -379,9 +402,15 @@ namespace BTCPayServer.Controllers
{
return BadRequest(e.Message);
}
}
private async Task<ViewCrowdfundViewModel> GetAppInfo(string appId)
{
var info = (ViewCrowdfundViewModel)await _AppService.GetAppInfo(appId);
info.HubPath = AppHub.GetHubPath(Request);
info.SimpleDisplay = Request.Query.ContainsKey("simple");
return info;
}
private string GetUserId()
{

View File

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Globalization;
@ -55,7 +56,7 @@ namespace BTCPayServer.Controllers.GreenField
[FromQuery]
[ModelBinder(typeof(ModelBinders.DateTimeOffsetModelBinder))]
DateTimeOffset? endDate = null,
string textSearch = null,
[FromQuery] string textSearch = null,
[FromQuery] bool includeArchived = false)
{
var store = HttpContext.GetStoreData();
@ -162,11 +163,6 @@ namespace BTCPayServer.Controllers.GreenField
{
ModelState.AddModelError(nameof(request.Amount), "The amount should be 0 or more.");
}
if (string.IsNullOrEmpty(request.Currency))
{
ModelState.AddModelError(nameof(request.Currency), "Currency is required");
}
request.Checkout = request.Checkout ?? new CreateInvoiceRequest.CheckoutOptions();
if (request.Checkout.PaymentMethods?.Any() is true)
{
@ -355,37 +351,49 @@ namespace BTCPayServer.Controllers.GreenField
PaymentMethod = method.GetId().ToStringNormalized(),
Destination = details.GetPaymentDestination(),
Rate = method.Rate,
Due = accounting.Due.ToDecimal(MoneyUnit.BTC),
Due = accounting.DueUncapped.ToDecimal(MoneyUnit.BTC),
TotalPaid = accounting.Paid.ToDecimal(MoneyUnit.BTC),
PaymentMethodPaid = accounting.CryptoPaid.ToDecimal(MoneyUnit.BTC),
Amount = accounting.Due.ToDecimal(MoneyUnit.BTC),
Amount = accounting.TotalDue.ToDecimal(MoneyUnit.BTC),
NetworkFee = accounting.NetworkFee.ToDecimal(MoneyUnit.BTC),
PaymentLink =
method.GetId().PaymentType.GetPaymentLink(method.Network, details, accounting.Due,
Request.GetAbsoluteRoot()),
Payments = payments.Select(paymentEntity =>
{
var data = paymentEntity.GetCryptoPaymentData();
return new InvoicePaymentMethodDataModel.Payment()
{
Destination = data.GetDestination(),
Id = data.GetPaymentId(),
Status = !paymentEntity.Accounted
? InvoicePaymentMethodDataModel.Payment.PaymentStatus.Invalid
: data.PaymentConfirmed(paymentEntity, entity.SpeedPolicy) ||
data.PaymentCompleted(paymentEntity)
? InvoicePaymentMethodDataModel.Payment.PaymentStatus.Settled
: InvoicePaymentMethodDataModel.Payment.PaymentStatus.Processing,
Fee = paymentEntity.NetworkFee,
Value = data.GetValue(),
ReceivedDate = paymentEntity.ReceivedTime.DateTime
};
}).ToList()
Payments = payments.Select(paymentEntity => ToPaymentModel(entity, paymentEntity)).ToList()
};
}).ToArray();
}
public static InvoicePaymentMethodDataModel.Payment ToPaymentModel(InvoiceEntity entity, PaymentEntity paymentEntity)
{
var data = paymentEntity.GetCryptoPaymentData();
return new InvoicePaymentMethodDataModel.Payment()
{
Destination = data.GetDestination(),
Id = data.GetPaymentId(),
Status = !paymentEntity.Accounted
? InvoicePaymentMethodDataModel.Payment.PaymentStatus.Invalid
: data.PaymentConfirmed(paymentEntity, entity.SpeedPolicy) || data.PaymentCompleted(paymentEntity)
? InvoicePaymentMethodDataModel.Payment.PaymentStatus.Settled
: InvoicePaymentMethodDataModel.Payment.PaymentStatus.Processing,
Fee = paymentEntity.NetworkFee,
Value = data.GetValue(),
ReceivedDate = paymentEntity.ReceivedTime.DateTime
};
}
private InvoiceData ToModel(InvoiceEntity entity)
{
var statuses = new List<InvoiceStatus>();
var state = entity.GetInvoiceState();
if (state.CanMarkComplete())
{
statuses.Add(InvoiceStatus.Settled);
}
if (state.CanMarkInvalid())
{
statuses.Add(InvoiceStatus.Invalid);
}
return new InvoiceData()
{
StoreId = entity.StoreId,
@ -400,6 +408,7 @@ namespace BTCPayServer.Controllers.GreenField
AdditionalStatus = entity.ExceptionStatus,
Currency = entity.Currency,
Metadata = entity.Metadata.ToJObject(),
AvailableStatusesForManualMarking = statuses.ToArray(),
Checkout = new CreateInvoiceRequest.CheckoutOptions()
{
Expiration = entity.ExpirationTime - entity.InvoiceTime,
@ -407,9 +416,11 @@ namespace BTCPayServer.Controllers.GreenField
PaymentTolerance = entity.PaymentTolerance,
PaymentMethods =
entity.GetPaymentMethods().Select(method => method.GetId().ToStringNormalized()).ToArray(),
DefaultPaymentMethod = entity.DefaultPaymentMethod,
SpeedPolicy = entity.SpeedPolicy,
DefaultLanguage = entity.DefaultLanguage,
RedirectAutomatically = entity.RedirectAutomatically,
RequiresRefundEmail = entity.RequiresRefundEmail,
RedirectURL = entity.RedirectURLTemplate
}
};

View File

@ -17,7 +17,6 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using NBitcoin;
using NBXplorer.Models;
using YamlDotNet.Core.Tokens;
using InvoiceData = BTCPayServer.Client.Models.InvoiceData;
using Language = BTCPayServer.Client.Models.Language;
using NotificationData = BTCPayServer.Client.Models.NotificationData;
@ -37,6 +36,7 @@ namespace BTCPayServer.Controllers.GreenField
private readonly StoreOnChainPaymentMethodsController _chainPaymentMethodsController;
private readonly StoreOnChainWalletsController _storeOnChainWalletsController;
private readonly StoreLightningNetworkPaymentMethodsController _storeLightningNetworkPaymentMethodsController;
private readonly StoreLNURLPayPaymentMethodsController _storeLnurlPayPaymentMethodsController;
private readonly HealthController _healthController;
private readonly GreenFieldPaymentRequestsController _paymentRequestController;
private readonly ApiKeysController _apiKeysController;
@ -58,6 +58,7 @@ namespace BTCPayServer.Controllers.GreenField
StoreOnChainPaymentMethodsController chainPaymentMethodsController,
StoreOnChainWalletsController storeOnChainWalletsController,
StoreLightningNetworkPaymentMethodsController storeLightningNetworkPaymentMethodsController,
StoreLNURLPayPaymentMethodsController storeLnurlPayPaymentMethodsController,
HealthController healthController,
GreenFieldPaymentRequestsController paymentRequestController,
ApiKeysController apiKeysController,
@ -79,6 +80,7 @@ namespace BTCPayServer.Controllers.GreenField
_chainPaymentMethodsController = chainPaymentMethodsController;
_storeOnChainWalletsController = storeOnChainWalletsController;
_storeLightningNetworkPaymentMethodsController = storeLightningNetworkPaymentMethodsController;
_storeLnurlPayPaymentMethodsController = storeLnurlPayPaymentMethodsController;
_healthController = healthController;
_paymentRequestController = paymentRequestController;
_apiKeysController = apiKeysController;
@ -141,6 +143,7 @@ namespace BTCPayServer.Controllers.GreenField
_storeLightningNodeApiController,
_internalLightningNodeApiController,
_storeLightningNetworkPaymentMethodsController,
_storeLnurlPayPaymentMethodsController,
_greenFieldInvoiceController,
_greenFieldServerInfoController,
_storeWebhooksController,
@ -165,6 +168,7 @@ namespace BTCPayServer.Controllers.GreenField
private readonly StoreLightningNodeApiController _storeLightningNodeApiController;
private readonly InternalLightningNodeApiController _lightningNodeApiController;
private readonly StoreLightningNetworkPaymentMethodsController _storeLightningNetworkPaymentMethodsController;
private readonly StoreLNURLPayPaymentMethodsController _storeLnurlPayPaymentMethodsController;
private readonly GreenFieldInvoiceController _greenFieldInvoiceController;
private readonly GreenFieldServerInfoController _greenFieldServerInfoController;
private readonly StoreWebhooksController _storeWebhooksController;
@ -183,6 +187,7 @@ namespace BTCPayServer.Controllers.GreenField
StoreLightningNodeApiController storeLightningNodeApiController,
InternalLightningNodeApiController lightningNodeApiController,
StoreLightningNetworkPaymentMethodsController storeLightningNetworkPaymentMethodsController,
StoreLNURLPayPaymentMethodsController storeLnurlPayPaymentMethodsController,
GreenFieldInvoiceController greenFieldInvoiceController,
GreenFieldServerInfoController greenFieldServerInfoController,
StoreWebhooksController storeWebhooksController,
@ -202,6 +207,7 @@ namespace BTCPayServer.Controllers.GreenField
_storeLightningNodeApiController = storeLightningNodeApiController;
_lightningNodeApiController = lightningNodeApiController;
_storeLightningNetworkPaymentMethodsController = storeLightningNetworkPaymentMethodsController;
_storeLnurlPayPaymentMethodsController = storeLnurlPayPaymentMethodsController;
_greenFieldInvoiceController = greenFieldInvoiceController;
_greenFieldServerInfoController = greenFieldServerInfoController;
_storeWebhooksController = storeWebhooksController;
@ -518,16 +524,21 @@ namespace BTCPayServer.Controllers.GreenField
}
public override async Task<OnChainPaymentMethodData> UpdateStoreOnChainPaymentMethod(string storeId,
string cryptoCode, OnChainPaymentMethodData paymentMethod,
string cryptoCode, UpdateOnChainPaymentMethodRequest paymentMethod,
CancellationToken token = default)
{
return GetFromActionResult<OnChainPaymentMethodData>(
await _chainPaymentMethodsController.UpdateOnChainPaymentMethod(storeId, cryptoCode, paymentMethod));
await _chainPaymentMethodsController.UpdateOnChainPaymentMethod(storeId, cryptoCode, new UpdateOnChainPaymentMethodRequest(
enabled: paymentMethod.Enabled,
label: paymentMethod.Label,
accountKeyPath: paymentMethod.AccountKeyPath,
derivationScheme: paymentMethod.DerivationScheme
)));
}
public override Task<OnChainPaymentMethodPreviewResultData> PreviewProposedStoreOnChainPaymentMethodAddresses(
string storeId, string cryptoCode,
OnChainPaymentMethodData paymentMethod, int offset = 0, int amount = 10, CancellationToken token = default)
UpdateOnChainPaymentMethodRequest paymentMethod, int offset = 0, int amount = 10, CancellationToken token = default)
{
return Task.FromResult(GetFromActionResult<OnChainPaymentMethodPreviewResultData>(
_chainPaymentMethodsController.GetProposedOnChainPaymentMethodPreview(storeId, cryptoCode,
@ -741,7 +752,39 @@ namespace BTCPayServer.Controllers.GreenField
{
return GetFromActionResult<StoreData>(await _storesController.UpdateStore(storeId, request));
}
public override Task<IEnumerable<LNURLPayPaymentMethodData>>
GetStoreLNURLPayPaymentMethods(string storeId, bool? enabled,
CancellationToken token = default)
{
return Task.FromResult(GetFromActionResult(
_storeLnurlPayPaymentMethodsController.GetLNURLPayPaymentMethods(storeId, enabled)));
}
public override Task<LNURLPayPaymentMethodData> GetStoreLNURLPayPaymentMethod(
string storeId, string cryptoCode, CancellationToken token = default)
{
return Task.FromResult(GetFromActionResult<LNURLPayPaymentMethodData>(
_storeLnurlPayPaymentMethodsController.GetLNURLPayPaymentMethod(storeId, cryptoCode)));
}
public override async Task RemoveStoreLNURLPayPaymentMethod(string storeId, string cryptoCode,
CancellationToken token = default)
{
HandleActionResult(
await _storeLnurlPayPaymentMethodsController.RemoveLNURLPayPaymentMethod(storeId,
cryptoCode));
}
public override async Task<LNURLPayPaymentMethodData> UpdateStoreLNURLPayPaymentMethod(
string storeId, string cryptoCode,
LNURLPayPaymentMethodData paymentMethod, CancellationToken token = default)
{
return GetFromActionResult<LNURLPayPaymentMethodData>(await
_storeLnurlPayPaymentMethodsController.UpdateLNURLPayPaymentMethod(storeId, cryptoCode,
paymentMethod));
}
public override Task<IEnumerable<LightningNetworkPaymentMethodData>>
GetStoreLightningNetworkPaymentMethods(string storeId, bool? enabled,
CancellationToken token = default)
@ -767,11 +810,11 @@ namespace BTCPayServer.Controllers.GreenField
public override async Task<LightningNetworkPaymentMethodData> UpdateStoreLightningNetworkPaymentMethod(
string storeId, string cryptoCode,
LightningNetworkPaymentMethodData paymentMethod, CancellationToken token = default)
UpdateLightningNetworkPaymentMethodRequest paymentMethod, CancellationToken token = default)
{
return GetFromActionResult<LightningNetworkPaymentMethodData>(await
_storeLightningNetworkPaymentMethodsController.UpdateLightningNetworkPaymentMethod(storeId, cryptoCode,
paymentMethod));
new UpdateLightningNetworkPaymentMethodRequest(paymentMethod.ConnectionString, paymentMethod.Enabled)));
}
public override async Task<IEnumerable<InvoiceData>> GetInvoices(string storeId, string[] orderId = null,
@ -873,16 +916,9 @@ namespace BTCPayServer.Controllers.GreenField
return Task.FromResult(GetFromActionResult<PermissionMetadata[]>(_homeController.Permissions()));
}
public override async Task<LightningNetworkPaymentMethodData> UpdateStoreLightningNetworkPaymentMethodToInternalNode(string storeId, string cryptoCode,
CancellationToken token = default)
public override async Task<Dictionary<string, GenericPaymentMethodData>> GetStorePaymentMethods(string storeId, bool? enabled = null, CancellationToken token = default)
{
//nothing to change, just local client sugar
return await base.UpdateStoreLightningNetworkPaymentMethodToInternalNode(storeId, cryptoCode, token);
}
public override Task<Dictionary<string, GenericPaymentMethodData>> GetStorePaymentMethods(string storeId, bool? enabled = null, CancellationToken token = default)
{
return Task.FromResult(GetFromActionResult(_storePaymentMethodsController.GetStorePaymentMethods(storeId, enabled)));
return GetFromActionResult(await _storePaymentMethodsController.GetStorePaymentMethods(storeId, enabled));
}
public override async Task<OnChainPaymentMethodDataWithSensitiveData> GenerateOnChainWallet(string storeId, string cryptoCode, GenerateOnChainWalletRequest request,

View File

@ -3,14 +3,12 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.Payments;
using BTCPayServer.Security;
using BTCPayServer.Services;
using BTCPayServer.Services.Rates;
using Microsoft.AspNetCore.Authorization;
@ -101,19 +99,21 @@ namespace BTCPayServer.Controllers.GreenField
ModelState.AddModelError(nameof(request.Period), $"The period should be positive");
}
PaymentMethodId[] paymentMethods = null;
if (request.PaymentMethods is string[] paymentMethodsStr)
if (request.PaymentMethods is { } paymentMethodsStr)
{
paymentMethods = paymentMethodsStr.Select(p => new PaymentMethodId(p, PaymentTypes.BTCLike)).ToArray();
foreach (var p in paymentMethods)
paymentMethods = paymentMethodsStr.Select(s =>
{
var n = _networkProvider.GetNetwork<BTCPayNetwork>(p.CryptoCode);
if (n is null)
ModelState.AddModelError(nameof(request.PaymentMethods), "Invalid payment method");
if (n.ReadonlyWallet)
ModelState.AddModelError(nameof(request.PaymentMethods), "Invalid payment method (We do not support the crypto currency for refund)");
}
if (paymentMethods.Any(p => _networkProvider.GetNetwork<BTCPayNetwork>(p.CryptoCode) is null))
ModelState.AddModelError(nameof(request.PaymentMethods), "Invalid payment method");
PaymentMethodId.TryParse(s, out var pmi);
return pmi;
}).ToArray();
var supported = _payoutHandlers.GetSupportedPaymentMethods().ToArray();
for (int i = 0; i < paymentMethods.Length; i++)
{
if (!supported.Contains(paymentMethods[i]))
{
request.AddModelError(paymentRequest => paymentRequest.PaymentMethods[i], "Invalid or unsupported payment method", this);
}
}
}
else
{
@ -233,7 +233,7 @@ namespace BTCPayServer.Controllers.GreenField
return this.CreateValidationError(ModelState);
}
var payoutHandler = _payoutHandlers.FirstOrDefault(handler => handler.CanHandle(paymentMethodId));
var payoutHandler = _payoutHandlers.FindPayoutHandler(paymentMethodId);
if (payoutHandler is null)
{
ModelState.AddModelError(nameof(request.PaymentMethod), "Invalid payment method");
@ -245,14 +245,23 @@ namespace BTCPayServer.Controllers.GreenField
if (pp is null)
return PullPaymentNotFound();
var ppBlob = pp.GetBlob();
IClaimDestination destination = await payoutHandler.ParseClaimDestination(paymentMethodId,request.Destination);
if (destination is null)
var destination = await payoutHandler.ParseClaimDestination(paymentMethodId,request.Destination, true);
if (destination.destination is null)
{
ModelState.AddModelError(nameof(request.Destination), "The destination must be an address or a BIP21 URI");
ModelState.AddModelError(nameof(request.Destination), destination.error??"The destination is invalid for the payment specified");
return this.CreateValidationError(ModelState);
}
if (request.Amount is decimal v && (v < ppBlob.MinimumClaim || v == 0.0m))
if (request.Amount is null && destination.destination.Amount != null)
{
request.Amount = destination.destination.Amount;
}
else if (request.Amount != null && destination.destination.Amount != null && request.Amount != destination.destination.Amount)
{
ModelState.AddModelError(nameof(request.Amount), $"Amount is implied in destination ({destination.destination.Amount}) that does not match the payout amount provided {request.Amount})");
return this.CreateValidationError(ModelState);
}
if (request.Amount is { } v && (v < ppBlob.MinimumClaim || v == 0.0m))
{
ModelState.AddModelError(nameof(request.Amount), $"Amount too small (should be at least {ppBlob.MinimumClaim})");
return this.CreateValidationError(ModelState);
@ -260,7 +269,7 @@ namespace BTCPayServer.Controllers.GreenField
var cd = _currencyNameTable.GetCurrencyData(pp.GetBlob().Currency, false);
var result = await _pullPaymentService.Claim(new ClaimRequest()
{
Destination = destination,
Destination = destination.destination,
PullPaymentId = pullPaymentId,
Value = request.Amount,
PaymentMethodId = paymentMethodId

View File

@ -0,0 +1,180 @@
#nullable enable
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Configuration;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.Lightning;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Security;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using StoreData = BTCPayServer.Data.StoreData;
namespace BTCPayServer.Controllers.GreenField
{
[ApiController]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public class StoreLNURLPayPaymentMethodsController : ControllerBase
{
private StoreData Store => HttpContext.GetStoreData();
private readonly StoreRepository _storeRepository;
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly IAuthorizationService _authorizationService;
private readonly ISettingsRepository _settingsRepository;
public StoreLNURLPayPaymentMethodsController(
StoreRepository storeRepository,
BTCPayNetworkProvider btcPayNetworkProvider,
IAuthorizationService authorizationService,
ISettingsRepository settingsRepository)
{
_storeRepository = storeRepository;
_btcPayNetworkProvider = btcPayNetworkProvider;
_authorizationService = authorizationService;
_settingsRepository = settingsRepository;
}
public static IEnumerable<LNURLPayPaymentMethodData> GetLNURLPayPaymentMethods(StoreData store,
BTCPayNetworkProvider networkProvider, bool? enabled)
{
var blob = store.GetStoreBlob();
var excludedPaymentMethods = blob.GetExcludedPaymentMethods();
return store.GetSupportedPaymentMethods(networkProvider)
.Where((method) => method.PaymentId.PaymentType == PaymentTypes.LNURLPay)
.OfType<LNURLPaySupportedPaymentMethod>()
.Select(paymentMethod =>
new LNURLPayPaymentMethodData(
paymentMethod.PaymentId.CryptoCode,
!excludedPaymentMethods.Match(paymentMethod.PaymentId),
paymentMethod.UseBech32Scheme, paymentMethod.EnableForStandardInvoices
)
)
.Where((result) => enabled is null || enabled == result.Enabled)
.ToList();
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/LNURLPay")]
public ActionResult<IEnumerable<LNURLPayPaymentMethodData>> GetLNURLPayPaymentMethods(
string storeId,
[FromQuery] bool? enabled)
{
return Ok(GetLNURLPayPaymentMethods(Store, _btcPayNetworkProvider, enabled));
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/LNURLPay/{cryptoCode}")]
public IActionResult GetLNURLPayPaymentMethod(string storeId, string cryptoCode)
{
if (!GetNetwork(cryptoCode, out BTCPayNetwork _))
{
return NotFound();
}
var method = GetExistingLNURLPayPaymentMethod(cryptoCode);
if (method is null)
{
return this.CreateAPIError(404, "paymentmethod-not-found", "The LNURL Payment Method isn't activated");
}
return Ok(method);
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpDelete("~/api/v1/stores/{storeId}/payment-methods/LNURLPay/{cryptoCode}")]
public async Task<IActionResult> RemoveLNURLPayPaymentMethod(
string storeId,
string cryptoCode)
{
if (!GetNetwork(cryptoCode, out BTCPayNetwork _))
{
return NotFound();
}
var id = new PaymentMethodId(cryptoCode, PaymentTypes.LNURLPay);
var store = Store;
store.SetSupportedPaymentMethod(id, null);
await _storeRepository.UpdateStore(store);
return Ok();
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPut("~/api/v1/stores/{storeId}/payment-methods/LNURLPay/{cryptoCode}")]
public async Task<IActionResult> UpdateLNURLPayPaymentMethod(string storeId, string cryptoCode,
[FromBody] LNURLPayPaymentMethodData paymentMethodData)
{
var paymentMethodId = new PaymentMethodId(cryptoCode, PaymentTypes.LNURLPay);
if (!GetNetwork(cryptoCode, out var network))
{
return NotFound();
}
var lnMethod = StoreLightningNetworkPaymentMethodsController.GetExistingLightningLikePaymentMethod(_btcPayNetworkProvider,
cryptoCode, Store);
if ((lnMethod is null || lnMethod.Enabled is false) && paymentMethodData.Enabled)
{
ModelState.AddModelError(nameof(LNURLPayPaymentMethodData.Enabled),
"LNURL Pay cannot be enabled unless the lightning payment method is configured and enabled on this store");
}
if (!ModelState.IsValid)
return this.CreateValidationError(ModelState);
LNURLPaySupportedPaymentMethod? paymentMethod = new LNURLPaySupportedPaymentMethod()
{
CryptoCode = cryptoCode,
UseBech32Scheme = paymentMethodData.UseBech32Scheme,
EnableForStandardInvoices = paymentMethodData.EnableForStandardInvoices
};
var store = Store;
var storeBlob = store.GetStoreBlob();
store.SetSupportedPaymentMethod(paymentMethodId, paymentMethod);
storeBlob.SetExcluded(paymentMethodId, !paymentMethodData.Enabled);
store.SetStoreBlob(storeBlob);
await _storeRepository.UpdateStore(store);
return Ok(GetExistingLNURLPayPaymentMethod(cryptoCode, store));
}
private LNURLPayPaymentMethodData? GetExistingLNURLPayPaymentMethod(string cryptoCode,
StoreData? store = null)
{
store ??= Store;
var storeBlob = store.GetStoreBlob();
var id = new PaymentMethodId(cryptoCode, PaymentTypes.LNURLPay);
var paymentMethod = store
.GetSupportedPaymentMethods(_btcPayNetworkProvider)
.OfType<LNURLPaySupportedPaymentMethod>()
.FirstOrDefault(method => method.PaymentId == id);
var excluded = storeBlob.IsExcluded(id);
return paymentMethod is null
? null
: new LNURLPayPaymentMethodData(
paymentMethod.PaymentId.CryptoCode,
!excluded,
paymentMethod.UseBech32Scheme, paymentMethod.EnableForStandardInvoices
);
}
private bool GetNetwork(string cryptoCode, [MaybeNullWhen(false)] out BTCPayNetwork network)
{
network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
network = network?.SupportLightning is true ? network : null;
return network != null;
}
}
}

View File

@ -58,7 +58,9 @@ namespace BTCPayServer.Controllers.GreenField
paymentMethod.PaymentId.CryptoCode,
paymentMethod.GetExternalLightningUrl()?.ToString() ??
paymentMethod.GetDisplayableConnectionString(),
!excludedPaymentMethods.Match(paymentMethod.PaymentId)
!excludedPaymentMethods.Match(paymentMethod.PaymentId),
paymentMethod.PaymentId.ToStringNormalized(),
paymentMethod.DisableBOLT11PaymentOption
)
)
.Where((result) => enabled is null || enabled == result.Enabled)
@ -83,7 +85,7 @@ namespace BTCPayServer.Controllers.GreenField
return NotFound();
}
var method = GetExistingLightningLikePaymentMethod(cryptoCode);
var method = GetExistingLightningLikePaymentMethod(_btcPayNetworkProvider, cryptoCode, Store);
if (method is null)
{
return NotFound();
@ -96,8 +98,7 @@ namespace BTCPayServer.Controllers.GreenField
[HttpDelete("~/api/v1/stores/{storeId}/payment-methods/LightningNetwork/{cryptoCode}")]
public async Task<IActionResult> RemoveLightningNetworkPaymentMethod(
string storeId,
string cryptoCode,
int offset = 0, int amount = 10)
string cryptoCode)
{
if (!GetNetwork(cryptoCode, out BTCPayNetwork _))
{
@ -114,7 +115,7 @@ namespace BTCPayServer.Controllers.GreenField
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPut("~/api/v1/stores/{storeId}/payment-methods/LightningNetwork/{cryptoCode}")]
public async Task<IActionResult> UpdateLightningNetworkPaymentMethod(string storeId, string cryptoCode,
[FromBody] LightningNetworkPaymentMethodData paymentMethodData)
[FromBody] UpdateLightningNetworkPaymentMethodRequest request)
{
var paymentMethodId = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
@ -123,7 +124,7 @@ namespace BTCPayServer.Controllers.GreenField
return NotFound();
}
if (string.IsNullOrEmpty(paymentMethodData.ConnectionString))
if (string.IsNullOrEmpty(request.ConnectionString))
{
ModelState.AddModelError(nameof(LightningNetworkPaymentMethodData.ConnectionString),
"Missing connectionString");
@ -133,13 +134,13 @@ namespace BTCPayServer.Controllers.GreenField
return this.CreateValidationError(ModelState);
LightningSupportedPaymentMethod? paymentMethod = null;
if (!string.IsNullOrEmpty(paymentMethodData!.ConnectionString))
if (!string.IsNullOrEmpty(request!.ConnectionString))
{
if (paymentMethodData.ConnectionString == LightningSupportedPaymentMethod.InternalNode)
if (request.ConnectionString == LightningSupportedPaymentMethod.InternalNode)
{
if (!await CanUseInternalLightning())
{
ModelState.AddModelError(nameof(paymentMethodData.ConnectionString),
ModelState.AddModelError(nameof(request.ConnectionString),
$"You are not authorized to use the internal lightning node");
return this.CreateValidationError(ModelState);
}
@ -152,23 +153,23 @@ namespace BTCPayServer.Controllers.GreenField
}
else
{
if (!LightningConnectionString.TryParse(paymentMethodData.ConnectionString, false,
if (!LightningConnectionString.TryParse(request.ConnectionString, false,
out var connectionString, out var error))
{
ModelState.AddModelError(nameof(paymentMethodData.ConnectionString), $"Invalid URL ({error})");
ModelState.AddModelError(nameof(request.ConnectionString), $"Invalid URL ({error})");
return this.CreateValidationError(ModelState);
}
if (connectionString.ConnectionType == LightningConnectionType.LndGRPC)
{
ModelState.AddModelError(nameof(paymentMethodData.ConnectionString),
ModelState.AddModelError(nameof(request.ConnectionString),
$"BTCPay does not support gRPC connections");
return this.CreateValidationError(ModelState);
}
if (!await CanManageServer() && !connectionString.IsSafe())
{
ModelState.AddModelError(nameof(paymentMethodData.ConnectionString),
ModelState.AddModelError(nameof(request.ConnectionString),
$"You do not have 'btcpay.server.canmodifyserversettings' rights, so the connection string should not contain 'cookiefilepath', 'macaroondirectorypath', 'macaroonfilepath', and should not point to a local ip or to a dns name ending with '.internal', '.local', '.lan' or '.'.");
return this.CreateValidationError(ModelState);
}
@ -184,20 +185,20 @@ namespace BTCPayServer.Controllers.GreenField
var store = Store;
var storeBlob = store.GetStoreBlob();
store.SetSupportedPaymentMethod(paymentMethodId, paymentMethod);
storeBlob.SetExcluded(paymentMethodId, !paymentMethodData.Enabled);
storeBlob.SetExcluded(paymentMethodId, !request.Enabled);
store.SetStoreBlob(storeBlob);
await _storeRepository.UpdateStore(store);
return Ok(GetExistingLightningLikePaymentMethod(cryptoCode, store));
return Ok(GetExistingLightningLikePaymentMethod(_btcPayNetworkProvider, cryptoCode, store));
}
private LightningNetworkPaymentMethodData? GetExistingLightningLikePaymentMethod(string cryptoCode,
StoreData? store = null)
public static LightningNetworkPaymentMethodData? GetExistingLightningLikePaymentMethod(BTCPayNetworkProvider btcPayNetworkProvider, string cryptoCode,
StoreData store)
{
store ??= Store;
var storeBlob = store.GetStoreBlob();
var id = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
var paymentMethod = store
.GetSupportedPaymentMethods(_btcPayNetworkProvider)
.GetSupportedPaymentMethods(btcPayNetworkProvider)
.OfType<LightningSupportedPaymentMethod>()
.FirstOrDefault(method => method.PaymentId == id);
@ -205,7 +206,8 @@ namespace BTCPayServer.Controllers.GreenField
return paymentMethod is null
? null
: new LightningNetworkPaymentMethodData(paymentMethod.PaymentId.CryptoCode,
paymentMethod.GetDisplayableConnectionString(), !excluded);
paymentMethod.GetDisplayableConnectionString(), !excluded,
paymentMethod.PaymentId.ToStringNormalized(), paymentMethod.DisableBOLT11PaymentOption);
}
private bool GetNetwork(string cryptoCode, [MaybeNullWhen(false)] out BTCPayNetwork network)

View File

@ -91,7 +91,7 @@ namespace BTCPayServer.Controllers.GreenField
await _storeRepository.UpdateStore(store);
var rawResult = GetExistingBtcLikePaymentMethod(cryptoCode, store);
var result = new OnChainPaymentMethodDataWithSensitiveData(rawResult.CryptoCode, rawResult.DerivationScheme,
rawResult.Enabled, rawResult.Label, rawResult.AccountKeyPath, response.GetMnemonic());
rawResult.Enabled, rawResult.Label, rawResult.AccountKeyPath, response.GetMnemonic(), derivationSchemeSettings.PaymentId.ToStringNormalized());
return Ok(result);
}

View File

@ -57,7 +57,9 @@ namespace BTCPayServer.Controllers.GreenField
.OfType<DerivationSchemeSettings>()
.Select(strategy =>
new OnChainPaymentMethodData(strategy.PaymentId.CryptoCode,
strategy.AccountDerivation.ToString(), !excludedPaymentMethods.Match(strategy.PaymentId), strategy.Label, strategy.GetSigningAccountKeySettings().GetRootedKeyPath()))
strategy.AccountDerivation.ToString(), !excludedPaymentMethods.Match(strategy.PaymentId),
strategy.Label, strategy.GetSigningAccountKeySettings().GetRootedKeyPath(),
strategy.PaymentId.ToStringNormalized()))
.Where((result) => enabled is null || enabled == result.Enabled)
.ToList();
}
@ -144,7 +146,7 @@ namespace BTCPayServer.Controllers.GreenField
public IActionResult GetProposedOnChainPaymentMethodPreview(
string storeId,
string cryptoCode,
[FromBody] OnChainPaymentMethodData paymentMethodData,
[FromBody] UpdateOnChainPaymentMethodRequest paymentMethodData,
int offset = 0, int amount = 10)
{
if (!GetCryptoCodeWallet(cryptoCode, out var network, out BTCPayWallet _))
@ -217,7 +219,7 @@ namespace BTCPayServer.Controllers.GreenField
public async Task<IActionResult> UpdateOnChainPaymentMethod(
string storeId,
string cryptoCode,
[FromBody] OnChainPaymentMethodData paymentMethodData)
[FromBody] UpdateOnChainPaymentMethodRequest request)
{
var id = new PaymentMethodId(cryptoCode, PaymentTypes.BTCLike);
@ -226,7 +228,7 @@ namespace BTCPayServer.Controllers.GreenField
return NotFound();
}
if (string.IsNullOrEmpty(paymentMethodData?.DerivationScheme))
if (string.IsNullOrEmpty(request?.DerivationScheme))
{
ModelState.AddModelError(nameof(OnChainPaymentMethodData.DerivationScheme),
"Missing derivationScheme");
@ -239,12 +241,12 @@ namespace BTCPayServer.Controllers.GreenField
{
var store = Store;
var storeBlob = store.GetStoreBlob();
var strategy = DerivationSchemeSettings.Parse(paymentMethodData.DerivationScheme, network);
var strategy = DerivationSchemeSettings.Parse(request.DerivationScheme, network);
if (strategy != null)
await wallet.TrackAsync(strategy.AccountDerivation);
strategy.Label = paymentMethodData.Label;
strategy.Label = request.Label;
var signing = strategy.GetSigningAccountKeySettings();
if (paymentMethodData.AccountKeyPath is RootedKeyPath r)
if (request.AccountKeyPath is RootedKeyPath r)
{
signing.AccountKeyPath = r.KeyPath;
signing.RootFingerprint = r.MasterFingerprint;
@ -256,7 +258,7 @@ namespace BTCPayServer.Controllers.GreenField
}
store.SetSupportedPaymentMethod(id, strategy);
storeBlob.SetExcluded(id, !paymentMethodData.Enabled);
storeBlob.SetExcluded(id, !request.Enabled);
store.SetStoreBlob(storeBlob);
await _storeRepository.UpdateStore(store);
return Ok(GetExistingBtcLikePaymentMethod(cryptoCode, store));
@ -291,7 +293,8 @@ namespace BTCPayServer.Controllers.GreenField
? null
: new OnChainPaymentMethodData(paymentMethod.PaymentId.CryptoCode,
paymentMethod.AccountDerivation.ToString(), !excluded, paymentMethod.Label,
paymentMethod.GetSigningAccountKeySettings().GetRootedKeyPath());
paymentMethod.GetSigningAccountKeySettings().GetRootedKeyPath(),
paymentMethod.PaymentId.ToStringNormalized());
}
}
}

View File

@ -313,10 +313,13 @@ namespace BTCPayServer.Controllers.GreenField
var address = string.Empty;
try
{
destination.Destination = destination.Destination.Replace(network.UriScheme+":", "bitcoin:", StringComparison.InvariantCultureIgnoreCase);
bip21 = new BitcoinUrlBuilder(destination.Destination, network.NBitcoinNetwork);
amount ??= bip21.Amount.GetValue(network);
address = bip21.Address.ToString();
if (bip21.Address is null)
request.AddModelError(transactionRequest => transactionRequest.Destinations[index],
"This BIP21 destination is missing a bitcoin address", this);
else
address = bip21.Address.ToString();
if (destination.SubtractFromAmount)
{
request.AddModelError(transactionRequest => transactionRequest.Destinations[index],

View File

@ -1,10 +1,12 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Security;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using StoreData = BTCPayServer.Data.StoreData;
@ -17,20 +19,24 @@ namespace BTCPayServer.Controllers.GreenField
{
private StoreData Store => HttpContext.GetStoreData();
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly IAuthorizationService _authorizationService;
public StorePaymentMethodsController(BTCPayNetworkProvider btcPayNetworkProvider)
public StorePaymentMethodsController(BTCPayNetworkProvider btcPayNetworkProvider, IAuthorizationService authorizationService)
{
_btcPayNetworkProvider = btcPayNetworkProvider;
_authorizationService = authorizationService;
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/payment-methods")]
public ActionResult<Dictionary<string, GenericPaymentMethodData>> GetStorePaymentMethods(
public async Task<ActionResult<Dictionary<string, GenericPaymentMethodData>>> GetStorePaymentMethods(
string storeId,
[FromQuery] bool? enabled)
{
var storeBlob = Store.GetStoreBlob();
var excludedPaymentMethods = storeBlob.GetExcludedPaymentMethods();
var canModifyStore = (await _authorizationService.AuthorizeAsync(User, null,
new PolicyRequirement(Policies.CanModifyStoreSettings))).Succeeded;;
return Ok(Store.GetSupportedPaymentMethods(_btcPayNetworkProvider)
.Where(method =>
enabled is null || (enabled is false && excludedPaymentMethods.Match(method.PaymentId)))
@ -38,8 +44,9 @@ namespace BTCPayServer.Controllers.GreenField
method => method.PaymentId.ToStringNormalized(),
method => new GenericPaymentMethodData()
{
CryptoCode = method.PaymentId.CryptoCode,
Enabled = enabled.GetValueOrDefault(!excludedPaymentMethods.Match(method.PaymentId)),
Data = method.PaymentId.PaymentType.GetGreenfieldData(method)
Data = method.PaymentId.PaymentType.GetGreenfieldData(method, canModifyStore)
}));
}
}

View File

@ -83,8 +83,8 @@ namespace BTCPayServer.Controllers.GreenField
var store = new Data.StoreData();
PaymentMethodId.TryParse(request.DefaultPaymentMethod, out var defaultPaymnetMethodId);
ToModel(request, store, defaultPaymnetMethodId);
PaymentMethodId.TryParse(request.DefaultPaymentMethod, out var defaultPaymentMethodId);
ToModel(request, store, defaultPaymentMethodId);
await _storeRepository.CreateStore(_userManager.GetUserId(User), store);
return Ok(FromModel(store));
}
@ -104,8 +104,9 @@ namespace BTCPayServer.Controllers.GreenField
return validationResult;
}
PaymentMethodId.TryParse(request.DefaultPaymentMethod, out var defaultPaymnetMethodId);
ToModel(request, store, defaultPaymnetMethodId);
PaymentMethodId.TryParse(request.DefaultPaymentMethod, out var defaultPaymentMethodId);
ToModel(request, store, defaultPaymentMethodId);
await _storeRepository.UpdateStore(store);
return Ok(FromModel(store));
}
@ -119,10 +120,9 @@ namespace BTCPayServer.Controllers.GreenField
Name = data.StoreName,
Website = data.StoreWebsite,
SpeedPolicy = data.SpeedPolicy,
DefaultPaymentMethod = data.GetDefaultPaymentId(_btcPayNetworkProvider)?.ToStringNormalized(),
DefaultPaymentMethod = data.GetDefaultPaymentId()?.ToStringNormalized(),
//blob
//we do not include DefaultCurrencyPairs,Spread, PreferredExchange, RateScripting, RateScript in this model and instead opt to set it in stores/storeid/rates endpoints
//we do not include CoinSwitchSettings in this model and instead opt to set it in stores/storeid/coinswitch endpoints
//we do not include ExcludedPaymentMethods in this model and instead opt to set it in stores/storeid/payment-methods endpoints
//we do not include EmailSettings in this model and instead opt to set it in stores/storeid/email endpoints
//we do not include PaymentMethodCriteria because moving the CurrencyValueJsonConverter to the Client csproj is hard and requires a refactor (#1571 & #1572)
@ -151,7 +151,6 @@ namespace BTCPayServer.Controllers.GreenField
private static void ToModel(StoreBaseData restModel, Data.StoreData model, PaymentMethodId defaultPaymentMethod)
{
var blob = model.GetStoreBlob();
model.StoreName = restModel.Name;
model.StoreName = restModel.Name;
model.StoreWebsite = restModel.Website;
@ -160,11 +159,11 @@ namespace BTCPayServer.Controllers.GreenField
//we do not include the default payment method in this model and instead opt to set it in the stores/storeid/payment-methods endpoints
//blob
//we do not include DefaultCurrencyPairs;Spread; PreferredExchange; RateScripting; RateScript in this model and instead opt to set it in stores/storeid/rates endpoints
//we do not include CoinSwitchSettings in this model and instead opt to set it in stores/storeid/coinswitch endpoints
//we do not include ExcludedPaymentMethods in this model and instead opt to set it in stores/storeid/payment-methods endpoints
//we do not include EmailSettings in this model and instead opt to set it in stores/storeid/email endpoints
//we do not include OnChainMinValue and LightningMaxValue because moving the CurrencyValueJsonConverter to the Client csproj is hard and requires a refactor (#1571 & #1572)
blob.NetworkFeeMode = restModel.NetworkFeeMode;
blob.DefaultCurrency = restModel.DefaultCurrency;
blob.RequiresRefundEmail = restModel.RequiresRefundEmail;
blob.LightningAmountInSatoshi = restModel.LightningAmountInSatoshi;
blob.LightningPrivateRouteHints = restModel.LightningPrivateRouteHints;
@ -194,7 +193,7 @@ namespace BTCPayServer.Controllers.GreenField
}
if (!string.IsNullOrEmpty(request.DefaultPaymentMethod) &&
!PaymentMethodId.TryParse(request.DefaultPaymentMethod, out var defaultPaymnetMethodId))
!PaymentMethodId.TryParse(request.DefaultPaymentMethod, out var defaultPaymentMethodId))
{
ModelState.AddModelError(nameof(request.Name), "DefaultPaymentMethod is invalid");
}

View File

@ -0,0 +1,111 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Mime;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Filters;
using BTCPayServer.Logging;
using BTCPayServer.HostedServices;
using BTCPayServer.Models;
using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Rating;
using BTCPayServer.Security;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Invoices.Export;
using BTCPayServer.Services.Rates;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Routing;
using Microsoft.EntityFrameworkCore;
using NBitcoin;
using NBitcoin.RPC;
using NBitpayClient;
using NBXplorer;
using Newtonsoft.Json.Linq;
using BitpayCreateInvoiceRequest = BTCPayServer.Models.BitpayCreateInvoiceRequest;
using StoreData = BTCPayServer.Data.StoreData;
using BTCPayServer.Services;
namespace BTCPayServer.Controllers
{
public partial class InvoiceController
{
public class FakePaymentRequest
{
public Decimal Amount { get; set; }
}
[HttpPost]
[Route("i/{invoiceId}/test-payment")]
[CheatModeRoute]
public async Task<IActionResult> TestPayment(string invoiceId, FakePaymentRequest request, [FromServices] Cheater cheater)
{
var invoice = await _InvoiceRepository.GetInvoice(invoiceId);
var store = await _StoreRepository.FindStore(invoice.StoreId);
// TODO support altcoins, not just bitcoin
//var network = invoice.Networks.GetNetwork(invoice.Currency);
var cryptoCode = "BTC";
var network = _NetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
var paymentMethodId = store.GetDefaultPaymentId();
//var network = NetworkProvider.GetNetwork<BTCPayNetwork>("BTC");
var bitcoinAddressString = invoice.GetPaymentMethod(paymentMethodId).GetPaymentMethodDetails().GetPaymentDestination();
var bitcoinAddressObj = BitcoinAddress.Create(bitcoinAddressString, network.NBitcoinNetwork);
var BtcAmount = request.Amount;
try
{
var paymentMethod = invoice.GetPaymentMethod(paymentMethodId);
var rate = paymentMethod.Rate;
var txid = cheater.CashCow.SendToAddress(bitcoinAddressObj, new Money(BtcAmount, MoneyUnit.BTC)).ToString();
// TODO The value of totalDue is wrong. How can we get the real total due? invoice.Price is only correct if this is the 2nd payment, not for a 3rd or 4th payment.
var totalDue = invoice.Price;
return Ok(new
{
Txid = txid,
AmountRemaining = (totalDue - (BtcAmount * rate)) / rate,
SuccessMessage = "Created transaction " + txid
});
}
catch (Exception e)
{
return BadRequest(new
{
ErrorMessage = e.Message,
AmountRemaining = invoice.Price
});
}
}
[HttpPost]
[Route("i/{invoiceId}/expire")]
[CheatModeRoute]
public async Task<IActionResult> TestExpireNow(string invoiceId, [FromServices] Cheater cheater)
{
try
{
await cheater.UpdateInvoiceExpiry(invoiceId, DateTimeOffset.Now);
return Ok(new { SuccessMessage = "Invoice is now expired." });
}
catch (Exception e)
{
return BadRequest(new { ErrorMessage = e.Message });
}
}
}
}

View File

@ -13,17 +13,13 @@ using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Filters;
using BTCPayServer.Logging;
using BTCPayServer.HostedServices;
using BTCPayServer.Models;
using BTCPayServer.Logging;
using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Plugins.CoinSwitch;
using BTCPayServer.Rating;
using BTCPayServer.Security;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Invoices.Export;
using BTCPayServer.Services.Rates;
@ -32,6 +28,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Routing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using NBitcoin;
using NBitpayClient;
using NBXplorer;
@ -105,14 +102,15 @@ namespace BTCPayServer.Controllers
return NotFound();
var store = await _StoreRepository.FindStore(invoice.StoreId);
var invoiceState = invoice.GetInvoiceState();
var model = new InvoiceDetailsModel()
{
StoreId = store.Id,
StoreName = store.StoreName,
StoreLink = Url.Action(nameof(StoresController.UpdateStore), "Stores", new { storeId = store.Id }),
StoreLink = Url.Action(nameof(StoresController.PaymentMethods), "Stores", new { storeId = store.Id }),
PaymentRequestLink = Url.Action(nameof(PaymentRequestController.ViewPaymentRequest), "PaymentRequest", new { id = invoice.Metadata.PaymentRequestId }),
Id = invoice.Id,
State = invoice.GetInvoiceState().ToString(),
State = invoiceState.ToString(),
TransactionSpeed = invoice.SpeedPolicy == SpeedPolicy.HighSpeed ? "high" :
invoice.SpeedPolicy == SpeedPolicy.MediumSpeed ? "medium" :
invoice.SpeedPolicy == SpeedPolicy.LowMediumSpeed ? "low-medium" :
@ -130,11 +128,13 @@ namespace BTCPayServer.Controllers
Events = invoice.Events,
PosData = PosDataParser.ParsePosData(invoice.Metadata.PosData),
Archived = invoice.Archived,
CanRefund = CanRefund(invoice.GetInvoiceState()),
CanRefund = CanRefund(invoiceState),
ShowCheckout = invoice.Status == InvoiceStatusLegacy.New,
Deliveries = (await _InvoiceRepository.GetWebhookDeliveries(invoiceId))
.Select(c => new Models.StoreViewModels.DeliveryViewModel(c))
.ToList()
.ToList(),
CanMarkInvalid = invoiceState.CanMarkInvalid(),
CanMarkComplete = invoiceState.CanMarkComplete(),
};
model.Addresses = invoice.HistoricalAddresses.Select(h =>
new InvoiceDetailsModel.AddressModel
@ -165,7 +165,7 @@ namespace BTCPayServer.Controllers
[HttpGet]
[Route("invoices/{invoiceId}/refund")]
[AllowAnonymous]
public async Task<IActionResult> Refund(string invoiceId, CancellationToken cancellationToken)
public async Task<IActionResult> Refund([FromServices]IEnumerable<IPayoutHandler> payoutHandlers, string invoiceId, CancellationToken cancellationToken)
{
using var ctx = _dbContextFactory.CreateContext();
ctx.ChangeTracker.QueryTrackingBehavior = Microsoft.EntityFrameworkCore.QueryTrackingBehavior.NoTracking;
@ -173,7 +173,7 @@ namespace BTCPayServer.Controllers
.Include(i => i.CurrentRefund)
.Include(i => i.CurrentRefund.PullPaymentData)
.Where(i => i.Id == invoiceId)
.FirstOrDefaultAsync();
.FirstOrDefaultAsync(cancellationToken: cancellationToken);
if (invoice is null)
return NotFound();
if (invoice.CurrentRefund?.PullPaymentDataId is null && GetUserId() is null)
@ -190,23 +190,18 @@ namespace BTCPayServer.Controllers
else
{
var paymentMethods = invoice.GetBlob(_NetworkProvider).GetPaymentMethods();
var options = paymentMethods
.Select(o => o.GetId())
.Select(o => o.CryptoCode)
.Where(o => _NetworkProvider.GetNetwork<BTCPayNetwork>(o) is BTCPayNetwork n && !n.ReadonlyWallet)
.Distinct()
.OrderBy(o => o)
.Select(o => new PaymentMethodId(o, PaymentTypes.BTCLike))
.ToList();
var pmis = paymentMethods.Select(method => method.GetId()).ToList();
var options = payoutHandlers.GetSupportedPaymentMethods(pmis);
var defaultRefund = invoice.Payments
.Select(p => p.GetBlob(_NetworkProvider))
.Select(p => p?.GetPaymentMethodId())
.FirstOrDefault(p => p != null && p.PaymentType == BitcoinPaymentType.Instance);
.FirstOrDefault(p => p != null && options.Contains(p));
// TODO: What if no option?
var refund = new RefundModel();
refund.Title = "Select a payment method";
refund.AvailablePaymentMethods = new SelectList(options, nameof(PaymentMethodId.CryptoCode), nameof(PaymentMethodId.CryptoCode));
refund.SelectedPaymentMethod = defaultRefund?.ToString() ?? options.Select(o => o.CryptoCode).First();
refund.AvailablePaymentMethods =
new SelectList(options.Select(id => new SelectListItem(id.ToPrettyString(), id.ToString())), "Value", "Text");
refund.SelectedPaymentMethod = defaultRefund?.ToString() ?? options.First().ToString();
// Nothing to select, skip to next
if (refund.AvailablePaymentMethods.Count() == 1)
@ -230,7 +225,7 @@ namespace BTCPayServer.Controllers
return NotFound();
if (!CanRefund(invoice.GetInvoiceState()))
return NotFound();
var paymentMethodId = new PaymentMethodId(model.SelectedPaymentMethod, PaymentTypes.BTCLike);
var paymentMethodId = PaymentMethodId.Parse(model.SelectedPaymentMethod);
var cdCurrency = _CurrencyNameTable.GetCurrencyData(invoice.Currency, true);
var paymentMethodDivisibility = _CurrencyNameTable.GetCurrencyData(paymentMethodId.CryptoCode, false)?.Divisibility ?? 8;
RateRules rules;
@ -241,7 +236,14 @@ namespace BTCPayServer.Controllers
case RefundSteps.SelectPaymentMethod:
model.RefundStep = RefundSteps.SelectRate;
model.Title = "What to refund?";
var paymentMethod = invoice.GetPaymentMethods()[paymentMethodId];
var pms = invoice.GetPaymentMethods();
var paymentMethod = pms.SingleOrDefault(method => method.GetId() == paymentMethodId);
//TODO: Make this clean
if (paymentMethod is null && paymentMethodId.PaymentType == LightningPaymentType.Instance)
{
paymentMethod = pms[new PaymentMethodId(paymentMethodId.CryptoCode, PaymentTypes.LNURLPay)];
}
var cryptoPaid = paymentMethod.Calculate().Paid.ToDecimal(MoneyUnit.BTC);
var paidCurrency =
Math.Round(cryptoPaid * paymentMethod.Rate,
@ -340,10 +342,10 @@ namespace BTCPayServer.Controllers
var ppId = await _paymentHostedService.CreatePullPayment(createPullPayment);
this.TempData.SetStatusMessageModel(new StatusMessageModel()
{
Html = "Refund successfully created!<br />Share the link to this page with a customer.<br />The customer needs to enter their address and claim the refund.<br />Once a customer claims the refund, you will get a notification and would need to approve and initiate it from your Wallet > Manage > Payouts.",
Html = "Refund successfully created!<br />Share the link to this page with a customer.<br />The customer needs to enter their address and claim the refund.<br />Once a customer claims the refund, you will get a notification and would need to approve and initiate it from your Store > Payouts.",
Severity = StatusMessageModel.StatusSeverity.Success
});
(await ctx.Invoices.FindAsync(invoice.Id)).CurrentRefundId = ppId;
(await ctx.Invoices.FindAsync(new[] { invoice.Id }, cancellationToken: cancellationToken)).CurrentRefundId = ppId;
ctx.Refunds.Add(new RefundData()
{
InvoiceDataId = invoice.Id,
@ -379,7 +381,8 @@ namespace BTCPayServer.Controllers
Overpaid = _CurrencyNameTable.DisplayFormatCurrency(
accounting.OverpaidHelper.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode),
Address = data.GetPaymentMethodDetails().GetPaymentDestination(),
Rate = ExchangeRate(data)
Rate = ExchangeRate(data),
PaymentMethodRaw = data
};
}).ToList()
};
@ -448,23 +451,9 @@ namespace BTCPayServer.Controllers
if (view == "modal")
model.IsModal = true;
_CSP.Add(new ConsentSecurityPolicy("script-src", "'unsafe-eval'")); // Needed by Vue
if (!string.IsNullOrEmpty(model.CustomCSSLink) &&
Uri.TryCreate(model.CustomCSSLink, UriKind.Absolute, out var uri))
{
_CSP.Clear();
}
if (!string.IsNullOrEmpty(model.CustomLogoLink) &&
Uri.TryCreate(model.CustomLogoLink, UriKind.Absolute, out uri))
{
_CSP.Clear();
}
return View(nameof(Checkout), model);
}
[HttpGet]
[Route("invoice-noscript")]
public async Task<IActionResult> CheckoutNoScript(string? invoiceId, string? id = null, string? paymentMethodId = null, [FromQuery] string? lang = null)
@ -490,19 +479,35 @@ namespace BTCPayServer.Controllers
bool isDefaultPaymentId = false;
if (paymentMethodId is null)
{
paymentMethodId = store.GetDefaultPaymentId(_NetworkProvider);
var enabledPaymentIds = store.GetEnabledPaymentIds(_NetworkProvider) ?? Array.Empty<PaymentMethodId>();
PaymentMethodId? invoicePaymentId = invoice.GetDefaultPaymentMethod();
PaymentMethodId? storePaymentId = store.GetDefaultPaymentId();
if (invoicePaymentId is PaymentMethodId)
{
if (enabledPaymentIds.Contains(invoicePaymentId))
paymentMethodId = invoicePaymentId;
}
if (paymentMethodId is null && storePaymentId is PaymentMethodId)
{
if (enabledPaymentIds.Contains(storePaymentId))
paymentMethodId = storePaymentId;
}
if (paymentMethodId is null && invoicePaymentId is PaymentMethodId)
{
paymentMethodId = invoicePaymentId.FindNearest(enabledPaymentIds);
}
if (paymentMethodId is null && storePaymentId is PaymentMethodId)
{
paymentMethodId = storePaymentId.FindNearest(enabledPaymentIds);
}
if (paymentMethodId is null)
{
paymentMethodId = enabledPaymentIds.First();
}
isDefaultPaymentId = true;
}
BTCPayNetworkBase network = _NetworkProvider.GetNetwork<BTCPayNetworkBase>(paymentMethodId.CryptoCode);
if (network == null && isDefaultPaymentId)
{
//TODO: need to look into a better way for this as it does not scale
network = _NetworkProvider.GetAll().OfType<BTCPayNetwork>().FirstOrDefault();
paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
}
if (invoice == null || network == null)
return null;
if (!invoice.Support(paymentMethodId))
if (network is null || !invoice.Support(paymentMethodId))
{
if (!isDefaultPaymentId)
return null;
@ -519,9 +524,11 @@ namespace BTCPayServer.Controllers
var paymentMethodDetails = paymentMethod.GetPaymentMethodDetails();
if (!paymentMethodDetails.Activated)
{
await _InvoiceRepository.ActivateInvoicePaymentMethod(_EventAggregator, _NetworkProvider,
_paymentMethodHandlerDictionary, store, invoice, paymentMethod.GetId());
return await GetInvoiceModel(invoiceId, paymentMethodId, lang);
if (await _InvoiceRepository.ActivateInvoicePaymentMethod(_EventAggregator, _NetworkProvider,
_paymentMethodHandlerDictionary, store, invoice, paymentMethod.GetId()))
{
return await GetInvoiceModel(invoiceId, paymentMethodId, lang);
}
}
var dto = invoice.EntityToDTO();
var storeBlob = store.GetStoreBlob();
@ -564,7 +571,7 @@ namespace BTCPayServer.Controllers
IsUnsetTopUp = invoice.IsUnsetTopUp(),
OrderAmountFiat = OrderAmountFromInvoice(network.CryptoCode, invoice),
CustomerEmail = invoice.RefundMail,
RequiresRefundEmail = storeBlob.RequiresRefundEmail,
RequiresRefundEmail = invoice.RequiresRefundEmail ?? storeBlob.RequiresRefundEmail,
ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds),
MaxTimeSeconds = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalSeconds,
MaxTimeMinutes = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalMinutes,
@ -655,7 +662,7 @@ namespace BTCPayServer.Controllers
[Route("invoice/{invoiceId}/status/ws")]
[Route("invoice/{invoiceId}/{paymentMethodId}/status")]
[Route("invoice/status/ws")]
public async Task<IActionResult> GetStatusWebSocket(string invoiceId)
public async Task<IActionResult> GetStatusWebSocket(string invoiceId, CancellationToken cancellationToken)
{
if (!HttpContext.WebSockets.IsWebSocketRequest)
return NotFound();
@ -666,12 +673,12 @@ namespace BTCPayServer.Controllers
CompositeDisposable leases = new CompositeDisposable();
try
{
leases.Add(_EventAggregator.Subscribe<Events.InvoiceDataChangedEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
leases.Add(_EventAggregator.Subscribe<Events.InvoiceNewPaymentDetailsEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
leases.Add(_EventAggregator.Subscribe<Events.InvoiceEvent>(async o => await NotifySocket(webSocket, o.Invoice.Id, invoiceId)));
leases.Add(_EventAggregator.SubscribeAsync<Events.InvoiceDataChangedEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
leases.Add(_EventAggregator.SubscribeAsync<Events.InvoiceNewPaymentDetailsEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
leases.Add(_EventAggregator.SubscribeAsync<Events.InvoiceEvent>(async o => await NotifySocket(webSocket, o.Invoice.Id, invoiceId)));
while (true)
{
var message = await webSocket.ReceiveAsync(DummyBuffer, default(CancellationToken));
var message = await webSocket.ReceiveAndPingAsync(DummyBuffer, default(CancellationToken));
if (message.MessageType == WebSocketMessageType.Close)
break;
}
@ -686,6 +693,8 @@ namespace BTCPayServer.Controllers
}
readonly ArraySegment<Byte> DummyBuffer = new ArraySegment<Byte>(new Byte[1]);
public string? CreatedInvoiceId;
private async Task NotifySocket(WebSocket webSocket, string invoiceId, string expectedId)
{
if (invoiceId != expectedId || webSocket.State != WebSocketState.Open)
@ -843,7 +852,12 @@ namespace BTCPayServer.Controllers
if (!store.GetSupportedPaymentMethods(_NetworkProvider).Any())
{
ModelState.AddModelError(nameof(model.StoreId), "You need to configure the derivation scheme in order to create an invoice");
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Error,
Html = $"To create an invoice, you need to <a href='{Url.Action(nameof(StoresController.PaymentMethods), "Stores", new { storeId = store.Id })}' class='alert-link'>set up your wallet</a> first",
AllowDismiss = false
});
return View(model);
}
@ -863,15 +877,23 @@ namespace BTCPayServer.Controllers
SupportedTransactionCurrencies = model.SupportedTransactionCurrencies?.ToDictionary(s => s, s => new InvoiceSupportedTransactionCurrency()
{
Enabled = true
})
}),
DefaultPaymentMethod = model.DefaultPaymentMethod,
NotificationEmail = model.NotificationEmail,
ExtendedNotifications = model.NotificationEmail != null,
RequiresRefundEmail = model.RequiresRefundEmail == RequiresRefundEmail.InheritFromStore
? store.GetStoreBlob().RequiresRefundEmail
: model.RequiresRefundEmail == RequiresRefundEmail.On
}, store, HttpContext.Request.GetAbsoluteRoot(), cancellationToken: cancellationToken);
TempData[WellKnownTempData.SuccessMessage] = $"Invoice {result.Data.Id} just created!";
CreatedInvoiceId = result.Data.Id;
return RedirectToAction(nameof(ListInvoices));
}
catch (BitpayHttpException ex)
{
ModelState.TryAddModelError(nameof(model.Currency), $"Error: {ex.Message}");
Logs.PayServer.LogError(ex, $"Invoice creation failed due to invalid currency {model.Currency}");
ModelState.TryAddModelError(nameof(model.Currency), "Please make sure you entered a valid currency symbol, a rate provider is configured in store settings, and your configured rate provider is both online and providing rates for your selected currency.");
return View(model);
}
}

View File

@ -32,7 +32,6 @@ namespace BTCPayServer.Controllers
public partial class InvoiceController : Controller
{
readonly InvoiceRepository _InvoiceRepository;
readonly ContentSecurityPolicies _CSP;
readonly RateFetcher _RateProvider;
readonly StoreRepository _StoreRepository;
readonly UserManager<ApplicationUser> _UserManager;
@ -72,7 +71,6 @@ namespace BTCPayServer.Controllers
_dbContextFactory = dbContextFactory;
_paymentHostedService = paymentHostedService;
WebhookNotificationManager = webhookNotificationManager;
_CSP = csp;
_languageService = languageService;
}
@ -96,7 +94,6 @@ namespace BTCPayServer.Controllers
{
throw new BitpayHttpException(400, "The expirationTime is set too soon");
}
invoice.Currency = invoice.Currency?.Trim().ToUpperInvariant() ?? "USD";
entity.Metadata.OrderId = invoice.OrderId;
entity.Metadata.PosData = invoice.PosData;
entity.ServerUrl = serverUrl;
@ -130,6 +127,7 @@ namespace BTCPayServer.Controllers
entity.RedirectURLTemplate = invoice.RedirectURL ?? store.StoreWebsite;
entity.RedirectAutomatically =
invoice.RedirectAutomatically.GetValueOrDefault(storeBlob.RedirectAutomatically);
entity.RequiresRefundEmail = invoice.RequiresRefundEmail;
entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy);
IPaymentFilter? excludeFilter = null;
@ -153,6 +151,8 @@ namespace BTCPayServer.Controllers
excludeFilter = PaymentFilter.Where(p => !supportedTransactionCurrencies.Contains(p));
}
entity.PaymentTolerance = storeBlob.PaymentTolerance;
entity.DefaultPaymentMethod = invoice.DefaultPaymentMethod;
entity.RequiresRefundEmail = invoice.RequiresRefundEmail;
return await CreateInvoiceCoreRaw(entity, store, excludeFilter, null, cancellationToken);
}
@ -160,12 +160,12 @@ namespace BTCPayServer.Controllers
{
var storeBlob = store.GetStoreBlob();
var entity = _InvoiceRepository.CreateNewInvoice();
entity.ServerUrl = serverUrl;
entity.ExpirationTime = entity.InvoiceTime + (invoice.Checkout.Expiration ?? storeBlob.InvoiceExpiration);
entity.MonitoringExpiration = entity.ExpirationTime + (invoice.Checkout.Monitoring ?? storeBlob.MonitoringExpiration);
if (invoice.Metadata != null)
entity.Metadata = InvoiceMetadata.FromJObject(invoice.Metadata);
invoice.Checkout ??= new CreateInvoiceRequest.CheckoutOptions();
invoice.Currency = invoice.Currency?.Trim().ToUpperInvariant() ?? "USD";
entity.Currency = invoice.Currency;
if (invoice.Amount is decimal v)
{
@ -179,7 +179,9 @@ namespace BTCPayServer.Controllers
}
entity.SpeedPolicy = invoice.Checkout.SpeedPolicy ?? store.SpeedPolicy;
entity.DefaultLanguage = invoice.Checkout.DefaultLanguage;
entity.DefaultPaymentMethod = invoice.Checkout.DefaultPaymentMethod;
entity.RedirectAutomatically = invoice.Checkout.RedirectAutomatically ?? storeBlob.RedirectAutomatically;
entity.RequiresRefundEmail = invoice.Checkout.RequiresRefundEmail;
IPaymentFilter? excludeFilter = null;
if (invoice.Checkout.PaymentMethods != null)
{
@ -190,6 +192,7 @@ namespace BTCPayServer.Controllers
}
entity.PaymentTolerance = invoice.Checkout.PaymentTolerance ?? storeBlob.PaymentTolerance;
entity.RedirectURLTemplate = invoice.Checkout.RedirectURL?.Trim();
entity.RequiresRefundEmail = invoice.Checkout.RequiresRefundEmail;
if (additionalTags != null)
entity.InternalTags.AddRange(additionalTags);
return await CreateInvoiceCoreRaw(entity, store, excludeFilter, invoice.AdditionalSearchTerms, cancellationToken);
@ -199,7 +202,10 @@ namespace BTCPayServer.Controllers
{
InvoiceLogs logs = new InvoiceLogs();
logs.Write("Creation of invoice starting", InvoiceEventData.EventSeverity.Info);
var storeBlob = store.GetStoreBlob();
if (string.IsNullOrEmpty(entity.Currency))
entity.Currency = storeBlob.DefaultCurrency;
entity.Currency = entity.Currency.Trim().ToUpperInvariant();
entity.Price = Math.Max(0.0m, entity.Price);
var currencyInfo = _CurrencyNameTable.GetNumberFormatInfo(entity.Currency, false);
if (currencyInfo != null)
@ -218,7 +224,6 @@ namespace BTCPayServer.Controllers
}
var getAppsTaggingStore = _InvoiceRepository.GetAppsTaggingStore(store.Id);
var storeBlob = store.GetStoreBlob();
if (entity.Metadata.BuyerEmail != null)
{
@ -257,39 +262,48 @@ namespace BTCPayServer.Controllers
List<ISupportedPaymentMethod> supported = new List<ISupportedPaymentMethod>();
var paymentMethods = new PaymentMethodDictionary();
// This loop ends with .ToList so we are querying all payment methods at once
// instead of sequentially to improve response time
foreach (var o in store.GetSupportedPaymentMethods(_NetworkProvider)
.Where(s => !excludeFilter.Match(s.PaymentId) && _paymentMethodHandlerDictionary.Support(s.PaymentId))
.Select(c =>
(Handler: _paymentMethodHandlerDictionary[c.PaymentId],
SupportedPaymentMethod: c,
Network: _NetworkProvider.GetNetwork<BTCPayNetworkBase>(c.PaymentId.CryptoCode)))
.Where(c => c.Network != null)
.Select(o =>
(SupportedPaymentMethod: o.SupportedPaymentMethod,
PaymentMethod: CreatePaymentMethodAsync(fetchingByCurrencyPair, o.Handler, o.SupportedPaymentMethod, o.Network, entity, store, logs)))
.ToList())
{
var paymentMethod = await o.PaymentMethod;
if (paymentMethod == null)
continue;
supported.Add(o.SupportedPaymentMethod);
paymentMethods.Add(paymentMethod);
}
bool noNeedForMethods = entity.Type != InvoiceType.TopUp && entity.Price == 0m;
if (supported.Count == 0)
if (!noNeedForMethods)
{
StringBuilder errors = new StringBuilder();
if (!store.GetSupportedPaymentMethods(_NetworkProvider).Any())
errors.AppendLine("Warning: No wallet has been linked to your BTCPay Store. See the following link for more information on how to connect your store and wallet. (https://docs.btcpayserver.org/WalletSetup/)");
foreach (var error in logs.ToList())
// This loop ends with .ToList so we are querying all payment methods at once
// instead of sequentially to improve response time
foreach (var o in store.GetSupportedPaymentMethods(_NetworkProvider)
.Where(s => !excludeFilter.Match(s.PaymentId) &&
_paymentMethodHandlerDictionary.Support(s.PaymentId))
.Select(c =>
(Handler: _paymentMethodHandlerDictionary[c.PaymentId],
SupportedPaymentMethod: c,
Network: _NetworkProvider.GetNetwork<BTCPayNetworkBase>(c.PaymentId.CryptoCode)))
.Where(c => c.Network != null)
.Select(o =>
(SupportedPaymentMethod: o.SupportedPaymentMethod,
PaymentMethod: CreatePaymentMethodAsync(fetchingByCurrencyPair, o.Handler,
o.SupportedPaymentMethod, o.Network, entity, store, logs)))
.ToList())
{
errors.AppendLine(error.ToString());
var paymentMethod = await o.PaymentMethod;
if (paymentMethod == null)
continue;
supported.Add(o.SupportedPaymentMethod);
paymentMethods.Add(paymentMethod);
}
throw new BitpayHttpException(400, errors.ToString());
}
if (supported.Count == 0)
{
StringBuilder errors = new StringBuilder();
if (!store.GetSupportedPaymentMethods(_NetworkProvider).Any())
errors.AppendLine(
"Warning: No wallet has been linked to your BTCPay Store. See the following link for more information on how to connect your store and wallet. (https://docs.btcpayserver.org/WalletSetup/)");
foreach (var error in logs.ToList())
{
errors.AppendLine(error.ToString());
}
throw new BitpayHttpException(400, errors.ToString());
}
}
entity.SetSupportedPaymentMethods(supported);
entity.SetPaymentMethods(paymentMethods);
foreach (var app in await getAppsTaggingStore)
@ -375,7 +389,7 @@ namespace BTCPayServer.Controllers
}
var criteria = storeBlob.PaymentMethodCriteria?.Find(methodCriteria => methodCriteria.PaymentMethod == supportedPaymentMethod.PaymentId);
if (criteria?.Value != null)
if (criteria?.Value != null && entity.Type != InvoiceType.TopUp)
{
var currentRateToCrypto =
await fetchingByCurrencyPair[new CurrencyPair(supportedPaymentMethod.PaymentId.CryptoCode, criteria.Value.Currency)];

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