Compare commits

..

159 Commits

Author SHA1 Message Date
845a7ec06f Update Changelog.md
Co-authored-by: d11n <mail@dennisreimann.de>
2023-05-08 14:03:20 +02:00
210fb090a8 Update Changelog.md
Co-authored-by: d11n <mail@dennisreimann.de>
2023-05-08 14:03:15 +02:00
92cb15e95a Update Changelog.md
Co-authored-by: d11n <mail@dennisreimann.de>
2023-05-08 14:03:06 +02:00
9c96c391ce Update Changelog.md 2023-05-08 12:12:15 +02:00
137e80c0c9 Update Changelog.md 2023-05-08 12:10:25 +02:00
44de5a1f88 Update Changelog.md 2023-05-08 11:03:37 +02:00
57358f8fc5 Apply suggestions from code review 2023-05-08 11:03:11 +02:00
190beeb70d Bump to 1.9.3 2023-05-05 11:55:13 +02:00
eddd458744 Fix missing shopify link
fixes 
2023-05-05 09:51:45 +02:00
439ea20a89 Resolving weird Firefox form autocomplete behavior on our POS () 2023-05-04 18:49:35 +02:00
cec223c8e7 Fix missing closing div () 2023-05-03 10:55:20 +02:00
25cb188d00 Bumping LND to 0.16.2-beta 2023-04-29 09:10:07 -05:00
31007a8d96 Merge pull request from dennisreimann/esc
Supporters: Add ESC
2023-04-27 18:30:09 +02:00
a4fa8db69b Supporters: Add ESC 2023-04-27 18:28:29 +02:00
6193835ea1 Make LightningListener nullable, fix some NRE 2023-04-27 13:37:32 +09:00
0c78e9e4ac Bumping LND to 0.16.1-beta () 2023-04-27 13:17:57 +09:00
58c409e7fa Fix tests 2023-04-27 13:00:25 +09:00
9577eed524 Validate input in greenfield for payout processors () 2023-04-27 10:59:19 +09:00
76f32cd064 Fix payment request overriding user provided email 2023-04-26 14:06:42 +02:00
92d9c17095 Fix payment request merge conflict marker in UI 2023-04-26 14:06:23 +02:00
b0396df33f Update changelog 2023-04-26 18:27:06 +09:00
c17572c76f Clip configuration values for payout processors 2023-04-26 18:24:46 +09:00
5c91e072a6 Prevent payout processors from stalling restart 2023-04-26 18:09:56 +09:00
4991d0f965 Update Changelog and bump version 2023-04-26 17:57:25 +09:00
45b74e1ce5 Fix cart receipt + fix pos email form forwarding ()
* Show correct array regardless of size

fixes 

* Email provided to pos form was not forwarded to form

fixes 

* Make invoice receipt url redirect to the invoice redirect url if receipt is not enabled

When setting up a default email rule upon invoice settlement, you would link to the receipt page naturally. However, if using the payment requests, receipts are disabled as the payment request itself is the receipt.  This commit makes the receipt url redirect to the invoice redirect url if available, and in the case of payment requests, it would mean the receipt url is the payment request url. fixes 

* Set the email address in the form when configured in the payment request

* fix pay request email copy

* fix payouts nav link

fixes 

* Update BTCPayServer/Views/UIPaymentRequest/EditPaymentRequest.cshtml

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

---------

Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com>
2023-04-26 16:45:35 +09:00
ccb4b9a9ba Merge pull request from dennisreimann/receipt-print
Receipt: Optimize print view
2023-04-26 15:41:41 +09:00
dd635071d6 Merge pull request from dennisreimann/fix-4889
POS: Fix choiceKey case
2023-04-26 15:16:54 +09:00
fe1448dfae Merge pull request from NicolasDorier/qiponbtq
Remove LNURLStandardInvoiceEnabled
2023-04-26 15:15:20 +09:00
56855bc54d Fix typo in PayJoinEndpointController.cs () 2023-04-25 22:29:06 +02:00
b51fa8df5a Fix indent 2023-04-25 21:03:12 +09:00
3aa979cb11 Test updates 2023-04-25 13:07:01 +02:00
c95f75bc6c Remove disable BOLT11 option 2023-04-25 12:21:27 +02:00
b13a636f89 Fix syntax error 2023-04-25 12:11:08 +02:00
8de55cef31 Unify checkout v1 behaviour with v2 2023-04-25 11:36:45 +02:00
cb781f42e3 POS: Fix choiceKey case
I came across this while debugging . This does not actually fix it, but it fixes an inconsistence in the casing of the parameter name.

However, I think the original issue is a caching problem in the browser. I was able to reproduce it on first load, after reloading the page once more it works as intended. The weird thing is: even though the values are correct on first load (verified via debugger), the `choiceKey` for the first item is set incorrectly to an integer value.
2023-04-25 09:16:09 +02:00
b59292dc24 Receipt: Optimize print view
Fixes 
2023-04-25 07:30:15 +02:00
03b793d7e2 Fix tests 2023-04-25 10:28:36 +09:00
bee18d1cfb Can set LazyPaymentMethod at the invoice creation level 2023-04-25 08:38:42 +09:00
d8698181f4 Remove LNURLStandardInvoiceEnabled 2023-04-24 23:52:40 +09:00
47f5d97eaf Prevent an NRE in LNURL ()
* Prevent an NRE in LNURL

In addition to f05a7f9f14e0a2f906c79951a15311085774db85. Fixes .

* Revert "Prevent an NRE in LNURL"

This reverts commit 0b241d61ab45b79297211e04da0e05c2cb10dada.

* Fix NRE

---------

Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2023-04-24 21:50:31 +09:00
cb3c5e56fd Fix: Poll of Lightning Invoice status might fail on LND if LNURL is used () 2023-04-24 18:15:39 +09:00
39b76c08de Fix: Form in Payment Requests was not setting its values to the invoices metadata () 2023-04-24 18:04:46 +09:00
c3c8cc21ff Security Page Refactor ()
* Update SECURITY.md

* typo

* project vs product

* Suggestion Update - Docker Deployment

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

* Suggestion - Email highlight

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

---------

Co-authored-by: d11n <mail@dennisreimann.de>
2023-04-24 15:04:56 +09:00
6dba1b6d8b Nullable on LanguageService 2023-04-20 09:44:24 +09:00
381fe70a79 Add UserAgent to IPN and webhook HTTP requests (Fix ) 2023-04-19 21:13:31 +09:00
feb927c2e4 Add missing param, fix response code. () 2023-04-19 13:07:36 +09:00
e77bd4c188 Swagger: FIx typos in pull payments docs ()
Fixes .
2023-04-19 13:06:51 +09:00
43436fc49e Remove BTCPayServer.Plugins.Test ()
Remove the plugin, which is superseded with the plugin template and move the PluginPacker to the root of th solution (which matches its location in the file system).
2023-04-17 11:38:03 +09:00
d738f797ec bump 2023-04-17 11:11:00 +09:00
b5de97f785 Update changelog 2023-04-17 10:54:00 +09:00
b0c1b0895d Fix crash if auto detect language on checkout page, and the language couldn't be detected (Fix ) () 2023-04-17 10:53:45 +09:00
8e60932f81 Migrate reference to AppType in server settings (Fix ) 2023-04-17 10:34:41 +09:00
7d14cd74f2 LightningLikePaymentData: Null-check preimage ()
Fixes .
2023-04-17 00:08:40 +02:00
717f1610f5 Update changelog 2023-04-13 16:28:30 +09:00
f1abe6497f Fix wrong data mapping to store data instead of store user data. () 2023-04-13 16:27:40 +09:00
046129a57d Bumping LND to 0.16.0-beta () 2023-04-13 14:48:47 +09:00
90d300a490 Remove superflous category in API doc 2023-04-13 08:41:46 +09:00
a2d506c0db Checkout v2: Confetti for processing payments ()
Let's not spoil the fun for those paying on-chain.
2023-04-13 08:40:21 +09:00
58748a24da What's New entry for v1.9 ()
Copy is just a proposal from my side. Link check expected to fail, because the blog post isn't up yet.
2023-04-12 20:40:20 +09:00
639e8a4a1d Add links to invoice metadata in the docs () 2023-04-12 16:30:34 +09:00
48ebaf5c5a Do not put payRequest in the metadata () 2023-04-12 16:30:22 +09:00
1aaccb1e6b Do not show empty JObject in AddtionalData of views 2023-04-11 13:21:02 +09:00
f05a7f9f14 Prevent an NRE in LNURL 2023-04-11 13:08:14 +09:00
98ddb348b0 Update changelog 2023-04-10 20:21:13 +09:00
a4aa85ebab Fix: If connection to Lightning node was interrupted, payments would be missed () 2023-04-10 19:35:01 +09:00
516efe56f4 Refactor: Dot not make LNURLPaymentMethodDetails depends on BTCPayInvoiceId ()
* Refactor: Dot not make LNURLPaymentMethodDetails depends on BTCPayInvoiceId

* Abstract PaymentProof

* fix bug

* Make the selenium container resolves the btcpay host name
2023-04-10 16:38:49 +09:00
a4d72d5bbc Fix tests 2023-04-10 15:06:59 +09:00
24b8ec16f1 Fix tests 2023-04-10 13:37:09 +09:00
ac25fef555 Remove warning 2023-04-10 12:01:11 +09:00
8302f082a2 Do not render the TruncateCenter if text null or empty 2023-04-10 11:17:29 +09:00
7546ef7a8e Dotnet reformat 2023-04-10 11:07:03 +09:00
f598c70a4f Add comment TODO for improving automatic translations efficiency. 2023-04-10 11:03:36 +09:00
422da21de5 Update Changelog 2023-04-07 17:52:48 +09:00
f530fb3241 Update changelog 2023-04-07 17:50:57 +09:00
5d39bb7466 Allow LN Address to customize invoice metadata, and various bug fixes on LNUrl ()
* Allow LN Address to customize invoice metadata

solves https://github.com/OpenSats/website/issues/8

* Refactor GetLNUrl

* Fix lightningAddresssettings.Max being ignored

* Fix: The payRequest generated by the callback wasn't the same as the original

---------

Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2023-04-07 17:48:58 +09:00
041cba72b6 Made Fake Custodian config field non-required () 2023-04-07 15:59:44 +09:00
892b3e273f Improve Labeling further ()
* If loading addresses into the send wallet page using bip21 or address,  (or clicking on "Send selected payouts"  from the payotus page), existing labels will be pre-populated.
*  Add the payout label to the address when the payoutis created instead of to the transaction when it is paid.
*  Add the label attachments when adding labels from an address to the transaction.
2023-04-07 15:58:41 +09:00
91faf5756d With core lightning, getting payment by paymenthash wouldn't return the successful payment if the first one failed. 2023-04-07 15:54:56 +09:00
e239390ebf Allow any bolt11 invoice for pullpayments/payouts ()
closes 

If users want to deal with expired payout destinations, then they should be able to conifugre it that way. Some wallets simply do not allow customizing the bolt11 expiry and the defaults are much less than a day. I think we should merge  if we introduce this as an automated payotu processor for lightning running every few minutes would work together with this and solve it.
2023-04-06 15:54:19 +09:00
b24764d679 bump 2023-04-05 23:38:39 +09:00
4d5a568fd7 Changelog 1.9 () 2023-04-05 23:37:01 +09:00
5ab55e71e0 Add cart items to receipt () 2023-04-05 15:42:23 +02:00
929d63ecf8 Fixed nav menu active states + Better Fake Custodian () 2023-04-05 15:11:46 +02:00
0ef7f3715f remove obsolete reference 2023-04-05 15:09:42 +02:00
2298f3901a Update langs () 2023-04-05 13:32:24 +09:00
3005f1937a Replace GetUriByAction by GetPathByAction 2023-04-05 10:58:56 +09:00
f48eec2e93 Fix: Redirecting to local absolute URL in wizard windows should work 2023-04-05 10:55:35 +09:00
754d304e54 Fix tx link in Recent Transactions 2023-04-05 08:53:34 +09:00
9b8d08a668 Make Checkout V2 the default ()
* Make Checkout V2 the default

* Fix align
2023-04-05 08:35:50 +09:00
1b672a1ace Allow multi-step settings in custodian ()
* Allow multi-step settings in custodian

* Fix CustodianAccount.Name not saved

* Reuse TradeQuantity for SimulateTrade

* TradeQuantityJsonConverter accepts numerics

* Fix build
2023-04-04 14:48:29 +09:00
60d6e98c67 Form System Flexibility improvements ()
* Introduce very flexible form input system

* Refactorings after rebase

* Test fix

* Update BTCPayServer/Forms/FormDataService.cs

---------

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2023-04-04 11:01:34 +09:00
11f05285a1 Invoice Details: Improve payments list and print view ()
Closes .

Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com>
2023-04-04 10:59:14 +09:00
2bd1842da1 Improve pagination ()
Based on the design repo updates by @benalleng in btcpayserver/btcpayserver-design#61. Closes .
2023-04-04 10:54:04 +09:00
57544068e9 Store rule emails in HTML + Test rule emails ()
* Store rule emails in HTML + Test rule emails

* Store rule emails in HTML + Test rule emails

* Update BTCPayServer/Controllers/UIStoresController.Email.cs

Thanks !

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

* change in StoreEmails() to work properly with add commande

* CanSetupEmailServer() : Remove Bosy sendkeys that doesn't work with new HTML control

* Update test for new HTML control

* better command treatment

---------

Co-authored-by: nisaba <infos@nisaba.solutions>
Co-authored-by: Andrew Camilleri <evilkukka@gmail.com>
2023-04-04 10:52:42 +09:00
60cfea9f94 Add presets in the checkout appearance () 2023-04-04 10:45:40 +09:00
eece001376 Add labels for recent txs dashboard widget ()
* Add labels for recent txs dashboard widget

It is not with the rich data for now, but a good start.

* Turn labels into links

* Add rich info to dashboard labels

* Use truncate-center component for recent transactions

---------

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2023-03-30 19:54:11 +02:00
98d8ef8e1a Checkout Appearance: Move language settings into the general section ()
The language settings work for both versions of the checkout, so let's make them available for both. Request by @petzsch.
2023-03-30 08:18:13 +02:00
0e43042217 Remove FakeCustodian as a separate project () 2023-03-30 12:38:30 +09:00
c8e6714207 Merge pull request from dennisreimann/unify-plugins-apps
Apps become plugins
2023-03-29 20:03:18 +02:00
824e779eb2 Hooks for Zaps () 2023-03-29 12:27:04 +02:00
83ea898780 Checkout: Update wording ()
We make it "Payment Received" instead of "Payment Sent", because in other places we are also talking from the merchant perspective.
2023-03-29 17:24:49 +09:00
5af3233fd6 Add link to forms doc 2023-03-28 22:02:58 +09:00
08ff2f3173 Checkout: Fix language select width in modal () 2023-03-28 13:12:47 +02:00
22657b66d7 NFC finetuning () 2023-03-28 12:36:32 +02:00
8b6c7a6061 Pull Payments: FIx column order in list ()
Date and name were swapped. Fixes .
2023-03-28 19:10:50 +09:00
1f197f6688 Merge pull request from dennisreimann/nfc 2023-03-28 09:19:58 +02:00
1055e61bb4 NFC improvements
Two changes which fix :

- Once permissions are granted we start scanning immediately, no need to ask for permissions or have the user click the button again
- We don't abort the scan, which gets rid of the cases in which the OS took over after the scan, because the user left the card on the device

Also adds feedback for the NFC states scanning and submitting.
2023-03-27 18:28:53 +02:00
7ad0aa82fc Apps become plugins
Unified navigation for apps that are now plugins. Part of .
2023-03-27 16:43:44 +02:00
d3f5576570 Remove store integrations list page ()
Co-authored-by: d11n <mail@dennisreimann.de>
2023-03-27 16:40:50 +02:00
45141d1391 Checkout v2: Payment processing state () 2023-03-27 12:12:11 +02:00
de9ac9fd43 Receipt: Add payment proof ()
* Receipt: Add payment proof

Closes .

* shice

* Add truncate-center component

* Improve view

* Hide button and link when printed

* Describe component

* Remove transaction ID from UI

* Remove modification to interface

---------

Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2023-03-27 14:07:12 +09:00
c53d5272d6 Wallet Transactions Export: Add BIP-329 support ()
* Wallet Transactions Export: Add BIP-329 support

* Adjust wording

* Export one line per label

* Join labels, fix type

* Rewrite the ProcessBip329 function to be more performant

* Add nullable on all TransactionsExport

---------

Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2023-03-27 13:59:33 +09:00
18c78192ec Reconstruct issue template ()
* reconstruct issue template

* provide a direct link for filing a tech question
2023-03-27 13:59:07 +09:00
632d67eef4 Fix casing in template example for forms 2023-03-27 12:54:12 +09:00
c23aa48688 Optimize invoice print view ()
Closes .
2023-03-26 20:44:05 +09:00
95f3e429b4 Wallet transactions: Add label manager ()
* Wallet transactions: Add label manager

* Update BTCPayServer/Views/UIWallets/WalletTransactions.cshtml

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

* Add rich label info

* Fixes

* support labels in wallet send

* add labels to tx info page

* Remove noscript parts

* Allow click on transaction label info

* update psbt info labelstyling

* revert red pixel fix as it broke all

---------

Co-authored-by: Andrew Camilleri <evilkukka@gmail.com>
2023-03-26 20:42:38 +09:00
8635fcfe84 UI: Redesign Recovery Seed view ()
* Improve recovery seed backup page

* Fix errors from Selenium tests (Sequence contains no elements)

* Revert previous commit

* Improve recovery seed backup page

* Recovery phrase UI update

* recovery seed UI format

* Improve word order

* One column version

---------

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2023-03-24 16:09:53 +01:00
d861537d9a Merge pull request from dennisreimann/gf-ln-array-fix
Greenfield: Fix Lightning transaction list return types
2023-03-23 18:59:36 +01:00
631ee99f60 Greenfield: Fix Lightning transaction list return types
The LocalBTCPayServerClient deserializes the results as arrays (`LightningPaymentData[]` and `LightningInvoiceData[]`) — if they are `IEnumerable` the `GetFromActionResult` does not return the data but null.
2023-03-23 17:42:10 +01:00
ffa1441ccd Delete code detecting whether the running version of nbx fixed a bug
The reason to delete this is that any version of NBX with this bug
wouldn't be able to run nowadays because of another bug which would
prevent NBXplorer from synching (Array size too big)
2023-03-23 13:45:40 +09:00
2f3e947027 Merge pull request from Kukks/lnurl-disable-if-no-node 2023-03-22 09:02:52 +01:00
a62aecfdfe Merge pull request from dennisreimann/fix-4794 2023-03-22 09:02:35 +01:00
5f829c68f2 Merge pull request from dennisreimann/fix-4790 2023-03-22 09:02:10 +01:00
0290d74aeb POS: Fix escaped HTML entities in item title
Properly escapes and the sanitized values. Fixes .
2023-03-21 15:31:54 +01:00
f6bc16007d Label tooltips: Use plain text instead of HTML
Fixes .
2023-03-21 15:21:24 +01:00
ad5752f09b Reuse LightningTimeout constant 2023-03-21 14:22:10 +01:00
55565f1718 Do not provide lnurl method if ln node is dead
fixes 
2023-03-21 13:48:25 +01:00
5f96d17b8c Update lang 2023-03-20 19:30:56 +09:00
fd22406e0a Fix PullTransifexTranslationsCore 2023-03-20 19:21:35 +09:00
64fe542c1e Update lang 2023-03-20 19:20:46 +09:00
fae1dc8dbb Adapt cookie auth to work with same API permission system ()
* Adapt cookie auth to work with same API permission system

* Handle unscoped store permission case

* Do not consider Unscoped as a valid policy

* Add tests

* Refactor permissions scopes

---------

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2023-03-20 10:46:46 +09:00
6f2b673021 Custodian withdrawal support + Some refactoring and cleanup ()
* Renamed "WithdrawAsync" to "WithdrawToStoreWalletAsync"

* WIP

* WIP withdrawal + Refactored Form saving to JObject

* WIP

* Form to fix bad values during withdrawing appears correctly

* WIP

* Lots of cleanup and refactoring + Password field and toggle password view

* Cleanup + Finishing touches on withdrawals

* Added "Destination" dummy text as this is always the destination.

* Fixed broken test

* Added support for withdrawing using qty as a percentage if it ends with "%". Needs more testing.

* Fixed broken build

* Fixed broken build (2)

* Update BTCPayServer/wwwroot/swagger/v1/swagger.template.custodians.json

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

* Update BTCPayServer/wwwroot/swagger/v1/swagger.template.custodians.json

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

* Improved unit tests

* Fixed swagger bug

* Test improvements

Make string conversion of quantity explicitely.

* Fix build warnings

* Swagger: Add missing operationId

* Made change Dennis requested

* Removed unused file

* Removed incorrect comment

* Extra contructor

* Renamed client methods

* Cleanup config before saving

* Fixed broken controller

* Refactor custodian

* Fix build

* Make decimal fields strings to match the rest of Greenfield

* Improve parsing of % quantities

---------

Co-authored-by: d11n <mail@dennisreimann.de>
Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2023-03-20 10:45:32 +09:00
b26679ca14 Prevent people from starting with --sqlitefile or --mysql () 2023-03-20 10:40:48 +09:00
04ba1430ca Refactor plugin apps ()
* Refactor plugins

* Add missing names to view models

* Cleanups

* Replace SalesAppBaseType by two interfaces

---------

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2023-03-20 10:39:26 +09:00
53f3758abc Replace text in copy buttons with icon ()
Closes .
2023-03-19 21:43:38 +01:00
c6742f5533 Language selector: Ensure correct font-size ()
* Language selector: Ensure correct font-size

Fixes the cut-off text on iOS, because somehow iOS uses a larger font-size by default.

* Fix select background color

Webkit-based browsers displayed transparent in a weird way.
2023-03-19 08:44:23 +01:00
cb44591a47 Derivation scheme parsing incorporates fingerprint and key path () 2023-03-17 14:35:30 +01:00
e02abb509f Allow plugins to do something before and after automatic payouts () 2023-03-17 13:50:37 +01:00
eff6be9643 Remove mention of LNUrl-Withdraw when paying by NFC () 2023-03-17 12:24:27 +01:00
348dbd7107 Support Form Select option ()
* Support Form Select option

* Add country select
2023-03-17 14:37:37 +09:00
f74ea14d8b Plugins can now build apps ()
* Plugins can now build apps

* fix tests

* fixup

* pluginize existing apps

* Test fixes part 1

* Test fixes part 2

* Fix Crowdfund namespace

* Syntax

* More namespace fixes

* Markup

* Test fix

* upstream fixes

* Add plugin icon

* Fix nullable build warnings

* allow pre popualting app creation

* Fixes after merge

* Make link methods async

* Use AppData as parameter for ConfigureLink

* GetApps by AppType

* Use ConfigureLink on dashboard

* Rename method

* Add properties to indicate stats support

* Property updates

* Test fixes

* Clean up imports

* Fixes after merge

---------

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2023-03-17 11:56:32 +09:00
a671632fde Dashboard: Fix app stats tiles ()
* Dashboard: Fix app stats tiles

They broke with , because they contain script blocks that are loaded asynchronuosly and need to get run once the chart data is added to the page.

* Refactor PoS dashboard component

* Collocate the component JS files in separate files

---------

Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2023-03-16 15:51:24 +09:00
e344622c9e Form quick fixes () 2023-03-15 10:23:33 +01:00
06d7483ca3 Remove obsolete cli argument 'plugin-remote' () 2023-03-15 09:06:06 +01:00
7fe041fc2c Changelog 1.8.4 2023-03-15 10:45:49 +09:00
3f18e5476a Error when indexing invoices with some field that are too long (Fix ) 2023-03-15 09:31:38 +09:00
2a31613fe8 Fix invoice paid after expiration icon 2023-03-14 14:54:01 +01:00
ded0c8a3bc Update price display ()
* Update price display

As proposed by @dstrukt in .

* Update format

* Unify price display across the app

* Add DisplayFormatter

* Replace DisplayFormatCurrency method

* Use symbol currency format for invoice

* Unify currency formats on backend pages

* Revert recent changes

* Do not show exchange rate and fiat order amount for crypto denominations

* Fix test and add test cases
2023-03-13 10:12:58 +09:00
f3d9e07c5e Checkout v2: Celebrate payment with confetti ()
* Checkout v2: Celebrate payment with confetti

Have a colorful celebration for successful payments.

* Make it default and add test
2023-03-13 10:09:56 +09:00
eb3ba95114 Make CanUsePullPaymentsViaUI more robust ()
Fixes this nasty flaky test failure:

```
Failed CanUsePullPaymentsViaUI [17 s]
  Error Message:
   Assert.Equal() Failure
           ↓ (pos 1)
Expected: payout
Actual:   pull-payment
           ↑ (pos 1)
  Stack Trace:
     at BTCPayServer.Tests.ChromeTests.CanUsePullPaymentsViaUI() in /source/BTCPayServer.Tests/SeleniumTests.cs:line 1622
```

Because there are actually two labels, the previous selector was dependent on the correct ordern, because it always chose the first one …
2023-03-13 10:02:07 +09:00
7951dcada6 make sure we have cors for all of greenfield ()
fixes 
2023-03-10 15:20:11 +01:00
06951a39c6 fix API breaking changefor payout processors ()
fixes 
2023-03-10 17:57:33 +09:00
abe29f21f0 Checkout v2: Option to display amount in Sats in BIP21 case () 2023-03-09 21:36:11 +01:00
f57eab3008 Store branding: Add complementing text and accent colors () 2023-03-09 21:34:15 +01:00
6d4b2348ac Update changelog 2023-03-08 21:56:40 +09:00
397ca6ef0c Checkout v2: Minor UI updates ()
* Checkout v2: Minor copy change

* Allow copying of invoice ID and order ID on results page

* Add copy icons for payment details on results view

* Add missing powered by class to store footers
2023-03-08 21:39:03 +09:00
d6e5ee2851 UI: Decrease content padding top on small screens ()
If the viewport height is less than 800px, decrease the content padding top for breakpoints L and on.
2023-03-08 21:37:25 +09:00
98d62e826b Do not through missing-permission error when no store on /api/v1/stores (Close ) () 2023-03-08 21:36:51 +09:00
7b5ce8f70c CSP: Remove unsafe-eval when vue isn't used ()
* CSP: Remove unsafe-eval when vue isn't used

* Prevent XSS injection via VueJS
2023-03-08 17:57:36 +09:00
454 changed files with 8602 additions and 4939 deletions
.github/ISSUE_TEMPLATE
.run
BTCPayServer.Abstractions
BTCPayServer.Client
BTCPayServer.Data
BTCPayServer.Rating
BTCPayServer.Tests
BTCPayServer
BTCPayServer.csprojBufferizedFormFile.csColorPalette.cs
Components
Configuration
Controllers
GreenField
LightningAddressService.csUIAccountController.csUIAppsController.Dashboard.csUIAppsController.csUICustodianAccountsController.csUIHomeController.csUIInvoiceController.Testing.csUIInvoiceController.UI.csUIInvoiceController.csUILNURLController.csUIPaymentRequestController.csUIPublicController.csUIPullPaymentController.csUIServerController.csUIStorePullPaymentsController.PullPayments.csUIStoresController.Dashboard.csUIStoresController.Email.csUIStoresController.Integrations.csUIStoresController.LightningLike.csUIStoresController.csUIUserStoresController.csUIWalletsController.PSBT.csUIWalletsController.cs
Data
DerivationSchemeSettings.cs
Extensions
FileTypeDetector.cs
Filters
Forms
HostedServices
Hosting
Models
PaymentRequest
Payments
PayoutProcessors
Plugins
Properties
Roles.cs
Security
Services
TagHelpers
Views
Shared
UIAccount
UIApps
UICustodianAccounts
UIForms
UIHome
UIInvoice
UILNURL
UILightningAutomatedPayoutProcessors
UIManage
UIOnChainAutomatedPayoutProcessors
UIPaymentRequest
UIPublicLightningNodeInfo
UIPullPayment
UIStorePullPayments
UIStores
UIUserStores
UIWallets
wwwroot
Build
Changelog.md
Plugins
README.mdSECURITY.mdbtcpayserver.sln

@ -1,8 +1,14 @@
blank_issues_enabled: true
contact_links:
- name: 🚀 Discussions
url: https://github.com/btcpayserver/btcpayserver/discussions
about: Technical discussions, questions and feature requests
- name: 💡 Request a feature
url: https://github.com/btcpayserver/btcpayserver/discussions/categories/ideas-feature-requests
about: Submit a feature request or vote on ideas posted by others. Features with most upvotes become roadmap candidates
- name: 🧑‍💻 Ask a technical question
url: https://github.com/btcpayserver/btcpayserver/discussions/new?category=technical-support
about: If you're experiencing a technical problem post it to our community support forum
- name: 🔌 Report a problem with a plugin
url: https://github.com/btcpayserver/btcpayserver/discussions/new?category=plugins-integrations
about: Experiencing a problem with a third-party plugin? Post it here and we will tag their developers to assist
- name: 📝 Official Documentation
url: https://docs.btcpayserver.org
about: Check our documentation for answers to common questions

@ -1,21 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Pack Test Plugin" type="DotNetProject" factoryName=".NET Project" singleton="false">
<option name="EXE_PATH" value="$PROJECT_DIR$/BTCPayServer.PluginPacker/bin/Debug/netcoreapp3.1/BTCPayServer.PluginPacker.dll" />
<option name="PROGRAM_PARAMETERS" value="../../../../BTCPayServer.Plugins.Test\bin\Debug\netcoreapp3.1 BTCPayServer.Plugins.Test &quot;../../../../Packed Plugins&quot;" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/BTCPayServer.PluginPacker/bin/Debug/netcoreapp3.1" />
<option name="PASS_PARENT_ENVS" value="1" />
<option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="USE_MONO" value="0" />
<option name="RUNTIME_ARGUMENTS" value="" />
<option name="PROJECT_PATH" value="$PROJECT_DIR$/BTCPayServer.PluginPacker/BTCPayServer.PluginPacker.csproj" />
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
<option name="PROJECT_KIND" value="DotNetCore" />
<option name="PROJECT_TFM" value=".NETCoreApp,Version=v3.1" />
<method v="2">
<option name="Build" default="false" projectName="BTCPayServer.Plugins.Test" projectPath="C:\Git\btcpayserver\Plugins\BTCPayServer.Plugins.Test\BTCPayServer.Plugins.Test.csproj" />
<option name="Build" />
</method>
</configuration>
</component>

@ -5,4 +5,8 @@ public class AssetBalancesUnavailableException : CustodianApiException
public AssetBalancesUnavailableException(System.Exception e) : base(500, "asset-balances-unavailable", $"Cannot fetch the asset balances: {e.Message}", e)
{
}
public AssetBalancesUnavailableException(string errorMsg) : base(500, "asset-balances-unavailable", $"Cannot fetch the asset balances: {errorMsg}")
{
}
}

@ -10,7 +10,7 @@ public class CustodianApiException : Exception
HttpStatus = httpStatus;
Code = code;
}
public CustodianApiException(int httpStatus, string code, string message) : this(httpStatus, code, message, null)
{
}

@ -0,0 +1,28 @@
using System.Collections.Generic;
using BTCPayServer.Client.Models;
using BTCPayServer.JsonConverters;
namespace BTCPayServer.Abstractions.Custodians.Client;
public class SimulateWithdrawalResult
{
public string PaymentMethod { get; }
public string Asset { get; }
public decimal MinQty { get; }
public decimal MaxQty { get; }
public List<LedgerEntryData> LedgerEntries { get; }
// Fee can be NULL if unknown.
public decimal? Fee { get; }
public SimulateWithdrawalResult(string paymentMethod, string asset, List<LedgerEntryData> ledgerEntries,
decimal minQty, decimal maxQty)
{
PaymentMethod = paymentMethod;
Asset = asset;
LedgerEntries = ledgerEntries;
MinQty = minQty;
MaxQty = maxQty;
}
}

@ -5,9 +5,14 @@ using Newtonsoft.Json.Linq;
namespace BTCPayServer.Abstractions.Custodians;
/// <summary>
/// Interface for custodians that can move funds to the store wallet.
/// </summary>
public interface ICanWithdraw
{
public Task<WithdrawResult> WithdrawAsync(string paymentMethod, decimal amount, JObject config, CancellationToken cancellationToken);
public Task<WithdrawResult> WithdrawToStoreWalletAsync(string paymentMethod, decimal amount, JObject config, CancellationToken cancellationToken);
public Task<SimulateWithdrawalResult> SimulateWithdrawalAsync(string paymentMethod, decimal qty, JObject config, CancellationToken cancellationToken);
public Task<WithdrawResult> GetWithdrawalInfoAsync(string paymentMethod, string withdrawalId, JObject config, CancellationToken cancellationToken);

@ -1,3 +1,4 @@
#nullable enable
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
@ -20,7 +21,6 @@ public interface ICustodian
*/
Task<Dictionary<string, decimal>> GetAssetBalancesAsync(JObject config, CancellationToken cancellationToken);
public Task<Form.Form> GetConfigForm(JObject config, string locale,
CancellationToken cancellationToken = default);
public Task<Form.Form> GetConfigForm(JObject config, CancellationToken cancellationToken = default);
}

@ -28,7 +28,7 @@ public class Field
public bool Constant;
// HTML5 compatible type string like "text", "textarea", "email", "password", etc. Each type is a class and may contain more fields (i.e. "select" would have options).
// HTML5 compatible type string like "text", "textarea", "email", "password", etc.
public string Type;
public static Field CreateFieldset()
@ -52,10 +52,10 @@ public class Field
public string HelpText;
[JsonExtensionData] public IDictionary<string, JToken> AdditionalData { get; set; }
public List<Field> Fields { get; set; } = new ();
public List<Field> Fields { get; set; } = new();
// The field is considered "valid" if there are no validation errors
public List<string> ValidationErrors = new ();
public List<string> ValidationErrors = new();
public virtual bool IsValid()
{

@ -32,6 +32,8 @@ public class Form
// Are all the fields valid in the form?
public bool IsValid()
{
if (TopMessages?.Any(t => t.Type == AlertMessage.AlertMessageType.Danger) is true)
return false;
return Fields.Select(f => f.IsValid()).All(o => o);
}
@ -50,7 +52,7 @@ public class Form
HashSet<string> nameReturned = new();
foreach (var f in GetAllFieldsCore(new List<string>(), Fields))
{
var fullName = string.Join('_', f.Path);
var fullName = string.Join('_', f.Path.Where(s => !string.IsNullOrEmpty(s)));
if (!nameReturned.Add(fullName))
continue;
yield return (fullName, f.Path, f.Field);
@ -63,7 +65,7 @@ public class Form
HashSet<string> nameReturned = new();
foreach (var f in GetAllFieldsCore(new List<string>(), Fields))
{
var fullName = string.Join('_', f.Path);
var fullName = string.Join('_', f.Path.Where(s => !string.IsNullOrEmpty(s)));
if (!nameReturned.Add(fullName))
{
errors.Add($"Form contains duplicate field names '{fullName}'");
@ -128,32 +130,12 @@ public class Form
}
else if (prop.Value.Type == JTokenType.String)
{
var fullname = String.Join('_', propPath);
if (fields.TryGetValue(fullname, out var f) && !f.Constant)
var fullName = string.Join('_', propPath.Where(s => !string.IsNullOrEmpty(s)));
if (fields.TryGetValue(fullName, out var f) && !f.Constant)
f.Value = prop.Value.Value<string>();
}
}
}
public JObject GetValues()
{
var r = new JObject();
foreach (var f in GetAllFields())
{
var node = r;
for (int i = 0; i < f.Path.Count - 1; i++)
{
var p = f.Path[i];
var child = node[p] as JObject;
if (child is null)
{
child = new JObject();
node[p] = child;
}
node = child;
}
node[f.Field.Name] = f.Field.Value;
}
return r;
}
}

@ -114,6 +114,11 @@ namespace BTCPayServer.Security
_Policies.Add(policy);
}
public void UnsafeEval()
{
Add("script-src", "'unsafe-eval'");
}
public IEnumerable<ConsentSecurityPolicy> Rules => _Policies;
public bool HasRules => _Policies.Count != 0;

@ -23,12 +23,12 @@ public class SVGUse : UrlResolutionTagHelper2
{
_fileVersionProvider = fileVersionProvider;
}
public override void Process(TagHelperContext context, TagHelperOutput output)
{
var attr = output.Attributes["href"].Value.ToString();
var symbolIndex = attr!.IndexOf("#", StringComparison.InvariantCulture);
var start = attr.IndexOf("~", StringComparison.InvariantCulture) + 1;
var start = attr.IndexOf("~", StringComparison.InvariantCulture) + 1;
var length = (symbolIndex != -1 ? symbolIndex : attr.Length) - start;
var filePath = attr.Substring(start, length);
if (!string.IsNullOrEmpty(filePath))

@ -51,7 +51,7 @@ namespace BTCPayServer.Client
method: HttpMethod.Get), token);
return await HandleResponse<AppDataBase>(response);
}
public virtual async Task<AppDataBase[]> GetAllApps(string storeId, CancellationToken token = default)
{
if (storeId == null)

@ -50,7 +50,7 @@ namespace BTCPayServer.Client
await HandleResponse(response);
}
public virtual async Task<DepositAddressData> GetDepositAddress(string storeId, string accountId, string paymentMethod, CancellationToken token = default)
public virtual async Task<DepositAddressData> GetCustodianAccountDepositAddress(string storeId, string accountId, string paymentMethod, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}/addresses/{paymentMethod}"), token);
return await HandleResponse<DepositAddressData>(response);
@ -58,7 +58,6 @@ namespace BTCPayServer.Client
public virtual async Task<MarketTradeResponseData> MarketTradeCustodianAccountAsset(string storeId, string accountId, TradeRequestData request, CancellationToken token = default)
{
//var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/users", null, request, HttpMethod.Post), token);
//return await HandleResponse<ApplicationUserData>(response);
var internalRequest = CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}/trades/market", null,
@ -67,13 +66,13 @@ namespace BTCPayServer.Client
return await HandleResponse<MarketTradeResponseData>(response);
}
public virtual async Task<MarketTradeResponseData> GetTradeInfo(string storeId, string accountId, string tradeId, CancellationToken token = default)
public virtual async Task<MarketTradeResponseData> GetCustodianAccountTradeInfo(string storeId, string accountId, string tradeId, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}/trades/{tradeId}", method: HttpMethod.Get), token);
return await HandleResponse<MarketTradeResponseData>(response);
}
public virtual async Task<TradeQuoteResponseData> GetTradeQuote(string storeId, string accountId, string fromAsset, string toAsset, CancellationToken token = default)
public virtual async Task<TradeQuoteResponseData> GetCustodianAccountTradeQuote(string storeId, string accountId, string fromAsset, string toAsset, CancellationToken token = default)
{
var queryPayload = new Dictionary<string, object>();
queryPayload.Add("fromAsset", fromAsset);
@ -82,13 +81,19 @@ namespace BTCPayServer.Client
return await HandleResponse<TradeQuoteResponseData>(response);
}
public virtual async Task<WithdrawalResponseData> CreateWithdrawal(string storeId, string accountId, WithdrawRequestData request, CancellationToken token = default)
public virtual async Task<WithdrawalResponseData> CreateCustodianAccountWithdrawal(string storeId, string accountId, WithdrawRequestData request, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}/withdrawals", bodyPayload: request, method: HttpMethod.Post), token);
return await HandleResponse<WithdrawalResponseData>(response);
}
public virtual async Task<WithdrawalResponseData> GetWithdrawalInfo(string storeId, string accountId, string paymentMethod, string withdrawalId, CancellationToken token = default)
public virtual async Task<WithdrawalSimulationResponseData> SimulateCustodianAccountWithdrawal(string storeId, string accountId, WithdrawRequestData request, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}/withdrawals/simulation", bodyPayload: request, method: HttpMethod.Post), token);
return await HandleResponse<WithdrawalSimulationResponseData>(response);
}
public virtual async Task<WithdrawalResponseData> GetCustodianAccountWithdrawalInfo(string storeId, string accountId, string paymentMethod, string withdrawalId, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}/withdrawals/{paymentMethod}/{withdrawalId}", method: HttpMethod.Get), token);
return await HandleResponse<WithdrawalResponseData>(response);

@ -113,7 +113,7 @@ namespace BTCPayServer.Client
CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/invoices", queryPayload), token);
return await HandleResponse<LightningInvoiceData[]>(response);
}
public virtual async Task<LightningPaymentData[]> GetLightningPayments(string cryptoCode,
bool? includePending = null, long? offsetIndex = null, CancellationToken token = default)
{

@ -115,7 +115,7 @@ namespace BTCPayServer.Client
CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices", queryPayload), token);
return await HandleResponse<LightningInvoiceData[]>(response);
}
public virtual async Task<LightningPaymentData[]> GetLightningPayments(string storeId, string cryptoCode,
bool? includePending = null, long? offsetIndex = null, CancellationToken token = default)
{

@ -54,7 +54,7 @@ namespace BTCPayServer.Client
CancellationToken token = default)
{
using var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/rates",
CreateHttpRequest($"api/v1/stores/{storeId}/rates",
queryPayload: new Dictionary<string, object>() { { "currencyPair", currencyPair } },
method: HttpMethod.Get),
token);

@ -30,9 +30,9 @@ namespace BTCPayServer.JsonConverters
case JTokenType.Integer:
case JTokenType.String:
if (objectType == typeof(decimal) || objectType == typeof(decimal?))
return decimal.Parse(token.ToString(), CultureInfo.InvariantCulture);
return decimal.Parse(token.ToString(), NumberStyles.Any, CultureInfo.InvariantCulture);
if (objectType == typeof(double) || objectType == typeof(double?))
return double.Parse(token.ToString(), CultureInfo.InvariantCulture);
return double.Parse(token.ToString(), NumberStyles.Any, CultureInfo.InvariantCulture);
throw new JsonSerializationException("Unexpected object type: " + objectType);
case JTokenType.Null when objectType == typeof(decimal?) || objectType == typeof(double?):
return null;

@ -0,0 +1,36 @@
using System;
using System.Globalization;
using BTCPayServer.Client.Models;
using BTCPayServer.Lightning;
using NBitcoin.JsonConverters;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client.JsonConverters
{
public class TradeQuantityJsonConverter : JsonConverter<TradeQuantity>
{
public override TradeQuantity ReadJson(JsonReader reader, Type objectType, TradeQuantity existingValue, bool hasExistingValue, JsonSerializer serializer)
{
JToken token = JToken.Load(reader);
switch (token.Type)
{
case JTokenType.Float:
case JTokenType.Integer:
case JTokenType.String:
if (TradeQuantity.TryParse(token.ToString(), out var q))
return q;
break;
case JTokenType.Null:
return null;
}
throw new JsonObjectException("Invalid TradeQuantity, expected string. Expected: \"1.50\" or \"50%\"", reader);
}
public override void WriteJson(JsonWriter writer, TradeQuantity value, JsonSerializer serializer)
{
if (value is not null)
writer.WriteValue(value.ToString());
}
}
}

@ -86,6 +86,7 @@ namespace BTCPayServer.Client.Models
public bool? RequiresRefundEmail { get; set; } = null;
public string DefaultLanguage { get; set; }
public CheckoutType? CheckoutType { get; set; }
public bool? LazyPaymentMethods { get; set; }
}
}
public class InvoiceData : InvoiceDataBase

@ -3,7 +3,6 @@ namespace BTCPayServer.Client.Models
public class LNURLPayPaymentMethodBaseData
{
public bool UseBech32Scheme { get; set; }
public bool EnableForStandardInvoices { get; set; }
public bool LUD12Enabled { get; set; }
public LNURLPayPaymentMethodBaseData()

@ -16,12 +16,11 @@ namespace BTCPayServer.Client.Models
{
}
public LNURLPayPaymentMethodData(string cryptoCode, bool enabled, bool useBech32Scheme, bool enableForStandardInvoices)
public LNURLPayPaymentMethodData(string cryptoCode, bool enabled, bool useBech32Scheme)
{
Enabled = enabled;
CryptoCode = cryptoCode;
UseBech32Scheme = useBech32Scheme;
EnableForStandardInvoices = enableForStandardInvoices;
}
}
}

@ -1,3 +1,4 @@
using BTCPayServer.JsonConverters;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
@ -6,6 +7,7 @@ namespace BTCPayServer.Client.Models;
public class LedgerEntryData
{
public string Asset { get; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal Qty { get; }
[JsonConverter(typeof(StringEnumConverter))]

@ -6,5 +6,5 @@ public class LightningAddressData
public string CurrencyCode { get; set; }
public decimal? Min { get; set; }
public decimal? Max { get; set; }
}

@ -16,7 +16,7 @@ namespace BTCPayServer.Client.Models
[JsonProperty("BOLT11")]
public string BOLT11 { get; set; }
public string PaymentHash { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
@ -24,7 +24,7 @@ namespace BTCPayServer.Client.Models
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
public DateTimeOffset? PaidAt { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
public DateTimeOffset ExpiresAt { get; set; }

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

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

@ -1,8 +1,13 @@
using BTCPayServer.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models;
public class TradeQuoteResponseData
{
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal Bid { get; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal Ask { get; }
public string ToAsset { get; }
public string FromAsset { get; }

@ -1,8 +1,11 @@
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models;
public class TradeRequestData
{
public string FromAsset { set; get; }
public string ToAsset { set; get; }
public string Qty { set; get; }
[JsonConverter(typeof(JsonConverters.TradeQuantityJsonConverter))]
public TradeQuantity Qty { set; get; }
}

@ -1,13 +1,85 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Net.Http.Headers;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models;
public class WithdrawRequestData
{
public string PaymentMethod { set; get; }
public decimal Qty { set; get; }
[JsonConverter(typeof(JsonConverters.TradeQuantityJsonConverter))]
public TradeQuantity Qty { set; get; }
public WithdrawRequestData(string paymentMethod, decimal qty)
public WithdrawRequestData()
{
}
public WithdrawRequestData(string paymentMethod, TradeQuantity qty)
{
PaymentMethod = paymentMethod;
Qty = qty;
}
}
#nullable enable
public record TradeQuantity
{
public TradeQuantity(decimal value, ValueType type)
{
Type = type;
Value = value;
}
public enum ValueType
{
Exact,
Percent
}
public ValueType Type { get; }
public decimal Value { get; set; }
public override string ToString()
{
if (Type == ValueType.Exact)
return Value.ToString(CultureInfo.InvariantCulture);
else
return Value.ToString(CultureInfo.InvariantCulture) + "%";
}
public static TradeQuantity Parse(string str)
{
if (!TryParse(str, out var r))
throw new FormatException("Invalid TradeQuantity");
return r;
}
public static bool TryParse(string str, [MaybeNullWhen(false)] out TradeQuantity quantity)
{
if (str is null)
throw new ArgumentNullException(nameof(str));
quantity = null;
str = str.Trim();
str = str.Replace(" ", "");
if (str.Length == 0)
return false;
if (str[^1] == '%')
{
if (!decimal.TryParse(str[..^1], NumberStyles.Any, CultureInfo.InvariantCulture, out var r))
return false;
if (r < 0.0m)
return false;
quantity = new TradeQuantity(r, TradeQuantity.ValueType.Percent);
}
else
{
if (!decimal.TryParse(str, NumberStyles.Any, CultureInfo.InvariantCulture, out var r))
return false;
if (r < 0.0m)
return false;
quantity = new TradeQuantity(r, TradeQuantity.ValueType.Exact);
}
return true;
}
}

@ -0,0 +1,22 @@
using System.Collections.Generic;
namespace BTCPayServer.Client.Models;
public abstract class WithdrawalBaseResponseData
{
public string Asset { get; }
public string PaymentMethod { get; }
public List<LedgerEntryData> LedgerEntries { get; }
public string AccountId { get; }
public string CustodianCode { get; }
public WithdrawalBaseResponseData(string paymentMethod, string asset, List<LedgerEntryData> ledgerEntries, string accountId,
string custodianCode)
{
PaymentMethod = paymentMethod;
Asset = asset;
LedgerEntries = ledgerEntries;
AccountId = accountId;
CustodianCode = custodianCode;
}
}

@ -5,18 +5,13 @@ using Newtonsoft.Json.Converters;
namespace BTCPayServer.Client.Models;
public class WithdrawalResponseData
public class WithdrawalResponseData : WithdrawalBaseResponseData
{
public string Asset { get; }
public string PaymentMethod { get; }
public List<LedgerEntryData> LedgerEntries { get; }
public string WithdrawalId { get; }
public string AccountId { get; }
public string CustodianCode { get; }
[JsonConverter(typeof(StringEnumConverter))]
public WithdrawalStatus Status { get; }
public string WithdrawalId { get; }
public DateTimeOffset CreatedTime { get; }
public string TransactionId { get; }
@ -24,14 +19,10 @@ public class WithdrawalResponseData
public string TargetAddress { get; }
public WithdrawalResponseData(string paymentMethod, string asset, List<LedgerEntryData> ledgerEntries, string withdrawalId, string accountId,
string custodianCode, WithdrawalStatus status, DateTimeOffset createdTime, string targetAddress, string transactionId)
string custodianCode, WithdrawalStatus status, DateTimeOffset createdTime, string targetAddress, string transactionId) : base(paymentMethod, asset, ledgerEntries, accountId,
custodianCode)
{
PaymentMethod = paymentMethod;
Asset = asset;
LedgerEntries = ledgerEntries;
WithdrawalId = withdrawalId;
AccountId = accountId;
CustodianCode = custodianCode;
TargetAddress = targetAddress;
TransactionId = transactionId;
Status = status;

@ -0,0 +1,21 @@
using System.Collections.Generic;
using BTCPayServer.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models;
public class WithdrawalSimulationResponseData : WithdrawalBaseResponseData
{
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal? MinQty { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal? MaxQty { get; set; }
public WithdrawalSimulationResponseData(string paymentMethod, string asset, string accountId,
string custodianCode, List<LedgerEntryData> ledgerEntries, decimal? minQty, decimal? maxQty) : base(paymentMethod,
asset, ledgerEntries, accountId, custodianCode)
{
MinQty = minQty;
MaxQty = maxQty;
}
}

@ -98,6 +98,37 @@ namespace BTCPayServer.Client
{
return policy.StartsWith("btcpay.plugin", StringComparison.OrdinalIgnoreCase);
}
public static bool IsUserPolicy(string policy)
{
return policy.StartsWith("btcpay.user", StringComparison.OrdinalIgnoreCase);
}
}
public class PermissionSet
{
public PermissionSet() : this(Array.Empty<Permission>())
{
}
public PermissionSet(Permission[] permissions)
{
Permissions = permissions;
}
public Permission[] Permissions { get; }
public bool Contains(Permission requestedPermission)
{
return Permissions.Any(p => p.Contains(requestedPermission));
}
public bool Contains(string permission, string store)
{
if (permission is null)
throw new ArgumentNullException(nameof(permission));
if (store is null)
throw new ArgumentNullException(nameof(store));
return Contains(Permission.Create(permission, store));
}
}
public class Permission
{
@ -105,7 +136,7 @@ namespace BTCPayServer.Client
{
Init();
}
public static Permission Create(string policy, string scope = null)
{
if (TryCreatePermission(policy, scope, out var r))
@ -121,7 +152,7 @@ namespace BTCPayServer.Client
policy = policy.Trim().ToLowerInvariant();
if (!Policies.IsValidPolicy(policy))
return false;
if (scope != null && !Policies.IsStorePolicy(policy))
if (!string.IsNullOrEmpty(scope) && !Policies.IsStorePolicy(policy))
return false;
permission = new Permission(policy, scope);
return true;
@ -174,7 +205,7 @@ namespace BTCPayServer.Client
}
if (!Policies.IsStorePolicy(subpermission.Policy))
return true;
return Scope == null || subpermission.Scope == this.Scope;
return Scope == null || subpermission.Scope == Scope;
}
public static IEnumerable<Permission> ToPermissions(string[] permissions)
@ -199,7 +230,8 @@ namespace BTCPayServer.Client
return true;
if (policy == subpolicy)
return true;
if (!PolicyMap.TryGetValue(policy, out var subPolicies)) return false;
if (!PolicyMap.TryGetValue(policy, out var subPolicies))
return false;
return subPolicies.Contains(subpolicy) || subPolicies.Any(s => ContainsPolicy(s, subpolicy));
}
@ -213,23 +245,23 @@ namespace BTCPayServer.Client
Policies.CanModifyInvoices,
Policies.CanViewStoreSettings,
Policies.CanModifyStoreWebhooks,
Policies.CanModifyPaymentRequests);
Policies.CanModifyPaymentRequests,
Policies.CanUseLightningNodeInStore);
PolicyHasChild(Policies.CanManageUsers, Policies.CanCreateUser);
PolicyHasChild(Policies.CanManagePullPayments, Policies.CanCreatePullPayments );
PolicyHasChild(Policies.CanCreatePullPayments, Policies.CanCreateNonApprovedPullPayments );
PolicyHasChild(Policies.CanModifyPaymentRequests, Policies.CanViewPaymentRequests );
PolicyHasChild(Policies.CanModifyProfile, Policies.CanViewProfile );
PolicyHasChild(Policies.CanUseLightningNodeInStore, Policies.CanViewLightningInvoiceInStore, Policies.CanCreateLightningInvoiceInStore );
PolicyHasChild(Policies.CanManageNotificationsForUser, Policies.CanViewNotificationsForUser );
PolicyHasChild(Policies.CanManagePullPayments, Policies.CanCreatePullPayments);
PolicyHasChild(Policies.CanCreatePullPayments, Policies.CanCreateNonApprovedPullPayments);
PolicyHasChild(Policies.CanModifyPaymentRequests, Policies.CanViewPaymentRequests);
PolicyHasChild(Policies.CanModifyProfile, Policies.CanViewProfile);
PolicyHasChild(Policies.CanUseLightningNodeInStore, Policies.CanViewLightningInvoiceInStore, Policies.CanCreateLightningInvoiceInStore);
PolicyHasChild(Policies.CanManageNotificationsForUser, Policies.CanViewNotificationsForUser);
PolicyHasChild(Policies.CanModifyServerSettings,
Policies.CanUseInternalLightningNode,
Policies.CanManageUsers);
PolicyHasChild(Policies.CanUseInternalLightningNode, Policies.CanCreateLightningInvoiceInternalNode,Policies.CanViewLightningInvoiceInternalNode );
PolicyHasChild(Policies.CanManageCustodianAccounts, Policies.CanViewCustodianAccounts );
PolicyHasChild(Policies.CanModifyInvoices, Policies.CanViewInvoices, Policies.CanCreateInvoice );
PolicyHasChild(Policies.CanViewStoreSettings, Policies.CanViewInvoices, Policies.CanViewPaymentRequests );
PolicyHasChild(Policies.CanUseInternalLightningNode, Policies.CanCreateLightningInvoiceInternalNode, Policies.CanViewLightningInvoiceInternalNode);
PolicyHasChild(Policies.CanManageCustodianAccounts, Policies.CanViewCustodianAccounts);
PolicyHasChild(Policies.CanModifyInvoices, Policies.CanViewInvoices, Policies.CanCreateInvoice, Policies.CanCreateLightningInvoiceInStore);
PolicyHasChild(Policies.CanViewStoreSettings, Policies.CanViewInvoices, Policies.CanViewPaymentRequests);
}
private static void PolicyHasChild(string policy, params string[] subPolicies)
@ -243,33 +275,26 @@ namespace BTCPayServer.Client
}
else
{
PolicyMap.Add(policy,subPolicies.ToHashSet());
PolicyMap.Add(policy, subPolicies.ToHashSet());
}
}
public string Scope { get; }
public string Policy { get; }
public override string ToString()
{
if (Scope != null)
{
return $"{Policy}:{Scope}";
}
return Policy;
return Scope != null ? $"{Policy}:{Scope}" : Policy;
}
public override bool Equals(object obj)
{
Permission item = obj as Permission;
if (item == null)
return false;
return ToString().Equals(item.ToString());
return item != null && ToString().Equals(item.ToString());
}
public static bool operator ==(Permission a, Permission b)
{
if (System.Object.ReferenceEquals(a, b))
if (ReferenceEquals(a, b))
return true;
if (((object)a == null) || ((object)b == null))
return false;

@ -124,14 +124,14 @@ namespace BTCPayServer.Data
#pragma warning disable CS0612 // Type or member is obsolete
WalletTransactionData.OnModelCreating(builder);
#pragma warning restore CS0612 // Type or member is obsolete
WebhookDeliveryData.OnModelCreating(builder, Database);
LightningAddressData.OnModelCreating(builder, Database);
PayoutProcessorData.OnModelCreating(builder, Database);
WebhookData.OnModelCreating(builder, Database);
FormData.OnModelCreating(builder, Database);
WebhookDeliveryData.OnModelCreating(builder, Database);
LightningAddressData.OnModelCreating(builder, Database);
PayoutProcessorData.OnModelCreating(builder, Database);
WebhookData.OnModelCreating(builder, Database);
FormData.OnModelCreating(builder, Database);
if (Database.IsSqlite() && !_designTime)
if (Database.IsSqlite() && !_designTime)
{
// SQLite does not have proper support for DateTimeOffset via Entity Framework Core, see the limitations
// here: https://docs.microsoft.com/en-us/ef/core/providers/sqlite/limitations#query-limitations

@ -13,14 +13,14 @@ public class FormData
public StoreData Store { get; set; }
public string Config { get; set; }
public bool Public { get; set; }
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
{
builder.Entity<FormData>()
.HasOne(o => o.Store)
.WithMany(o => o.Forms).OnDelete(DeleteBehavior.Cascade);
builder.Entity<FormData>().HasIndex(o => o.StoreId);
if (databaseFacade.IsNpgsql())
{
builder.Entity<FormData>()

@ -1,6 +1,7 @@
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Data;
@ -38,4 +39,6 @@ public class LightningAddressDataBlob
public string CurrencyCode { get; set; }
public decimal? Min { get; set; }
public decimal? Max { get; set; }
public JObject InvoiceMetadata { get; set; }
}

@ -24,8 +24,8 @@ namespace BTCPayServer.Data
public string Blob2 { get; set; }
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
{
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
{
builder.Entity<NotificationData>()
.HasOne(o => o.ApplicationUser)
.WithMany(n => n.Notifications)

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;

@ -1,4 +1,4 @@
using System;
using System;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;

@ -0,0 +1,31 @@
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;
#nullable disable
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20230315062447_fixmaxlength")]
public partial class fixmaxlength : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
if (migrationBuilder.IsNpgsql())
{
migrationBuilder.Sql("ALTER TABLE \"InvoiceSearches\" ALTER COLUMN \"Value\" TYPE TEXT USING \"Value\"::TEXT;");
migrationBuilder.Sql("ALTER TABLE \"Invoices\" ALTER COLUMN \"OrderId\" TYPE TEXT USING \"OrderId\"::TEXT;");
}
}
protected override void Down(MigrationBuilder migrationBuilder)
{
// Not supported
}
}
}

@ -1305,7 +1305,7 @@
"name":"Satoshis",
"code":"SATS",
"divisibility":0,
"symbol":"Sats",
"symbol":"sats",
"crypto":true
},
{

@ -5,7 +5,6 @@ using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using BTCPayServer.Rating;
using NBitcoin;
using Newtonsoft.Json;
@ -28,14 +27,6 @@ namespace BTCPayServer.Services.Rates
}
static readonly Dictionary<string, IFormatProvider> _CurrencyProviders = new Dictionary<string, IFormatProvider>();
public string FormatCurrency(string price, string currency)
{
return FormatCurrency(decimal.Parse(price, CultureInfo.InvariantCulture), currency);
}
public string FormatCurrency(decimal price, string currency)
{
return price.ToString("C", GetCurrencyProvider(currency));
}
public NumberFormatInfo GetNumberFormatInfo(string currency, bool useFallback)
{
@ -56,6 +47,7 @@ namespace BTCPayServer.Services.Rates
currencyInfo.CurrencySymbol = currency;
return currencyInfo;
}
public NumberFormatInfo GetNumberFormatInfo(string currency)
{
var curr = GetCurrencyProvider(currency);
@ -65,6 +57,7 @@ namespace BTCPayServer.Services.Rates
return ni;
return null;
}
public IFormatProvider GetCurrencyProvider(string currency)
{
lock (_CurrencyProviders)
@ -104,30 +97,6 @@ namespace BTCPayServer.Services.Rates
currencyProviders.TryAdd(code, number);
}
/// <summary>
/// Format a currency like "0.004 $ (USD)", round to significant divisibility
/// </summary>
/// <param name="value">The value</param>
/// <param name="currency">Currency code</param>
/// <returns></returns>
public string DisplayFormatCurrency(decimal value, string currency)
{
var provider = GetNumberFormatInfo(currency, true);
var currencyData = GetCurrencyData(currency, true);
var divisibility = currencyData.Divisibility;
value = value.RoundToSignificant(ref divisibility);
if (divisibility != provider.CurrencyDecimalDigits)
{
provider = (NumberFormatInfo)provider.Clone();
provider.CurrencyDecimalDigits = divisibility;
}
if (currencyData.Crypto)
return value.ToString("C", provider);
else
return value.ToString("C", provider) + $" ({currency})";
}
readonly Dictionary<string, CurrencyData> _Currencies;
static CurrencyData[] LoadCurrency()

File diff suppressed because one or more lines are too long

@ -20,7 +20,7 @@ namespace BTCPayServer.Services.Rates
}
}
public RateSourceInfo RateSourceInfo => new RateSourceInfo("NULL","NULL", "https://NULL.NULL");
public RateSourceInfo RateSourceInfo => new RateSourceInfo("NULL", "NULL", "https://NULL.NULL");
public Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{

@ -89,7 +89,7 @@ namespace BTCPayServer.Services.Rates
AvailableRateProviders.Add(new(rsi.Id, rsi.DisplayName, rsi.Url, RateSource.Coingecko));
}
}
AvailableRateProviders.Sort((a,b) => StringComparer.Ordinal.Compare(a.DisplayName, b.DisplayName));
AvailableRateProviders.Sort((a, b) => StringComparer.Ordinal.Compare(a.DisplayName, b.DisplayName));
}
public List<AvailableRateProvider> AvailableRateProviders { get; } = new List<AvailableRateProvider>();

@ -11,6 +11,7 @@ using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Models.WalletViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Plugins.PointOfSale;
using BTCPayServer.Plugins.PointOfSale.Controllers;
using BTCPayServer.Plugins.PointOfSale.Models;
using BTCPayServer.Services.Apps;
@ -50,6 +51,7 @@ namespace BTCPayServer.Tests
user.RegisterDerivationScheme(cryptoCode);
user.RegisterDerivationScheme("LTC");
user.RegisterLightningNode(cryptoCode, LightningConnectionType.CLightning);
user.SetLNUrl("BTC", false);
var btcNetwork = tester.PayTester.Networks.GetNetwork<BTCPayNetwork>(cryptoCode);
var invoice = await user.BitPay.CreateInvoiceAsync(
new Invoice
@ -386,7 +388,7 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("BOLT11Expiration")).SendKeys("5" + Keys.Enter);
s.GoToInvoice(invoice.Id);
s.Driver.FindElement(By.Id("IssueRefund")).Click();
if (multiCurrency)
{
s.Driver.WaitUntilAvailable(By.Id("RefundForm"), TimeSpan.FromSeconds(1));
@ -396,21 +398,21 @@ namespace BTCPayServer.Tests
}
s.Driver.WaitUntilAvailable(By.Id("RefundForm"), TimeSpan.FromSeconds(1));
Assert.Contains("$5,500.00", s.Driver.PageSource); // Should propose reimburse in fiat
Assert.Contains("1.10000000 ", s.Driver.PageSource); // Should propose reimburse in BTC at the rate of before
Assert.Contains("2.20000000 ", s.Driver.PageSource); // Should propose reimburse in BTC at the current rate
Assert.Contains("5,500.00 USD", s.Driver.PageSource); // Should propose reimburse in fiat
Assert.Contains("1.10000000 BTC", s.Driver.PageSource); // Should propose reimburse in BTC at the rate of before
Assert.Contains("2.20000000 BTC", s.Driver.PageSource); // Should propose reimburse in BTC at the current rate
s.Driver.WaitForAndClick(By.Id(rateSelection));
s.Driver.FindElement(By.Id("ok")).Click();
s.Driver.WaitUntilAvailable(By.Id("Destination"), TimeSpan.FromSeconds(1));
Assert.Contains("pull-payments", s.Driver.Url);
if (rateSelection == "FiatOption")
Assert.Contains("$5,500.00", s.Driver.PageSource);
Assert.Contains("5,500.00 USD", s.Driver.PageSource);
if (rateSelection == "CurrentOption")
Assert.Contains("2.20000000 ", s.Driver.PageSource);
Assert.Contains("2.20000000 BTC", s.Driver.PageSource);
if (rateSelection == "RateThenOption")
Assert.Contains("1.10000000 ", s.Driver.PageSource);
Assert.Contains("1.10000000 BTC", s.Driver.PageSource);
s.GoToInvoice(invoice.Id);
s.Driver.FindElement(By.Id("IssueRefund")).Click();
s.Driver.WaitUntilAvailable(By.Id("Destination"), TimeSpan.FromSeconds(1));
@ -584,7 +586,7 @@ namespace BTCPayServer.Tests
Assert.True(invoice.SupportedTransactionCurrencies["LTC"].Enabled);
Assert.True(invoice.PaymentSubtotals.ContainsKey("LTC"));
Assert.True(invoice.PaymentTotals.ContainsKey("LTC"));
// Check if we can disable LTC
invoice = await user.BitPay.CreateInvoiceAsync(
new Invoice
@ -622,10 +624,11 @@ namespace BTCPayServer.Tests
var apps = user.GetController<UIAppsController>();
var pos = user.GetController<UIPointOfSaleController>();
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId)).Model);
var appType = AppType.PointOfSale.ToString();
var appType = PointOfSaleAppType.AppType;
vm.AppName = "test";
vm.SelectedAppType = appType;
Assert.IsType<RedirectToActionResult>(apps.CreateApp(user.StoreId, vm).Result);
var redirect = Assert.IsType<RedirectResult>(apps.CreateApp(user.StoreId, vm).Result);
Assert.EndsWith("/settings/pos", redirect.Url);
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
var app = appList.Apps[0];
var appData = new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType };
@ -680,7 +683,7 @@ donation:
var appleInvoice = invoices.SingleOrDefault(invoice => invoice.ItemCode.Equals("apple"));
Assert.NotNull(appleInvoice);
Assert.Equal("good apple", appleInvoice.ItemDesc);
// testing custom amount
var action = Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(app.Id, PosViewType.Cart, 6.6m, null, null, null, null, "donation").Result);
@ -735,7 +738,7 @@ donation:
Assert.Equal(test.ExpectedDivisibility, vmview.CurrencyInfo.Divisibility);
Assert.Equal(test.ExpectedSymbolSpace, vmview.CurrencyInfo.SymbolSpace);
}
//test inventory related features
vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
vmpos.Title = "hello";
@ -756,7 +759,7 @@ noninventoryitem:
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, null, null, null, null, "inventoryitem").Result);
return Task.CompletedTask;
});
//we already bought all available stock so this should fail
await Task.Delay(100);
Assert.IsType<RedirectToActionResult>(publicApps
@ -819,13 +822,13 @@ 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
@ -843,7 +846,7 @@ f:
g:
custom: topup
";
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
Assert.DoesNotContain("custom", vmpos.Template);
@ -855,7 +858,7 @@ g:
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(app.Id, PosViewType.Static, null, null, null, null, null, "g").Result);
invoices = user.BitPay.GetInvoices();

@ -286,7 +286,7 @@ namespace BTCPayServer.Tests
if (permissions.Contains(canModifyAllStores) || storePermissions.Any())
{
var resultStores =
await TestApiAgainstAccessToken<StoreData[]>(accessToken, $"{TestApiPath}/me/stores",
await TestApiAgainstAccessToken<Client.Models.StoreData[]>(accessToken, $"{TestApiPath}/me/stores",
tester.PayTester.HttpClient);
foreach (var selectiveStorePermission in storePermissions)

@ -23,7 +23,7 @@
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.14" />
<PackageReference Include="Selenium.Support" Version="4.1.1" />
<PackageReference Include="Selenium.WebDriver" Version="4.1.1" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="110.0.5481.7700" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="112.0.5615.4900" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PrivateAssets>all</PrivateAssets>

@ -1,5 +1,6 @@
using System;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
using BTCPayServer.Payments;
using BTCPayServer.Tests.Logging;
using BTCPayServer.Views.Stores;
@ -27,6 +28,7 @@ namespace BTCPayServer.Tests
s.GoToRegister();
s.RegisterNewUser();
s.CreateNewStore();
s.EnableCheckout(CheckoutType.V1);
s.AddDerivationScheme();
s.GoToStore(StoreNavPages.CheckoutAppearance);
s.Driver.FindElement(By.Id("RequiresRefundEmail")).Click();
@ -72,6 +74,7 @@ namespace BTCPayServer.Tests
s.GoToRegister();
s.RegisterNewUser();
s.CreateNewStore();
s.EnableCheckout(CheckoutType.V1);
s.AddDerivationScheme();
// Now create an invoice that requires a refund email
@ -124,6 +127,7 @@ namespace BTCPayServer.Tests
s.GoToRegister();
s.RegisterNewUser();
s.CreateNewStore();
s.EnableCheckout(CheckoutType.V1);
s.AddDerivationScheme();
var invoiceId = s.CreateInvoice();
@ -154,13 +158,13 @@ namespace BTCPayServer.Tests
s.GoToRegister();
s.RegisterNewUser(true);
s.CreateNewStore();
s.EnableCheckout(CheckoutType.V1);
s.AddLightningNode();
s.AddDerivationScheme();
var invoiceId = s.CreateInvoice(defaultPaymentMethod: "BTC_LightningLike");
s.GoToInvoiceCheckout(invoiceId);
Assert.Equal("Bitcoin (Lightning) (BTC)", s.Driver.FindElement(By.ClassName("payment__currencies")).Text);
Assert.Equal("Bitcoin (Lightning)", s.Driver.FindElement(By.ClassName("payment__currencies")).Text);
s.Driver.Quit();
}
@ -174,6 +178,7 @@ namespace BTCPayServer.Tests
s.GoToRegister();
s.RegisterNewUser(true);
s.CreateNewStore();
s.EnableCheckout(CheckoutType.V1);
s.AddLightningNode();
s.GoToLightningSettings();
s.Driver.SetCheckbox(By.Id("LightningAmountInSatoshi"), true);
@ -182,7 +187,7 @@ namespace BTCPayServer.Tests
var invoiceId = s.CreateInvoice(10, "USD", "a@g.com");
s.GoToInvoiceCheckout(invoiceId);
Assert.Contains("Sats", s.Driver.FindElement(By.ClassName("payment__currencies_noborder")).Text);
Assert.Contains("sats", s.Driver.FindElement(By.ClassName("buyerTotalLine")).Text);
}
[Fact(Timeout = TestTimeout)]
@ -193,6 +198,7 @@ namespace BTCPayServer.Tests
s.GoToRegister();
s.RegisterNewUser();
s.CreateNewStore();
s.EnableCheckout(CheckoutType.V1);
s.GoToStore();
s.AddDerivationScheme();
var invoiceId = s.CreateInvoice(0.001m, "BTC", "a@x.com");

@ -1,11 +1,13 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
using BTCPayServer.Payments;
using BTCPayServer.Tests.Logging;
using BTCPayServer.Views.Stores;
using NBitcoin;
using OpenQA.Selenium;
using OpenQA.Selenium.Support.Extensions;
using OpenQA.Selenium.Support.UI;
using Xunit;
using Xunit.Abstractions;
@ -32,7 +34,6 @@ namespace BTCPayServer.Tests
s.GoToRegister();
s.RegisterNewUser(true);
s.CreateNewStore();
s.EnableCheckoutV2();
s.AddLightningNode();
// Use non-legacy derivation scheme
s.AddDerivationScheme("BTC", "tpubDD79XF4pzhmPSJ9AyUay9YbXAeD1c6nkUqC32pnKARJH6Ja5hGUfGc76V82ahXpsKqN6UcSGXMkzR34aZq4W23C6DAdZFaVrzWqzj24F8BC");
@ -43,15 +44,14 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("StoreWebsite")).SendKeys(storeUrl);
s.Driver.FindElement(By.Id("Save")).Click();
Assert.Contains("Store successfully updated", s.FindAlertMessage().Text);
// Enable LNURL, which we will need for (non-)presence checks throughout this test
s.GoToHome();
s.GoToLightningSettings();
s.Driver.SetCheckbox(By.Id("LNURLEnabled"), true);
s.Driver.SetCheckbox(By.Id("LNURLStandardInvoiceEnabled"), true);
s.Driver.FindElement(By.Id("save")).Click();
Assert.Contains("BTC Lightning settings successfully updated", s.FindAlertMessage().Text);
s.GoToStore(StoreNavPages.CheckoutAppearance);
s.Driver.WaitForAndClick(By.Id("Presets"));
s.Driver.WaitForAndClick(By.Id("Presets_InStore"));
Assert.True(s.Driver.SetCheckbox(By.Id("ShowPayInWalletButton"), true));
s.Driver.FindElement(By.Id("Save")).SendKeys(Keys.Enter);
Assert.Contains("Store successfully updated", s.FindAlertMessage().Text);
// Top up/zero amount invoices
var invoiceId = s.CreateInvoice(amount: null);
s.GoToInvoiceCheckout(invoiceId);
@ -71,8 +71,15 @@ namespace BTCPayServer.Tests
Assert.Equal(address, copyAddress);
Assert.Equal($"bitcoin:{address.ToUpperInvariant()}", qrValue);
s.Driver.ElementDoesNotExist(By.Id("Lightning_BTC"));
s.Driver.ElementDoesNotExist(By.Id("PayByLNURL"));
// Details should show exchange rate
s.Driver.ToggleCollapse("PaymentDetails");
s.Driver.ElementDoesNotExist(By.Id("PaymentDetails-TotalPrice"));
s.Driver.ElementDoesNotExist(By.Id("PaymentDetails-TotalFiat"));
s.Driver.ElementDoesNotExist(By.Id("PaymentDetails-AmountDue"));
Assert.Contains("$", s.Driver.FindElement(By.Id("PaymentDetails-ExchangeRate")).Text);
Assert.Contains("sat/byte", s.Driver.FindElement(By.Id("PaymentDetails-RecommendedFee")).Text);
// Switch to LNURL
s.Driver.FindElement(By.CssSelector(".payment-method:nth-child(2)")).Click();
TestUtils.Eventually(() =>
@ -81,12 +88,11 @@ namespace BTCPayServer.Tests
Assert.StartsWith("lightning:lnurl", payUrl);
Assert.StartsWith("lnurl", s.Driver.WaitForElement(By.Id("Lightning_BTC")).GetAttribute("value"));
s.Driver.ElementDoesNotExist(By.Id("Address_BTC"));
s.Driver.FindElement(By.Id("PayByLNURL"));
});
// Default payment method
s.GoToHome();
invoiceId = s.CreateInvoice(defaultPaymentMethod: "BTC_LightningLike");
invoiceId = s.CreateInvoice(21000, "SATS", defaultPaymentMethod: "BTC_LightningLike");
s.GoToInvoiceCheckout(invoiceId);
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
Assert.Equal(2, s.Driver.FindElements(By.CssSelector(".payment-method")).Count);
@ -100,9 +106,8 @@ namespace BTCPayServer.Tests
Assert.Equal(address, copyAddress);
Assert.Equal($"lightning:{address.ToUpperInvariant()}", qrValue);
s.Driver.ElementDoesNotExist(By.Id("Address_BTC"));
s.Driver.FindElement(By.Id("PayByLNURL"));
// Lightning amount in Sats
// Lightning amount in sats
Assert.Contains("BTC", s.Driver.FindElement(By.Id("AmountDue")).Text);
s.GoToHome();
s.GoToLightningSettings();
@ -111,7 +116,15 @@ namespace BTCPayServer.Tests
Assert.Contains("BTC Lightning settings successfully updated", s.FindAlertMessage().Text);
s.GoToInvoiceCheckout(invoiceId);
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
Assert.Contains("Sats", s.Driver.FindElement(By.Id("AmountDue")).Text);
Assert.Contains("sats", s.Driver.FindElement(By.Id("AmountDue")).Text);
// Details should not show exchange rate
s.Driver.ToggleCollapse("PaymentDetails");
s.Driver.ElementDoesNotExist(By.Id("PaymentDetails-ExchangeRate"));
s.Driver.ElementDoesNotExist(By.Id("PaymentDetails-TotalFiat"));
s.Driver.ElementDoesNotExist(By.Id("PaymentDetails-RecommendedFee"));
Assert.Contains("21 000 sats", s.Driver.FindElement(By.Id("PaymentDetails-AmountDue")).Text);
Assert.Contains("21 000 sats", s.Driver.FindElement(By.Id("PaymentDetails-TotalPrice")).Text);
// Expire
var expirySeconds = s.Driver.FindElement(By.Id("ExpirySeconds"));
@ -124,11 +137,11 @@ namespace BTCPayServer.Tests
Assert.DoesNotContain("Please send", paymentInfo.Text);
TestUtils.Eventually(() =>
{
var expiredSection = s.Driver.FindElement(By.Id("expired"));
var expiredSection = s.Driver.FindElement(By.Id("unpaid"));
Assert.True(expiredSection.Displayed);
Assert.Contains("Invoice Expired", expiredSection.Text);
});
Assert.True(s.Driver.ElementDoesNotExist(By.Id("ReceiptLink")));
Assert.True(s.Driver.ElementDoesNotExist(By.Id("receipt-btn")));
Assert.Equal(storeUrl, s.Driver.FindElement(By.Id("StoreLink")).GetAttribute("href"));
// Test payment
@ -145,6 +158,10 @@ namespace BTCPayServer.Tests
Assert.Contains("Exchange Rate", details.Text);
Assert.Contains("Amount Due", details.Text);
Assert.Contains("Recommended Fee", details.Text);
Assert.Contains("$", s.Driver.FindElement(By.Id("PaymentDetails-ExchangeRate")).Text);
Assert.Contains("$", s.Driver.FindElement(By.Id("PaymentDetails-TotalFiat")).Text);
Assert.Contains("BTC", s.Driver.FindElement(By.Id("PaymentDetails-AmountDue")).Text);
Assert.Contains("BTC", s.Driver.FindElement(By.Id("PaymentDetails-TotalPrice")).Text);
// Pay partial amount
await Task.Delay(200);
@ -155,44 +172,52 @@ namespace BTCPayServer.Tests
await s.Server.ExplorerNode.GenerateAsync(1);
// Fake Pay
s.Driver.FindElement(By.Id("FakePayAmount")).FillIn(amountFraction);
s.Driver.FindElement(By.Id("FakePay")).Click();
TestUtils.Eventually(() =>
{
Assert.Contains("Created transaction",
s.Driver.WaitForElement(By.Id("CheatSuccessMessage")).Text);
s.Server.ExplorerNode.Generate(1);
paymentInfo = s.Driver.WaitForElement(By.Id("PaymentInfo"));
Assert.Contains("The invoice hasn't been paid in full", paymentInfo.Text);
Assert.Contains("Please send", paymentInfo.Text);
});
s.Driver.Navigate().Refresh();
// Pay full amount
s.PayInvoice();
// Processing
TestUtils.Eventually(() =>
{
var processingSection = s.Driver.WaitForElement(By.Id("processing"));
Assert.True(processingSection.Displayed);
Assert.Contains("Payment Received", processingSection.Text);
Assert.Contains("Your payment has been received and is now processing", processingSection.Text);
});
s.Driver.FindElement(By.Id("confetti"));
// Mine
s.Driver.FindElement(By.Id("Mine")).Click();
s.MineBlockOnInvoiceCheckout();
TestUtils.Eventually(() =>
{
Assert.Contains("Mined 1 block",
s.Driver.WaitForElement(By.Id("CheatSuccessMessage")).Text);
});
// Pay full amount
var amountDue = s.Driver.FindElement(By.Id("AmountDue")).GetAttribute("data-amount-due");
s.Driver.FindElement(By.Id("FakePayAmount")).FillIn(amountDue);
s.Driver.FindElement(By.Id("FakePay")).Click();
// Settled
TestUtils.Eventually(() =>
{
s.Server.ExplorerNode.Generate(1);
var paidSection = s.Driver.WaitForElement(By.Id("paid"));
Assert.True(paidSection.Displayed);
Assert.Contains("Invoice Paid", paidSection.Text);
var settledSection = s.Driver.WaitForElement(By.Id("settled"));
Assert.True(settledSection.Displayed);
Assert.Contains("Invoice Paid", settledSection.Text);
});
s.Driver.FindElement(By.Id("ReceiptLink"));
s.Driver.FindElement(By.Id("confetti"));
s.Driver.FindElement(By.Id("receipt-btn"));
Assert.Equal(storeUrl, s.Driver.FindElement(By.Id("StoreLink")).GetAttribute("href"));
// BIP21
s.GoToHome();
s.GoToStore(StoreNavPages.CheckoutAppearance);
s.Driver.SetCheckbox(By.Id("OnChainWithLnInvoiceFallback"), true);
s.Driver.SetCheckbox(By.Id("LightningAmountInSatoshi"), false);
s.Driver.FindElement(By.Id("Save")).Click();
Assert.Contains("Store successfully updated", s.FindAlertMessage().Text);
@ -200,6 +225,7 @@ namespace BTCPayServer.Tests
s.GoToInvoiceCheckout(invoiceId);
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
Assert.Empty(s.Driver.FindElements(By.CssSelector(".payment-method")));
Assert.Contains("BTC", s.Driver.FindElement(By.Id("AmountDue")).Text);
qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
@ -213,7 +239,32 @@ namespace BTCPayServer.Tests
Assert.StartsWith("lnbcrt", copyAddressLightning);
Assert.StartsWith($"bitcoin:{address.ToUpperInvariant()}?amount=", qrValue);
Assert.Contains("&lightning=LNBCRT", qrValue);
s.Driver.FindElement(By.Id("PayByLNURL"));
// Check details
s.Driver.ToggleCollapse("PaymentDetails");
Assert.Contains("1 BTC = ", s.Driver.FindElement(By.Id("PaymentDetails-ExchangeRate")).Text);
Assert.Contains("$", s.Driver.FindElement(By.Id("PaymentDetails-ExchangeRate")).Text);
Assert.Contains("$", s.Driver.FindElement(By.Id("PaymentDetails-TotalFiat")).Text);
Assert.Contains("BTC", s.Driver.FindElement(By.Id("PaymentDetails-AmountDue")).Text);
Assert.Contains("BTC", s.Driver.FindElement(By.Id("PaymentDetails-TotalPrice")).Text);
// Switch to amount displayed in sats
s.GoToHome();
s.GoToStore(StoreNavPages.CheckoutAppearance);
s.Driver.SetCheckbox(By.Id("LightningAmountInSatoshi"), true);
s.Driver.FindElement(By.Id("Save")).Click();
Assert.Contains("Store successfully updated", s.FindAlertMessage().Text);
s.GoToInvoiceCheckout(invoiceId);
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
Assert.Contains("sats", s.Driver.FindElement(By.Id("AmountDue")).Text);
// Check details
s.Driver.ToggleCollapse("PaymentDetails");
Assert.Contains("1 sat = ", s.Driver.FindElement(By.Id("PaymentDetails-ExchangeRate")).Text);
Assert.Contains("$", s.Driver.FindElement(By.Id("PaymentDetails-ExchangeRate")).Text);
Assert.Contains("$", s.Driver.FindElement(By.Id("PaymentDetails-TotalFiat")).Text);
Assert.Contains("sats", s.Driver.FindElement(By.Id("PaymentDetails-AmountDue")).Text);
Assert.Contains("sats", s.Driver.FindElement(By.Id("PaymentDetails-TotalPrice")).Text);
// BIP21 with LN as default payment method
s.GoToHome();
@ -224,14 +275,20 @@ namespace BTCPayServer.Tests
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
Assert.StartsWith("bitcoin:", payUrl);
Assert.Contains("&lightning=lnbcrt", payUrl);
s.Driver.FindElement(By.Id("PayByLNURL"));
// Check details
s.Driver.ToggleCollapse("PaymentDetails");
Assert.Contains("1 sat = ", s.Driver.FindElement(By.Id("PaymentDetails-ExchangeRate")).Text);
Assert.Contains("$", s.Driver.FindElement(By.Id("PaymentDetails-ExchangeRate")).Text);
Assert.Contains("$", s.Driver.FindElement(By.Id("PaymentDetails-TotalFiat")).Text);
Assert.Contains("sats", s.Driver.FindElement(By.Id("PaymentDetails-AmountDue")).Text);
Assert.Contains("sats", s.Driver.FindElement(By.Id("PaymentDetails-TotalPrice")).Text);
// Ensure LNURL is enabled
s.GoToHome();
s.GoToLightningSettings();
Assert.True(s.Driver.FindElement(By.Id("LNURLEnabled")).Selected);
Assert.True(s.Driver.FindElement(By.Id("LNURLStandardInvoiceEnabled")).Selected);
// BIP21 with top-up invoice
invoiceId = s.CreateInvoice(amount: null);
s.GoToInvoiceCheckout(invoiceId);
@ -249,7 +306,14 @@ namespace BTCPayServer.Tests
Assert.Equal(address, copyAddressOnchain);
Assert.StartsWith("lnurl", copyAddressLightning);
Assert.StartsWith($"bitcoin:{address.ToUpperInvariant()}?lightning=LNURL", qrValue);
s.Driver.FindElement(By.Id("PayByLNURL"));
// Check details
s.Driver.ToggleCollapse("PaymentDetails");
Assert.Contains("1 sat = ", s.Driver.FindElement(By.Id("PaymentDetails-ExchangeRate")).Text);
Assert.Contains("$", s.Driver.FindElement(By.Id("PaymentDetails-ExchangeRate")).Text);
s.Driver.ElementDoesNotExist(By.Id("PaymentDetails-TotalFiat"));
s.Driver.ElementDoesNotExist(By.Id("PaymentDetails-AmountDue"));
s.Driver.ElementDoesNotExist(By.Id("PaymentDetails-TotalPrice"));
// Expiry message should not show amount for top-up invoice
expirySeconds = s.Driver.FindElement(By.Id("ExpirySeconds"));
@ -261,7 +325,7 @@ namespace BTCPayServer.Tests
Assert.Contains("This invoice will expire in", paymentInfo.Text);
Assert.Contains("00:0", paymentInfo.Text);
Assert.DoesNotContain("Please send", paymentInfo.Text);
// Configure countdown timer
s.GoToHome();
invoiceId = s.CreateInvoice();
@ -273,13 +337,13 @@ namespace BTCPayServer.Tests
displayExpirationTimer.SendKeys("10");
s.Driver.FindElement(By.Id("Save")).Click();
Assert.Contains("Store successfully updated", s.FindAlertMessage().Text);
s.GoToInvoiceCheckout(invoiceId);
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
paymentInfo = s.Driver.FindElement(By.Id("PaymentInfo"));
Assert.False(paymentInfo.Displayed);
Assert.DoesNotContain("This invoice will expire in", paymentInfo.Text);
expirySeconds = s.Driver.FindElement(By.Id("ExpirySeconds"));
expirySeconds.Clear();
expirySeconds.SendKeys("599");
@ -289,7 +353,7 @@ namespace BTCPayServer.Tests
Assert.True(paymentInfo.Displayed);
Assert.Contains("This invoice will expire in", paymentInfo.Text);
Assert.Contains("09:5", paymentInfo.Text);
// Disable LNURL again
s.GoToHome();
s.GoToLightningSettings();
@ -308,8 +372,7 @@ namespace BTCPayServer.Tests
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
Assert.StartsWith("bitcoin:", payUrl);
Assert.Contains("&lightning=lnbcrt", payUrl);
s.Driver.FindElement(By.Id("PayByLNURL"));
// Language Switch
var languageSelect = new SelectElement(s.Driver.FindElement(By.Id("DefaultLang")));
Assert.Equal("English", languageSelect.SelectedOption.Text);
@ -318,9 +381,9 @@ namespace BTCPayServer.Tests
languageSelect.SelectByText("Deutsch");
Assert.Equal("Details anzeigen", s.Driver.FindElement(By.Id("DetailsToggle")).Text);
Assert.Contains("lang=de", s.Driver.Url);
s.Driver.Navigate().Refresh();
languageSelect = new SelectElement(s.Driver.FindElement(By.Id("DefaultLang")));
languageSelect = new SelectElement(s.Driver.WaitForElement(By.Id("DefaultLang")));
Assert.Equal("Deutsch", languageSelect.SelectedOption.Text);
Assert.Equal("Details anzeigen", s.Driver.FindElement(By.Id("DetailsToggle")).Text);
languageSelect.SelectByText("English");
@ -336,7 +399,6 @@ namespace BTCPayServer.Tests
s.GoToRegister();
s.RegisterNewUser();
s.CreateNewStore();
s.EnableCheckoutV2();
s.GoToStore();
s.AddDerivationScheme();
var invoiceId = s.CreateInvoice(0.001m, "BTC", "a@x.com");

@ -4,11 +4,11 @@ using BTCPayServer.Client.Models;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Plugins.Crowdfund;
using BTCPayServer.Plugins.Crowdfund.Controllers;
using BTCPayServer.Plugins.Crowdfund.Models;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Tests.Logging;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBitpayClient;
@ -34,18 +34,16 @@ namespace BTCPayServer.Tests
await user.GrantAccessAsync();
var user2 = tester.NewAccount();
await user2.GrantAccessAsync();
var stores = user.GetController<UIStoresController>();
var apps = user.GetController<UIAppsController>();
var apps2 = user2.GetController<UIAppsController>();
var crowdfund = user.GetController<UICrowdfundController>();
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId)).Model);
var appType = AppType.Crowdfund.ToString();
Assert.NotNull(vm.SelectedAppType);
var appType = CrowdfundAppType.AppType;
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId, appType)).Model);
Assert.Equal(appType, vm.SelectedAppType);
Assert.Null(vm.AppName);
vm.AppName = "test";
vm.SelectedAppType = appType;
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.CreateApp(user.StoreId, vm).Result);
Assert.Equal(nameof(crowdfund.UpdateCrowdfund), redirectToAction.ActionName);
var redirect = Assert.IsType<RedirectResult>(apps.CreateApp(user.StoreId, vm).Result);
Assert.EndsWith("/settings/crowdfund", redirect.Url);
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
var app = appList.Apps[0];
var appData = new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType };
@ -61,8 +59,8 @@ namespace BTCPayServer.Tests
Assert.Equal(user.StoreId, appList.Apps[0].StoreId);
Assert.IsType<NotFoundResult>(apps2.DeleteApp(appList.Apps[0].Id));
Assert.IsType<ViewResult>(apps.DeleteApp(appList.Apps[0].Id));
redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(appList.Apps[0].Id).Result);
Assert.Equal(nameof(stores.Dashboard), redirectToAction.ActionName);
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(appList.Apps[0].Id).Result);
Assert.Equal(nameof(UIStoresController.Dashboard), redirectToAction.ActionName);
appList = await apps.ListApps(user.StoreId).AssertViewModelAsync<ListAppsViewModel>();
Assert.Empty(appList.Apps);
}
@ -79,10 +77,11 @@ namespace BTCPayServer.Tests
var apps = user.GetController<UIAppsController>();
var crowdfund = user.GetController<UICrowdfundController>();
var vm = apps.CreateApp(user.StoreId).AssertViewModel<CreateAppViewModel>();
var appType = AppType.Crowdfund.ToString();
var appType = CrowdfundAppType.AppType;
vm.AppName = "test";
vm.SelectedAppType = appType;
Assert.IsType<RedirectToActionResult>(apps.CreateApp(user.StoreId, vm).Result);
var redirect = Assert.IsType<RedirectResult>(apps.CreateApp(user.StoreId, vm).Result);
Assert.EndsWith("/settings/crowdfund", redirect.Url);
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
var app = appList.Apps[0];
var appData = new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType };
@ -105,7 +104,7 @@ namespace BTCPayServer.Tests
Amount = new decimal(0.01)
}, default));
Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(app.Id, string.Empty));
Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(app.Id));
//Scenario 2: Not Enabled But Admin - Allowed
Assert.IsType<OkObjectResult>(await crowdfundController.ContributeToCrowdfund(app.Id, new ContributeToCrowdfund()
@ -113,8 +112,8 @@ namespace BTCPayServer.Tests
RedirectToCheckout = false,
Amount = new decimal(0.01)
}, default));
Assert.IsType<ViewResult>(await crowdfundController.ViewCrowdfund(app.Id, string.Empty));
Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(app.Id, string.Empty));
Assert.IsType<ViewResult>(await crowdfundController.ViewCrowdfund(app.Id));
Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(app.Id));
//Scenario 3: Enabled But Start Date > Now - Not Allowed
crowdfundViewModel.StartDate = DateTime.Today.AddDays(2);
@ -170,10 +169,10 @@ namespace BTCPayServer.Tests
var apps = user.GetController<UIAppsController>();
var crowdfund = user.GetController<UICrowdfundController>();
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId)).Model);
var appType = AppType.Crowdfund.ToString();
var appType = CrowdfundAppType.AppType;
vm.AppName = "test";
vm.SelectedAppType = appType;
Assert.IsType<RedirectToActionResult>(apps.CreateApp(user.StoreId, vm).Result);
Assert.IsType<RedirectResult>(apps.CreateApp(user.StoreId, vm).Result);
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
var app = appList.Apps[0];
var appData = new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType };
@ -193,7 +192,7 @@ namespace BTCPayServer.Tests
var publicApps = user.GetController<UICrowdfundController>();
var model = Assert.IsType<ViewCrowdfundViewModel>(Assert
.IsType<ViewResult>(publicApps.ViewCrowdfund(app.Id, String.Empty).Result).Model);
.IsType<ViewResult>(publicApps.ViewCrowdfund(app.Id).Result).Model);
Assert.Equal(crowdfundViewModel.TargetAmount, model.TargetAmount);
Assert.Equal(crowdfundViewModel.EndDate, model.EndDate);
@ -217,7 +216,7 @@ namespace BTCPayServer.Tests
}, Facade.Merchant);
model = Assert.IsType<ViewCrowdfundViewModel>(Assert
.IsType<ViewResult>(publicApps.ViewCrowdfund(app.Id, string.Empty).Result).Model);
.IsType<ViewResult>(publicApps.ViewCrowdfund(app.Id).Result).Model);
Assert.Equal(0m, model.Info.CurrentAmount);
Assert.Equal(1m, model.Info.CurrentPendingAmount);
@ -226,12 +225,12 @@ namespace BTCPayServer.Tests
TestLogs.LogInformation("Let's check current amount change once payment is confirmed");
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, tester.ExplorerNode.Network);
tester.ExplorerNode.SendToAddress(invoiceAddress, invoice.BtcDue);
tester.ExplorerNode.Generate(1); // By default invoice confirmed at 1 block
await tester.ExplorerNode.SendToAddressAsync(invoiceAddress, invoice.BtcDue);
await tester.ExplorerNode.GenerateAsync(1); // By default invoice confirmed at 1 block
TestUtils.Eventually(() =>
{
model = Assert.IsType<ViewCrowdfundViewModel>(Assert
.IsType<ViewResult>(publicApps.ViewCrowdfund(app.Id, String.Empty).Result).Model);
.IsType<ViewResult>(publicApps.ViewCrowdfund(app.Id).Result).Model);
Assert.Equal(1m, model.Info.CurrentAmount);
Assert.Equal(0m, model.Info.CurrentPendingAmount);
});
@ -279,7 +278,7 @@ namespace BTCPayServer.Tests
TestUtils.Eventually(() =>
{
model = Assert.IsType<ViewCrowdfundViewModel>(Assert
.IsType<ViewResult>(publicApps.ViewCrowdfund(app.Id, string.Empty).Result).Model);
.IsType<ViewResult>(publicApps.ViewCrowdfund(app.Id).Result).Model);
Assert.Equal(0.7m, model.Info.CurrentPendingAmount);
});
}

@ -199,11 +199,15 @@ retry:
return true;
}
public static void SetCheckbox(this IWebDriver driver, By selector, bool value)
public static bool SetCheckbox(this IWebDriver driver, By selector, bool value)
{
var element = driver.FindElement(selector);
if (value != element.Selected)
{
driver.WaitForAndClick(selector);
return true;
}
return false;
}
}
}

@ -51,7 +51,6 @@ namespace BTCPayServer.Tests
{
public FastTests(ITestOutputHelper helper) : base(helper)
{
}
class DockerImage
{
@ -135,6 +134,33 @@ namespace BTCPayServer.Tests
}
}
[Fact]
public void CanParseDecimals()
{
CanParseDecimalsCore("{\"qty\": 1}", 1.0m);
CanParseDecimalsCore("{\"qty\": \"1\"}", 1.0m);
CanParseDecimalsCore("{\"qty\": 1.0}", 1.0m);
CanParseDecimalsCore("{\"qty\": \"1.0\"}", 1.0m);
CanParseDecimalsCore("{\"qty\": 6.1e-7}", 6.1e-7m);
CanParseDecimalsCore("{\"qty\": \"6.1e-7\"}", 6.1e-7m);
var data = JsonConvert.DeserializeObject<TradeRequestData>("{\"qty\": \"6.1e-7\", \"fromAsset\":\"Test\"}");
Assert.Equal(6.1e-7m, data.Qty.Value);
Assert.Equal("Test", data.FromAsset);
data = JsonConvert.DeserializeObject<TradeRequestData>("{\"fromAsset\":\"Test\", \"qty\": \"6.1e-7\"}");
Assert.Equal(6.1e-7m, data.Qty.Value);
Assert.Equal("Test", data.FromAsset);
}
private void CanParseDecimalsCore(string str, decimal expected)
{
var d = JsonConvert.DeserializeObject<LedgerEntryData>(str);
Assert.Equal(expected, d.Qty);
var d2 = JsonConvert.DeserializeObject<TradeRequestData>(str);
Assert.Equal(new TradeQuantity(expected, TradeQuantity.ValueType.Exact), d2.Qty);
}
[Fact]
public void CanMergeReceiptOptions()
{
@ -326,7 +352,7 @@ namespace BTCPayServer.Tests
var networkProvider = new BTCPayNetworkProvider(ChainName.Regtest);
var paymentMethodHandlerDictionary = new PaymentMethodHandlerDictionary(new IPaymentMethodHandler[]
{
new BitcoinLikePaymentHandler(null, networkProvider, null, null, null),
new BitcoinLikePaymentHandler(null, networkProvider, null, null, null, null),
new LightningLikePaymentHandler(null, null, networkProvider, null, null, null),
});
var entity = new InvoiceEntity();
@ -512,7 +538,7 @@ namespace BTCPayServer.Tests
var networkProvider = new BTCPayNetworkProvider(ChainName.Regtest);
var paymentMethodHandlerDictionary = new PaymentMethodHandlerDictionary(new IPaymentMethodHandler[]
{
new BitcoinLikePaymentHandler(null, networkProvider, null, null, null),
new BitcoinLikePaymentHandler(null, networkProvider, null, null, null, null),
new LightningLikePaymentHandler(null, null, networkProvider, null, null, null),
});
var entity = new InvoiceEntity();
@ -600,15 +626,16 @@ namespace BTCPayServer.Tests
[Fact]
public void RoundupCurrenciesCorrectly()
{
DisplayFormatter displayFormatter = new(CurrencyNameTable.Instance);
foreach (var test in new[]
{
(0.0005m, "$0.0005 (USD)", "USD"), (0.001m, "$0.001 (USD)", "USD"), (0.01m, "$0.01 (USD)", "USD"),
(0.1m, "$0.10 (USD)", "USD"), (0.1m, "0,10 € (EUR)", "EUR"), (1000m, "¥1,000 (JPY)", "JPY"),
(1000.0001m, "1,000.00 (INR)", "INR"),
(0.0m, "$0.00 (USD)", "USD")
(0.0005m, "0.0005 USD", "USD"), (0.001m, "0.001 USD", "USD"), (0.01m, "0.01 USD", "USD"),
(0.1m, "0.10 USD", "USD"), (0.1m, "0,10 EUR", "EUR"), (1000m, "1,000 JPY", "JPY"),
(1000.0001m, "1,000.00 INR", "INR"),
(0.0m, "0.00 USD", "USD")
})
{
var actual = CurrencyNameTable.Instance.DisplayFormatCurrency(test.Item1, test.Item3);
var actual = displayFormatter.Currency(test.Item1, test.Item3);
actual = actual.Replace("¥", "¥"); // Hack so JPY test pass on linux as well
Assert.Equal(test.Item2, actual);
}
@ -706,22 +733,69 @@ namespace BTCPayServer.Tests
Assert.Throws<ParsingException>(() => { parser.ParseOutputDescriptor("wpkh([8b60afd1/49h/0h/0h]xpub661MyMwAFXkMnyoBjyHndD3QwRbcGVBsTGeNZN6QGVHcfz4MPzBUxjSevweNFQx7SqmMHLdSA4FteGsRrEriu4pnVZMZWnruFFAYZATtcDw/0/*)#9x4vkw48"); }); // invalid checksum
}
[Fact]
public void ParseTradeQuantity()
{
Assert.Throws<FormatException>(() => TradeQuantity.Parse("1.2345o"));
Assert.Throws<FormatException>(() => TradeQuantity.Parse("o"));
Assert.Throws<FormatException>(() => TradeQuantity.Parse(""));
Assert.Throws<FormatException>(() => TradeQuantity.Parse("1.353%%"));
Assert.Throws<FormatException>(() => TradeQuantity.Parse("1.353 %%"));
Assert.Throws<FormatException>(() => TradeQuantity.Parse("-1.353%"));
Assert.Throws<FormatException>(() => TradeQuantity.Parse("-1.353"));
var qty = TradeQuantity.Parse("1.3%");
Assert.Equal(1.3m, qty.Value);
Assert.Equal(TradeQuantity.ValueType.Percent, qty.Type);
var qty2 = TradeQuantity.Parse("1.3");
Assert.Equal(1.3m, qty2.Value);
Assert.Equal(TradeQuantity.ValueType.Exact, qty2.Type);
Assert.NotEqual(qty, qty2);
Assert.Equal(qty, TradeQuantity.Parse("1.3%"));
Assert.Equal(qty2, TradeQuantity.Parse("1.3"));
Assert.Equal(TradeQuantity.Parse(qty.ToString()), TradeQuantity.Parse("1.3%"));
Assert.Equal(TradeQuantity.Parse(qty2.ToString()), TradeQuantity.Parse("1.3"));
Assert.Equal(TradeQuantity.Parse(qty2.ToString()), TradeQuantity.Parse(" 1.3 "));
}
[Fact]
public void ParseDerivationSchemeSettings()
{
var testnet = new BTCPayNetworkProvider(ChainName.Testnet).GetNetwork<BTCPayNetwork>("BTC");
var mainnet = new BTCPayNetworkProvider(ChainName.Mainnet).GetNetwork<BTCPayNetwork>("BTC");
var root = new Mnemonic(
"usage fever hen zero slide mammal silent heavy donate budget pulse say brain thank sausage brand craft about save attract muffin advance illegal cabbage")
.DeriveExtKey();
// xpub
var tpub = "tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS";
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(tpub, testnet, out var settings, out var error));
Assert.Null(error);
Assert.True(settings.AccountDerivation is DirectDerivationStrategy { Segwit: false });
Assert.Equal($"{tpub}-[legacy]", ((DirectDerivationStrategy)settings.AccountDerivation).ToString());
// xpub with fingerprint and account
tpub = "tpubDCXK98mNrPWuoWweaoUkqwxQF5NMWpQLy7n7XJgDCpwYfoZRXGafPaVM7mYqD7UKhsbMxkN864JY2PniMkt1Uk4dNuAMnWFVqdquyvZNyca";
var vpub = "vpub5YVA1ZbrqkUVq8NZTtvRDrS2a1yoeBvHbG9NbxqJ6uRtpKGFwjQT11WEqKYsgoDF6gpqrDf8ddmPZe4yXWCjzqF8ad2Cw9xHiE8DSi3X3ik";
var fingerprint = "e5746fd9";
var account = "84'/1'/0'";
var str = $"[{fingerprint}/{account}]{vpub}";
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(str, testnet, out settings, out error));
Assert.Null(error);
Assert.True(settings.AccountDerivation is DirectDerivationStrategy { Segwit: true });
Assert.Equal(vpub, settings.AccountOriginal);
Assert.Equal(tpub, ((DirectDerivationStrategy)settings.AccountDerivation).ToString());
Assert.Equal(HDFingerprint.TryParse(fingerprint, out var hd) ? hd : default, settings.AccountKeySettings[0].RootFingerprint);
Assert.Equal(account, settings.AccountKeySettings[0].AccountKeyPath.ToString());
// ColdCard
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
"{\"keystore\": {\"ckcc_xpub\": \"xpub661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw\", \"xpub\": \"ypub6WWc2gWwHbdnAAyJDnR4SPL1phRh7REqrPBfZeizaQ1EmTshieRXJC3Z5YoU4wkcdKHEjQGkh6AYEzCQC1Kz3DNaWSwdc1pc8416hAjzqyD\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/0'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}",
mainnet, out var settings, out var error));
mainnet, out settings, out error));
Assert.Null(error);
Assert.Equal(root.GetPublicKey().GetHDFingerPrint(), settings.AccountKeySettings[0].RootFingerprint);
Assert.Equal(settings.AccountKeySettings[0].RootFingerprint,
HDFingerprint.TryParse("8bafd160", out var hd) ? hd : default);
HDFingerprint.TryParse("8bafd160", out hd) ? hd : default);
Assert.Equal("Coldcard Import 0x60d1af8b", settings.Label);
Assert.Equal("49'/0'/0'", settings.AccountKeySettings[0].AccountKeyPath.ToString());
Assert.Equal(
@ -729,28 +803,26 @@ namespace BTCPayServer.Tests
settings.AccountOriginal);
Assert.Equal(root.Derive(new KeyPath("m/49'/0'/0'")).Neuter().PubKey.WitHash.ScriptPubKey.Hash.ScriptPubKey,
settings.AccountDerivation.GetDerivation().ScriptPubKey);
var testnet = new BTCPayNetworkProvider(ChainName.Testnet).GetNetwork<BTCPayNetwork>("BTC");
// Should be legacy
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
"{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"tpubDDWYqT3P24znfsaGX7kZcQhNc5LAjnQiKQvUCHF2jS6dsgJBRtymopEU5uGpMaR5YChjuiExZG1X2aTbqXkp82KqH5qnqwWHp6EWis9ZvKr\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/44'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}",
testnet, out settings, out error));
Assert.True(settings.AccountDerivation is DirectDerivationStrategy s && !s.Segwit);
Assert.True(settings.AccountDerivation is DirectDerivationStrategy { Segwit: false });
Assert.Null(error);
// Should be segwit p2sh
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
"{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"upub5DSddA9NoRUyJrQ4p86nsCiTSY7kLHrSxx3joEJXjHd4HPARhdXUATuk585FdWPVC2GdjsMePHb6BMDmf7c6KG4K4RPX6LVqBLtDcWpQJmh\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}",
testnet, out settings, out error));
Assert.True(settings.AccountDerivation is P2SHDerivationStrategy p &&
p.Inner is DirectDerivationStrategy s2 && s2.Segwit);
Assert.True(settings.AccountDerivation is P2SHDerivationStrategy { Inner: DirectDerivationStrategy { Segwit: true } });
Assert.Null(error);
// Should be segwit
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
"{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"vpub5YjYxTemJ39tFRnuAhwduyxG2tKGjoEpmvqVQRPqdYrqa6YGoeSzBtHXaJUYB19zDbXs3JjbEcVWERjQBPf9bEfUUMZNMv1QnMyHV8JPqyf\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/84'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}",
testnet, out settings, out error));
Assert.True(settings.AccountDerivation is DirectDerivationStrategy s3 && s3.Segwit);
Assert.True(settings.AccountDerivation is DirectDerivationStrategy { Segwit: true });
Assert.Null(error);
// Specter
@ -1466,14 +1538,14 @@ namespace BTCPayServer.Tests
Assert.Equal(1m / 0.000061m, rule2.BidAsk.Bid);
// testing rounding
rule2 = rules.GetRuleFor(CurrencyPair.Parse("Sats_EUR"));
rule2 = rules.GetRuleFor(CurrencyPair.Parse("SATS_EUR"));
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_EUR"), new BidAsk(1.23m, 2.34m));
Assert.True(rule2.Reevaluate());
Assert.Equal("0.00000001 * (1.23, 2.34)", rule2.ToString(true));
Assert.Equal(0.0000000234m, rule2.BidAsk.Ask);
Assert.Equal(0.0000000123m, rule2.BidAsk.Bid);
rule2 = rules.GetRuleFor(CurrencyPair.Parse("EUR_Sats"));
rule2 = rules.GetRuleFor(CurrencyPair.Parse("EUR_SATS"));
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_EUR"), new BidAsk(1.23m, 2.34m));
Assert.True(rule2.Reevaluate());
Assert.Equal("1 / (0.00000001 * (1.23, 2.34))", rule2.ToString(true));
@ -1715,7 +1787,7 @@ namespace BTCPayServer.Tests
var networkProvider = new BTCPayNetworkProvider(ChainName.Regtest);
var paymentMethodHandlerDictionary = new PaymentMethodHandlerDictionary(new IPaymentMethodHandler[]
{
new BitcoinLikePaymentHandler(null, networkProvider, null, null, null),
new BitcoinLikePaymentHandler(null, networkProvider, null, null, null, null),
new LightningLikePaymentHandler(null, null, networkProvider, null, null, null),
});
var networkBTC = networkProvider.GetNetwork("BTC");

@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using BTCPayServer.Abstractions.Form;
using BTCPayServer.Forms;
@ -42,9 +40,10 @@ public class FormTests : UnitTestBase
}
}
};
var service = new FormDataService(null, null);
var providers = new FormComponentProviders(new List<IFormComponentProvider>());
var service = new FormDataService(null, providers);
Assert.False(service.IsFormSchemaValid(form.ToString(), out _, out _));
form = new Form()
form = new Form
{
Fields = new List<Field>
{
@ -117,7 +116,7 @@ public class FormTests : UnitTestBase
break;
}
}
form = new Form()
{
Fields = new List<Field>
@ -144,7 +143,7 @@ public class FormTests : UnitTestBase
{"invoice_item3", new StringValues("updated")},
{"invoice_test", new StringValues("updated")}
}));
foreach (var f in form.GetAllFields())
{
var field = f.Field;
@ -161,12 +160,12 @@ public class FormTests : UnitTestBase
}
}
var obj = form.GetValues();
var obj = service.GetValues(form);
Assert.Equal("original", obj["invoice"]["test"].Value<string>());
Assert.Equal("updated", obj["invoice_item3"].Value<string>());
Clear(form);
form.SetValues(obj);
obj = form.GetValues();
obj = service.GetValues(form);
Assert.Equal("original", obj["invoice"]["test"].Value<string>());
Assert.Equal("updated", obj["invoice_item3"].Value<string>());
@ -184,10 +183,10 @@ public class FormTests : UnitTestBase
}
};
form.SetValues(obj);
obj = form.GetValues();
obj = service.GetValues(form);
Assert.Null(obj["test"].Value<string>());
form.SetValues(new JObject{ ["test"] = "hello" });
obj = form.GetValues();
form.SetValues(new JObject { ["test"] = "hello" });
obj = service.GetValues(form);
Assert.Equal("hello", obj["test"].Value<string>());
}

@ -15,12 +15,15 @@ using BTCPayServer.Lightning;
using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.PayoutProcessors;
using BTCPayServer.PayoutProcessors.OnChain;
using BTCPayServer.Services;
using BTCPayServer.Services.Custodian.Client.MockCustodian;
using BTCPayServer.Services.Notifications;
using BTCPayServer.Services.Notifications.Blobs;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Hosting;
using NBitcoin;
using NBitpayClient;
using Newtonsoft.Json;
@ -302,7 +305,8 @@ namespace BTCPayServer.Tests
{
await client.GetApp("some random ID lol");
});
await AssertHttpError(404, async () => {
await AssertHttpError(404, async () =>
{
await client.GetPosApp("some random ID lol");
});
@ -451,7 +455,7 @@ namespace BTCPayServer.Tests
// Test creating a crowdfund app
var app = await client.CreateCrowdfundApp(
user.StoreId,
new CreateCrowdfundAppRequest()
new CreateCrowdfundAppRequest()
{
AppName = "test app from API",
Title = "test app title"
@ -462,10 +466,12 @@ namespace BTCPayServer.Tests
Assert.Equal("Crowdfund", app.AppType);
// Make sure we return a 404 if we try to get an app that doesn't exist
await AssertHttpError(404, async () => {
await AssertHttpError(404, async () =>
{
await client.GetApp("some random ID lol");
});
await AssertHttpError(404, async () => {
await AssertHttpError(404, async () =>
{
await client.GetCrowdfundApp("some random ID lol");
});
@ -488,7 +494,8 @@ namespace BTCPayServer.Tests
// Test deleting the newly created app
await client.DeleteApp(retrievedApp.Id);
await AssertHttpError(404, async () => {
await AssertHttpError(404, async () =>
{
await client.GetApp(retrievedApp.Id);
});
}
@ -512,8 +519,8 @@ namespace BTCPayServer.Tests
}
);
var crowdfundApp = await client.CreateCrowdfundApp(user.StoreId, new CreateCrowdfundAppRequest() { AppName = "test app from API" });
// Create another store and one app on it so we can get all apps from all stores for the user below
// Create another store and one app on it so we can get all apps from all stores for the user below
var newStore = await client.CreateStore(new CreateStoreRequest() { Name = "A" });
var newApp = await client.CreateCrowdfundApp(newStore.Id, new CreateCrowdfundAppRequest() { AppName = "new app" });
@ -544,7 +551,7 @@ namespace BTCPayServer.Tests
Assert.Equal(crowdfundApp.Name, apps[1].Name);
Assert.Equal(crowdfundApp.StoreId, apps[1].StoreId);
Assert.Equal(crowdfundApp.AppType, apps[1].AppType);
Assert.Equal(newApp.Name, apps[2].Name);
Assert.Equal(newApp.StoreId, apps[2].StoreId);
Assert.Equal(newApp.AppType, apps[2].AppType);
@ -1066,7 +1073,7 @@ namespace BTCPayServer.Tests
var lnrURLs = await unauthenticated.GetPullPaymentLNURL(test4.Id);
Assert.IsType<string>(lnrURLs.LNURLBech32);
Assert.IsType<string>(lnrURLs.LNURLUri);
//permission test around auto approved pps and payouts
var nonApproved = await acc.CreateClient(Policies.CanCreateNonApprovedPullPayments);
var approved = await acc.CreateClient(Policies.CanCreatePullPayments);
@ -1091,7 +1098,7 @@ namespace BTCPayServer.Tests
Destination = new Key().GetAddress(ScriptPubKeyType.TaprootBIP86, Network.RegTest).ToString()
});
});
var pullPayment = await approved.CreatePullPayment(acc.StoreId, new CreatePullPaymentRequest()
{
Amount = 100,
@ -1100,7 +1107,7 @@ namespace BTCPayServer.Tests
PaymentMethods = new[] { "BTC" },
AutoApproveClaims = true
});
var p = await approved.CreatePayout(acc.StoreId, new CreatePayoutThroughStoreRequest()
{
Amount = 100,
@ -1256,7 +1263,10 @@ namespace BTCPayServer.Tests
//update store
Assert.Empty(newStore.PaymentMethodCriteria);
await client.GenerateOnChainWallet(newStore.Id, "BTC", new GenerateOnChainWalletRequest());
var updatedStore = await client.UpdateStore(newStore.Id, new UpdateStoreRequest() { Name = "B", PaymentMethodCriteria = new List<PaymentMethodCriteriaData>()
var updatedStore = await client.UpdateStore(newStore.Id, new UpdateStoreRequest()
{
Name = "B",
PaymentMethodCriteria = new List<PaymentMethodCriteriaData>()
{
new()
{
@ -1265,7 +1275,8 @@ namespace BTCPayServer.Tests
PaymentMethod = "BTC",
CurrencyCode = "USD"
}
}});
}
});
Assert.Equal("B", updatedStore.Name);
var s = (await client.GetStore(newStore.Id));
Assert.Equal("B", s.Name);
@ -1275,9 +1286,9 @@ namespace BTCPayServer.Tests
Assert.True(pmc.Above);
Assert.Equal("BTC", pmc.PaymentMethod);
Assert.Equal("USD", pmc.CurrencyCode);
updatedStore = await client.UpdateStore(newStore.Id, new UpdateStoreRequest() { Name = "B"});
updatedStore = await client.UpdateStore(newStore.Id, new UpdateStoreRequest() { Name = "B" });
Assert.Empty(newStore.PaymentMethodCriteria);
//list stores
var stores = await client.GetStores();
var storeIds = stores.Select(data => data.Id);
@ -1305,15 +1316,21 @@ namespace BTCPayServer.Tests
await user.CreateClient(Permission.Create(Policies.CanViewStoreSettings, user.StoreId).ToString());
Assert.Single(await scopedClient.GetStores());
var noauth = await user.CreateClient(Array.Empty<string>());
await AssertAPIError("missing-permission", () => noauth.GetStores());
// We strip the user's Owner right, so the key should not work
using var ctx = tester.PayTester.GetService<Data.ApplicationDbContextFactory>().CreateContext();
var storeEntity = await ctx.UserStore.SingleAsync(u => u.ApplicationUserId == user.UserId && u.StoreDataId == newStore.Id);
storeEntity.Role = "Guest";
await ctx.SaveChangesAsync();
await AssertHttpError(403, async () => await client.UpdateStore(newStore.Id, new UpdateStoreRequest() { Name = "B" }));
client = await user.CreateClient(Policies.Unrestricted);
stores = await client.GetStores();
foreach (var s2 in stores)
{
await tester.PayTester.StoreRepository.DeleteStore(s2.Id);
}
tester.DeleteStore = false;
Assert.Empty(await client.GetStores());
}
private async Task<GreenfieldValidationException> AssertValidationError(string[] fields, Func<Task> act)
@ -1460,6 +1477,7 @@ namespace BTCPayServer.Tests
var newDeliveryId = await clientProfile.RedeliverWebhook(user.StoreId, hook.Id, delivery.Id);
req = await fakeServer.GetNextRequest();
req.Response.StatusCode = 404;
Assert.StartsWith("BTCPayServer", Assert.Single(req.Request.Headers.UserAgent));
await TestUtils.EventuallyAsync(async () =>
{
// Releasing semaphore several times may help making this test less flaky
@ -2325,10 +2343,10 @@ namespace BTCPayServer.Tests
Assert.NotNull(merchantInvoice.Id);
Assert.NotNull(merchantInvoice.PaymentHash);
Assert.Equal(merchantInvoice.Id, merchantInvoice.PaymentHash);
// The default client is using charge, so we should not be able to query channels
var chargeClient = await user.CreateClient(Policies.CanUseInternalLightningNode);
var info = await chargeClient.GetLightningNodeInfo("BTC");
Assert.Single(info.NodeURIs);
Assert.NotEqual(0, info.BlockHeight);
@ -2397,7 +2415,7 @@ namespace BTCPayServer.Tests
Assert.NotNull(payResponse.FeeAmount);
Assert.NotNull(payResponse.TotalAmount);
Assert.NotNull(payResponse.PaymentHash);
// check the get invoice response
var merchInvoice = await merchantClient.GetLightningInvoice(merchant.StoreId, "BTC", merchantInvoice.Id);
Assert.NotNull(merchInvoice);
@ -2436,7 +2454,7 @@ namespace BTCPayServer.Tests
// Amount received might be bigger because of internal implementation shit from lightning
Assert.True(LightMoney.Satoshis(1000) <= invoice.AmountReceived);
// check payments list for store node
var payments = await client.GetLightningPayments(user.StoreId, "BTC");
Assert.NotEmpty(payments);
@ -2482,7 +2500,7 @@ namespace BTCPayServer.Tests
var user = tester.NewAccount();
await user.GrantAccessAsync(true);
user.RegisterLightningNode("BTC", LightningConnectionType.CLightning);
var client = await user.CreateClient(Policies.Unrestricted);
var invoice = await client.CreateInvoice(user.StoreId,
new CreateInvoiceRequest
@ -2497,12 +2515,12 @@ namespace BTCPayServer.Tests
});
var pm = Assert.Single(await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id));
Assert.False(pm.AdditionalData.HasValues);
var resp = await tester.CustomerLightningD.Pay(pm.Destination);
Assert.Equal(PayResult.Ok, resp.Result);
Assert.NotNull(resp.Details.PaymentHash);
Assert.NotNull(resp.Details.Preimage);
pm = Assert.Single(await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id));
Assert.True(pm.AdditionalData.HasValues);
Assert.Equal(resp.Details.PaymentHash.ToString(), pm.AdditionalData.GetValue("paymentHash"));
@ -3187,7 +3205,7 @@ namespace BTCPayServer.Tests
}
[Fact(Timeout =TestTimeout)]
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task StoreLightningAddressesAPITests()
{
@ -3199,14 +3217,14 @@ namespace BTCPayServer.Tests
var store = await adminClient.GetStore(admin.StoreId);
Assert.Empty(await adminClient.GetStorePaymentMethods(store.Id));
var store2 = (await adminClient.CreateStore(new CreateStoreRequest() {Name = "test2"})).Id;
var store2 = (await adminClient.CreateStore(new CreateStoreRequest() { Name = "test2" })).Id;
var address1 = Guid.NewGuid().ToString("n").Substring(0, 8);
var address2 = Guid.NewGuid().ToString("n").Substring(0, 8);
Assert.Empty(await adminClient.GetStoreLightningAddresses(store.Id));
Assert.Empty(await adminClient.GetStoreLightningAddresses(store2));
Assert.Empty(await adminClient.GetStoreLightningAddresses(store.Id));
Assert.Empty(await adminClient.GetStoreLightningAddresses(store2));
await adminClient.AddOrUpdateStoreLightningAddress(store.Id, address1, new LightningAddressData());
await adminClient.AddOrUpdateStoreLightningAddress(store.Id, address1, new LightningAddressData()
{
Max = 1
@ -3215,8 +3233,8 @@ namespace BTCPayServer.Tests
{
await adminClient.AddOrUpdateStoreLightningAddress(store2, address1, new LightningAddressData());
});
Assert.Equal(1,Assert.Single(await adminClient.GetStoreLightningAddresses(store.Id)).Max);
Assert.Empty(await adminClient.GetStoreLightningAddresses(store2));
Assert.Equal(1, Assert.Single(await adminClient.GetStoreLightningAddresses(store.Id)).Max);
Assert.Empty(await adminClient.GetStoreLightningAddresses(store2));
await adminClient.AddOrUpdateStoreLightningAddress(store2, address2, new LightningAddressData());
@ -3227,8 +3245,8 @@ namespace BTCPayServer.Tests
await adminClient.RemoveStoreLightningAddress(store2, address1);
});
await adminClient.RemoveStoreLightningAddress(store2, address2);
Assert.Empty(await adminClient.GetStoreLightningAddresses(store2));
Assert.Empty(await adminClient.GetStoreLightningAddresses(store2));
}
[Fact(Timeout = 60 * 2 * 1000)]
@ -3394,8 +3412,8 @@ namespace BTCPayServer.Tests
});
Assert.Empty(await adminClient.GetStoreLightningAutomatedPayoutProcessors(admin.StoreId, "BTC_LightningNetwork"));
await adminClient.UpdateStoreLightningAutomatedPayoutProcessors(admin.StoreId, "BTC_LightningNetwork",
new LightningAutomatedPayoutSettings() { IntervalSeconds = TimeSpan.FromSeconds(2) });
Assert.Equal(2, Assert.Single(await adminClient.GetStoreLightningAutomatedPayoutProcessors(admin.StoreId, "BTC_LightningNetwork")).IntervalSeconds.TotalSeconds);
new LightningAutomatedPayoutSettings() { IntervalSeconds = TimeSpan.FromSeconds(600) });
Assert.Equal(600, Assert.Single(await adminClient.GetStoreLightningAutomatedPayoutProcessors(admin.StoreId, "BTC_LightningNetwork")).IntervalSeconds.TotalSeconds);
await TestUtils.EventuallyAsync(async () =>
{
var payoutC =
@ -3480,8 +3498,8 @@ namespace BTCPayServer.Tests
Assert.Empty(await adminClient.GetPayoutProcessors(admin.StoreId));
await adminClient.UpdateStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC",
new OnChainAutomatedPayoutSettings() { IntervalSeconds = TimeSpan.FromSeconds(100000) });
Assert.Equal(100000, Assert.Single(await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC")).IntervalSeconds.TotalSeconds);
new OnChainAutomatedPayoutSettings() { IntervalSeconds = TimeSpan.FromSeconds(3600) });
Assert.Equal(3600, Assert.Single(await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC")).IntervalSeconds.TotalSeconds);
var tpGen = Assert.Single(await adminClient.GetPayoutProcessors(admin.StoreId));
Assert.Equal("BTC", Assert.Single(tpGen.PaymentMethods));
@ -3494,8 +3512,10 @@ namespace BTCPayServer.Tests
Assert.Empty(await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC"));
Assert.Empty(await adminClient.GetPayoutProcessors(admin.StoreId));
// Send just enough money to cover the smallest of the payouts.
var fee = (await tester.PayTester.GetService<IFeeProviderFactory>().CreateFeeProvider(tester.DefaultNetwork).GetFeeRateAsync(100)).GetFee(150);
await tester.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create((await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
tester.ExplorerClient.Network.NBitcoinNetwork), Money.Coins(0.000012m));
tester.ExplorerClient.Network.NBitcoinNetwork), Money.Coins(0.00001m) + fee);
await tester.ExplorerNode.GenerateAsync(1);
await TestUtils.EventuallyAsync(async () =>
{
@ -3505,8 +3525,9 @@ namespace BTCPayServer.Tests
Assert.Equal(3, payouts.Length);
});
await adminClient.UpdateStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC",
new OnChainAutomatedPayoutSettings() { IntervalSeconds = TimeSpan.FromSeconds(5) });
Assert.Equal(5, Assert.Single(await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC")).IntervalSeconds.TotalSeconds);
new OnChainAutomatedPayoutSettings() { IntervalSeconds = TimeSpan.FromSeconds(600), FeeBlockTarget = 1000 });
Assert.Equal(600, Assert.Single(await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC")).IntervalSeconds.TotalSeconds);
await TestUtils.EventuallyAsync(async () =>
{
Assert.Equal(2, (await adminClient.ShowOnChainWalletTransactions(admin.StoreId, "BTC")).Count());
@ -3514,8 +3535,10 @@ namespace BTCPayServer.Tests
Assert.Single(payouts.Where(data => data.State == PayoutState.InProgress));
});
await tester.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create((await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
tester.ExplorerClient.Network.NBitcoinNetwork), Money.Coins(0.01m));
var txid = await tester.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create((await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
tester.ExplorerClient.Network.NBitcoinNetwork), Money.Coins(0.01m) + fee);
await tester.WaitForEvent<NewOnChainTransactionEvent>(null, correctEvent: ev => ev.NewTransactionEvent.TransactionData.TransactionHash == txid);
await tester.PayTester.GetService<PayoutProcessorService>().Restart(new PayoutProcessorService.PayoutProcessorQuery(admin.StoreId, "BTC"));
await TestUtils.EventuallyAsync(async () =>
{
Assert.Equal(4, (await adminClient.ShowOnChainWalletTransactions(admin.StoreId, "BTC")).Count());
@ -3700,7 +3723,7 @@ namespace BTCPayServer.Tests
Assert.Equal(0.9m,
Assert.Single(await clientBasic.GetStoreRates(user.StoreId, new[] { "BTC_XYZ" })).Rate);
config = await clientBasic.GetStoreRateConfiguration(user.StoreId);
Assert.NotNull(config);
Assert.NotNull(config.EffectiveScript);
@ -3935,7 +3958,6 @@ clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId, new StoreRateConfi
var depositClient = await admin.CreateClient(Policies.CanDepositToCustodianAccounts);
var tradeClient = await admin.CreateClient(Policies.CanTradeCustodianAccount);
var store = await adminClient.GetStore(admin.StoreId);
var storeId = store.Id;
@ -3975,28 +3997,28 @@ clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId, new StoreRateConfi
// Test: GetDepositAddress, unauth
await AssertHttpError(401, async () => await unauthClient.GetDepositAddress(storeId, accountId, MockCustodian.DepositPaymentMethod));
await AssertHttpError(401, async () => await unauthClient.GetCustodianAccountDepositAddress(storeId, accountId, MockCustodian.DepositPaymentMethod));
// Test: GetDepositAddress, auth, but wrong permission
await AssertHttpError(403, async () => await managerClient.GetDepositAddress(storeId, accountId, MockCustodian.DepositPaymentMethod));
await AssertHttpError(403, async () => await managerClient.GetCustodianAccountDepositAddress(storeId, accountId, MockCustodian.DepositPaymentMethod));
// Test: GetDepositAddress, wrong payment method
await AssertHttpError(400, async () => await depositClient.GetDepositAddress(storeId, accountId, "WRONG-PaymentMethod"));
await AssertApiError(400, "unsupported-payment-method", async () => await depositClient.GetCustodianAccountDepositAddress(storeId, accountId, "WRONG-PaymentMethod"));
// Test: GetDepositAddress, wrong store ID
await AssertHttpError(403, async () => await depositClient.GetDepositAddress("WRONG-STORE", accountId, MockCustodian.DepositPaymentMethod));
await AssertHttpError(403, async () => await depositClient.GetCustodianAccountDepositAddress("WRONG-STORE", accountId, MockCustodian.DepositPaymentMethod));
// Test: GetDepositAddress, wrong account ID
await AssertHttpError(404, async () => await depositClient.GetDepositAddress(storeId, "WRONG-ACCOUNT-ID", MockCustodian.DepositPaymentMethod));
await AssertHttpError(404, async () => await depositClient.GetCustodianAccountDepositAddress(storeId, "WRONG-ACCOUNT-ID", MockCustodian.DepositPaymentMethod));
// Test: GetDepositAddress, correct payment method
var depositAddress = await depositClient.GetDepositAddress(storeId, accountId, MockCustodian.DepositPaymentMethod);
var depositAddress = await depositClient.GetCustodianAccountDepositAddress(storeId, accountId, MockCustodian.DepositPaymentMethod);
Assert.NotNull(depositAddress);
Assert.Equal(MockCustodian.DepositAddress, depositAddress.Address);
// Test: Trade, unauth
var tradeRequest = new TradeRequestData { FromAsset = MockCustodian.TradeFromAsset, ToAsset = MockCustodian.TradeToAsset, Qty = MockCustodian.TradeQtyBought.ToString(CultureInfo.InvariantCulture) };
var tradeRequest = new TradeRequestData { FromAsset = MockCustodian.TradeFromAsset, ToAsset = MockCustodian.TradeToAsset, Qty = new TradeQuantity(MockCustodian.TradeQtyBought, TradeQuantity.ValueType.Exact) };
await AssertHttpError(401, async () => await unauthClient.MarketTradeCustodianAccountAsset(storeId, accountId, tradeRequest));
// Test: Trade, auth, but wrong permission
@ -4023,17 +4045,13 @@ clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId, new StoreRateConfi
Assert.Equal(LedgerEntryData.LedgerEntryType.Fee, newTradeResult.LedgerEntries[2].Type);
// Test: GetTradeQuote, SATS
var satsTradeRequest = new TradeRequestData { FromAsset = MockCustodian.TradeFromAsset, ToAsset = "SATS", Qty = MockCustodian.TradeQtyBought.ToString(CultureInfo.InvariantCulture) };
var satsTradeRequest = new TradeRequestData { FromAsset = MockCustodian.TradeFromAsset, ToAsset = "SATS", Qty = new TradeQuantity(MockCustodian.TradeQtyBought, TradeQuantity.ValueType.Exact) };
await AssertApiError(400, "use-asset-synonym", async () => await tradeClient.MarketTradeCustodianAccountAsset(storeId, accountId, satsTradeRequest));
// TODO Test: Trade with percentage qty
// Test: Trade with wrong decimal format (example: JavaScript scientific format)
var wrongQtyTradeRequest = new TradeRequestData { FromAsset = MockCustodian.TradeFromAsset, ToAsset = MockCustodian.TradeToAsset, Qty = "6.1e-7" };
await AssertApiError(400, "bad-qty-format", async () => await tradeClient.MarketTradeCustodianAccountAsset(storeId, accountId, wrongQtyTradeRequest));
// Test: Trade, wrong assets method
var wrongAssetsTradeRequest = new TradeRequestData { FromAsset = "WRONG", ToAsset = MockCustodian.TradeToAsset, Qty = MockCustodian.TradeQtyBought.ToString(CultureInfo.InvariantCulture) };
var wrongAssetsTradeRequest = new TradeRequestData { FromAsset = "WRONG", ToAsset = MockCustodian.TradeToAsset, Qty = new TradeQuantity(MockCustodian.TradeQtyBought, TradeQuantity.ValueType.Exact) };
await AssertHttpError(WrongTradingPairException.HttpCode, async () => await tradeClient.MarketTradeCustodianAccountAsset(storeId, accountId, wrongAssetsTradeRequest));
// Test: wrong account ID
@ -4043,18 +4061,18 @@ clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId, new StoreRateConfi
await AssertHttpError(403, async () => await tradeClient.MarketTradeCustodianAccountAsset("WRONG-STORE-ID", accountId, tradeRequest));
// Test: Trade, correct assets, wrong amount
var insufficientFundsTradeRequest = new TradeRequestData { FromAsset = MockCustodian.TradeFromAsset, ToAsset = MockCustodian.TradeToAsset, Qty = "0.01" };
var insufficientFundsTradeRequest = new TradeRequestData { FromAsset = MockCustodian.TradeFromAsset, ToAsset = MockCustodian.TradeToAsset, Qty = new TradeQuantity(0.01m, TradeQuantity.ValueType.Exact) };
await AssertApiError(400, "insufficient-funds", async () => await tradeClient.MarketTradeCustodianAccountAsset(storeId, accountId, insufficientFundsTradeRequest));
// Test: GetTradeQuote, unauth
await AssertHttpError(401, async () => await unauthClient.GetTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset));
await AssertHttpError(401, async () => await unauthClient.GetCustodianAccountTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset));
// Test: GetTradeQuote, auth, but wrong permission
await AssertHttpError(403, async () => await managerClient.GetTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset));
await AssertHttpError(403, async () => await managerClient.GetCustodianAccountTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset));
// Test: GetTradeQuote, auth, correct permission
var tradeQuote = await tradeClient.GetTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset);
var tradeQuote = await tradeClient.GetCustodianAccountTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset);
Assert.NotNull(tradeQuote);
Assert.Equal(MockCustodian.TradeFromAsset, tradeQuote.FromAsset);
Assert.Equal(MockCustodian.TradeToAsset, tradeQuote.ToAsset);
@ -4062,30 +4080,30 @@ clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId, new StoreRateConfi
Assert.Equal(MockCustodian.BtcPriceInEuro, tradeQuote.Ask);
// Test: GetTradeQuote, SATS
await AssertApiError(400, "use-asset-synonym", async () => await tradeClient.GetTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset, "SATS"));
await AssertApiError(400, "use-asset-synonym", async () => await tradeClient.GetCustodianAccountTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset, "SATS"));
// Test: GetTradeQuote, wrong asset
await AssertHttpError(404, async () => await tradeClient.GetTradeQuote(storeId, accountId, "WRONG-ASSET", MockCustodian.TradeToAsset));
await AssertHttpError(404, async () => await tradeClient.GetTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset, "WRONG-ASSET"));
await AssertHttpError(404, async () => await tradeClient.GetCustodianAccountTradeQuote(storeId, accountId, "WRONG-ASSET", MockCustodian.TradeToAsset));
await AssertHttpError(404, async () => await tradeClient.GetCustodianAccountTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset, "WRONG-ASSET"));
// Test: wrong account ID
await AssertHttpError(404, async () => await tradeClient.GetTradeQuote(storeId, "WRONG-ACCOUNT-ID", MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset));
await AssertHttpError(404, async () => await tradeClient.GetCustodianAccountTradeQuote(storeId, "WRONG-ACCOUNT-ID", MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset));
// Test: wrong store ID
await AssertHttpError(403, async () => await tradeClient.GetTradeQuote("WRONG-STORE-ID", accountId, MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset));
await AssertHttpError(403, async () => await tradeClient.GetCustodianAccountTradeQuote("WRONG-STORE-ID", accountId, MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset));
// Test: GetTradeInfo, unauth
await AssertHttpError(401, async () => await unauthClient.GetTradeInfo(storeId, accountId, MockCustodian.TradeId));
await AssertHttpError(401, async () => await unauthClient.GetCustodianAccountTradeInfo(storeId, accountId, MockCustodian.TradeId));
// Test: GetTradeInfo, auth, but wrong permission
await AssertHttpError(403, async () => await managerClient.GetTradeInfo(storeId, accountId, MockCustodian.TradeId));
await AssertHttpError(403, async () => await managerClient.GetCustodianAccountTradeInfo(storeId, accountId, MockCustodian.TradeId));
// Test: GetTradeInfo, auth, correct permission
var tradeResult = await tradeClient.GetTradeInfo(storeId, accountId, MockCustodian.TradeId);
var tradeResult = await tradeClient.GetCustodianAccountTradeInfo(storeId, accountId, MockCustodian.TradeId);
Assert.NotNull(tradeResult);
Assert.Equal(accountId, tradeResult.AccountId);
Assert.Equal(mockCustodian.Code, tradeResult.CustodianCode);
@ -4105,66 +4123,93 @@ clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId, new StoreRateConfi
Assert.Equal(LedgerEntryData.LedgerEntryType.Fee, tradeResult.LedgerEntries[2].Type);
// Test: GetTradeInfo, wrong trade ID
await AssertHttpError(404, async () => await tradeClient.GetTradeInfo(storeId, accountId, "WRONG-TRADE-ID"));
await AssertHttpError(404, async () => await tradeClient.GetCustodianAccountTradeInfo(storeId, accountId, "WRONG-TRADE-ID"));
// Test: wrong account ID
await AssertHttpError(404, async () => await tradeClient.GetTradeInfo(storeId, "WRONG-ACCOUNT-ID", MockCustodian.TradeId));
await AssertHttpError(404, async () => await tradeClient.GetCustodianAccountTradeInfo(storeId, "WRONG-ACCOUNT-ID", MockCustodian.TradeId));
// Test: wrong store ID
await AssertHttpError(403, async () => await tradeClient.GetTradeInfo("WRONG-STORE-ID", accountId, MockCustodian.TradeId));
await AssertHttpError(403, async () => await tradeClient.GetCustodianAccountTradeInfo("WRONG-STORE-ID", accountId, MockCustodian.TradeId));
var qty = new TradeQuantity(MockCustodian.WithdrawalAmount, TradeQuantity.ValueType.Exact);
// Test: SimulateWithdrawal, unauth
var simulateWithdrawalRequest = new WithdrawRequestData(MockCustodian.WithdrawalPaymentMethod, qty);
await AssertHttpError(401, async () => await unauthClient.SimulateCustodianAccountWithdrawal(storeId, accountId, simulateWithdrawalRequest));
// Test: SimulateWithdrawal, auth, but wrong permission
await AssertHttpError(403, async () => await managerClient.SimulateCustodianAccountWithdrawal(storeId, accountId, simulateWithdrawalRequest));
// Test: SimulateWithdrawal, correct payment method, correct amount
var simulateWithdrawResponse = await withdrawalClient.SimulateCustodianAccountWithdrawal(storeId, accountId, simulateWithdrawalRequest);
AssertMockWithdrawal(simulateWithdrawResponse, custodianAccountData);
// Test: SimulateWithdrawal, wrong payment method
var wrongPaymentMethodSimulateWithdrawalRequest = new WithdrawRequestData("WRONG-PAYMENT-METHOD", qty);
await AssertApiError(400, "unsupported-payment-method", async () => await withdrawalClient.SimulateCustodianAccountWithdrawal(storeId, accountId, wrongPaymentMethodSimulateWithdrawalRequest));
// Test: SimulateWithdrawal, wrong account ID
await AssertHttpError(404, async () => await withdrawalClient.SimulateCustodianAccountWithdrawal(storeId, "WRONG-ACCOUNT-ID", simulateWithdrawalRequest));
// Test: SimulateWithdrawal, wrong store ID
// TODO it is wierd that 403 is considered normal, but it is like this for all calls where the store is wrong... I'd have preferred a 404 error, because the store cannot be found.
await AssertHttpError(403, async () => await withdrawalClient.SimulateCustodianAccountWithdrawal("WRONG-STORE-ID", accountId, simulateWithdrawalRequest));
// Test: SimulateWithdrawal, correct payment method, wrong amount
var wrongAmountSimulateWithdrawalRequest = new WithdrawRequestData(MockCustodian.WithdrawalPaymentMethod, TradeQuantity.Parse("0.666"));
await AssertHttpError(400, async () => await withdrawalClient.SimulateCustodianAccountWithdrawal(storeId, accountId, wrongAmountSimulateWithdrawalRequest));
// Test: CreateWithdrawal, unauth
var createWithdrawalRequest = new WithdrawRequestData(MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalAmount);
await AssertHttpError(401, async () => await unauthClient.CreateWithdrawal(storeId, accountId, createWithdrawalRequest));
var createWithdrawalRequest = new WithdrawRequestData(MockCustodian.WithdrawalPaymentMethod, qty);
var createWithdrawalRequestPercentage = new WithdrawRequestData(MockCustodian.WithdrawalPaymentMethod, qty);
await AssertHttpError(401, async () => await unauthClient.CreateCustodianAccountWithdrawal(storeId, accountId, createWithdrawalRequest));
// Test: CreateWithdrawal, auth, but wrong permission
await AssertHttpError(403, async () => await managerClient.CreateWithdrawal(storeId, accountId, createWithdrawalRequest));
await AssertHttpError(403, async () => await managerClient.CreateCustodianAccountWithdrawal(storeId, accountId, createWithdrawalRequest));
// Test: CreateWithdrawal, correct payment method, correct amount
var withdrawResponse = await withdrawalClient.CreateWithdrawal(storeId, accountId, createWithdrawalRequest);
var withdrawResponse = await withdrawalClient.CreateCustodianAccountWithdrawal(storeId, accountId, createWithdrawalRequest);
AssertMockWithdrawal(withdrawResponse, custodianAccountData);
// Test: CreateWithdrawal, correct payment method, correct amount, but as a percentage
var withdrawWithPercentageResponse = await withdrawalClient.CreateCustodianAccountWithdrawal(storeId, accountId, createWithdrawalRequestPercentage);
AssertMockWithdrawal(withdrawWithPercentageResponse, custodianAccountData);
// Test: CreateWithdrawal, wrong payment method
var wrongPaymentMethodCreateWithdrawalRequest = new WithdrawRequestData("WRONG-PAYMENT-METHOD", MockCustodian.WithdrawalAmount);
await AssertHttpError(403, async () => await withdrawalClient.CreateWithdrawal(storeId, accountId, wrongPaymentMethodCreateWithdrawalRequest));
var wrongPaymentMethodCreateWithdrawalRequest = new WithdrawRequestData("WRONG-PAYMENT-METHOD", qty);
await AssertApiError(400, "unsupported-payment-method", async () => await withdrawalClient.CreateCustodianAccountWithdrawal(storeId, accountId, wrongPaymentMethodCreateWithdrawalRequest));
// Test: CreateWithdrawal, wrong account ID
await AssertHttpError(404, async () => await withdrawalClient.CreateWithdrawal(storeId, "WRONG-ACCOUNT-ID", createWithdrawalRequest));
await AssertHttpError(404, async () => await withdrawalClient.CreateCustodianAccountWithdrawal(storeId, "WRONG-ACCOUNT-ID", createWithdrawalRequest));
// Test: CreateWithdrawal, wrong store ID
// TODO it is wierd that 403 is considered normal, but it is like this for all calls where the store is wrong... I'd have preferred a 404 error, because the store cannot be found.
await AssertHttpError(403, async () => await withdrawalClient.CreateWithdrawal("WRONG-STORE-ID", accountId, createWithdrawalRequest));
await AssertHttpError(403, async () => await withdrawalClient.CreateCustodianAccountWithdrawal("WRONG-STORE-ID", accountId, createWithdrawalRequest));
// Test: CreateWithdrawal, correct payment method, wrong amount
var wrongAmountCreateWithdrawalRequest = new WithdrawRequestData(MockCustodian.WithdrawalPaymentMethod, new decimal(0.666));
await AssertHttpError(400, async () => await withdrawalClient.CreateWithdrawal(storeId, accountId, wrongAmountCreateWithdrawalRequest));
var wrongAmountCreateWithdrawalRequest = new WithdrawRequestData(MockCustodian.WithdrawalPaymentMethod, TradeQuantity.Parse("0.666"));
await AssertHttpError(400, async () => await withdrawalClient.CreateCustodianAccountWithdrawal(storeId, accountId, wrongAmountCreateWithdrawalRequest));
// Test: GetWithdrawalInfo, unauth
await AssertHttpError(401, async () => await unauthClient.GetWithdrawalInfo(storeId, accountId, MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId));
await AssertHttpError(401, async () => await unauthClient.GetCustodianAccountWithdrawalInfo(storeId, accountId, MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId));
// Test: GetWithdrawalInfo, auth, but wrong permission
await AssertHttpError(403, async () => await managerClient.GetWithdrawalInfo(storeId, accountId, MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId));
await AssertHttpError(403, async () => await managerClient.GetCustodianAccountWithdrawalInfo(storeId, accountId, MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId));
// Test: GetWithdrawalInfo, auth, correct permission
var withdrawalInfo = await withdrawalClient.GetWithdrawalInfo(storeId, accountId, MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId);
var withdrawalInfo = await withdrawalClient.GetCustodianAccountWithdrawalInfo(storeId, accountId, MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId);
AssertMockWithdrawal(withdrawalInfo, custodianAccountData);
// Test: GetWithdrawalInfo, wrong withdrawal ID
await AssertHttpError(404, async () => await withdrawalClient.GetWithdrawalInfo(storeId, accountId, MockCustodian.WithdrawalPaymentMethod, "WRONG-WITHDRAWAL-ID"));
await AssertHttpError(404, async () => await withdrawalClient.GetCustodianAccountWithdrawalInfo(storeId, accountId, MockCustodian.WithdrawalPaymentMethod, "WRONG-WITHDRAWAL-ID"));
// Test: wrong account ID
await AssertHttpError(404, async () => await withdrawalClient.GetWithdrawalInfo(storeId, "WRONG-ACCOUNT-ID", MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId));
await AssertHttpError(404, async () => await withdrawalClient.GetCustodianAccountWithdrawalInfo(storeId, "WRONG-ACCOUNT-ID", MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId));
// Test: wrong store ID
// TODO shouldn't this be 404? I cannot change this without bigger impact, as it would affect all API endpoints that are store centered
await AssertHttpError(403, async () => await withdrawalClient.GetWithdrawalInfo("WRONG-STORE-ID", accountId, MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId));
await AssertHttpError(403, async () => await withdrawalClient.GetCustodianAccountWithdrawalInfo("WRONG-STORE-ID", accountId, MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId));
// TODO assert API error codes, not just status codes by using AssertCustodianApiError()
// TODO also test withdrawals for the various "Status" (Queued, Complete, Failed)
// TODO create a mock custodian with only ICustodian
// TODO create a mock custodian with only ICustodian + ICanWithdraw
@ -4172,12 +4217,11 @@ clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId, new StoreRateConfi
// TODO create a mock custodian with only ICustodian + ICanDeposit
}
private void AssertMockWithdrawal(WithdrawalResponseData withdrawResponse, CustodianAccountData account)
private void AssertMockWithdrawal(WithdrawalBaseResponseData withdrawResponse, CustodianAccountData account)
{
Assert.NotNull(withdrawResponse);
Assert.Equal(MockCustodian.WithdrawalAsset, withdrawResponse.Asset);
Assert.Equal(MockCustodian.WithdrawalPaymentMethod, withdrawResponse.PaymentMethod);
Assert.Equal(MockCustodian.WithdrawalStatus, withdrawResponse.Status);
Assert.Equal(account.Id, withdrawResponse.AccountId);
Assert.Equal(account.CustodianCode, withdrawResponse.CustodianCode);
@ -4191,10 +4235,20 @@ clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId, new StoreRateConfi
Assert.Equal(MockCustodian.WithdrawalFee, withdrawResponse.LedgerEntries[1].Qty);
Assert.Equal(LedgerEntryData.LedgerEntryType.Fee, withdrawResponse.LedgerEntries[1].Type);
Assert.Equal(MockCustodian.WithdrawalTargetAddress, withdrawResponse.TargetAddress);
Assert.Equal(MockCustodian.WithdrawalTransactionId, withdrawResponse.TransactionId);
Assert.Equal(MockCustodian.WithdrawalId, withdrawResponse.WithdrawalId);
Assert.NotEqual(default, withdrawResponse.CreatedTime);
if (withdrawResponse is WithdrawalResponseData withdrawalResponseData)
{
Assert.Equal(MockCustodian.WithdrawalStatus, withdrawalResponseData.Status);
Assert.Equal(MockCustodian.WithdrawalTargetAddress, withdrawalResponseData.TargetAddress);
Assert.Equal(MockCustodian.WithdrawalTransactionId, withdrawalResponseData.TransactionId);
Assert.Equal(MockCustodian.WithdrawalId, withdrawalResponseData.WithdrawalId);
Assert.NotEqual(default, withdrawalResponseData.CreatedTime);
}
if (withdrawResponse is WithdrawalSimulationResponseData withdrawalSimulationResponseData)
{
Assert.Equal(MockCustodian.WithdrawalMinAmount, withdrawalSimulationResponseData.MinQty);
Assert.Equal(MockCustodian.WithdrawalMaxAmount, withdrawalSimulationResponseData.MaxQty);
}
}
}
}

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Custodians;
@ -24,6 +25,9 @@ public class MockCustodian : ICustodian, ICanDeposit, ICanTrade, ICanWithdraw
public const string WithdrawalAsset = "BTC";
public const string WithdrawalId = "WITHDRAWAL-ID-001";
public static readonly decimal WithdrawalAmount = new decimal(0.5);
public static readonly string WithdrawalAmountPercentage = "12.5%";
public static readonly decimal WithdrawalMinAmount = new decimal(0.001);
public static readonly decimal WithdrawalMaxAmount = new decimal(0.6);
public static readonly decimal WithdrawalFee = new decimal(0.0005);
public const string WithdrawalTransactionId = "yyy";
public const string WithdrawalTargetAddress = "bc1qyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy";
@ -52,7 +56,7 @@ public class MockCustodian : ICustodian, ICanDeposit, ICanTrade, ICanWithdraw
return Task.FromResult(r);
}
public Task<Form> GetConfigForm(JObject config, string locale, CancellationToken cancellationToken = default)
public Task<Form> GetConfigForm(JObject config, CancellationToken cancellationToken = default)
{
return null;
}
@ -136,13 +140,37 @@ public class MockCustodian : ICustodian, ICanDeposit, ICanTrade, ICanWithdraw
return r;
}
public Task<WithdrawResult> WithdrawAsync(string paymentMethod, decimal amount, JObject config, CancellationToken cancellationToken)
private SimulateWithdrawalResult CreateWithdrawSimulationResult()
{
var ledgerEntries = new List<LedgerEntryData>();
ledgerEntries.Add(new LedgerEntryData(WithdrawalAsset, WithdrawalAmount - WithdrawalFee, LedgerEntryData.LedgerEntryType.Withdrawal));
ledgerEntries.Add(new LedgerEntryData(WithdrawalAsset, WithdrawalFee, LedgerEntryData.LedgerEntryType.Fee));
var r = new SimulateWithdrawalResult(WithdrawalPaymentMethod, WithdrawalAsset, ledgerEntries, WithdrawalMinAmount, WithdrawalMaxAmount);
return r;
}
public Task<WithdrawResult> WithdrawToStoreWalletAsync(string paymentMethod, decimal amount, JObject config, CancellationToken cancellationToken)
{
if (paymentMethod == WithdrawalPaymentMethod)
{
if (amount.ToString(CultureInfo.InvariantCulture).Equals("" + WithdrawalAmount, StringComparison.InvariantCulture) || WithdrawalAmountPercentage.Equals(amount))
{
return Task.FromResult(CreateWithdrawResult());
}
throw new InsufficientFundsException($"{Name} only supports withdrawals of {WithdrawalAmount} or {WithdrawalAmountPercentage}");
}
throw new CannotWithdrawException(this, paymentMethod, $"Only {WithdrawalPaymentMethod} can be withdrawn from {Name}");
}
public Task<SimulateWithdrawalResult> SimulateWithdrawalAsync(string paymentMethod, decimal amount, JObject config, CancellationToken cancellationToken)
{
if (paymentMethod == WithdrawalPaymentMethod)
{
if (amount == WithdrawalAmount)
{
return Task.FromResult(CreateWithdrawResult());
return Task.FromResult(CreateWithdrawSimulationResult());
}
throw new InsufficientFundsException($"{Name} only supports withdrawals of {WithdrawalAmount}");

@ -2,10 +2,9 @@ using System.Threading.Tasks;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Plugins.PointOfSale;
using BTCPayServer.Plugins.PointOfSale.Controllers;
using BTCPayServer.Plugins.PointOfSale.Models;
using BTCPayServer.Services.Apps;
using BTCPayServer.Tests.Logging;
using Microsoft.AspNetCore.Mvc;
using Xunit;
using Xunit.Abstractions;
@ -32,10 +31,11 @@ namespace BTCPayServer.Tests
var apps = user.GetController<UIAppsController>();
var pos = user.GetController<UIPointOfSaleController>();
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId)).Model);
var appType = AppType.PointOfSale.ToString();
var appType = PointOfSaleAppType.AppType;
vm.AppName = "test";
vm.SelectedAppType = appType;
Assert.IsType<RedirectToActionResult>(apps.CreateApp(user.StoreId, vm).Result);
var redirect = Assert.IsType<RedirectResult>(apps.CreateApp(user.StoreId, vm).Result);
Assert.EndsWith("/settings/pos", redirect.Url);
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
var app = appList.Apps[0];
var appData = new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType };

@ -221,7 +221,7 @@ namespace BTCPayServer.Tests
var receiverCoin = await receiverUser.ReceiveUTXO(Money.Satoshis(810), network);
string errorCode = receiverAddressType == senderAddressType ? null : "unavailable|any UTXO available";
var invoice = receiverUser.BitPay.CreateInvoice(new Invoice() { Price = 50000, Currency = "sats", FullNotifications = true });
var invoice = receiverUser.BitPay.CreateInvoice(new Invoice() { Price = 50000, Currency = "SATS", FullNotifications = true });
if (unsupportedFormats.Contains(receiverAddressType))
{
Assert.Null(TestAccount.GetPayjoinBitcoinUrl(invoice, cashCow.Network));
@ -249,6 +249,7 @@ namespace BTCPayServer.Tests
await s.StartAsync();
s.RegisterNewUser(true);
var receiver = s.CreateNewStore();
s.EnableCheckout(CheckoutType.V1);
var receiverSeed = s.GenerateWallet("BTC", "", true, true, ScriptPubKeyType.Segwit);
var receiverWalletId = new WalletId(receiver.storeId, "BTC");
@ -303,6 +304,7 @@ namespace BTCPayServer.Tests
{
var cryptoCode = "BTC";
var receiver = s.CreateNewStore();
s.EnableCheckout(CheckoutType.V1);
var receiverSeed = s.GenerateWallet(cryptoCode, "", true, true, format);
var receiverWalletId = new WalletId(receiver.storeId, cryptoCode);

@ -1,10 +1,12 @@
using System;
using System.ComponentModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client.Models;
using BTCPayServer.Lightning;
using BTCPayServer.Lightning.CLightning;
using BTCPayServer.Views.Manage;
@ -63,7 +65,6 @@ namespace BTCPayServer.Tests
var containerIp = File.ReadAllText("/etc/hosts").Split('\n', StringSplitOptions.RemoveEmptyEntries).Last()
.Split('\t', StringSplitOptions.RemoveEmptyEntries)[0].Trim();
TestLogs.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
{
@ -75,8 +76,8 @@ namespace BTCPayServer.Tests
Driver = new ChromeDriver(cds, options,
// A bit less than test timeout
TimeSpan.FromSeconds(50));
ServerUri = Server.PayTester.ServerUri;
}
ServerUri = Server.PayTester.ServerUri;
Driver.Manage().Window.Maximize();
TestLogs.LogInformation($"Selenium: Using {Driver.GetType()}");
@ -86,7 +87,7 @@ namespace BTCPayServer.Tests
Driver.AssertNoError();
}
public void PayInvoice(bool mine = false, decimal? amount= null)
public void PayInvoice(bool mine = false, decimal? amount = null)
{
if (amount is not null)
@ -94,6 +95,7 @@ namespace BTCPayServer.Tests
Driver.FindElement(By.Id("test-payment-amount")).Clear();
Driver.FindElement(By.Id("test-payment-amount")).SendKeys(amount.ToString());
}
Driver.WaitUntilAvailable(By.Id("FakePayment"));
Driver.FindElement(By.Id("FakePayment")).Click();
if (mine)
{
@ -193,16 +195,22 @@ namespace BTCPayServer.Tests
StoreId = storeId;
return (name, storeId);
}
public void EnableCheckoutV2(bool bip21 = false)
public void EnableCheckout(CheckoutType checkoutType, bool bip21 = false)
{
GoToStore(StoreNavPages.CheckoutAppearance);
Driver.SetCheckbox(By.Id("UseNewCheckout"), true);
Driver.WaitForElement(By.Id("OnChainWithLnInvoiceFallback"));
Driver.SetCheckbox(By.Id("OnChainWithLnInvoiceFallback"), bip21);
if (checkoutType == CheckoutType.V2)
{
Driver.SetCheckbox(By.Id("UseClassicCheckout"), false);
Driver.WaitForElement(By.Id("OnChainWithLnInvoiceFallback"));
Driver.SetCheckbox(By.Id("OnChainWithLnInvoiceFallback"), bip21);
}
else
{
Driver.SetCheckbox(By.Id("UseClassicCheckout"), true);
}
Driver.FindElement(By.Id("Save")).SendKeys(Keys.Enter);
Assert.Contains("Store successfully updated", FindAlertMessage().Text);
Assert.True(Driver.FindElement(By.Id("UseNewCheckout")).Selected);
Assert.True(Driver.FindElement(By.Id("UseClassicCheckout")).Selected);
}
public Mnemonic GenerateWallet(string cryptoCode = "BTC", string seed = "", bool? importkeys = null, bool isHotWallet = false, ScriptPubKeyType format = ScriptPubKeyType.Segwit)

@ -9,13 +9,15 @@ using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Lightning;
using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Payments;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Wallets;
using BTCPayServer.Views.Manage;
using BTCPayServer.Views.Server;
@ -27,6 +29,7 @@ using Microsoft.EntityFrameworkCore;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBitcoin.Payment;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OpenQA.Selenium;
using OpenQA.Selenium.Support.Extensions;
@ -75,8 +78,7 @@ namespace BTCPayServer.Tests
s.GenerateWallet(isHotWallet: true);
// Point Of Sale
s.Driver.FindElement(By.Id("StoreNav-CreateApp")).Click();
new SelectElement(s.Driver.FindElement(By.Id("SelectedAppType"))).SelectByValue("PointOfSale");
s.Driver.FindElement(By.Id("StoreNav-CreatePointOfSale")).Click();
s.Driver.FindElement(By.Id("AppName")).SendKeys(Guid.NewGuid().ToString());
s.Driver.FindElement(By.Id("Create")).Click();
Assert.Contains("App successfully created", s.FindAlertMessage().Text);
@ -116,8 +118,11 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Name("buyerEmail")).SendKeys("aa@aa.com");
s.Driver.FindElement(By.CssSelector("input[type='submit']")).Click();
invoiceId = s.Driver.Url.Split('/').Last();
s.Driver.Navigate().GoToUrl(editUrl);
Assert.Contains("aa@aa.com", s.Driver.PageSource);
var invoice = await s.Server.PayTester.GetService<InvoiceRepository>().GetInvoice(invoiceId);
Assert.Equal("aa@aa.com", invoice.Metadata.BuyerEmail);
//Custom Forms
s.GoToStore(StoreNavPages.Forms);
@ -148,7 +153,7 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.LinkText("Remove")).Click();
s.Driver.WaitForElement(By.Id("ConfirmInput")).SendKeys("DELETE");
s.Driver.FindElement(By.Id("ConfirmContinue")).Click();
Assert.DoesNotContain("Custom Form 1", s.Driver.PageSource);
s.Driver.FindElement(By.Id("CreateForm")).Click();
s.Driver.FindElement(By.Name("Name")).SendKeys("Custom Form 2");
@ -163,23 +168,23 @@ namespace BTCPayServer.Tests
formurl = s.Driver.Url;
result = await s.Server.PayTester.HttpClient.GetAsync(formurl);
Assert.NotEqual(HttpStatusCode.NotFound, result.StatusCode);
s.GoToHome();
s.GoToStore(StoreNavPages.Forms);
Assert.Contains("Custom Form 2", s.Driver.PageSource);
s.Driver.FindElement(By.LinkText("Custom Form 2")).Click();
s.Driver.FindElement(By.Name("Name")).Clear();
s.Driver.FindElement(By.Name("Name")).SendKeys("Custom Form 3");
s.Driver.FindElement(By.Id("SaveButton")).Click();
s.GoToStore(StoreNavPages.Forms);
Assert.Contains("Custom Form 3", s.Driver.PageSource);
s.Driver.FindElement(By.Id("StoreNav-PaymentRequests")).Click();
s.Driver.FindElement(By.Id("CreatePaymentRequest")).Click();
Assert.Equal(4, new SelectElement(s.Driver.FindElement(By.Id("FormId"))).Options.Count);
Assert.Equal(4, new SelectElement(s.Driver.FindElement(By.Id("FormId"))).Options.Count);
}
[Fact(Timeout = TestTimeout)]
@ -466,7 +471,7 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("Rules_0__To")).SendKeys("test@gmail.com");
s.Driver.FindElement(By.Id("Rules_0__CustomerEmail")).Click();
s.Driver.FindElement(By.Id("Rules_0__Subject")).SendKeys("Thanks!");
s.Driver.FindElement(By.Id("Rules_0__Body")).SendKeys("Your invoice is settled");
s.Driver.FindElement(By.ClassName("note-editable")).SendKeys("Your invoice is settled");
s.Driver.FindElement(By.Id("SaveEmailRules")).Click();
Assert.Contains("Store email rules saved", s.FindAlertMessage().Text);
}
@ -578,8 +583,7 @@ namespace BTCPayServer.Tests
Assert.DoesNotContain("invoice-processing", s.Driver.PageSource);
});
Assert.Contains(s.Server.PayTester.GetService<CurrencyNameTable>().DisplayFormatCurrency(100, "USD"),
s.Driver.PageSource);
Assert.Contains("100.00 USD", s.Driver.PageSource);
Assert.Contains(i, s.Driver.PageSource);
s.GoToInvoices(s.StoreId);
@ -592,7 +596,7 @@ namespace BTCPayServer.Tests
s.GoToInvoices(s.StoreId);
s.GoToInvoiceCheckout(i);
var checkouturi = s.Driver.Url;
s.PayInvoice();
s.PayInvoice(mine: true);
TestUtils.Eventually(() =>
{
s.Driver.Navigate().Refresh();
@ -602,7 +606,7 @@ namespace BTCPayServer.Tests
{
s.Driver.Navigate().Refresh();
Assert.DoesNotContain("invoice-unsettled", s.Driver.PageSource);
Assert.Contains("invoice-processing", s.Driver.PageSource);
Assert.Contains("\"PaymentDetails\"", s.Driver.PageSource);
});
s.GoToUrl(checkouturi);
@ -832,6 +836,105 @@ namespace BTCPayServer.Tests
AssertUrlHasPairingCode(s);
}
[Fact(Timeout = TestTimeout)]
public async Task CookieReflectProperPermissions()
{
using var s = CreateSeleniumTester();
await s.StartAsync();
var alice = s.Server.NewAccount();
alice.Register(false);
await alice.CreateStoreAsync();
var bob = s.Server.NewAccount();
await bob.CreateStoreAsync();
await bob.AddGuest(alice.UserId);
s.GoToLogin();
s.LogIn(alice.Email, alice.Password);
s.GoToUrl($"/cheat/permissions/stores/{bob.StoreId}");
var pageSource = s.Driver.PageSource;
AssertPermissions(pageSource, true,
new[]
{
Policies.CanViewInvoices,
Policies.CanModifyInvoices,
Policies.CanViewPaymentRequests,
Policies.CanViewStoreSettings,
Policies.CanModifyStoreSettingsUnscoped,
Policies.CanDeleteUser
});
AssertPermissions(pageSource, false,
new[]
{
Policies.CanModifyStoreSettings,
Policies.CanCreateNonApprovedPullPayments,
Policies.CanCreatePullPayments,
Policies.CanManagePullPayments,
Policies.CanModifyServerSettings
});
s.GoToUrl($"/cheat/permissions/stores/{alice.StoreId}");
pageSource = s.Driver.PageSource;
AssertPermissions(pageSource, true,
new[]
{
Policies.CanViewInvoices,
Policies.CanModifyInvoices,
Policies.CanViewPaymentRequests,
Policies.CanViewStoreSettings,
Policies.CanModifyStoreSettingsUnscoped,
Policies.CanDeleteUser,
Policies.CanModifyStoreSettings,
Policies.CanCreateNonApprovedPullPayments,
Policies.CanCreatePullPayments,
Policies.CanManagePullPayments
});
AssertPermissions(pageSource, false,
new[]
{
Policies.CanModifyServerSettings
});
await alice.MakeAdmin();
s.Logout();
s.GoToLogin();
s.LogIn(alice.Email, alice.Password);
s.GoToUrl($"/cheat/permissions/stores/{alice.StoreId}");
pageSource = s.Driver.PageSource;
AssertPermissions(pageSource, true,
new[]
{
Policies.CanViewInvoices,
Policies.CanModifyInvoices,
Policies.CanViewPaymentRequests,
Policies.CanViewStoreSettings,
Policies.CanModifyStoreSettingsUnscoped,
Policies.CanDeleteUser,
Policies.CanModifyStoreSettings,
Policies.CanCreateNonApprovedPullPayments,
Policies.CanCreatePullPayments,
Policies.CanManagePullPayments,
Policies.CanModifyServerSettings,
Policies.CanCreateUser,
Policies.CanManageUsers
});
}
void AssertPermissions(string source, bool expected, string[] permissions)
{
if (expected)
{
foreach (var p in permissions)
Assert.Contains(p + "<", source);
}
else
{
foreach (var p in permissions)
Assert.DoesNotContain(p + "<", source);
}
}
[Fact(Timeout = TestTimeout)]
public async Task CanCreateAppPoS()
{
@ -839,9 +942,8 @@ namespace BTCPayServer.Tests
await s.StartAsync();
var userId = s.RegisterNewUser(true);
s.CreateNewStore();
s.Driver.FindElement(By.Id("StoreNav-CreateApp")).Click();
s.Driver.FindElement(By.Id("StoreNav-CreatePointOfSale")).Click();
s.Driver.FindElement(By.Name("AppName")).SendKeys("PoS" + Guid.NewGuid());
s.Driver.FindElement(By.Id("SelectedAppType")).SendKeys("Point of Sale");
s.Driver.FindElement(By.Id("Create")).Click();
Assert.Contains("App successfully created", s.FindAlertMessage().Text);
@ -927,9 +1029,8 @@ namespace BTCPayServer.Tests
s.CreateNewStore();
s.AddDerivationScheme();
s.Driver.FindElement(By.Id("StoreNav-CreateApp")).Click();
s.Driver.FindElement(By.Id("StoreNav-CreateCrowdfund")).Click();
s.Driver.FindElement(By.Name("AppName")).SendKeys("CF" + Guid.NewGuid());
s.Driver.FindElement(By.Id("SelectedAppType")).SendKeys("Crowdfund");
s.Driver.FindElement(By.Id("Create")).Click();
Assert.Contains("App successfully created", s.FindAlertMessage().Text);
@ -970,6 +1071,7 @@ namespace BTCPayServer.Tests
await s.StartAsync();
s.RegisterNewUser();
s.CreateNewStore();
s.EnableCheckout(CheckoutType.V1);
s.AddDerivationScheme();
s.Driver.FindElement(By.Id("StoreNav-PaymentRequests")).Click();
@ -1274,15 +1376,15 @@ namespace BTCPayServer.Tests
// Can add a label?
await TestUtils.EventuallyAsync(async () =>
{
s.Driver.WaitForElement(By.CssSelector("div.label-manager input ")).Click();
s.Driver.WaitForElement(By.CssSelector("div.label-manager input")).Click();
await Task.Delay(500);
s.Driver.WaitForElement(By.CssSelector("div.label-manager input ")).SendKeys("test-label" + Keys.Enter);
s.Driver.WaitForElement(By.CssSelector("div.label-manager input")).SendKeys("test-label" + Keys.Enter);
await Task.Delay(500);
s.Driver.WaitForElement(By.CssSelector("div.label-manager input ")).SendKeys("label2" + Keys.Enter);
s.Driver.WaitForElement(By.CssSelector("div.label-manager input")).SendKeys("label2" + Keys.Enter);
});
TestUtils.Eventually(() =>
{
{
s.Driver.Navigate().Refresh();
Assert.NotNull(s.Driver.FindElement(By.CssSelector("[data-value='test-label']")));
});
@ -1304,10 +1406,10 @@ namespace BTCPayServer.Tests
s.Driver.WaitForElement(By.CssSelector("[data-value='test-label']")).Click();
await Task.Delay(500);
s.Driver.ExecuteJavaScript("document.querySelector('[data-value=\"test-label\"]').nextSibling.dispatchEvent(new KeyboardEvent('keydown', {'key': 'Delete', keyCode: 46}));");
});
TestUtils.Eventually(() =>
{
{
s.Driver.Navigate().Refresh();
Assert.DoesNotContain("test-label", s.Driver.PageSource);
});
@ -1470,9 +1572,20 @@ namespace BTCPayServer.Tests
Assert.Contains("\"Amount\": \"3.00000000\"", s.Driver.PageSource);
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
// BIP-329 export
s.Driver.FindElement(By.Id("ExportDropdownToggle")).Click();
s.Driver.FindElement(By.Id("ExportBIP329")).Click();
Thread.Sleep(1000);
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
Assert.Contains(s.WalletId.ToString(), s.Driver.Url);
Assert.EndsWith("export?format=bip329", s.Driver.Url);
Assert.Contains("{\"type\":\"tx\",\"ref\":\"", s.Driver.PageSource);
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
// CSV export
s.Driver.FindElement(By.Id("ExportDropdownToggle")).Click();
s.Driver.FindElement(By.Id("ExportCSV")).Click();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
}
[Fact(Timeout = TestTimeout)]
@ -1619,7 +1732,10 @@ namespace BTCPayServer.Tests
s.Driver.Navigate().Refresh();
Assert.Contains("transaction-label", s.Driver.PageSource);
});
Assert.Equal("payout", s.Driver.FindElement(By.ClassName("transaction-label")).Text);
var labels = s.Driver.FindElements(By.CssSelector("#WalletTransactionsList tr:first-child div.transaction-label"));
Assert.Equal(2, labels.Count);
Assert.Contains(labels, element => element.Text == "payout");
Assert.Contains(labels, element => element.Text == "pull-payment");
s.GoToStore(s.StoreId, StoreNavPages.Payouts);
s.Driver.FindElement(By.Id($"{PayoutState.InProgress}-view")).Click();
@ -1892,9 +2008,7 @@ namespace BTCPayServer.Tests
s.AddLightningNode(LightningConnectionType.CLightning, false);
s.GoToLightningSettings();
s.Driver.SetCheckbox(By.Id("LNURLEnabled"), true);
s.Driver.FindElement(By.Id("StoreNav-CreateApp")).Click();
s.Driver.FindElement(By.Id("SelectedAppType")).Click();
s.Driver.FindElement(By.CssSelector("option[value='PointOfSale']")).Click();
s.Driver.FindElement(By.Id("StoreNav-CreatePointOfSale")).Click();
s.Driver.FindElement(By.Id("AppName")).SendKeys(Guid.NewGuid().ToString());
s.Driver.FindElement(By.Id("Create")).Click();
TestUtils.Eventually(() => Assert.Contains("App successfully created", s.FindAlertMessage().Text));
@ -1929,6 +2043,7 @@ namespace BTCPayServer.Tests
new[] { s.Server.MerchantLnd.Client });
s.RegisterNewUser(true);
(_, string storeId) = s.CreateNewStore();
s.EnableCheckout(CheckoutType.V1);
var network = s.Server.NetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode).NBitcoinNetwork;
s.AddLightningNode(LightningConnectionType.CLightning, false);
s.GoToLightningSettings();
@ -1960,7 +2075,8 @@ namespace BTCPayServer.Tests
var res = await s.Server.CustomerLightningD.Pay(lnurlResponse.Pr);
Assert.Equal(PayResult.Error, res.Result);
await s.Server.CustomerLightningD.Pay(lnurlResponse2.Pr);
res = await s.Server.CustomerLightningD.Pay(lnurlResponse2.Pr);
Assert.Equal(PayResult.Ok, res.Result);
await TestUtils.EventuallyAsync(async () =>
{
var inv = await s.Server.PayTester.InvoiceRepository.GetInvoice(i);
@ -1974,22 +2090,23 @@ namespace BTCPayServer.Tests
});
// Standard invoice test
s.GoToStore(storeId);
s.GoToLightningSettings();
s.Driver.SetCheckbox(By.Id("LNURLStandardInvoiceEnabled"), true);
SudoForceSaveLightningSettingsRightNowAndFast(s, cryptoCode);
i = s.CreateInvoice(storeId, 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.ClassName("payment__currencies_noborder")).Click();
// BOLT11 is also displayed for standard invoice (not LNURL, even if it is available)
s.Driver.FindElement(By.Id("copy-tab")).Click();
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()));
var bolt11 = s.Driver.FindElement(By.CssSelector("input.checkoutTextbox")).GetAttribute("value");
var bolt11Parsed = Lightning.BOLT11PaymentRequest.Parse(bolt11, s.Server.ExplorerNode.Network);
var invoiceId = s.Driver.Url.Split('/').Last();
using (var resp = await s.Server.PayTester.HttpClient.GetAsync("BTC/lnurl/pay/i/" + invoiceId))
{
resp.EnsureSuccessStatusCode();
fetchedReuqest = JsonConvert.DeserializeObject<LNURLPayRequest>(await resp.Content.ReadAsStringAsync());
}
Assert.Equal(0.0000001m, fetchedReuqest.MaxSendable.ToDecimal(LightMoneyUnit.BTC));
Assert.Equal(0.0000001m, fetchedReuqest.MinSendable.ToDecimal(LightMoneyUnit.BTC));
await Assert.ThrowsAsync<LNUrlException>(async () =>
{
await fetchedReuqest.SendRequest(new LightMoney(0.0000002m, LightMoneyUnit.BTC),
@ -2011,13 +2128,6 @@ namespace BTCPayServer.Tests
Assert.Equal(new LightMoney(0.0000001m, LightMoneyUnit.BTC),
lnurlResponse2.GetPaymentRequest(network).MinimumAmount);
s.GoToHome();
s.GoToLightningSettings();
// 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(storeId, 0.000001m, cryptoCode);
s.GoToInvoiceCheckout(i);
@ -2031,23 +2141,12 @@ namespace BTCPayServer.Tests
s.GoToHome();
s.GoToLightningSettings();
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.GoToLightningSettings();
//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("LNURLBech32Mode")).Selected);
Assert.False(s.Driver.FindElement(By.Id("LNURLStandardInvoiceEnabled")).Selected);
//even though we set DisableBolt11PaymentMethod to true, logic when saving it turns it back off as otherwise no lightning option is available at all!
Assert.False(s.Driver.FindElement(By.Id("DisableBolt11PaymentMethod")).Selected);
// Invoice creation should fail, because it is a standard invoice with amount, but DisableBolt11PaymentMethod = true and LNURLStandardInvoiceEnabled = false
s.CreateInvoice(storeId, 0.0000001m, cryptoCode, "", null, expectedSeverity: StatusMessageModel.StatusSeverity.Success);
i = s.CreateInvoice(storeId, null, cryptoCode);
s.GoToInvoiceCheckout(i);
@ -2059,18 +2158,17 @@ namespace BTCPayServer.Tests
s.GoToHome();
s.CreateNewStore(false);
s.EnableCheckout(CheckoutType.V1);
s.AddLightningNode(LightningConnectionType.LndREST, false);
s.GoToLightningSettings();
s.Driver.SetCheckbox(By.Id("LNURLEnabled"), true);
s.Driver.SetCheckbox(By.Id("DisableBolt11PaymentMethod"), true);
s.Driver.SetCheckbox(By.Id("LNURLStandardInvoiceEnabled"), true);
s.Driver.FindElement(By.Id("save")).Click();
Assert.Contains($"{cryptoCode} Lightning settings successfully updated", s.FindAlertMessage().Text);
var invForPP = s.CreateInvoice(0.0000001m, cryptoCode);
var invForPP = s.CreateInvoice(null, 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);
LNURL.LNURL.Parse(lnurl, out tag);
// Check that pull payment has lightning option
s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
@ -2144,13 +2242,16 @@ namespace BTCPayServer.Tests
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success);
s.Driver.ToggleCollapse("AddAddress");
var lnaddress2 = "EUR" + Guid.NewGuid().ToString();
s.Driver.FindElement(By.Id("Add_Username")).SendKeys(lnaddress2);
lnaddress2 = lnaddress2.ToLowerInvariant();
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.Id("Add_InvoiceMetadata")).SendKeys("{\"test\":\"lol\"}");
s.Driver.FindElement(By.CssSelector("button[value='add']")).Click();
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success);
@ -2166,20 +2267,101 @@ namespace BTCPayServer.Tests
var lnurl = new Uri(LNURL.LNURL.ExtractUriFromInternetIdentifier(value).ToString()
.Replace("https", "http"));
var request = (LNURL.LNURLPayRequest)await LNURL.LNURL.FetchInformation(lnurl, new HttpClient());
var m = request.ParsedMetadata.ToDictionary(o => o.Key, o => o.Value);
switch (value)
{
case { } v when v.StartsWith(lnaddress2):
Assert.StartsWith(lnaddress2 + "@", m["text/identifier"]);
lnaddress2 = m["text/identifier"];
Assert.Equal(2, request.MinSendable.ToDecimal(LightMoneyUnit.Satoshi));
Assert.Equal(10, request.MaxSendable.ToDecimal(LightMoneyUnit.Satoshi));
break;
case { } v when v.StartsWith(lnaddress1):
Assert.StartsWith(lnaddress1 + "@", m["text/identifier"]);
lnaddress1 = m["text/identifier"];
Assert.Equal(1, request.MinSendable.ToDecimal(LightMoneyUnit.Satoshi));
Assert.Equal(6.12m, request.MaxSendable.ToDecimal(LightMoneyUnit.BTC));
break;
default:
Assert.False(true, "Should have matched");
break;
}
}
var repo = s.Server.PayTester.GetService<InvoiceRepository>();
var invoices = await repo.GetInvoices(new InvoiceQuery() { StoreId = new[] { s.StoreId } });
Assert.Equal(2, invoices.Length);
var emailSuffix = $"@{s.Server.PayTester.HostName}:{s.Server.PayTester.Port}";
foreach (var i in invoices)
{
var lightningPaymentMethod = i.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.LNURLPay));
var paymentMethodDetails =
lightningPaymentMethod.GetPaymentMethodDetails() as LNURLPayPaymentMethodDetails;
Assert.Contains(
paymentMethodDetails.ConsumedLightningAddress,
new[] { lnaddress1, lnaddress2 });
if (paymentMethodDetails.ConsumedLightningAddress == lnaddress2)
{
Assert.Equal("lol", i.Metadata.AdditionalData["test"].Value<string>());
}
}
var lnUsername = lnaddress1.Split('@')[0];
LNURLPayRequest req;
using (var resp = await s.Server.PayTester.HttpClient.GetAsync($"/.well-known/lnurlp/{lnUsername}"))
{
var str = await resp.Content.ReadAsStringAsync();
req = JsonConvert.DeserializeObject<LNURLPayRequest>(str);
Assert.Contains(req.ParsedMetadata, m => m.Key == "text/identifier" && m.Value == lnaddress1);
Assert.Contains(req.ParsedMetadata, m => m.Key == "text/plain" && m.Value.StartsWith("Paid to"));
Assert.NotNull(req.Callback);
Assert.Equal(new LightMoney(1000), req.MinSendable);
Assert.Equal(LightMoney.FromUnit(6.12m, LightMoneyUnit.BTC), req.MaxSendable);
}
lnUsername = lnaddress2.Split('@')[0];
using (var resp = await s.Server.PayTester.HttpClient.GetAsync($"/.well-known/lnurlp/{lnUsername}"))
{
var str = await resp.Content.ReadAsStringAsync();
req = JsonConvert.DeserializeObject<LNURLPayRequest>(str);
Assert.Equal(new LightMoney(2000), req.MinSendable);
Assert.Equal(new LightMoney(10_000), req.MaxSendable);
}
// Check if we can get the same payrequest through the callback
using (var resp = await s.Server.PayTester.HttpClient.GetAsync(req.Callback))
{
var str = await resp.Content.ReadAsStringAsync();
req = JsonConvert.DeserializeObject<LNURLPayRequest>(str);
Assert.Equal(new LightMoney(2000), req.MinSendable);
Assert.Equal(new LightMoney(10_000), req.MaxSendable);
}
// Can we ask for invoice? (Should fail, below minSpendable)
using (var resp = await s.Server.PayTester.HttpClient.GetAsync(req.Callback + "?amount=1999"))
{
var str = await resp.Content.ReadAsStringAsync();
var err = JsonConvert.DeserializeObject<LNUrlStatusResponse>(str);
Assert.Equal("Amount is out of bounds.", err.Reason);
}
// Can we ask for invoice?
using (var resp = await s.Server.PayTester.HttpClient.GetAsync(req.Callback + "?amount=2000"))
{
var str = await resp.Content.ReadAsStringAsync();
var succ = JsonConvert.DeserializeObject<LNURLPayRequest.LNURLPayRequestCallbackResponse>(str);
Assert.NotNull(succ.Pr);
Assert.Equal(new LightMoney(2000), BOLT11PaymentRequest.Parse(succ.Pr, Network.RegTest).MinimumAmount);
}
// Can we change comment?
using (var resp = await s.Server.PayTester.HttpClient.GetAsync(req.Callback + "?amount=2001"))
{
var str = await resp.Content.ReadAsStringAsync();
var succ = JsonConvert.DeserializeObject<LNURLPayRequest.LNURLPayRequestCallbackResponse>(str);
Assert.NotNull(succ.Pr);
Assert.Equal(new LightMoney(2001), BOLT11PaymentRequest.Parse(succ.Pr, Network.RegTest).MinimumAmount);
}
}
[Fact]

@ -177,7 +177,7 @@ namespace BTCPayServer.Tests
public async Task<PayResponse> SendLightningPaymentAsync(Invoice invoice)
{
var bolt11 = invoice.CryptoInfo.Where(o => o.PaymentUrls.BOLT11 != null).First().PaymentUrls.BOLT11;
var bolt11 = invoice.CryptoInfo.Where(o => o.PaymentUrls?.BOLT11 != null).First().PaymentUrls.BOLT11;
bolt11 = bolt11.Replace("lightning:", "", StringComparison.OrdinalIgnoreCase);
return await CustomerLightningD.Pay(bolt11);
}
@ -194,7 +194,8 @@ namespace BTCPayServer.Tests
tcs.TrySetResult(evt);
}
});
await action.Invoke();
if (action != null)
await action.Invoke();
var result = await tcs.Task;
sub.Dispose();
return result;
@ -246,15 +247,20 @@ namespace BTCPayServer.Tests
}
public List<string> Stores { get; internal set; } = new List<string>();
public bool DeleteStore { get; set; } = true;
public BTCPayNetworkBase DefaultNetwork => NetworkProvider.DefaultNetwork;
public void Dispose()
{
foreach (var r in this.Resources)
r.Dispose();
TestLogs.LogInformation("Disposing the BTCPayTester...");
foreach (var store in Stores)
if (DeleteStore)
{
Xunit.Assert.True(PayTester.StoreRepository.DeleteStore(store).GetAwaiter().GetResult());
foreach (var store in Stores)
{
Xunit.Assert.True(PayTester.StoreRepository.DeleteStore(store).GetAwaiter().GetResult());
}
}
if (PayTester != null)
PayTester.Dispose();

@ -214,14 +214,21 @@ namespace BTCPayServer.Tests
get => GenerateWalletResponseV.DerivationScheme;
}
public void SetLNUrl(string cryptoCode, bool activated)
{
var lnSettingsVm = GetController<UIStoresController>().LightningSettings(StoreId, cryptoCode).AssertViewModel<LightningSettingsViewModel>();
lnSettingsVm.LNURLEnabled = activated;
Assert.IsType<RedirectToActionResult>(GetController<UIStoresController>().LightningSettings(lnSettingsVm).Result);
}
private async Task RegisterAsync(bool isAdmin = false)
{
var account = parent.PayTester.GetController<UIAccountController>();
RegisterDetails = new RegisterViewModel()
{
Email = Utils.GenerateEmail(),
ConfirmPassword = "Kitten0@",
Password = "Kitten0@",
ConfirmPassword = Password,
Password = Password,
IsAdmin = isAdmin
};
await account.Register(RegisterDetails);
@ -240,6 +247,7 @@ namespace BTCPayServer.Tests
Email = RegisterDetails.Email;
IsAdmin = account.RegisteredAdmin;
}
public string Password { get; set; } = "Kitten0@";
public RegisterViewModel RegisterDetails { get; set; }

@ -17,7 +17,7 @@ namespace BTCPayServer.Tests
#if DEBUG && !SHORT_TIMEOUT
public const int TestTimeout = 600_000;
#else
public const int TestTimeout = 60_000;
public const int TestTimeout = 90_000;
#endif
public static DirectoryInfo TryGetSolutionDirectoryInfo(string currentPath = null)
{
@ -112,7 +112,14 @@ namespace BTCPayServer.Tests
}
catch (XunitException) when (!cts.Token.IsCancellationRequested)
{
await Task.Delay(500, cts.Token);
bool timeout =false;
try
{
await Task.Delay(500, cts.Token);
}
catch { timeout = true; }
if (timeout)
throw;
}
}
}

@ -353,6 +353,11 @@ retry:
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "tom-select", "tom-select.complete.min.js").Trim();
expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/js/tom-select.complete.min.js")).Content.ReadAsStringAsync()).Trim();
Assert.Equal(expected, actual);
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "dom-confetti", "dom-confetti.min.js").Trim();
version = Regex.Match(actual, "Original file: /npm/dom-confetti@([0-9]+.[0-9]+.[0-9]+)/lib/main.js").Groups[1].Value;
expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/dom-confetti@{version}/lib/main.min.js")).Content.ReadAsStringAsync()).Trim();
Assert.Equal(expected, actual);
}
string GetFileContent(params string[] path)

@ -35,6 +35,8 @@ using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Payments.PayJoin.Sender;
using BTCPayServer.Plugins.PayButton;
using BTCPayServer.Plugins.PointOfSale;
using BTCPayServer.Plugins.PointOfSale.Controllers;
using BTCPayServer.Security.Bitpay;
using BTCPayServer.Services;
@ -1607,7 +1609,7 @@ namespace BTCPayServer.Tests
// Check correct casing: Addresses in payment URI need to be …
// - lowercase in link version
// - uppercase in QR version
// Standard for all uppercase characters in QR codes is still not implemented in all wallets
// But we're proceeding with BECH32 being uppercase
Assert.Equal($"bitcoin:{paymentMethodUnified.BtcAddress}", paymentMethodUnified.InvoiceBitcoinUrl.Split('?')[0]);
@ -1633,6 +1635,7 @@ namespace BTCPayServer.Tests
var cryptoCode = "BTC";
user.GrantAccess(true);
user.RegisterLightningNode(cryptoCode, LightningConnectionType.Charge);
user.SetLNUrl(cryptoCode, false);
var vm = user.GetController<UIStoresController>().CheckoutAppearance().AssertViewModel<CheckoutAppearanceViewModel>();
var criteria = Assert.Single(vm.PaymentMethodCriteria);
Assert.Equal(new PaymentMethodId(cryptoCode, LightningPaymentType.Instance).ToString(), criteria.PaymentMethod);
@ -1647,16 +1650,12 @@ namespace BTCPayServer.Tests
Price = 1.5m,
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 = user.GetController<UIStoresController>().LightningSettings(user.StoreId, cryptoCode).AssertViewModel<LightningSettingsViewModel>();
lnSettingsVm.LNURLEnabled = true;
lnSettingsVm.LNURLStandardInvoiceEnabled = true;
Assert.IsType<RedirectToActionResult>(user.GetController<UIStoresController>().LightningSettings(lnSettingsVm).Result);
user.SetLNUrl(cryptoCode, true);
vm = user.GetController<UIStoresController>().CheckoutAppearance().AssertViewModel<CheckoutAppearanceViewModel>();
criteria = Assert.Single(vm.PaymentMethodCriteria);
Assert.Equal(new PaymentMethodId(cryptoCode, LightningPaymentType.Instance).ToString(), criteria.PaymentMethod);
@ -1953,14 +1952,13 @@ namespace BTCPayServer.Tests
var apps = user.GetController<UIAppsController>();
var apps2 = user2.GetController<UIAppsController>();
var pos = user.GetController<UIPointOfSaleController>();
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId)).Model);
var appType = AppType.PointOfSale.ToString();
Assert.NotNull(vm.SelectedAppType);
var appType = PointOfSaleAppType.AppType;
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId, appType)).Model);
Assert.Equal(appType, vm.SelectedAppType);
Assert.Null(vm.AppName);
vm.AppName = "test";
vm.SelectedAppType = appType;
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.CreateApp(user.StoreId, vm).Result);
Assert.Equal(nameof(pos.UpdatePointOfSale), redirectToAction.ActionName);
var redirect = Assert.IsType<RedirectResult>(apps.CreateApp(user.StoreId, vm).Result);
Assert.EndsWith("/settings/pos", redirect.Url);
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
var appList2 =
Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps2.ListApps(user2.StoreId).Result).Model);
@ -1976,7 +1974,7 @@ namespace BTCPayServer.Tests
Assert.Equal(user.StoreId, appList.Apps[0].StoreId);
Assert.IsType<NotFoundResult>(apps2.DeleteApp(appList.Apps[0].Id));
Assert.IsType<ViewResult>(apps.DeleteApp(appList.Apps[0].Id));
redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(appList.Apps[0].Id).Result);
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(appList.Apps[0].Id).Result);
Assert.Equal(nameof(stores.Dashboard), redirectToAction.ActionName);
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
Assert.Empty(appList.Apps);
@ -2444,6 +2442,31 @@ namespace BTCPayServer.Tests
Assert.False(fn.Seen);
}
[Fact(Timeout = LongRunningTestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanFixMappedDomainAppType()
{
using var tester = CreateServerTester(newDb: true);
await tester.StartAsync();
var f = tester.PayTester.GetService<ApplicationDbContextFactory>();
using (var ctx = f.CreateContext())
{
var setting = new SettingData() { Id = "BTCPayServer.Services.PoliciesSettings" };
setting.Value = JObject.Parse("{\"RootAppId\": null, \"RootAppType\": 1, \"Experimental\": false, \"PluginSource\": null, \"LockSubscription\": false, \"DisableSSHService\": false, \"PluginPreReleases\": false, \"BlockExplorerLinks\": [],\"DomainToAppMapping\": [{\"AppId\": \"87kj5yKay8mB4UUZcJhZH5TqDKMD3CznjwLjiu1oYZXe\", \"Domain\": \"donate.nicolas-dorier.com\", \"AppType\": 0}], \"CheckForNewVersions\": false, \"AllowHotWalletForAll\": false, \"RequiresConfirmedEmail\": false, \"DiscourageSearchEngines\": false, \"DisableInstantNotifications\": false, \"DisableNonAdminCreateUserApi\": false, \"AllowHotWalletRPCImportForAll\": false, \"AllowLightningInternalNodeForAll\": false, \"DisableStoresToUseServerEmailSettings\": false}").ToString();
ctx.Settings.Add(setting);
await ctx.SaveChangesAsync();
}
await RestartMigration(tester);
using (var ctx = f.CreateContext())
{
var setting = await ctx.Settings.FirstOrDefaultAsync(c => c.Id == "BTCPayServer.Services.PoliciesSettings");
var o = JObject.Parse(setting.Value);
Assert.Equal("Crowdfund", o["RootAppType"].Value<string>());
o = (JObject)((JArray)o["DomainToAppMapping"])[0];
Assert.Equal("PointOfSale", o["AppType"].Value<string>());
}
}
[Fact(Timeout = LongRunningTestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanDoLightningInternalNodeMigration()

@ -14,6 +14,7 @@ using BTCPayServer.Client.Models;
using BTCPayServer.Controllers;
using ExchangeSharp;
using NBitcoin;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
@ -60,31 +61,36 @@ namespace BTCPayServer.Tests
return description;
}
// /// <summary>
// /// This will take the translations from v1 or v2
// /// and upload them to transifex if not found
// /// </summary>
// [FactWithSecret("TransifexAPIToken")]
// [Trait("Utilities", "Utilities")]
//#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
// public async Task UpdateTransifex()
// {
// // DO NOT RUN IT, THIS WILL ERASE THE CURRENT TRANSIFEX TRANSLATIONS
// /// <summary>
// /// This will take the translations from v1 or v2
// /// and upload them to transifex if not found
// /// </summary>
// [FactWithSecret("TransifexAPIToken")]
// [Trait("Utilities", "Utilities")]
//#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
// public async Task UpdateTransifex()
// {
// // DO NOT RUN IT, THIS WILL ERASE THE CURRENT TRANSIFEX TRANSLATIONS
// var client = GetTransifexClient();
// var translations = JsonTranslation.GetTranslations(TranslationFolder.CheckoutV1);
// var enTranslations = translations["en"];
// translations.Remove("en");
// var client = GetTransifexClient();
// var translations = JsonTranslation.GetTranslations(TranslationFolder.CheckoutV2);
// var enTranslations = translations["en"];
// translations.Remove("en");
// foreach (var t in translations)
// foreach (var w in t.Value.Words.ToArray())
// {
// if (w.Value == enTranslations.Words[w.Key])
// t.Value.Words[w.Key] = null;
// }
// await client.UpdateTranslations(translations);
// }
//#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
// foreach (var t in translations)
// {
// foreach (var w in t.Value.Words.ToArray())
// {
// if (t.Value.Words[w.Key] == null)
// t.Value.Words[w.Key] = enTranslations.Words[w.Key];
// }
// t.Value.Words.Remove("code");
// t.Value.Words.Remove("NOTICE_WARN");
// }
// await client.UpdateTranslations(translations);
// }
//#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
///// <summary>
///// This utility will copy translations made on checkout v1 to checkout v2
@ -125,7 +131,6 @@ namespace BTCPayServer.Tests
// return name.Replace("_", "").ToLowerInvariant();
//}
/// <summary>
/// This utility will use selenium to pilot your browser to
/// automatically translate a language.
@ -141,8 +146,8 @@ namespace BTCPayServer.Tests
[FactWithSecret("TransifexAPIToken")]
public async Task AutoTranslateChatGPT()
{
var file = TranslationFolder.CheckoutV1;
var file = TranslationFolder.CheckoutV2;
using var driver = new ChromeDriver(new ChromeOptions()
{
DebuggerAddress = "127.0.0.1:9222"
@ -180,6 +185,19 @@ namespace BTCPayServer.Tests
var english = englishTranslations.Words[translation.Key];
if (translation.Value != null)
continue; // Already translated
//TODO: A better way to avoid rate limits is to use this format:
//I am translating a checkout crypto payment page, and I want you to translate it from English (en-US) to French (fr-FR).
//##
//English: This invoice will expire in
//French:
//##
//English: Scan the QR code, or tap to copy the address.
//French:
//##
//English: Your payment has been received and is now processing.
//French:
if (!askedPrompt)
{
driver.FindElement(By.XPath("//a[contains(text(), \"New chat\")]")).Click();
@ -245,7 +263,6 @@ retry:
{
// 1. Generate an API Token on https://www.transifex.com/user/settings/api/
// 2. Run "dotnet user-secrets set TransifexAPIToken <youapitoken>"
await PullTransifexTranslationsCore(TranslationFolder.CheckoutV1);
await PullTransifexTranslationsCore(TranslationFolder.CheckoutV2);
@ -282,6 +299,7 @@ retry:
{
translation.Words["InvoiceExpired_Body_3"] = string.Empty;
}
translation.Translate(langTranslations);
translation.Save();
}
catch
@ -529,7 +547,7 @@ retry:
public string FullPath { get; set; }
public string TransifexProject { get; set; }
public string TransifexProject { get; set; }
public string TransifexResource { get; private set; }
public void Save()
@ -559,7 +577,7 @@ retry:
}
}
public void Translate(Dictionary<string,string> sourceTranslations)
public void Translate(Dictionary<string, string> sourceTranslations)
{
foreach (var o in sourceTranslations)
if (o.Value != null)

@ -38,6 +38,10 @@ services:
- selenium
extra_hosts:
- "tests:127.0.0.1"
networks:
default:
custom:
ipv4_address: 172.23.0.18
volumes:
- "sshd_datadir:/root/.ssh"
- "customer_lightningd_datadir:/etc/customer_lightningd_datadir"
@ -89,6 +93,8 @@ services:
image: selenium/standalone-chrome:101.0
expose:
- "4444"
extra_hosts:
- "tests:172.18.0.18"
nbxplorer:
image: nicolasdorier/nbxplorer:2.3.58
restart: unless-stopped
@ -237,7 +243,7 @@ services:
- "5432"
merchant_lnd:
image: btcpayserver/lnd:v0.15.4-beta-1
image: btcpayserver/lnd:v0.16.2-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -272,7 +278,7 @@ services:
- bitcoind
customer_lnd:
image: btcpayserver/lnd:v0.15.4-beta-1
image: btcpayserver/lnd:v0.16.2-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -404,3 +410,12 @@ volumes:
torrcdir:
tor_servicesdir:
monero_data:
networks:
default:
driver: bridge
custom:
driver: bridge
ipam:
config:
- subnet: 172.23.0.0/16

@ -36,6 +36,10 @@ services:
- selenium
extra_hosts:
- "tests:127.0.0.1"
networks:
default:
custom:
ipv4_address: 172.23.0.18
volumes:
- "sshd_datadir:/root/.ssh"
- "customer_lightningd_datadir:/etc/customer_lightningd_datadir"
@ -84,6 +88,8 @@ services:
- merchant_lnd
selenium:
image: selenium/standalone-chrome:101.0
extra_hosts:
- "tests:172.18.0.18"
expose:
- "4444"
nbxplorer:
@ -110,7 +116,6 @@ services:
depends_on:
- bitcoind
bitcoind:
restart: unless-stopped
image: btcpayserver/bitcoin:24.0
@ -225,7 +230,7 @@ services:
- "5432"
merchant_lnd:
image: btcpayserver/lnd:v0.15.4-beta-1
image: btcpayserver/lnd:v0.16.2-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -262,7 +267,7 @@ services:
- bitcoind
customer_lnd:
image: btcpayserver/lnd:v0.15.4-beta-1
image: btcpayserver/lnd:v0.16.2-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -323,3 +328,12 @@ volumes:
tor_datadir:
torrcdir:
tor_servicesdir:
networks:
default:
driver: bridge
custom:
driver: bridge
ipam:
config:
- subnet: 172.23.0.0/16

@ -47,13 +47,13 @@
<ItemGroup>
<PackageReference Include="BIP78.Sender" Version="0.2.2" />
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.2" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.4.21" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.4.23" />
<PackageReference Include="CsvHelper" Version="15.0.5" />
<PackageReference Include="Dapper" Version="2.0.123" />
<PackageReference Include="Fido2" Version="2.0.2" />
<PackageReference Include="Fido2.AspNet" Version="2.0.2" />
<PackageReference Include="HtmlSanitizer" Version="5.0.372" />
<PackageReference Include="LNURL" Version="0.0.28" />
<PackageReference Include="LNURL" Version="0.0.29" />
<PackageReference Include="MailKit" Version="3.3.0" />
<PackageReference Include="BTCPayServer.NETCore.Plugins.Mvc" Version="1.4.4" />
<PackageReference Include="QRCoder" Version="1.4.3" />
@ -134,11 +134,11 @@
<ProjectReference Include="..\BTCPayServer.Data\BTCPayServer.Data.csproj" />
<ProjectReference Include="..\BTCPayServer.Rating\BTCPayServer.Rating.csproj" />
<ProjectReference Include="..\BTCPayServer.Common\BTCPayServer.Common.csproj" />
<ProjectReference Include="..\Plugins\BTCPayServer.Plugins.Custodians.FakeCustodian\BTCPayServer.Plugins.Custodians.FakeCustodian.csproj" />
</ItemGroup>
<ItemGroup>
<Watch Include="Views\**\*.*"></Watch>
<Watch Remove="Views\UIAccount\CheatPermissions.cshtml" />
<Content Update="Views\UIApps\_ViewImports.cshtml">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<Pack>$(IncludeRazorContentInPack)</Pack>

@ -1,7 +1,7 @@
using Microsoft.AspNetCore.Http;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace BTCPayServer
{

@ -19,7 +19,7 @@ namespace BTCPayServer
var bg = ColorTranslator.FromHtml(bgColor);
int bgDelta = Convert.ToInt32((bg.R * 0.299) + (bg.G * 0.587) + (bg.B * 0.114));
Color color = (255 - bgDelta < nThreshold) ? Color.Black : Color.White;
return ColorTranslator.ToHtml(color);
return ColorTranslator.ToHtml(color).ToLowerInvariant();
}
// Borrowed from https://github.com/ManageIQ/guides/blob/master/labels.md
public static readonly ColorPalette Default = new ColorPalette(new string[] {
@ -59,5 +59,44 @@ namespace BTCPayServer
return Labels[num % Labels.Length];
}
}
/// https://gist.github.com/zihotki/09fc41d52981fb6f93a81ebf20b35cd5
/// <summary>
/// Creates color with corrected brightness.
/// </summary>
/// <param name="color">Color to correct.</param>
/// <param name="correctionFactor">The brightness correction factor. Must be between -1 and 1.
/// Negative values produce darker colors.</param>
/// <returns>
/// Corrected <see cref="Color"/> structure.
/// </returns>
public Color AdjustBrightness(Color color, float correctionFactor)
{
float red = color.R;
float green = color.G;
float blue = color.B;
if (correctionFactor < 0)
{
correctionFactor = 1 + correctionFactor;
red *= correctionFactor;
green *= correctionFactor;
blue *= correctionFactor;
}
else
{
red = (255 - red) * correctionFactor + red;
green = (255 - green) * correctionFactor + green;
blue = (255 - blue) * correctionFactor + blue;
}
return Color.FromArgb(color.A, (int)red, (int)green, (int)blue);
}
public string AdjustBrightness(string html, float correctionFactor)
{
var color = AdjustBrightness(ColorTranslator.FromHtml(html), correctionFactor);
return ColorTranslator.ToHtml(color);
}
}
}

@ -1,4 +1,5 @@
using System;
using System.Security.AccessControl;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Models.AppViewModels;
@ -6,6 +7,8 @@ using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewComponents;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
namespace BTCPayServer.Components.AppSales;
@ -24,17 +27,28 @@ public class AppSales : ViewComponent
_appService = appService;
}
public async Task<IViewComponentResult> InvokeAsync(AppSalesViewModel vm)
public async Task<IViewComponentResult> InvokeAsync(string appId, string appType)
{
if (vm.App == null)
throw new ArgumentNullException(nameof(vm.App));
var type = _appService.GetAppType(appType);
if (type is not IHasSaleStatsAppType salesAppType || type is not AppBaseType appBaseType)
return new HtmlContentViewComponentResult(new StringHtmlContent(string.Empty));
var vm = new AppSalesViewModel
{
Id = appId,
AppType = appType,
DataUrl = Url.Action("AppSales", "UIApps", new { appId }),
InitialRendering = HttpContext.GetAppData()?.Id != appId
};
if (vm.InitialRendering)
return View(vm);
var stats = await _appService.GetSalesStats(vm.App);
var app = HttpContext.GetAppData();
var stats = await _appService.GetSalesStats(app);
vm.SalesCount = stats.SalesCount;
vm.Series = stats.Series;
vm.AppType = app.AppType;
vm.AppUrl = await appBaseType.ConfigureLink(app);
vm.Name = app.Name;
return View(vm);
}

@ -1,14 +1,17 @@
using System.Collections.Generic;
using BTCPayServer.Data;
using BTCPayServer.Services.Apps;
namespace BTCPayServer.Components.AppSales;
public class AppSalesViewModel
{
public AppData App { get; set; }
public AppSalesPeriod Period { get; set; } = AppSalesPeriod.Week;
public int SalesCount { get; set; }
public string Id { get; set; }
public string Name { get; set; }
public string AppType { get; set; }
public AppSalesPeriod Period { get; set; }
public string AppUrl { get; set; }
public string DataUrl { get; set; }
public long SalesCount { get; set; }
public IEnumerable<SalesStatsItem> Series { get; set; }
public bool InitialRendering { get; set; }
}

@ -1,17 +1,18 @@
@using BTCPayServer.Services.Apps
@using BTCPayServer.Components.AppSales
@using BTCPayServer.Abstractions.TagHelpers
@using BTCPayServer.Plugins.Crowdfund
@model BTCPayServer.Components.AppSales.AppSalesViewModel
@{
var controller = $"UI{Model.App.AppType}";
var action = $"Update{Model.App.AppType}";
var label = Model.App.AppType == nameof(AppType.Crowdfund) ? "Contributions" : "Sales";
var label = Model.AppType == CrowdfundAppType.AppType ? "Contributions" : "Sales";
}
<div id="AppSales-@Model.App.Id" class="widget app-sales">
<div id="AppSales-@Model.Id" class="widget app-sales">
<header class="mb-3">
<h3>@Model.App.Name @label</h3>
<a asp-controller="@controller" asp-action="@action" asp-route-appId="@Model.App.Id">Manage</a>
<h3>@Model.Name @label</h3>
@if (!string.IsNullOrEmpty(Model.AppUrl))
{
<a href="@Model.AppUrl">Manage</a>
}
</header>
@if (Model.InitialRendering)
{
@ -20,15 +21,16 @@
<span class="visually-hidden">Loading...</span>
</div>
</div>
<script src="~/Components/AppSales/Default.cshtml.js" asp-append-version="true"></script>
<script>
(async () => {
const url = @Safe.Json(Url.Action("AppSales", "UIApps", new { appId = Model.App.Id }));
const appId = @Safe.Json(Model.App.Id);
const url = @Safe.Json(Model.DataUrl);
const appId = @Safe.Json(Model.Id);
const response = await fetch(url);
if (response.ok) {
document.getElementById(`AppSales-${appId}`).outerHTML = await response.text();
const initScript = document.querySelector(`#AppSales-${appId} script`);
if (initScript) eval(initScript.innerHTML);
const data = document.querySelector(`#AppSales-${appId} template`);
if (data) window.appSales.dataLoaded(JSON.parse(data.innerHTML));
}
})();
</script>
@ -40,54 +42,15 @@
<span class="sales-count">@Model.SalesCount</span> Total @label
</span>
<div class="btn-group only-for-js" role="group" aria-label="Filter">
<input type="radio" class="btn-check" name="AppSalesPeriod-@Model.App.Id" id="AppSalesPeriodWeek-@Model.App.Id" value="@AppSalesPeriod.Week" @(Model.Period == AppSalesPeriod.Week ? "checked" : "")>
<label class="btn btn-link" for="AppSalesPeriodWeek-@Model.App.Id">1W</label>
<input type="radio" class="btn-check" name="AppSalesPeriod-@Model.App.Id" id="AppSalesPeriodMonth-@Model.App.Id" value="@AppSalesPeriod.Month" @(Model.Period == AppSalesPeriod.Month ? "checked" : "")>
<label class="btn btn-link" for="AppSalesPeriodMonth-@Model.App.Id">1M</label>
<input type="radio" class="btn-check" name="AppSalesPeriod-@Model.Id" id="AppSalesPeriodWeek-@Model.Id" value="@AppSalesPeriod.Week" @(Model.Period == AppSalesPeriod.Week ? "checked" : "")>
<label class="btn btn-link" for="AppSalesPeriodWeek-@Model.Id">1W</label>
<input type="radio" class="btn-check" name="AppSalesPeriod-@Model.Id" id="AppSalesPeriodMonth-@Model.Id" value="@AppSalesPeriod.Month" @(Model.Period == AppSalesPeriod.Month ? "checked" : "")>
<label class="btn btn-link" for="AppSalesPeriodMonth-@Model.Id">1M</label>
</div>
</header>
<div class="ct-chart"></div>
<script>
(function () {
const id = @Safe.Json($"AppSales-{Model.App.Id}");
const appId = @Safe.Json(Model.App.Id);
const period = @Safe.Json(Model.Period.ToString());
const baseUrl = @Safe.Json(Url.Action("AppSales", "UIApps", new { appId = Model.App.Id }));
const data = { series: @Safe.Json(Model.Series), salesCount: @Safe.Json(Model.SalesCount) };
const render = (data, period) => {
const series = data.series.map(s => s.salesCount);
const labels = data.series.map((s, i) => period === @Safe.Json(Model.Period.ToString()) ? s.label : (i % 5 === 0 ? s.label : ''));
const min = Math.min(...series);
const max = Math.max(...series);
const low = min === max ? 0 : Math.max(min - ((max - min) / 5), 0);
document.querySelectorAll(`#${id} .sales-count`).innerText = data.salesCount;
new Chartist.Bar(`#${id} .ct-chart`, {
labels,
series: [series]
}, {
low,
});
};
render(data, period);
const update = async period => {
const url = `${baseUrl}/${period}`;
const response = await fetch(url);
if (response.ok) {
const data = await response.json();
render(data, period);
}
};
delegate('change', `#${id} [name="AppSalesPeriod-${appId}"]`, async e => {
const type = e.target.value;
await update(type);
});
})();
</script>
<template>
@Safe.Json(Model)
</template>
}
</div>

@ -0,0 +1,45 @@
if (!window.appSales) {
window.appSales =
{
dataLoaded: function (model) {
const id = "AppSales-" + model.id;
const appId = model.id;
const period = model.period;
const baseUrl = model.url;
const data = model;
const render = (data, period) => {
const series = data.series.map(s => s.salesCount);
const labels = data.series.map((s, i) => period === model.period ? s.label : (i % 5 === 0 ? s.label : ''));
const min = Math.min(...series);
const max = Math.max(...series);
const low = min === max ? 0 : Math.max(min - ((max - min) / 5), 0);
document.querySelectorAll(`#${id} .sales-count`).innerText = data.salesCount;
new Chartist.Bar(`#${id} .ct-chart`, {
labels,
series: [series]
}, {
low,
});
};
render(data, period);
const update = async period => {
const url = `${baseUrl}/${period}`;
const response = await fetch(url);
if (response.ok) {
const data = await response.json();
render(data, period);
}
};
delegate('change', `#${id} [name="AppSalesPeriod-${appId}"]`, async e => {
const type = e.target.value;
await update(type);
});
}
};
}

@ -1,11 +1,14 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Components.AppSales;
using BTCPayServer.Data;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewComponents;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
namespace BTCPayServer.Components.AppTopItems;
@ -18,18 +21,29 @@ public class AppTopItems : ViewComponent
_appService = appService;
}
public async Task<IViewComponentResult> InvokeAsync(AppTopItemsViewModel vm)
public async Task<IViewComponentResult> InvokeAsync(string appId, string appType)
{
if (vm.App == null)
throw new ArgumentNullException(nameof(vm.App));
var type = _appService.GetAppType(appType);
if (type is not IHasItemStatsAppType salesAppType || type is not AppBaseType appBaseType)
return new HtmlContentViewComponentResult(new StringHtmlContent(string.Empty));
var vm = new AppTopItemsViewModel
{
Id = appId,
AppType = appType,
DataUrl = Url.Action("AppTopItems", "UIApps", new { appId }),
InitialRendering = HttpContext.GetAppData()?.Id != appId
};
if (vm.InitialRendering)
return View(vm);
var entries = Enum.Parse<AppType>(vm.App.AppType) == AppType.Crowdfund
? await _appService.GetPerkStats(vm.App)
: await _appService.GetItemStats(vm.App);
var app = HttpContext.GetAppData();
var entries = await _appService.GetItemStats(app);
vm.SalesCount = entries.Select(e => e.SalesCount).ToList();
vm.Entries = entries.ToList();
vm.AppType = app.AppType;
vm.AppUrl = await appBaseType.ConfigureLink(app);
vm.Name = app.Name;
return View(vm);
}

@ -1,12 +1,16 @@
using System.Collections.Generic;
using BTCPayServer.Data;
using BTCPayServer.Services.Apps;
namespace BTCPayServer.Components.AppTopItems;
public class AppTopItemsViewModel
{
public AppData App { get; set; }
public string Id { get; set; }
public string Name { get; set; }
public string AppType { get; set; }
public string AppUrl { get; set; }
public string DataUrl { get; set; }
public List<ItemStats> Entries { get; set; }
public List<int> SalesCount { get; set; }
public bool InitialRendering { get; set; }
}

@ -1,16 +1,16 @@
@using BTCPayServer.Services.Apps
@using BTCPayServer.Plugins.Crowdfund
@model BTCPayServer.Components.AppTopItems.AppTopItemsViewModel
@{
var controller = $"UI{Model.App.AppType}";
var action = $"Update{Model.App.AppType}";
var label = Model.App.AppType == nameof(AppType.Crowdfund) ? "contribution" : "sale";
var label = Model.AppType == CrowdfundAppType.AppType ? "contribution" : "sale";
}
<div id="AppTopItems-@Model.App.Id" class="widget app-top-items">
<div id="AppTopItems-@Model.Id" class="widget app-top-items">
<header class="mb-3">
<h3>Top @(Model.App.AppType == nameof(AppType.Crowdfund) ? "Perks" : "Items")</h3>
<a asp-controller="@controller" asp-action="@action" asp-route-appId="@Model.App.Id">View All</a>
<h3>Top @(Model.AppType == CrowdfundAppType.AppType ? "Perks" : "Items")</h3>
@if (!string.IsNullOrEmpty(Model.AppUrl))
{
<a href="@Model.AppUrl">View All</a>
}
</header>
@if (Model.InitialRendering)
{
@ -19,37 +19,26 @@
<span class="visually-hidden">Loading...</span>
</div>
</div>
<script src="~/Components/AppTopItems/Default.cshtml.js" asp-append-version="true"></script>
<script>
(async () => {
const url = @Safe.Json(Url.Action("AppTopItems", "UIApps", new { appId = Model.App.Id }));
const appId = @Safe.Json(Model.App.Id);
const response = await fetch(url);
if (response.ok) {
document.getElementById(`AppTopItems-${appId}`).outerHTML = await response.text();
const initScript = document.querySelector(`#AppTopItems-${appId} script`);
if (initScript) eval(initScript.innerHTML);
}
})();
</script>
(async () => {
const url = @Safe.Json(Model.DataUrl);
const appId = @Safe.Json(Model.Id);
const response = await fetch(url);
if (response.ok) {
document.getElementById(`AppTopItems-${appId}`).outerHTML = await response.text();
const data = document.querySelector(`#AppSales-${appId} template`);
if (data) window.appSales.dataLoaded(JSON.parse(data.innerHTML));
}
})();
</script>
}
else if (Model.Entries.Any())
{
<div class="ct-chart mb-3"></div>
<script>
(function () {
const id = @Safe.Json($"AppTopItems-{Model.App.Id}");
const series = @Safe.Json(Model.Entries.Select(i => i.SalesCount));
new Chartist.Bar(`#${id} .ct-chart`, { series }, {
distributeSeries: true,
horizontalBars: true,
showLabel: false,
stackBars: true,
axisY: {
offset: 0
}
});
})();
</script>
<template>
@Safe.Json(Model)
</template>
<div class="app-items">
@for (var i = 0; i < Model.Entries.Count; i++)
{

@ -0,0 +1,18 @@
if (!window.appTopItems) {
window.appTopItems =
{
dataLoaded: function (model) {
const id = "AppTopItems-" + model.id;
const series = model.salesCount;
new Chartist.Bar(`#${id} .ct-chart`, { series }, {
distributeSeries: true,
horizontalBars: true,
showLabel: false,
stackBars: true,
axisY: {
offset: 0
}
});
}
};
}

@ -1,3 +1,5 @@
using System.Collections.Generic;
namespace BTCPayServer.Components.Icon
{
public class IconViewModel

@ -1,104 +1,22 @@
@using NBitcoin.DataEncoders
@using NBitcoin
@using BTCPayServer.Abstractions.TagHelpers
@model BTCPayServer.Components.LabelManager.LabelViewModel
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Antiforgery
@{
var commonCall = Model.ObjectId.Type + Model.ObjectId.Id;
var elementId = "a" + Encoders.Base58.EncodeData(RandomUtils.GetBytes(16));
var fetchUrl = Url.Action("GetLabels", "UIWallets", new {
walletId = Model.WalletObjectId.WalletId,
excludeTypes = Safe.Json(Model.ExcludeTypes)
});
var updateUrl = Model.AutoUpdate? Url.Action("UpdateLabels", "UIWallets", new {
walletId = Model.WalletObjectId.WalletId
}): string.Empty;
}
<link href="~/vendor/tom-select/tom-select.bootstrap5.min.css" rel="stylesheet">
<script src="~/vendor/tom-select/tom-select.complete.min.js"></script>
<script>
const updateUrl = @Safe.Json(Url.Action("UpdateLabels", "UIWallets", new {
Model.ObjectId.WalletId
}));
const getUrl = @Safe.Json(@Url.Action("GetLabels", "UIWallets", new {
walletId = Model.ObjectId.WalletId,
excludeTypes = true
}));
const commonCall = @Safe.Json(commonCall);
const elementId = @Safe.Json(elementId);
if (!window[commonCall]) {
window[commonCall] = fetch(getUrl, {
method: 'GET',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
}).then(response => {
return response.json();
});
}
document.addEventListener("DOMContentLoaded", async () => {
const element = document.querySelector(`#${elementId}`);
if (element) {
const labelsFetchTask = await window[commonCall];
const config = {
create: true,
items: @Safe.Json(Model.SelectedLabels),
options: labelsFetchTask,
valueField: "label",
labelField: "label",
searchField: "label",
allowEmptyOption: false,
closeAfterSelect: false,
persist: true,
render: {
option: function(data, escape) {
return `<div ${data.color? `style='background-color:${data.color}; color:${data.textColor}'`: ""}>${escape(data.label)}</div>`;
},
item: function(data, escape) {
return `<div ${data.color? `style='background-color:${data.color}; color:${data.textColor}'`: ""}>${escape(data.label)}</div>`;
}
},
onItemAdd: (val) => {
window[commonCall] = window[commonCall].then(labels => {
return [...labels, { label: val }]
});
document.dispatchEvent(new CustomEvent(`${commonCall}-option-added`, {
detail: val
}));
},
onChange: async (values) => {
select.lock();
try {
const response = await fetch(updateUrl, {
method: "POST",
credentials: "include",
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
address: @Safe.Json(Model.ObjectId.Id),
labels: select.items
})
});
if (!response.ok) {
throw new Error('Network response was not OK');
}
} catch (error) {
console.error('There has been a problem with your fetch operation:', error);
} finally {
select.unlock();
}
}
};
const select = new TomSelect(element, config);
document.addEventListener(`${commonCall}-option-added`, evt => {
if (!(evt.detail in select.options)) {
select.addOption({
label: evt.detail
})
}
})
}
})
</script>
<input id="@elementId" placeholder="Select labels to associate with this object" autocomplete="off" class="form-control label-manager"/>
<input id="@elementId" placeholder="Select labels" autocomplete="off" value="@string.Join(",", Model.SelectedLabels)"
class="only-for-js form-control label-manager ts-wrapper @(Model.DisplayInline ? "ts-inline" : "")"
data-fetch-url="@fetchUrl"
data-update-url="@updateUrl"
data-wallet-id="@Model.WalletObjectId.WalletId"
data-wallet-object-id="@Model.WalletObjectId.Id"
data-wallet-object-type="@Model.WalletObjectId.Type"
data-select-element="@Model.SelectElement"
data-labels='@Safe.Json(Model.RichLabelInfo)' />

@ -1,3 +1,5 @@
using System;
using System.Collections.Generic;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Mvc;
@ -5,14 +7,25 @@ namespace BTCPayServer.Components.LabelManager
{
public class LabelManager : ViewComponent
{
public IViewComponentResult Invoke(WalletObjectId walletObjectId, string[] selectedLabels)
public IViewComponentResult Invoke(WalletObjectId walletObjectId, string[] selectedLabels, bool excludeTypes = true, bool displayInline = false, Dictionary<string, RichLabelInfo> richLabelInfo = null, bool autoUpdate = true, string selectElement = null)
{
var vm = new LabelViewModel
{
ObjectId = walletObjectId,
SelectedLabels = selectedLabels
ExcludeTypes = excludeTypes,
WalletObjectId = walletObjectId,
SelectedLabels = selectedLabels ?? Array.Empty<string>(),
DisplayInline = displayInline,
RichLabelInfo = richLabelInfo,
AutoUpdate = autoUpdate,
SelectElement = selectElement
};
return View(vm);
}
}
public class RichLabelInfo
{
public string Link { get; set; }
public string Tooltip { get; set; }
}
}

@ -1,3 +1,4 @@
using System.Collections.Generic;
using BTCPayServer.Services;
namespace BTCPayServer.Components.LabelManager
@ -5,6 +6,11 @@ namespace BTCPayServer.Components.LabelManager
public class LabelViewModel
{
public string[] SelectedLabels { get; set; }
public WalletObjectId ObjectId { get; set; }
public WalletObjectId WalletObjectId { get; set; }
public bool ExcludeTypes { get; set; }
public bool DisplayInline { get; set; }
public Dictionary<string, RichLabelInfo> RichLabelInfo { get; set; }
public bool AutoUpdate { get; set; }
public string SelectElement { get; set; }
}
}

@ -33,7 +33,7 @@
</a>
</li>
<li class="nav-item">
<a asp-area="" asp-controller="UIStores" asp-action="GeneralSettings" asp-route-storeId="@Model.Store.Id" class="nav-link @ViewData.IsActivePage(new [] {StoreNavPages.Rates, StoreNavPages.CheckoutAppearance, StoreNavPages.General, StoreNavPages.Tokens, StoreNavPages.Users, StoreNavPages.Plugins, StoreNavPages.Webhooks, StoreNavPages.PayoutProcessors, StoreNavPages.Emails})" id="StoreNav-StoreSettings">
<a asp-area="" asp-controller="UIStores" asp-action="GeneralSettings" asp-route-storeId="@Model.Store.Id" class="nav-link @ViewData.IsActivePage(new [] {StoreNavPages.Rates, StoreNavPages.CheckoutAppearance, StoreNavPages.General, StoreNavPages.Tokens, StoreNavPages.Users, StoreNavPages.Webhooks, StoreNavPages.PayoutProcessors, StoreNavPages.Emails})" id="StoreNav-StoreSettings">
<vc:icon symbol="settings"/>
<span>Settings</span>
</a>
@ -145,7 +145,10 @@
</a>
</li>
<li class="nav-item" permission="@Policies.CanModifyStoreSettings">
<a asp-area="" asp-controller="UIStorePullPayments" asp-action="Payouts" asp-route-storeId="@Model.Store.Id" class="nav-link @ViewData.IsActivePage(StoreNavPages.Payouts)" id="StoreNav-Payouts">
<a asp-area=""
asp-controller="UIStorePullPayments" asp-action="Payouts"
asp-route-pullPaymentId=""
asp-route-storeId="@Model.Store.Id" class="nav-link @ViewData.IsActivePage(StoreNavPages.Payouts)" id="StoreNav-Payouts">
<vc:icon symbol="payouts"/>
<span>Payouts</span>
</a>
@ -155,30 +158,6 @@
</div>
</div>
</div>
<div class="accordion-item" permission="@Policies.CanModifyStoreSettings">
<header class="accordion-header" id="Nav-Apps-Header">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#Nav-Apps" aria-expanded="true" aria-controls="Nav-Apps">
Apps
<vc:icon symbol="caret-down"/>
</button>
</header>
<div id="Nav-Apps" class="accordion-collapse collapse show" aria-labelledby="Nav-Apps-Header">
<div class="accordion-body">
<ul class="navbar-nav">
@foreach (var app in Model.Apps)
{
<vc:ui-extension-point location="apps-nav" model="@app"/>
}
<li class="nav-item">
<a asp-area="" asp-controller="UIApps" asp-action="CreateApp" asp-route-storeId="@Model.Store.Id" class="nav-link @ViewData.IsActivePage(AppsNavPages.Create)" id="StoreNav-CreateApp">
<vc:icon symbol="new"/>
<span>New App</span>
</a>
</li>
</ul>
</div>
</div>
</div>
}
<div class="accordion-item">
<header class="accordion-header" id="Nav-Plugins-Header">
@ -195,9 +174,11 @@
{
<vc:ui-extension-point location="store-integrations-nav" model="@Model"/>
}
</ul>
<ul class="navbar-nav">
<li class="nav-item" permission="@Policies.CanModifyServerSettings">
<a asp-area="" asp-controller="UIServer" asp-action="ListPlugins" class="nav-link @ViewData.IsActivePage(ServerNavPages.Plugins)" id="Nav-ManagePlugins">
<vc:icon symbol="new"/>
<vc:icon symbol="plugin"/>
<span>Manage Plugins</span>
</a>
</li>

@ -74,7 +74,7 @@ namespace BTCPayServer.Components.MainNav
Id = a.Id,
IsOwner = a.IsOwner,
AppName = a.AppName,
AppType = Enum.Parse<AppType>(a.AppType)
AppType = a.AppType
}).ToList();
if (PoliciesSettings.Experimental)

@ -19,7 +19,7 @@ namespace BTCPayServer.Components.MainNav
{
public string Id { get; set; }
public string AppName { get; set; }
public AppType AppType { get; set; }
public string AppType { get; set; }
public bool IsOwner { get; set; }
}
}

@ -9,7 +9,7 @@
"invoice_expiredpaidpartial" => "notifications-invoice-failure",
"invoice_failedtoconfirm" => "notifications-invoice-failure",
"invoice_confirmed" => "notifications-invoice-settled",
"invoice_paidafterexpiration" => "notifications-settled",
"invoice_paidafterexpiration" => "notifications-invoice-settled",
"external-payout-transaction" => "notifications-payout",
"payout_awaitingapproval" => "notifications-payout",
"payout_awaitingpayment" => "notifications-payout-approved",

@ -6,51 +6,53 @@
@if (Model.Total is null || Model.Total.Value > pageSizeOptions.Min())
{
<nav aria-label="..." class="w-100 clearfix">
<nav aria-label="..." class="d-flex flex-wrap gap-3 justify-content-between">
@if (Model.Total is null || Model.Total.Value > Model.Count)
{
<ul class="pagination float-start">
<li class="page-item @(Model.Skip == 0 ? "disabled" : null)">
<a class="page-link" tabindex="-1" href="@NavigatePages(-1, Model.Count)">&laquo;</a>
</li>
<ul class="pagination me-auto">
@if (Model.Skip > 0)
{
<li class="page-item">
<a class="page-link" tabindex="-1" href="@NavigatePages(-1, Model.Count)">Prev</a>
</li>
}
<li class="page-item disabled">
<span class="page-link @(Model.Total is null ? "px-0" : (Model.Skip == 0 ? "ps-0" : null))">
@if (Model.Total is null)
{
<span class="page-link">
@(Model.Skip + 1)@(Model.Skip + Model.Count)
</span>
}
else if (Model.Total.Value <= Model.Count)
{
<span class="page-link">
1@Model.Total.Value
</span>
@($"{Model.Skip + 1} {Model.Skip + Model.Count}")
}
else if (Model.Total.Value <= Model.Count)
{
@($"1 {Model.Total.Value}")
}
else
{
<span class="page-link">
@(Model.Skip + 1)@(Model.Skip + Model.Count), Total: @Model.Total.Value
</span>
@($"{Model.Skip + 1} {Model.Skip + Model.Count}, Total: {Model.Total.Value}")
}
</span>
</li>
<li class="page-item @(((Model.Total is null && Model.CurrentPageCount >= Model.Count) || (Model.Total is not null && Model.Total.Value > (Model.Skip + Model.Count))) ? null : "disabled")">
<a class="page-link" href="@NavigatePages(1, Model.Count)">&raquo;</a>
</li>
@if ((Model.Total is null && Model.CurrentPageCount >= Model.Count) || (Model.Total is not null && Model.Total.Value > Model.Skip + Model.Count))
{
<li class="page-item">
<a class="page-link" href="@NavigatePages(1, Model.Count)">Next</a>
</li>
}
</ul>
}
@if (Model.Total is null || Model.Total.Value >= pageSizeOptions.Min())
{
<ul class="pagination float-end">
<ul class="pagination ms-auto">
<li class="page-item disabled">
<span class="page-link">Page Size:</span>
<span class="page-link">Page Size</span>
</li>
@foreach (int pageSize in pageSizeOptions)
@foreach (var pageSize in pageSizeOptions)
{
if (Model.Total is null || Model.Total.Value >= pageSize)
{
<li class="page-item @(Model.Count == pageSize ? "active" : null)">
<a class="page-link" href="@NavigatePages(0, pageSize)">@pageSize</a>
<a class="page-link @(Model.Count != pageSize && pageSize == pageSizeOptions.Max() ? "pe-0" : null)" href="@NavigatePages(0, pageSize)">@pageSize</a>
</li>
}
}

@ -9,8 +9,8 @@ namespace BTCPayServer.Components.QRCode
{
public class QRCode : ViewComponent
{
private static QRCodeGenerator _qrGenerator = new ();
private static QRCodeGenerator _qrGenerator = new();
public IViewComponentResult Invoke(string data)
{
var qrCodeData = _qrGenerator.CreateQrCode(data, QRCodeGenerator.ECCLevel.Q);

@ -1,6 +1,8 @@
@using BTCPayServer.Abstractions.Extensions
@using BTCPayServer.Client.Models
@using BTCPayServer.Services
@using BTCPayServer.Services.Invoices
@inject DisplayFormatter DisplayFormatter
@model BTCPayServer.Components.StoreRecentInvoices.StoreRecentInvoicesViewModel
<div class="widget store-recent-invoices" id="StoreRecentInvoices-@Model.Store.Id">
@ -63,7 +65,7 @@
</span>
}
</td>
<td class="text-end">@invoice.AmountCurrency</td>
<td class="text-end">@DisplayFormatter.Currency(invoice.Amount, invoice.Currency)</td>
</tr>
}
</tbody>

@ -7,7 +7,8 @@ public class StoreRecentInvoiceViewModel
{
public string InvoiceId { get; set; }
public string OrderId { get; set; }
public string AmountCurrency { get; set; }
public decimal Amount { get; set; }
public string Currency { get; set; }
public InvoiceState Status { get; set; }
public DateTimeOffset Date { get; set; }
public bool HasRefund { get; set; }

@ -61,7 +61,8 @@ public class StoreRecentInvoices : ViewComponent
HasRefund = invoice.Refunds.Any(),
InvoiceId = invoice.Id,
OrderId = invoice.Metadata.OrderId ?? string.Empty,
AmountCurrency = _currencyNameTable.DisplayFormatCurrency(invoice.Price, invoice.Currency),
Amount = invoice.Price,
Currency = invoice.Currency
}).ToList();
return View(vm);

@ -1,4 +1,6 @@
@using BTCPayServer.Abstractions.Extensions
@using BTCPayServer.Services
@inject DisplayFormatter DisplayFormatter
@model BTCPayServer.Components.StoreRecentTransactions.StoreRecentTransactionsViewModel
<div class="widget store-recent-transactions" id="StoreRecentTransactions-@Model.Store.Id">
@ -34,6 +36,7 @@
<tr>
<th class="w-125px">Date</th>
<th>Transaction</th>
<th>Labels</th>
<th class="text-end">Amount</th>
</tr>
</thead>
@ -43,17 +46,37 @@
<tr>
<td>@tx.Timestamp.ToTimeAgo()</td>
<td>
<a href="@tx.Link" target="_blank" rel="noreferrer noopener" class="text-break">
@tx.Id
</a>
<vc:truncate-center text="@tx.Id" link="@tx.Link" classes="truncate-center-id" />
</td>
<td>
@if (tx.Labels.Any())
{
<div class="d-flex flex-wrap gap-2 align-items-center">
@foreach (var label in tx.Labels)
{
<div class="transaction-label" style="--label-bg:@label.Color;--label-fg:@label.TextColor">
<span>@label.Text</span>
@if (!string.IsNullOrEmpty(label.Link))
{
<a class="transaction-label-info transaction-details-icon" href="@label.Link"
target="_blank" rel="noreferrer noopener" title="@label.Tooltip" data-bs-html="true"
data-bs-toggle="tooltip" data-bs-custom-class="transaction-label-tooltip">
<vc:icon symbol="info" />
</a>
}
</div>
}
</div>
}
</td>
@if (tx.Positive)
{
<td class="text-end text-success">@tx.Balance</td>
<td class="text-end text-success">@DisplayFormatter.Currency(tx.Balance, tx.Currency)</td>
}
else
{
<td class="text-end text-danger">@tx.Balance</td>
<td class="text-end text-danger">@DisplayFormatter.Currency(tx.Balance, tx.Currency)</td>
}
</tr>
}

@ -1,13 +1,17 @@
using System;
using System.Collections.Generic;
using BTCPayServer.Models.WalletViewModels;
namespace BTCPayServer.Components.StoreRecentTransactions;
public class StoreRecentTransactionViewModel
{
public string Id { get; set; }
public string Currency { get; set; }
public string Balance { get; set; }
public bool Positive { get; set; }
public bool IsConfirmed { get; set; }
public string Link { get; set; }
public DateTimeOffset Timestamp { get; set; }
public IEnumerable<TransactionTagModel> Labels { get; set; }
}

@ -8,6 +8,7 @@ using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Data;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Services;
using BTCPayServer.Services.Labels;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
using Dapper;
@ -23,14 +24,20 @@ namespace BTCPayServer.Components.StoreRecentTransactions;
public class StoreRecentTransactions : ViewComponent
{
private readonly BTCPayWalletProvider _walletProvider;
private readonly WalletRepository _walletRepository;
private readonly LabelService _labelService;
public BTCPayNetworkProvider NetworkProvider { get; }
public StoreRecentTransactions(
BTCPayNetworkProvider networkProvider,
BTCPayWalletProvider walletProvider)
BTCPayWalletProvider walletProvider,
WalletRepository walletRepository,
LabelService labelService)
{
NetworkProvider = networkProvider;
_walletProvider = walletProvider;
_walletRepository = walletRepository;
_labelService = labelService;
}
public async Task<IViewComponentResult> InvokeAsync(StoreRecentTransactionsViewModel vm)
@ -52,15 +59,25 @@ public class StoreRecentTransactions : ViewComponent
var network = derivationSettings.Network;
var wallet = _walletProvider.GetWallet(network);
var allTransactions = await wallet.FetchTransactionHistory(derivationSettings.AccountDerivation, 0, 5, TimeSpan.FromDays(31.0));
var walletTransactionsInfo = await _walletRepository.GetWalletTransactionsInfo(vm.WalletId, allTransactions.Select(t => t.TransactionId.ToString()).ToArray());
transactions = allTransactions
.Select(tx => new StoreRecentTransactionViewModel
.Select(tx =>
{
Id = tx.TransactionId.ToString(),
Positive = tx.BalanceChange.GetValue(network) >= 0,
Balance = tx.BalanceChange.ShowMoney(network),
IsConfirmed = tx.Confirmations != 0,
Link = string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink, tx.TransactionId.ToString()),
Timestamp = tx.SeenAt
walletTransactionsInfo.TryGetValue(tx.TransactionId.ToString(), out var transactionInfo);
var labels = _labelService.CreateTransactionTagModels(transactionInfo, Request);
return new StoreRecentTransactionViewModel
{
Id = tx.TransactionId.ToString(),
Positive = tx.BalanceChange.GetValue(network) >= 0,
Balance = tx.BalanceChange.ShowMoney(network),
Currency = vm.CryptoCode,
IsConfirmed = tx.Confirmations != 0,
Link = string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink,
tx.TransactionId.ToString()),
Timestamp = tx.SeenAt,
Labels = labels
};
})
.ToList();
}

@ -0,0 +1,17 @@
@model BTCPayServer.Components.TruncateCenter.TruncateCenterViewModel
<span class="truncate-center @Model.Classes">
<span class="truncate-center-truncated" @(Model.Truncated != Model.Text ? $"data-bs-toggle=tooltip title={Model.Text}" : "")>@Model.Truncated</span>
<span class="truncate-center-text">@Model.Text</span>
@if (Model.Copy)
{
<button type="button" class="btn btn-link p-0" data-clipboard="@Model.Text">
<vc:icon symbol="copy" />
</button>
}
@if (!string.IsNullOrEmpty(Model.Link))
{
<a href="@Model.Link" rel="noreferrer noopener" target="_blank">
<vc:icon symbol="info" />
</a>
}
</span>

@ -0,0 +1,33 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewComponents;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
namespace BTCPayServer.Components.TruncateCenter;
/// <summary>
/// Truncates long strings in the center with ellipsis: Turns e.g. a BOLT11 into "lnbcrt7…q2ns60y"
/// </summary>
/// <param name="text">The full text, e.g. a Bitcoin address or BOLT11</param>
/// <param name="link">Optional link, e.g. a block explorer URL</param>
/// <param name="classes">Optional additional CSS classes</param>
/// <param name="padding">The number of characters to show on each side</param>
/// <param name="copy">Display a copy button</param>
/// <returns>HTML with truncated string</returns>
public class TruncateCenter : ViewComponent
{
public IViewComponentResult Invoke(string text, string link = null, string classes = null, int padding = 7, bool copy = true)
{
if (string.IsNullOrEmpty(text))
return new HtmlContentViewComponentResult(new StringHtmlContent(string.Empty));
var vm = new TruncateCenterViewModel
{
Classes = classes,
Padding = padding,
Copy = copy,
Text = text,
Link = link,
Truncated = text.Length > 2 * padding ? $"{text[..padding]}…{text[^padding..]}" : text
};
return View(vm);
}
}

@ -0,0 +1,12 @@
namespace BTCPayServer.Components.TruncateCenter
{
public class TruncateCenterViewModel
{
public string Text { get; set; }
public string Truncated { get; set; }
public string Classes { get; set; }
public string Link { get; set; }
public int Padding { get; set; }
public bool Copy { get; set; }
}
}

@ -67,11 +67,21 @@ namespace BTCPayServer.Configuration
if (conf.GetOrDefault<string>("POSTGRES", null) == null)
{
if (conf.GetOrDefault<string>("SQLITEFILE", null) != null)
Logs.Configuration.LogWarning("SQLITE backend support is out of support. Please migrate to Postgres by following the following instructions https://github.com/btcpayserver/btcpayserver/blob/master/docs/db-migration.md");
if (conf.GetOrDefault<string>("MYSQL", null) != null)
Logs.Configuration.LogWarning("MYSQL backend support is out of support. Please migrate to Postgres by following the following instructions (https://github.com/btcpayserver/btcpayserver/blob/master/docs/db-migration.md)");
var allowDeprecated = conf.GetOrDefault<bool>("DEPRECATED", false);
if (allowDeprecated)
{
if (conf.GetOrDefault<string>("SQLITEFILE", null) != null)
Logs.Configuration.LogWarning("SQLITE backend support is out of support. Please migrate to Postgres by following the following instructions https://github.com/btcpayserver/btcpayserver/blob/master/docs/db-migration.md");
if (conf.GetOrDefault<string>("MYSQL", null) != null)
Logs.Configuration.LogWarning("MYSQL backend support is out of support. Please migrate to Postgres by following the following instructions (https://github.com/btcpayserver/btcpayserver/blob/master/docs/db-migration.md)");
}
else
{
if (conf.GetOrDefault<string>("SQLITEFILE", null) != null)
throw new ConfigException("SQLITE backend support is out of support. Please migrate to Postgres by following the following instructions (https://github.com/btcpayserver/btcpayserver/blob/master/docs/db-migration.md). If you don't want to update, you can try to start this instance by using the command line argument --deprecated");
if (conf.GetOrDefault<string>("MYSQL", null) != null)
throw new ConfigException("MYSQL backend support is out of support. Please migrate to Postgres by following the following instructions (https://github.com/btcpayserver/btcpayserver/blob/master/docs/db-migration.md). If you don't want to update, you can try to start this instance by using the command line argument --deprecated");
}
}
DockerDeployment = conf.GetOrDefault<bool>("dockerdeployment", true);
TorrcFile = conf.GetOrDefault<string>("torrcfile", null);
@ -148,9 +158,6 @@ namespace BTCPayServer.Configuration
}
DisableRegistration = conf.GetOrDefault<bool>("disable-registration", true);
var pluginRemote = conf.GetOrDefault<string>("plugin-remote", null);
if (pluginRemote != null)
Logs.Configuration.LogWarning("plugin-remote is an obsolete configuration setting, please remove it from configuration");
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)

@ -30,6 +30,7 @@ namespace BTCPayServer.Configuration
app.Option("--mysql", $"DEPRECATED: Connection string to a MySQL database", CommandOptionType.SingleValue);
app.Option("--nocsp", $"Disable CSP (default false)", CommandOptionType.BoolValue);
app.Option("--sqlitefile", $"DEPRECATED: File name to an SQLite database file inside the data directory", CommandOptionType.SingleValue);
app.Option("--deprecated", $"Allow deprecated settings (default:false)", CommandOptionType.BoolValue);
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("--rootpath", "The root path in the URL to access BTCPay (default: /)", CommandOptionType.SingleValue);
app.Option("--sshconnection", "SSH server to manage BTCPay under the form user@server:port (default: root@externalhost or empty)", CommandOptionType.SingleValue);
@ -45,7 +46,6 @@ namespace BTCPayServer.Configuration
app.Option("--debuglog", "A rolling log file for debug messages.", CommandOptionType.SingleValue);
app.Option("--debugloglevel", "The severity you log (default:information)", CommandOptionType.SingleValue);
app.Option("--disable-registration", "Disables new user registrations (default:true)", CommandOptionType.SingleValue);
app.Option("--plugin-remote", "Obsolete, do not use", 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 some helper UI to facilitate dev-time testing (Default false)", CommandOptionType.BoolValue);
@ -137,7 +137,7 @@ namespace BTCPayServer.Configuration
foreach (var n in new BTCPayNetworkProvider(networkType).GetAll().OfType<BTCPayNetwork>())
{
builder.AppendLine(CultureInfo.InvariantCulture, $"#{n.CryptoCode}.explorer.url={n.NBXplorerNetwork.DefaultSettings.DefaultUrl}");
builder.AppendLine(CultureInfo.InvariantCulture, $"#{n.CryptoCode}.explorer.cookiefile={ n.NBXplorerNetwork.DefaultSettings.DefaultCookieFile}");
builder.AppendLine(CultureInfo.InvariantCulture, $"#{n.CryptoCode}.explorer.cookiefile={n.NBXplorerNetwork.DefaultSettings.DefaultCookieFile}");
builder.AppendLine(CultureInfo.InvariantCulture, $"#{n.CryptoCode}.blockexplorerlink=https://mempool.space/tx/{{0}}");
if (n.SupportLightning)
{

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