Compare commits

..

252 Commits

Author SHA1 Message Date
78f33f0ca4 UI: Allow iframe context to get defined by opener (#6615)
The CSS to hide certain elements in the iframe context was introduced in #6574.

The mechanism to define the iframe context is now shifted to the opener, so that we can limit that behaviour to the mobile app.

Closes #6614.
2025-03-03 22:27:49 +09:00
a7e3cbb105 Adding endpoint to set server email settings (#6601)
* Adding endpoint in Greenfield to allow server email settings

* Adding related swagger file

* Refactoring EmailSettingsData to be more readable

* Adding server email masking

* Adding tests

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

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

* Masking smtp server email returned over greenfield api and test

* Retaining password if password mask is used

* Remove magic string *****

* Flatten request for server's settings. Fix bug on shared setting instances

* Remove useless doc

* Simplify code

* Fix Store Email settings page

---------

Co-authored-by: d11n <mail@dennisreimann.de>
Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2025-02-27 15:59:17 +09:00
8b5c5895f0 Selenium tests for Multisig on server (#6487)
* Adding MultisigTests

* Adding fetching of receive address and creating pending transaction

* Completing multisig test flow

* Reverting Selenium ChromeDriver version

* Adding generation of PSBTs

* Removing unnecessary lines

* PSBT test signing now working with multisig dervation scheme

* Updating SignTestPSBT test

* Reducing number of iterations for test funding, to speed up tests

* Bugfixing PSBT problem

* Ensuring that PSBT signing also works for pending transactions

* Ensuring we don't collect count duplicate signatures for same PSBTs

* Resolving bug in PendingTransactionService where Combine was modifying object

* Fixing bug where pending transaction was not broadcased if there was ReturnUrl

* Finally finishing Multisig Selenium test flow with signing PSBTs, broadcasting and cancelling them

* Small nit, waiting loaded element

* Nit: Use AssetElementNotFound

* Fix warning

* Remove code dups

---------

Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2025-02-25 14:39:57 +09:00
9d5baabc2c UI: Fix spacing of Lightning Address info on invoice details page 2025-02-19 09:38:12 +01:00
a7e910f7ff Merge pull request #6602 from btcpayserver/feat/lnd-0.18.5
Bumping LND to 0.18.5-beta
2025-02-18 23:36:32 -05:00
148e31721b Bumping LND to 0.18.5-beta in all yml files we've got 2025-02-18 22:17:24 -06:00
df16ad6418 Bumping LND to 0.18.5-beta 2025-02-18 22:09:55 -06:00
1c8ded9362 Simple nullability fix for StoreRepo.UpdateSettings 2025-02-15 10:18:22 +09:00
9e58a50dfd Merge pull request #6598 from dennisreimann/dashboard-remove-store-name
Dashboard: Remove store name headline
2025-02-11 14:25:06 +09:00
63750d69c4 Merge pull request #6596 from dennisreimann/fix-6592
Forms: Properly support checkbox type
2025-02-11 14:24:16 +09:00
cf6a356e08 Merge pull request #6597 from dennisreimann/fix-6590
Lightning Address: Display validation messages on failed creation
2025-02-11 14:23:21 +09:00
1c6657b8b4 Dashboard: Remove store name headline
Saves some space and isn't really necessary as the current store name is also visible in the sidebar.
2025-02-10 18:47:20 +01:00
32f1d5ea1d Lightning Address: Display validation messages on failed creation
Fixes #6590.
2025-02-10 15:27:36 +01:00
1d44cad847 Forms: Remove unsupported input types 2025-02-10 15:08:58 +01:00
88d8d1b848 Forms: Properly support checkbox type
Fixes #6592.
2025-02-10 15:08:03 +01:00
df82860ada Merge pull request #6594 from NicolasDorier/bump-nbx
Bump NBX
2025-02-10 16:00:36 +09:00
b184067df7 Merge pull request #6595 from NicolasDorier/replacement-log
If an On-Chain payment get replaced, log it in invoice logs rather than console
2025-02-10 15:46:19 +09:00
31c1d4795f If an On-Chain payment get replaced, log it in invoice logs rather than console 2025-02-10 15:29:38 +09:00
e59684fc6a Bump NBX 2025-02-10 15:26:35 +09:00
ddea59cb1b Merge pull request #6589 from NicolasDorier/refactbackreturn
Refactor: Remove cshtml duplication for back/url buttons in wizards
2025-02-07 17:13:04 +09:00
c37584328b Refactor: Remove cshtml duplication for back/url buttons in wizards 2025-02-07 16:57:50 +09:00
60b317a972 Merge pull request #6580 from reneaaron/fix/remove-lnurl-desc-hash-check
Remove LNURL description hash check
2025-02-06 19:02:42 +09:00
4fbcd89bb6 Only change opacity of amount 2025-02-06 17:27:09 +09:00
039e613524 fix: remove lnurl description hash check 2025-01-28 14:57:44 +01:00
192d339a79 Fix: Wallet's transaction had null blockhash on greenfield 2025-01-28 14:42:06 +09:00
7faf95552a Merge pull request #6577 from NicolasDorier/fartcoin
Fix: Invalid currency pair (FARTCOIN_USDC) may show in the logs when using kraken rate provider
2025-01-24 22:58:16 +09:00
bc1a1cf34f Fix: Invalid currency pair (FARTCOIN_USDC) may show in the logs when using kraken rate provider 2025-01-24 21:56:20 +09:00
9f4acdf8be Merge pull request #6576 from dennisreimann/fix-unconf
UI: Display unconfirmed transactions with lower opacity
2025-01-24 21:00:58 +09:00
8fc4aefa8f UI: Display unconfirmed transactions with lower opacity
Fixes a regression introduced in #6190 and also adds this display style to the dashboard list of recent transactions.
2025-01-24 10:08:14 +01:00
ba8feeddd9 Update translations 2025-01-22 00:11:48 +09:00
4e57d27cc8 Bump and changelog 2025-01-22 00:03:51 +09:00
580518e5aa Merge pull request #6572 from dennisreimann/mwb-integration-plugin
Plugins: Convert relative to absolute path when loading a plugin
2025-01-21 23:54:14 +09:00
81cd51cc66 Merge pull request #6570 from btcpayserver/fix-get-store-payment
Fix: Incorrect enabled value for v1/stores/{storeId}/payment-methods/{paymentMethod}
2025-01-21 23:52:18 +09:00
499ddbde65 Dashboard: Fix Lightning balance display for tiny amounts (#6573)
Fixes #6541.
2025-01-21 23:51:31 +09:00
20e8db7307 App: Allow receipt to be shown in iframe (#6574)
Also detects if the checkout page is shown within an iframe and hides the back to store link in that case.
2025-01-21 23:50:27 +09:00
8e927eee73 Better validation of inputs when setting on-chain payment method by API 2025-01-21 23:37:56 +09:00
cdf7cbfeb7 Plugins: Convert relative to absolute path when loading a plugin
Avoids having to provide absolute paths, which differ across machines. We use this for the app to [reference the app plugin](https://github.com/btcpayserver/btcpayserver/blob/mobile-working-branch/BTCPayServer/Properties/launchSettings.json#L7) in the launch settings.
2025-01-21 10:33:56 +01:00
97173c9811 Fix server/info returning incorrect data for XMR/ZCash (#6569) 2025-01-21 17:48:59 +09:00
bbe95d780f Fix: Incorrect enabled value for v1/stores/{storeId}/payment-methods/{paymentMethod} 2025-01-21 17:48:37 +09:00
5534ddeb28 Revert "Bump dependency versions (#6568)"
This reverts commit 2725bbfe2a660cacf5515b7ae1444914df51d1c2.
2025-01-21 16:41:42 +09:00
1e93dccea2 Make Validate.ValidationResult nullable 2025-01-21 16:18:20 +09:00
4e2684b917 Remove app models and hub classes from client lib (#6567) 2025-01-21 15:42:30 +09:00
2725bbfe2a Bump dependency versions (#6568)
Adapt to the updates made on mobile-working-branch
2025-01-21 15:39:28 +09:00
01250f6aee Add ability to move a payout from InProgress to AwaitingPayment (#6564) 2025-01-21 15:37:45 +09:00
6180ee9b80 Add warning on Shopify V1 plugin (#6559)
* Add warning on Shopify V1 plugin

* missing rel in link
2025-01-19 18:19:33 +09:00
d6b547d8bf Bump Payjoin libraries (#6561) 2025-01-19 18:19:05 +09:00
d3d6b2d15c Make HtmlTags safer, properly sanitize in the view as well 2025-01-19 11:00:09 +09:00
1830d398a4 Bump Bitcoin Core to 18.1 (#6563) 2025-01-17 18:59:43 +09:00
846fa30be1 Set kraken as default rate source for CAD 2025-01-17 14:15:12 +09:00
b13d0a4300 Add tooltip and link to pull-payment tags in wallet's transaction list (#6562) 2025-01-16 23:51:00 +09:00
ef8071ba93 App: SignalR-related updates (#6537)
Adds Newtonsoft JSON converters for the SignalR connection, which we need to pass data back and forth.
2025-01-16 14:36:23 +09:00
2e458af4fb App: Authentication updates (#6536)
- Updates API key extraction to also accept "Bearer" auth header. This is necessary for non-cookie based SignalR connections.
- Adds authentication related models to the client lib
- Succeeds and replaces #6484.
2025-01-16 14:34:57 +09:00
1c25d793c7 Store: Allow resetting custom email server (#6547)
* Store: Allow resetting custom email server

Fixes #6546.

* Do not show fallback settings in form
2025-01-16 14:32:53 +09:00
92a92a7fff Rename and add to greenfield HtmlTags and HtmlMetaTags for apps (#6557) 2025-01-15 23:48:10 +09:00
2255da4a56 Merge pull request #6555 from dennisreimann/fix-6554
UI: Fix store's custom CSS URL
2025-01-15 15:15:11 +01:00
955091c793 Merge pull request #6552 from dennisreimann/fix-6548
UI: Fix mobile sidebar height and behaviour for Firefox
2025-01-15 15:14:37 +01:00
e479522d9f Fix: Migration bug on old instances (Could not cast or convert from System.String to BTCPayServer.DerivationSchemeSettings) (#6551) 2025-01-15 23:06:17 +09:00
2250853b3e HTML lang setting and Head tags for POS and Crowdfund public pages (#6229)
* HTML lang setting and Head tags for POS and Crowdfund public pages

* updates #6229

* updates 6229

* resolve conflict

* updated according to Nicolas' recommendations

* updates #6229

* Add RawMeta method in safe.cs

* ...

* resolve conflicts

* resolve conflict

* resolve conflicts

* Updates as Nicolas request

* updates

---------

Co-authored-by: d11n <mail@dennisreimann.de>
2025-01-15 14:49:25 +09:00
ae1eb67677 UI: Fix store's custom CSS URL
Fixes #6554.
2025-01-14 08:13:19 +01:00
c430d3b4ed UI: Fix mobile sidebar height and behaviour for Firefox
Fixes #6548.
2025-01-13 12:45:15 +01:00
e1f47b2738 Bump bitcoin core in tests (#6550) 2025-01-13 12:41:13 +09:00
9d8a231274 Fix warnings 2025-01-11 00:16:17 +09:00
5a8c959b6b Server hanging in debug mode if altcoin node is unavailable (#6545) 2025-01-11 00:15:04 +09:00
e0a51537a1 Fix: Monero payments in mempool not detected if accountIndex != 0 (Fix #6430) (#6544) 2025-01-10 20:21:33 +09:00
d2c52c10bd Always allow to mine a block in cheat mode 2025-01-10 19:16:48 +09:00
5536935ff8 Make Checkout Cheat Mode extensible by plugins (#6543) 2025-01-10 16:17:55 +09:00
0754a809e7 Do not disable a plugin if it crash during debug session 2025-01-09 20:02:12 +09:00
286bf7d822 Fix formatting YML in Update bug_report.yml 2025-01-09 11:04:49 +01:00
a6f67e82b6 Fix formatting error in Update bug_report.yml 2025-01-09 10:53:30 +01:00
408bba6225 Add scammers disclaimer on the issue template
This commits adds a warning notice to the bug template informing users about ignoring fake bot support agents.
2025-01-09 10:51:43 +01:00
ce55666ba3 Merge pull request #6538 from btcpayserver/feat/lnd-0.18.4
Bumping LND to 0.18.4-beta
2025-01-08 17:16:56 -05:00
c766047f63 Bumping LND to 0.18.4-beta in other files as well 2025-01-08 15:59:47 -06:00
07b9eb079d Fix crash while migrating ETH payment data (#6539) 2025-01-08 19:24:37 +09:00
b59182f169 Bumping LND to 0.18.4-beta 2025-01-07 19:11:43 -06:00
6ae36825d5 Changelog 2.0.5 (#6520) 2024-12-23 23:07:53 +09:00
f1719ed3d2 Revert "Add All time to the Time filter, PlanB Assignment (#6514)" (#6519)
This reverts commit 79c5ff9ed00407cd08498a348d408e22885967bf.
2024-12-23 18:56:22 +09:00
79c5ff9ed0 Add All time to the Time filter, PlanB Assignment (#6514)
* Add All time to the Time filter, wip: Clear All funtionality

* Update ListInvoices.cshtml

* Cleanup commented code

* Toggle on 'All Time' if there no startdate selected

---------

Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2024-12-23 18:27:49 +09:00
76a880a30e Disable lightning payments histogram (#6518) 2024-12-23 18:04:07 +09:00
08835895e9 Invoices: Allow admin to see invoices of users (#6517)
* Invoices: Allow admin to see invoices of users

Fixes #6489. As discussed with @TChukwuleta, this succeeds and closes #6497.

* Invoices: Allow admin to see invoices of users

Fixes #6489. As discussed with @TChukwuleta, this succeeds and closes #6497.

* Update controller to allow admin access for basic invoice actions
2024-12-23 17:50:44 +09:00
4ee12b41b1 Dashboard: Fix missing data in Lightning Balance component (#6482)
I wasn't able to reproduce the problem, but this most likely fixes #6480 and #6458.
2024-12-23 16:45:49 +09:00
4fb43cbbad Improve wording for setting up NBXplorer connection string (#6507) 2024-12-23 09:45:49 +09:00
adce1dffb1 Checkout: Add support link to footer (#6511)
Closes #6495.
2024-12-23 09:45:36 +09:00
0f049eee1b UI: Fix inconsistent responsiveness of labels (#6508)
Fixes #6501.
2024-12-23 09:26:07 +09:00
cfc2b9c236 Pull Payment: Minor improvements (#6516)
- Changes the column title from "Refunded" to "Claimed"
- Adds "Copy Link" button to action column

Closes #6515.
2024-12-23 09:25:27 +09:00
637c06fc01 Greenfield: Improve invoice receipt (#6483)
Assembles and provides the same data as the UIInvoiceController. Merging the invoice's receipt options with the store ones was missing in Greenfield.
2024-12-23 09:25:05 +09:00
1ef177ba0f Checkout: Fix plugins integration (#6481)
Fixes a regression introduced in #6316: Moving the `checkout-payment-method` integration point outside the div above broke the UI for the SiodeShift, FixedFloat and Trocador plugins.

Also updated the URL change handler, so that it works for all plugins.
2024-12-23 09:23:47 +09:00
8acf1c2d62 Refactoring: Introduce IUrlHelper.ActionAbsolute (#6513) 2024-12-21 00:16:04 +09:00
44dc6499cd POS: Fix form redirect in conjunction with root path setting (#6506)
* POS: Fix form redirect in conjunction with root path setting

Fixes #6493 and also adds missing status messages on the forms pages.

* Fix other occurrences
2024-12-20 22:59:07 +09:00
f5a420a272 Merge pull request #6503 from ConstantineGhost/master
Removing Baillie Gifford as BTCPay Foundation supporter
2024-12-19 18:53:35 +01:00
d24e0cd1a2 Greenfield: Remove authorization requirement for app data (#6499)
As discussed with @NicolasDorier on Mattermost: Right now only store owners can access the app data, which doesn't contain sensitive info or something beyond what one would see as a regular customer. The app would need a way to access the data for roles other than `Owner` as well, e.g. `Employee`.
2024-12-19 09:54:28 +09:00
b3bc11c19d Resolving Inconsistent responsiveness of the labels on the dashboard and removing BG as a supporter
Adressing Issues #6500 and #6501
2024-12-18 09:41:22 -06:00
fe3bccf3ce Greenfield: Resolve store user's image URL
Amendment to #6427.
2024-12-15 10:02:13 +01:00
7829a93251 POS: Create Invoice action optionally responds with JSON (#6439)
* POS: Create Invoice action optionally responds with JSON

We adapted this action, which is full of custom POS logic, for the app to avoid creating a separate API endpoint.

* Add test and improve error handling
2024-12-13 12:09:55 +09:00
d4b76823a2 Using Bare Bitcoin’s new public API endpoint for rates (#6473) 2024-12-12 09:59:10 +09:00
00cc16455c App: Add events which the app subscribes to (#6435)
* App: Add events which the app subscribes to

Various events, which are relevant for the app to react to changes made on the server.

* Refactor events

* Do not extend NewBlockEvent

* Refactoring events

* Add store role events

* Refactoring: Rename StoreUserEvent

* Fix: Subscribe to UserEvent.Invited

---------

Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2024-12-11 20:11:51 +09:00
6e222c573b Remove altcoins docker build 2024-12-11 00:05:40 +09:00
6f9e0dca51 Add translations 2024-12-10 23:59:20 +09:00
d2c3c37f4d Add changelog for 2.0.4 (#6472) 2024-12-10 23:56:13 +09:00
b797cc9af8 Multisig/watchonly wallet transaction creation flow proof of concept (#5743) 2024-12-10 21:56:52 +09:00
cc915df10e Merge pull request #6469 from NicolasDorier/validatinglightningclient
Plugins: Add a way for LightningClient to be more customizable (Fix #6467)
2024-12-10 19:01:09 +09:00
fe53424710 Merge pull request #6470 from NicolasDorier/globalcheckoutmodelext
Plugin: Add IGlobalCheckoutModelExtension
2024-12-10 18:59:46 +09:00
6cfd4cb633 Merge pull request #6471 from dennisreimann/fix-throttle
POS: Fix throttling for unauthenticated users
2024-12-10 18:54:40 +09:00
4d38f91bd5 POS: Fix throttling for unauthenticated users
Fixes a regression introduced with d24adda70084f0f807d2a98d204b16b509ae2397: The negation for the `_rateLimitService.Throttle` result was removed with that commit, which lead to all unauthenticated request getting throttled. (It was correctly implemented in #6415.

Fixes btcpayserver/app#131.
2024-12-09 18:01:09 +01:00
4f63f08aeb Plugin: Add IGlobalCheckoutModelExtension 2024-12-09 21:29:04 +09:00
e7e7fab1a9 Plugins: Add a way for LightningClient to validate the connection string asynchronously 2024-12-09 21:25:22 +09:00
1214367503 bump nbx image 2024-12-09 16:31:56 +09:00
d24adda700 Fix throttling on PoS 2024-12-09 16:18:07 +09:00
eb89f4636b Merge pull request #6415 from Kukks/rate-limit-pos
Do not throttle pos when logged in
2024-12-09 12:42:51 +09:00
7da247ffd6 Fix test 2024-12-09 09:44:00 +09:00
cadcf27ebc Merge pull request #6433 from dennisreimann/mwb-integration-docker
App: Docker Compose additions
2024-12-09 09:35:14 +09:00
61b882d426 fix: truncate center css for icons (#6465)
currently, the info icon used for displaying a link is way too big since
its not matched by the current css
2024-12-06 18:11:54 +01:00
e4d780417d Merge pull request #6464 from NicolasDorier/refactormails
Access email settings through the factory
2024-12-06 16:35:55 +09:00
4d01e3a16a Access email settings through the factory 2024-12-06 16:19:17 +09:00
52a1627d81 Merge pull request #6438 from dennisreimann/mwb-integration-email-qr
Add QR Code with link to invitation email
2024-12-06 13:39:32 +09:00
d5b9d91a75 Merge pull request #6462 from dennisreimann/fix-flaky-tests
Tests: Fix flakyness of two tests
2024-12-05 19:51:40 +09:00
cd507b10e0 Tests: Fix flakyness 2024-12-05 10:19:08 +01:00
f7a96272c1 Merge pull request #6461 from NicolasDorier/fix6437
Fix: Combination of status filters on invoices page causes 500 fatal server error (Fix #6437)
2024-12-04 23:39:53 +09:00
a8d1f55544 Editor: Use offcanvas for all breakpoints (#6441)
Avoids scrolling the editor out of the viewport. Closes #6436.

Done for the POS/Crowdfund editor, as well as the Forms one.
2024-12-04 15:18:27 +01:00
fb8ca19327 Fix: Combination of status filters on invoices page causes 500 fatal server error (Fix #6437) 2024-12-04 22:07:32 +09:00
341bea48f8 UI: Fix close icon in wizard across views 2024-12-04 12:12:54 +01:00
aa336e7d8a Update and fix docker-compose test 2024-12-04 12:10:45 +01:00
77f9b6a88c Merge branch 'master' into mwb-integration-email-qr 2024-12-04 11:53:10 +01:00
3b94c2798c Updates from code review 2024-12-04 11:48:47 +01:00
70f97382a4 Merge pull request #6412 from NicolasDorier/payreqfix
Fix: Payment Requests should show payments of invalid invoices
2024-12-04 19:18:43 +09:00
e7b98fbae3 Merge pull request #6442 from dennisreimann/mwb-integration-signin
Account: Sign in user after accepting invitation or resetting password
2024-12-04 19:16:59 +09:00
ad3a635f59 Merge pull request #6457 from NicolasDorier/updatefido
Update Dependencies
2024-12-04 19:16:24 +09:00
eea9ba10fd Merge pull request #6452 from schjonhaug/rates-from-norwegian-exchanges
Add Rate Providers for BTC/NOK
2024-12-04 19:14:58 +09:00
f2dc033067 Bugfix: Removed unnecessary references to javascript files (#6459) (#6460) 2024-12-04 11:13:56 +01:00
bf37ee9cf6 Explicitely reference some dotnet packages 2024-12-03 23:14:20 +09:00
9b4fe5930e Bump TwentyTwenty.Storage 2024-12-03 22:37:10 +09:00
a4e950343a Bump NTag424 2024-12-03 22:30:46 +09:00
af54e7ebc2 Merge pull request #6429 from dennisreimann/app-invoices-link
UI: Move app's invoices link to the top
2024-12-03 22:22:06 +09:00
8edb4eae3b Bump MimeKit and DigitalRuby 2024-12-03 21:23:07 +09:00
694b8e111c Update FIDO library 2024-12-03 21:13:23 +09:00
cccaf0e72c Added Bare Bitcoin as exchange recommendation for NOK 2024-12-03 09:06:27 +01:00
7d65817acd Merge pull request #6449 from NicolasDorier/bumpnet
bump dotnet
2024-12-03 15:42:25 +09:00
43f159cd81 Bump dotnet 2024-12-03 14:42:43 +09:00
6e2f355aa6 Merge pull request #6456 from NicolasDorier/razorbuild
Do not use razor runtime compilation when views have been compiled on build
2024-12-03 14:17:09 +09:00
e2f868df52 Do not use razor runtime compilation when views have been compiled on build 2024-12-03 13:30:05 +09:00
898f0f4481 Greenfield: Improve store users API (#6427)
* Greenfield: Improve store users API

- Adds an endpoint to update store users (before they had to be removed ad re-added)
- Checks for the existance of a user and responds with 404 in that case (fixes #6423)
- Allows retrieval of user by user id or email for add and update (consistent with the other endpoints)
- Improves the API docs for the store users endpoints

* Swagger: Reuse UserIdOrEmail parameter component

* Add details to store user data
2024-12-02 15:35:33 +01:00
615b7842db Added tests 2024-12-02 15:01:58 +01:00
2bc6499f1b Added support for Bitmynt and Bare Bitcoin 2024-12-02 15:01:58 +01:00
9175af4abe Cleanup old checkout related settings code 2024-12-02 11:53:21 +01:00
3046fe0f0b Replace PayjoinLocks handling with Dapper (avoid logs being written) 2024-12-02 10:16:07 +09:00
6822608c14 Replace .GetAwaiter().GetResult() to await in tests 2024-12-02 09:36:31 +09:00
177ddb4117 Remove possible NRE in the LightningListener 2024-12-02 09:28:11 +09:00
3b876413c1 Merge pull request #6443 from btcpayserver/feat/nuget-update
Updating NuGet dependencies to new version
2024-12-02 09:03:02 +09:00
6397c8e07c Resolving xunit warnings after update 2024-11-29 20:53:29 -06:00
9fce9fcb7b Bumping Lightning.Common and NBitcoin 2024-11-29 20:47:26 -06:00
0eec3eefd6 Bumping more entity framework dependencies, razor, newtonsoft.json 2024-11-29 20:44:33 -06:00
83ce5a2107 Bumping xunit, EntityFrameworkCore and PostgreSQL 2024-11-29 20:35:34 -06:00
4ea6a03b4b Bumping Nbxplorer.Client and BtcPayServer.Lightning dependencies 2024-11-29 20:27:43 -06:00
bdf12aab0f App: Sales stats should only include paid invoices (#6444)
Fixes btcpayserver/app#110
2024-11-29 08:23:53 +01:00
299527fe16 bump nbx 2024-11-29 14:30:03 +09:00
e14c086fed bump nbx 2024-11-28 19:36:59 +09:00
fbf707cde2 Account: Sign in user after accepting invitation or resetting password
UX improvements, which we are porting from the app to unify the experience.
2024-11-26 11:42:13 +01:00
45a6bc8ae2 Add QR Code with link to invitation email
This will help onboarding via the app.
2024-11-26 11:23:39 +01:00
71c578f866 App: Docker Compose additions
For this one, @kukks is the owner and can procide more detail.
2024-11-26 08:25:59 +01:00
5cf3819743 Show Invoices link regardless of archival status 2024-11-26 06:52:51 +01:00
d3315c2fa6 Integrate mobile-working-branch part 1 (#6428) 2024-11-26 14:17:40 +09:00
f7f3244b3d UI: Move app's invoices link to the top 2024-11-25 18:02:07 +01:00
d6211327db Greenfield: Users API fixes (#6425)
* Do not crash when creatuing users without password

* More precise error message for user approval toggling
2024-11-25 09:38:44 +09:00
76f87011a2 Handle undefined AudioContext, resolves #6422 (#6424) 2024-11-23 10:52:23 +01:00
c6fc0302d2 fix: correct < plugin dependency implementation (#6420)
was wrongly implemented using >=
2024-11-22 10:17:16 +01:00
843c813f86 Replace entity upsert in PlannedTransaction by plain SQL (#6416)
* Replace entity upsert in PlannedTransaction by plain SQL

* Fix flaky test from #6411
2024-11-21 15:47:03 +09:00
57742ad871 Do not throttle pos when logged in 2024-11-20 15:50:46 +01:00
7db510b5ca Swagger: Fix errors, warnings and formatting (#6410)
* Fix linting errors

* Fix linting warnings

* Fix inconsistend indentation and unify formatting

* Extract StoreId schemas

* Extract CryptoCode parameter

* Extract AppId parameter

* More StoreId parameter references

* Extract WebhookId and DeliveryId parameters

* Extract InvoiceId parameter

* Formatting
2024-11-20 22:46:55 +09:00
1490fbb721 Fix: Payment Requests should show payments of invalid invoices 2024-11-19 11:02:59 +09:00
dd4400f8dd Refactoring: Use pattern matching for CanRefund/Mark invoices 2024-11-19 10:51:36 +09:00
4279ee1962 Add 1.13.7 changelog 2024-11-14 22:47:15 +09:00
558f45a48c Bump btcpay lib 2024-11-14 22:21:06 +09:00
6a76167637 Bump NBitcoin libraries 2024-11-14 22:19:10 +09:00
f56ad3408c Remove \r\n in the translations 2024-11-14 22:10:03 +09:00
8e688b7f28 Fix: Getting notifications via API would crash 2024-11-14 21:53:37 +09:00
6d737ce119 Bump Libraries 2024-11-14 21:46:15 +09:00
2075e16767 bump libs 2024-11-14 21:17:55 +09:00
2c4b1798ad Bump and changelog (#6402) 2024-11-14 21:12:54 +09:00
b040f78f70 Update checkout translations 2024-11-14 16:10:49 +09:00
c221cb7ccb Update Default Translations 2024-11-14 16:09:29 +09:00
8a51e21310 Add test 2024-11-14 14:32:53 +09:00
31b73f0d82 Fix: Boltcard would get bricked during reset from the balance view with wrong card (#6400) 2024-11-14 13:14:36 +09:00
777748d79e UI: Fix escaped HTML tags in UTXO rescan message (#6399)
Fixes #6398.
2024-11-13 21:07:30 +09:00
d0f97e85d2 UI: Allow text break in labels to avoid horizontal scrolling (#6366) 2024-11-13 21:05:40 +09:00
2eff7523c3 UI: Fix mising navigation links for store managers (#6368) 2024-11-13 21:05:23 +09:00
12d4803c5c Fix: Incorrect calculation for crowdfund and payment request status (#6381) 2024-11-13 20:59:52 +09:00
fdbee350b8 Greenfield: Create Pull Request payoutMethods is now optional (#6396) 2024-11-13 18:31:55 +09:00
78f47516b8 Fix: Pay button shouldn't throw exception if currency isn't specific (#6324) (#6395) 2024-11-13 16:25:23 +09:00
088c0c7f85 UI: Do not escape apostrophe in custom server name (#6391)
Fixes #6352.
2024-11-13 16:24:47 +09:00
809e475e29 UI: Fix close icon in create store wizard 2024-11-13 08:12:44 +01:00
8b776ed9e5 POS: Update button icons (#6390)
Adjusts the POS buttons to match what we defined for the app in btcpayserver/app#93.
2024-11-13 07:50:15 +01:00
984475b9d0 Fix: Pull payment could get stuck in Pending mode (#6259) (#6394) 2024-11-13 15:30:41 +09:00
894f68f2ae Improve error messages for on-chain related greenfield operations (#6393)
* Improve error messages for on-chain related greenfield operations

* Fix test
2024-11-13 10:11:04 +09:00
796d8d4406 Fix: Activating the automated payout processor in the UI would crash 2024-11-12 17:57:34 +09:00
7ac6cad18c Be more flexible on derivation scheme parsing 2024-11-12 15:26:30 +09:00
b3abc8f161 Fix: Newline during import of multisig xpub results in different addresses for wallet (#6328) (#6386) 2024-11-12 12:15:42 +09:00
e80296fa85 UI: Escape translated strings in JS (#6373)
Fixes #6370.
2024-11-12 11:16:56 +09:00
d0779b88e0 Fix: InvoiceCurrencyAmount and Rate columns in reports displays 0.00 (#6385) 2024-11-12 11:15:02 +09:00
acd7765159 fix: center qr code (#6362) 2024-11-12 10:43:00 +09:00
0a51031334 Docs: Improve invoice payemnt tolerance API docs (#6383)
Fixes #6378.
2024-11-12 09:59:17 +09:00
0f7c0341c5 Fix: Do not allow retry of payouts if they are non interactive (Boltcard) (#6382) 2024-11-12 09:58:10 +09:00
d40669c7bd Fix flakyness in CanUseLightningAPI test 2024-11-11 18:20:45 +01:00
fe8360e870 Remove old Altcoins folder 2024-11-11 08:01:15 +01:00
72fd2ec424 Add additional error message if payout fail with no route 2024-11-11 12:09:17 +09:00
7e7d4086cd Fix: The lightning symbol was missing in the payment stats (#6376) 2024-11-09 23:09:31 +09:00
f5d26a1555 Store: Fix missing invitation email when adding new user (#6372)
Fixes #6369, a regression introduced with#6188: Sending invite emails required a flag to be set, which defaulted to false. This fixes the code to explicitely set the flag and also defaults it to true (sending an invite email unless declared otherwise).
2024-11-09 13:55:34 +01:00
540fb6c9f6 UI: Improve brand color adjustment (#6351)
Uses a more finegrained method of deciding whether or not to adjust the brand color: Instead of simply using the brightness of the color, we calculate the contrast ratio and adjust the color by increasing the contrast until it is sufficient.

The thresholds also try to preserve the brand color in its original form, so that the color only gets adjusted if the contrast is very low.
2024-11-08 09:01:28 +01:00
12681eda36 bump 2024-11-08 16:36:27 +09:00
a05dbef337 Fix: Payouts were incorrectly marked as canceled even after successful (#6365)
completion
2024-11-08 16:13:13 +09:00
a8581510e5 improve UX description (#6129)
* Include sparrow 2fa wallet import plus improve UX description

* Add test

* Remove 2FA inclusion

* udate the btcpay network

* 2FA clean up

---------

Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2024-11-07 17:00:28 +09:00
bbba7551b7 Minor forgotten dispose in tests 2024-11-07 10:47:02 +09:00
a129114603 Greenfield: Add image upload for app items (#6226)
Upload endpoints for app item images. Follow-up to #6075.
Tested to work with the app item editor.

Uses UploadImage consistently in API and UI.
2024-11-07 10:43:22 +09:00
392ec623c0 Refactoring: Remove StoreData object from view models (#6363) 2024-11-07 08:58:47 +09:00
641bdcff31 Histograms: Add Lightning data and API endpoints (#6217)
* Histograms: Add Lightning data and API endpoints

Ported over from the mobile-working-branch.

Adds histogram data for Lightning and exposes the wallet/lightning histogram data via the API. It also add a dashboard graph for the Lightning balance.

Caveat: The Lightning histogram is calculated by using the current channel balance and going backwards through as much invoices and transactions as we have. The "start" of the LN graph data might not be accurate though. That's because we don't track (and not even have) the LN onchain data. It is calculated by using the current channel balance and going backwards through as much invoices and transactions as we have. So the historic graph data for LN is basically a best effort of trying to reconstruct it with what we have: The LN channel transactions.

* More timeframes

* Refactoring: Remove redundant WalletHistogram types

* Remove store property from dashboard tile view models

* JS error fixes
2024-11-05 21:40:37 +09:00
b3945d758a chore: add rider .run folder to .gitignore (#6358)
custom run configurations in rider are stored inside .run folder
2024-11-05 19:00:29 +09:00
4272ea97db refactor: use PaymentHash instead of Id when checking pending ln payout (#6360)
Does the same, but the `GetPayment` call explicitly wants a payment
hash, which is clearer this way (thought there might be a bug here when
I first read the code)
2024-11-05 19:00:15 +09:00
5e438b84e1 chore: rm unnecessary try catch block (#6359)
this one is redundant since the calls to `Pay` and `GetPayment` are
inside their own try catch blocks now
2024-11-05 17:54:58 +09:00
ff79a31066 Refactoring: Move AppItem to Client lib and use the class for item list (#6258)
* Refactoring: Move AppItem to Client lib and use the class for item list

This makes it available for the app, which would otherwise have to replicate the model. Also uses the proper class for the item/perk list of the app models.

* Remove unused app item payment methods property

* Do not ignore nullable values in JSON

* Revert to use Newtonsoft types
2024-11-05 11:49:30 +09:00
225264a283 Reports: Fix export (#6357)
Regression from the translation PRs, in which the proper button ID was replaced. Fixes #6356.
2024-11-04 11:50:15 +01:00
8a5a160645 bump 2024-11-04 13:12:35 +09:00
5cbadc09f9 Changelog 2.0.1 2024-11-04 13:08:57 +09:00
7aa87d397e Fix: Wrong manifest downloaded when installing plugin on old btcpay (Fix #6344) (#6354) 2024-11-04 13:05:10 +09:00
693eceb80f Reolve pull payment timezone (#6348) 2024-11-01 08:28:43 +09:00
7d8fc14159 fix: save proof blob if payout is in progress (#6343)
the payout cant be tracked later otherwise and will be marked as
cancelled
2024-11-01 08:24:21 +09:00
4687bb95cb Fix: Incorrect percentage accounting of raised money in crowdfunding (#6347) 2024-11-01 08:23:10 +09:00
e3ec07da76 Fix: Crowdfund page was crashing from 2.0.0 (#6342) (#6346) 2024-10-31 23:42:18 +09:00
910801d305 Replace font-awesome icon on Policies page 2024-10-31 12:23:30 +01:00
5ad0b128aa Dummy commit 2024-10-30 23:39:11 +09:00
5cbeea4fb3 Changelog 2.0 (#6313)
* Changelog 2.0

* Update Changelog.md

---------

Co-authored-by: Andrew Camilleri <evilkukka@gmail.com>
2024-10-30 15:35:00 +01:00
a6e18736d6 Keypad updates (#6338)
* Add keypad icons

Closes #6195.

* Keypad JS fixes
2024-10-29 23:44:37 +09:00
373b90e3b5 Liquid fixes (#6340)
make sure link provider is per payment method of liquid assets. Also remove ETB as it has been unused. Also hide the send button as it is not supported thrrough BTCPay
2024-10-29 23:43:37 +09:00
92f9b226fe Prevent additional concurrency issues with LightnignPendingPayoutListener 2024-10-28 22:12:29 +09:00
0ac6553840 Add download icon 2024-10-28 08:25:30 +01:00
41a2241ae1 feat: log download button (#6330)
* feat: add download button to logs view

* fix: add using block for `fileStream` if it isnt downloaded
2024-10-27 21:43:47 +09:00
9bb1a5b80a Prevent concurrency race on lightning payout update 2024-10-27 19:55:30 +09:00
0e59107eee Fix tests with LightningPendingPayoutListener overriding automated payouts state changes 2024-10-27 19:34:20 +09:00
c9fe68b812 fix: pass current offset to log route (#6329)
the current offset is lost otherwise and will cause a 404 if it was
greater than 0
2024-10-27 19:12:39 +09:00
e7b9688602 refactor: make BitcoinCheckoutModelExtension support other payment handlers (#6311)
* refactor: make `BitcoinCheckoutModelExtension` support other payment handlers

The bitcoin checkout extension doesn't have to be tied to the native
bitcoin handler since it only really needs the payment details to be in
a specific format, which can be provided by other handlers aswell,
allowing for better code reuse.

* refactor: initialize payment methods in constructor
2024-10-25 22:50:46 +09:00
a962e60de9 More Translations (#6318)
* Store selector

* Footer

* Notifications

* Checkout Appearance

* Users list

* Forms

* Emails

* Pay Button

* Edit Dictionary

* Remove newlines, fix typos

* Forms

* Pull payments and payouts

* Various pages

* Use local docs link

* Fix

* Even more translations

* Fixes #6325

* Account pages

* Notifications

* Placeholders

* Various pages and components

* Add more
2024-10-25 22:48:53 +09:00
e5611f9165 Fix tests (#6333) 2024-10-25 22:23:27 +09:00
540ad13265 Paging improvements (#6332)
* Domain Mapping: Passthrough query params when redirecting

* Clean up Pager

* Use current URL when paging

* Refactor
2024-10-25 22:23:03 +09:00
2849426092 Checkout: Allow breaking long item description texts 2024-10-25 13:11:26 +02:00
c4a2b4e975 Merge pull request #6327 from btcpayserver/bugfix/vaulticons
Properly cleaning up old feedback in vault feedback items
2024-10-24 15:33:44 -05:00
d508f5dc09 Properly cleaning up old feedback in vault feedback items 2024-10-22 21:50:22 -05:00
81ce8b0469 Fix flaky test 2024-10-23 00:17:02 +09:00
5a3a661e91 Revert "Revert "Fix flaky test""
This reverts commit bb5c6bd68d15bab091e3835c2d51687da8a55def.
2024-10-22 23:44:16 +09:00
bb5c6bd68d Revert "Fix flaky test"
This reverts commit 9dfabeab52644ba863230be39eca7496f4377365.
2024-10-22 23:41:41 +09:00
9dfabeab52 Fix flaky test 2024-10-22 23:36:12 +09:00
ad07330bf1 Update CLN support to 24.08.2 (#6323) 2024-10-21 00:25:50 +09:00
74011e50e3 Do not translate checkout with the backend language 2024-10-20 11:49:36 +09:00
3dfdbf544a Automated processor get disabled after some repeated failures (#6320) 2024-10-20 00:08:28 +09:00
4bf0b79c2a Simple rename 2024-10-19 22:07:20 +09:00
cc0ea0b3f8 Refactor payouts processing (#6314) 2024-10-19 21:33:34 +09:00
583 changed files with 11873 additions and 5993 deletions

View File

@ -2,7 +2,7 @@ version: 2
jobs:
fast_tests:
machine:
image: ubuntu-2004:202111-02
image: ubuntu-2004:2024.11.1
steps:
- checkout
- run:
@ -10,7 +10,7 @@ jobs:
cd .circleci && ./run-tests.sh "Fast=Fast|ThirdParty=ThirdParty" && ./can-build.sh
selenium_tests:
machine:
image: ubuntu-2004:202111-02
image: ubuntu-2004:2024.11.1
steps:
- checkout
- run:
@ -18,7 +18,7 @@ jobs:
cd .circleci && ./run-tests.sh "Selenium=Selenium"
integration_tests:
machine:
image: ubuntu-2004:202111-02
image: ubuntu-2004:2024.11.1
steps:
- checkout
- run:
@ -26,7 +26,7 @@ jobs:
cd .circleci && ./run-tests.sh "Integration=Integration"
trigger_docs_build:
machine:
image: ubuntu-2004:202111-02
image: ubuntu-2004:2024.11.1
steps:
- run:
command: |
@ -47,7 +47,6 @@ jobs:
docker buildx create --use
DOCKER_BUILDX_OPTS="--platform linux/amd64,linux/arm64,linux/arm/v7 --build-arg GIT_COMMIT=${GIT_COMMIT} --push"
docker buildx build $DOCKER_BUILDX_OPTS -t $DOCKERHUB_REPO:$LATEST_TAG .
docker buildx build $DOCKER_BUILDX_OPTS -t $DOCKERHUB_REPO:$LATEST_TAG-altcoins --build-arg CONFIGURATION_NAME=Altcoins-Release .
workflows:
version: 2
build_and_test:

View File

@ -2,8 +2,8 @@
set -e
cd ../BTCPayServer.Tests
docker-compose -v
docker-compose -f "docker-compose.altcoins.yml" down --v
docker-compose --version
docker-compose -f "docker-compose.altcoins.yml" down -v
# For some reason, docker-compose pull fails time to time, so we try several times
n=0

View File

@ -62,7 +62,7 @@ body:
id: terms
attributes:
label: Are you sure this is a bug report?
description: By submitting this report, you agree that this is not a support or a feature request. For general questions please read our [documentation](https://docs.btcpayserver.org). You can ask questions in [discussions](https://github.com/btcpayserver/btcpayserver/discussions) and [on our community chat](https://chat.btcpayserver.org)
description: By submitting this report, you agree that this is not a support or a feature request. For general questions please read our [documentation](https://docs.btcpayserver.org). You can ask questions in [discussions](https://github.com/btcpayserver/btcpayserver/discussions) and [on our community chat](https://chat.btcpayserver.org) Beware of scammers we will never direct you to third-party sites for support or ask for sensitive information your private key especially. Ignore any bots and scammers replies to GitHub issues claiming to be support agents
options:
- label: I confirm this is a bug report
required: true

1
.gitignore vendored
View File

@ -266,6 +266,7 @@ paket-files/
# JetBrains Rider
.idea/
*.sln.iml
.run
# CodeRush
.cr/

View File

@ -32,8 +32,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="HtmlSanitizer" Version="8.0.838" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.6" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.11" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.11" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BTCPayServer.Client\BTCPayServer.Client.csproj" />

View File

@ -106,8 +106,8 @@ public static class HttpRequestExtensions
/// <summary>
/// Will return an absolute URL.
/// If `relativeOrAsbolute` is absolute, returns it.
/// If `relativeOrAsbolute` is relative, send absolute url based on the HOST of this request (without PathBase)
/// If `relativeOrAbsolute` is absolute, returns it.
/// If `relativeOrAbsolute` is relative, send absolute url based on the HOST of this request (without PathBase)
/// </summary>
/// <param name="request"></param>
/// <param name="relativeOrAbsolte"></param>

View File

@ -16,6 +16,8 @@ namespace BTCPayServer.Abstractions.Services
_htmlHelper = htmlHelper;
_jsonHelper = jsonHelper;
_htmlSanitizer = htmlSanitizer;
}
public IHtmlContent Raw(string value)
@ -32,5 +34,39 @@ namespace BTCPayServer.Abstractions.Services
{
return _htmlHelper.Raw(_jsonHelper.Serialize(model));
}
public IHtmlContent Meta(string inputHtml) => _htmlHelper.Raw(RawMeta(inputHtml, out _));
public string RawMeta(string inputHtml, out bool isHtmlModified)
{
bool bHtmlModified;
HtmlSanitizer sane = new HtmlSanitizer();
sane.AllowedTags.Clear();
sane.AllowedTags.Add("meta");
sane.AllowedAttributes.Clear();
sane.AllowedAttributes.Add("name");
sane.AllowedAttributes.Add("http-equiv");
sane.AllowedAttributes.Add("content");
sane.AllowedAttributes.Add("value");
sane.AllowedAttributes.Add("property");
sane.AllowDataAttributes = false;
sane.RemovingTag += (sender, e) => bHtmlModified = true;
sane.RemovingAtRule += (sender, e) => bHtmlModified = true;
sane.RemovingAttribute += (sender, e) => bHtmlModified = true;
sane.RemovingComment += (sender, e) => bHtmlModified = true;
sane.RemovingCssClass += (sender, e) => bHtmlModified = true;
sane.RemovingStyle += (sender, e) => bHtmlModified = true;
bHtmlModified = false;
var sRet = sane.Sanitize(inputHtml);
isHtmlModified = bHtmlModified;
return sRet;
}
}
}

View File

@ -16,7 +16,7 @@
<Platforms>AnyCPU</Platforms>
</PropertyGroup>
<PropertyGroup>
<Version Condition=" '$(Version)' == '' ">2.0.0</Version>
<Version Condition=" '$(Version)' == '' ">2.0.1</Version>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<PublishRepositoryUrl>true</PublishRepositoryUrl>
@ -30,8 +30,8 @@
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.5.1" />
<PackageReference Include="NBitcoin" Version="7.0.37" />
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.5.2" />
<PackageReference Include="NBitcoin" Version="7.0.48" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>

View File

@ -78,4 +78,14 @@ public partial class BTCPayServerClient
if (appId == null) throw new ArgumentNullException(nameof(appId));
await SendHttpRequest($"api/v1/apps/{appId}", null, HttpMethod.Delete, token);
}
public virtual async Task<FileData> UploadAppItemImage(string appId, string filePath, string mimeType, CancellationToken token = default)
{
return await UploadFileRequest<FileData>($"api/v1/apps/{appId}/image", filePath, mimeType, "file", HttpMethod.Post, token);
}
public virtual async Task DeleteAppItemImage(string appId, string fileId, CancellationToken token = default)
{
await SendHttpRequest($"api/v1/apps/{appId}/image/{fileId}", null, HttpMethod.Delete, token);
}
}

View File

@ -21,6 +21,13 @@ public partial class BTCPayServerClient
return await SendHttpRequest<LightningNodeBalanceData>($"api/v1/server/lightning/{cryptoCode}/balance", null, HttpMethod.Get, token);
}
public virtual async Task<HistogramData> GetLightningNodeHistogram(string cryptoCode, HistogramType? type = null,
CancellationToken token = default)
{
var queryPayload = type == null ? null : new Dictionary<string, object> { { "type", type.ToString() } };
return await SendHttpRequest<HistogramData>($"api/v1/server/lightning/{cryptoCode}/histogram", queryPayload, HttpMethod.Get, token);
}
public virtual async Task ConnectToLightningNode(string cryptoCode, ConnectToNodeRequest request,
CancellationToken token = default)
{

View File

@ -21,6 +21,13 @@ public partial class BTCPayServerClient
return await SendHttpRequest<LightningNodeBalanceData>($"api/v1/stores/{storeId}/lightning/{cryptoCode}/balance", null, HttpMethod.Get, token);
}
public virtual async Task<HistogramData> GetLightningNodeHistogram(string storeId, string cryptoCode, HistogramType? type = null,
CancellationToken token = default)
{
var queryPayload = type == null ? null : new Dictionary<string, object> { { "type", type.ToString() } };
return await SendHttpRequest<HistogramData>($"api/v1/stores/{storeId}/lightning/{cryptoCode}/histogram", queryPayload, HttpMethod.Get, token);
}
public virtual async Task ConnectToLightningNode(string storeId, string cryptoCode, ConnectToNodeRequest request,
CancellationToken token = default)
{

View File

@ -16,6 +16,14 @@ public partial class BTCPayServerClient
{
return await SendHttpRequest<OnChainWalletOverviewData>($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet", null, HttpMethod.Get, token);
}
public virtual async Task<HistogramData> GetOnChainWalletHistogram(string storeId, string cryptoCode, HistogramType? type = null,
CancellationToken token = default)
{
var queryPayload = type == null ? null : new Dictionary<string, object> { { "type", type.ToString() } };
return await SendHttpRequest<HistogramData>($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet/histogram", queryPayload, HttpMethod.Get, token);
}
public virtual async Task<OnChainWalletFeeRateData> GetOnChainFeeRate(string storeId, string cryptoCode, int? blockTarget = null,
CancellationToken token = default)
{

View File

@ -0,0 +1,20 @@
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
namespace BTCPayServer.Client;
public partial class BTCPayServerClient
{
public virtual async Task<ServerEmailSettingsData> GetServerEmailSettings(CancellationToken token = default)
{
return await SendHttpRequest<ServerEmailSettingsData>("api/v1/server/email", null, HttpMethod.Get, token);
}
public virtual async Task<bool> UpdateServerEmailSettings(ServerEmailSettingsData request, CancellationToken token = default)
{
return await SendHttpRequest<bool>("api/v1/server/email", request, HttpMethod.Put, token);
}
}

View File

@ -29,4 +29,10 @@ public partial class BTCPayServerClient
if (request == null) throw new ArgumentNullException(nameof(request));
await SendHttpRequest<StoreUserData>($"api/v1/stores/{storeId}/users", request, HttpMethod.Post, token);
}
public virtual async Task UpdateStoreUser(string storeId, string userId, StoreUserData request, CancellationToken token = default)
{
if (request == null) throw new ArgumentNullException(nameof(request));
await SendHttpRequest<StoreUserData>($"api/v1/stores/{storeId}/users/{userId}", request, HttpMethod.Put, token);
}
}

View File

@ -156,7 +156,7 @@ public partial class BTCPayServerClient
protected virtual async Task<T> UploadFileRequest<T>(string apiPath, string filePath, string mimeType, string formFieldName, HttpMethod method = null, CancellationToken token = default)
{
using MultipartFormDataContent multipartContent = new();
var fileContent = new StreamContent(File.OpenRead(filePath));
using var fileContent = new StreamContent(File.OpenRead(filePath));
var fileName = Path.GetFileName(filePath);
fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse(mimeType);
multipartContent.Add(fileContent, formFieldName, fileName);

View File

@ -0,0 +1,13 @@
using BTCPayServer.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models;
public class AppCartItem
{
public string Id { get; set; }
public string Title { get; set; }
public int Count { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal Price { get; set; }
}

View File

@ -0,0 +1,35 @@
using System.Collections.Generic;
using BTCPayServer.JsonConverters;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client.Models;
public enum AppItemPriceType
{
Fixed,
Topup,
Minimum
}
public class AppItem
{
public string Id { get; set; }
public string Title { get; set; }
public bool Disabled { get; set; }
public string Description { get; set; }
public string[] Categories { get; set; }
public string Image { get; set; }
[JsonConverter(typeof(StringEnumConverter))]
public AppItemPriceType PriceType { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal? Price { get; set; }
public string BuyButtonText { get; set; }
public int? Inventory { get; set; }
[JsonExtensionData]
public Dictionary<string, JToken> AdditionalData { get; set; }
}

View File

@ -0,0 +1,25 @@
using System.Collections.Generic;
using BTCPayServer.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models;
public class CreatePosInvoiceRequest
{
public string AppId { get; set; }
public List<AppCartItem> Cart { get; set; }
[JsonProperty(ItemConverterType = typeof(NumericStringJsonConverter))]
public List<decimal> Amounts { get; set; }
public int? DiscountPercent { get; set; }
public int? TipPercent { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal? DiscountAmount { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal? Tip { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal? Subtotal { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal? Total { get; set; }
public string PosData { get; set; }
}

View File

@ -32,12 +32,14 @@ public abstract class CrowdfundBaseData : AppBaseData
public bool? SortPerksByPopularity { get; set; }
public string[]? Sounds { get; set; }
public string[]? AnimationColors { get; set; }
public string? HtmlLang { get; set; }
public string? HtmlMetaTags { get; set; }
public string? FormId { get; set; }
}
public class CrowdfundAppData : CrowdfundBaseData
{
public object? Perks { get; set; }
public AppItem[]? Perks { get; set; }
}
public class CrowdfundAppRequest : CrowdfundBaseData, IAppRequest

View File

@ -4,29 +4,11 @@ namespace BTCPayServer.Client.Models;
public class EmailSettingsData
{
public string Server
{
get; set;
}
public int? Port
{
get; set;
}
public string Login
{
get; set;
}
public string Password
{
get; set;
}
public string From
{
get; set;
}
public string Server { get; set; }
public int? Port { get; set; }
public string Login { get; set; }
public string Password { get; set; }
public string From { get; set; }
public bool DisableCertificateCheck { get; set; }
[JsonIgnore]

View File

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using BTCPayServer.JsonConverters;
using NBitcoin.JsonConverters;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace BTCPayServer.Client.Models;
public enum HistogramType
{
Week,
Month,
YTD,
Year,
TwoYears,
Day
}
public class HistogramData
{
[JsonConverter(typeof(StringEnumConverter))]
public HistogramType Type { get; set; }
[JsonProperty(ItemConverterType = typeof(NumericStringJsonConverter))]
public List<decimal> Series { get; set; }
[JsonProperty(ItemConverterType = typeof(DateTimeToUnixTimeConverter))]
public List<DateTimeOffset> Labels { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal Balance { get; set; }
}

View File

@ -11,7 +11,7 @@ namespace BTCPayServer.Client.Models
public string Body { get; set; }
public string StoreId { get; set; }
public bool Seen { get; set; }
public Uri Link { get; set; }
public string Link { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
public DateTimeOffset CreatedTime { get; set; }

View File

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models
@ -11,17 +10,17 @@ namespace BTCPayServer.Client.Models
static PermissionMetadata()
{
Dictionary<string, PermissionMetadata> nodes = new Dictionary<string, PermissionMetadata>();
foreach (var policy in Client.Policies.AllPolicies)
foreach (var policy in Policies.AllPolicies)
{
nodes.Add(policy, new PermissionMetadata() { PermissionName = policy });
}
foreach (var n in nodes)
{
foreach (var policy in Client.Policies.AllPolicies)
foreach (var policy in Policies.AllPolicies)
{
if (policy.Equals(n.Key, StringComparison.OrdinalIgnoreCase))
continue;
if (Client.Permission.Create(n.Key).Contains(Client.Permission.Create(policy)))
if (Permission.Create(n.Key).Contains(Permission.Create(policy)))
n.Value.SubPermissions.Add(policy);
}
}

View File

@ -1,8 +1,6 @@
#nullable enable
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client.Models;
@ -26,12 +24,14 @@ public abstract class PointOfSaleBaseData : AppBaseData
public string? Description { get; set; }
public bool? RedirectAutomatically { get; set; }
public int[]? CustomTipPercentages { get; set; }
public string? HtmlLang { get; set; }
public string? HtmlMetaTags { get; set; }
public string? FormId { get; set; }
}
public class PointOfSaleAppData : PointOfSaleBaseData
{
public object? Items { get; set; }
public AppItem[]? Items { get; set; }
}
public class PointOfSaleAppRequest : PointOfSaleBaseData, IAppRequest

View File

@ -0,0 +1,7 @@
namespace BTCPayServer.Client.Models
{
public class ServerEmailSettingsData : EmailSettingsData
{
public bool EnableStoresToUseServerEmailSettings { get; set; }
}
}

View File

@ -17,7 +17,25 @@ namespace BTCPayServer.Client.Models
/// </summary>
public string UserId { get; set; }
/// <summary>
/// the store role of the user
/// </summary>
public string Role { get; set; }
/// <summary>
/// the email AND username of the user
/// </summary>
public string Email { get; set; }
/// <summary>
/// the name of the user
/// </summary>
public string Name { get; set; }
/// <summary>
/// the image url of the user
/// </summary>
public string ImageUrl { get; set; }
}
public class RoleData

View File

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Text.RegularExpressions;
namespace BTCPayServer.Client

View File

@ -0,0 +1,50 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client;
public static class PosDataParser
{
public static Dictionary<string, object> ParsePosData(JToken? posData)
{
var result = new Dictionary<string, object>();
if (posData is JObject jobj)
{
foreach (var item in jobj)
{
ParsePosDataItem(item, ref result);
}
}
return result;
}
static void ParsePosDataItem(KeyValuePair<string, JToken?> item, ref Dictionary<string, object> result)
{
switch (item.Value?.Type)
{
case JTokenType.Array:
var items = item.Value.AsEnumerable().ToList();
var arrayResult = new List<object>();
for (var i = 0; i < items.Count; i++)
{
arrayResult.Add(items[i] is JObject
? ParsePosData(items[i])
: items[i].ToString());
}
result.TryAdd(item.Key, arrayResult);
break;
case JTokenType.Object:
result.TryAdd(item.Key, ParsePosData(item.Value));
break;
case null:
break;
default:
result.TryAdd(item.Key, item.Value.ToString());
break;
}
}
}

View File

@ -4,8 +4,6 @@ using System.Globalization;
using System.IO;
using System.Linq;
using BTCPayServer.Common;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.DependencyInjection;
using NBitcoin;
using NBXplorer;
using NBXplorer.Models;

View File

@ -3,13 +3,9 @@ using System.Collections.Generic;
using System.Linq;
using BTCPayServer.Configuration;
using BTCPayServer.Logging;
using Microsoft.AspNetCore.DataProtection.KeyManagement;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using NBitcoin;
using NBXplorer;
using StandardConfiguration;
namespace BTCPayServer
{

View File

@ -1,13 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="../Build/Version.csproj" Condition="Exists('../Build/Version.csproj')" />
<Import Project="../Build/Common.csproj" />
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="NBXplorer.Client" Version="4.3.1" />
<PackageReference Include="NBXplorer.Client" Version="4.3.9" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="2.0.1" />
</ItemGroup>
<ItemGroup>
<Folder Include="Altcoins\" />
</ItemGroup>
</Project>

View File

@ -1,97 +0,0 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace BTCPayServer
{
public class CustomThreadPool : IDisposable
{
readonly CancellationTokenSource _Cancel = new CancellationTokenSource();
readonly TaskCompletionSource<bool> _Exited;
int _ExitedCount = 0;
readonly Thread[] _Threads;
Exception _UnhandledException;
readonly BlockingCollection<(Action, TaskCompletionSource<object>)> _Actions = new BlockingCollection<(Action, TaskCompletionSource<object>)>(new ConcurrentQueue<(Action, TaskCompletionSource<object>)>());
public CustomThreadPool(int threadCount, string threadName)
{
if (threadCount <= 0)
throw new ArgumentOutOfRangeException(nameof(threadCount));
_Exited = new TaskCompletionSource<bool>();
_Threads = Enumerable.Range(0, threadCount).Select(_ => new Thread(RunLoop) { Name = threadName }).ToArray();
foreach (var t in _Threads)
t.Start();
}
public void Do(Action act)
{
DoAsync(act).GetAwaiter().GetResult();
}
public T Do<T>(Func<T> act)
{
return DoAsync(act).GetAwaiter().GetResult();
}
public async Task<T> DoAsync<T>(Func<T> act)
{
TaskCompletionSource<object> done = new TaskCompletionSource<object>();
_Actions.Add((() =>
{
try
{
done.TrySetResult(act());
}
catch (Exception ex) { done.TrySetException(ex); }
}
, done));
return (T)(await done.Task.ConfigureAwait(false));
}
public Task DoAsync(Action act)
{
return DoAsync<object>(() =>
{
act();
return null;
});
}
void RunLoop()
{
try
{
foreach (var act in _Actions.GetConsumingEnumerable(_Cancel.Token))
{
act.Item1();
}
}
catch (OperationCanceledException) when (_Cancel.IsCancellationRequested) { }
catch (Exception ex)
{
_Cancel.Cancel();
_UnhandledException = ex;
}
if (Interlocked.Increment(ref _ExitedCount) == _Threads.Length)
{
foreach (var action in _Actions)
{
try
{
action.Item2.TrySetCanceled();
}
catch { }
}
_Exited.TrySetResult(true);
}
}
public void Dispose()
{
_Cancel.Cancel();
_Exited.Task.GetAwaiter().GetResult();
}
}
}

View File

@ -1,7 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;

View File

@ -4,7 +4,6 @@ using System.Linq;
using BTCPayServer.Logging;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Bson;
namespace BTCPayServer
{

View File

@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
@ -61,6 +63,7 @@ namespace BTCPayServer.Data
public DbSet<LightningAddressData> LightningAddresses { get; set; }
public DbSet<PayoutProcessorData> PayoutProcessors { get; set; }
public DbSet<FormData> Forms { get; set; }
public DbSet<PendingTransaction> PendingTransactions { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{
@ -106,7 +109,7 @@ namespace BTCPayServer.Data
WebhookData.OnModelCreating(builder, Database);
FormData.OnModelCreating(builder, Database);
StoreRole.OnModelCreating(builder, Database);
PendingTransaction.OnModelCreating(builder, Database);
}
}
}

View File

@ -3,12 +3,12 @@
<Import Project="../Build/Common.csproj" />
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6">
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.11">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.7" />
<PackageReference Include="NBitcoin.Altcoins" Version="3.0.24" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.11" />
<PackageReference Include="NBitcoin.Altcoins" Version="3.0.31" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BTCPayServer.Abstractions\BTCPayServer.Abstractions.csproj" />

View File

@ -126,6 +126,7 @@ namespace BTCPayServer.Data
{ Type: "LN" } or { Type: "LNURL" } => 11,
{ Type: "CHAIN", CryptoCode: var code } when code == "XMR" => 12,
{ Type: "CHAIN" } => 8,
{ Type: "LEGACY" } => 18,
_ => 8
};
}
@ -140,7 +141,7 @@ namespace BTCPayServer.Data
"BTCLike" or "MoneroLike" or "ZcashLike" => $"{cryptoCode}-CHAIN",
"LightningLike" or "LightningNetwork" => $"{cryptoCode}-LN",
"LNURLPAY" => $"{cryptoCode}-LNURL",
"EthereumLike" => $"{cryptoCode}-LEGACY",
_ => throw new NotSupportedException("Unknown payment type " + paymentType)
};
}

View File

@ -1,11 +1,9 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text;
using BTCPayServer.Client.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using NBitcoin;
namespace BTCPayServer.Data
@ -73,7 +71,7 @@ namespace BTCPayServer.Data
builder.Entity<PayoutData>()
.Property(o => o.Blob)
.HasColumnType("JSONB");
.HasColumnType("jsonb");
builder.Entity<PayoutData>()
.Property(o => o.Proof)
.HasColumnType("JSONB");

View File

@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
namespace BTCPayServer.Data;
public class PendingTransaction: IHasBlob<PendingTransactionBlob>
{
public string TransactionId { get; set; }
public string CryptoCode { get; set; }
public string StoreId { get; set; }
public StoreData Store { get; set; }
public DateTimeOffset? Expiry { get; set; }
public PendingTransactionState State { get; set; }
public string[] OutpointsUsed { get; set; }
[NotMapped][Obsolete("Use Blob2 instead")]
public byte[] Blob { get; set; }
public string Blob2 { get; set; }
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
{
builder.Entity<PendingTransaction>()
.HasOne(o => o.Store)
.WithMany(i => i.PendingTransactions)
.HasForeignKey(i => i.StoreId)
.OnDelete(DeleteBehavior.Cascade);
builder.Entity<PendingTransaction>().HasKey(transaction => new {transaction.CryptoCode, transaction.TransactionId});
builder.Entity<PendingTransaction>()
.Property(o => o.Blob2)
.HasColumnType("JSONB");
builder.Entity<PendingTransaction>()
.Property(o => o.OutpointsUsed)
.HasColumnType("text[]");
}
}
public enum PendingTransactionState
{
Pending,
Cancelled,
Expired,
Invalidated,
Signed,
Broadcast
}
public class PendingTransactionBlob
{
public string PSBT { get; set; }
public List<CollectedSignature> CollectedSignatures { get; set; } = new();
}
public class CollectedSignature
{
public DateTimeOffset Timestamp { get; set; }
public string ReceivedPSBT { get; set; }
}

View File

@ -49,6 +49,7 @@ namespace BTCPayServer.Data
public IEnumerable<FormData> Forms { get; set; }
public IEnumerable<StoreRole> StoreRoles { get; set; }
public bool Archived { get; set; }
public IEnumerable<PendingTransaction> PendingTransactions { get; set; }
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
{

View File

@ -0,0 +1,53 @@
using System;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20241029163147_AddingPendingTransactionsTable")]
public partial class AddingPendingTransactionsTable : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "PendingTransactions",
columns: table => new
{
TransactionId = table.Column<string>(type: "text", nullable: false),
CryptoCode = table.Column<string>(type: "text", nullable: false),
StoreId = table.Column<string>(type: "text", nullable: true),
Expiry = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
State = table.Column<int>(type: "integer", nullable: false),
OutpointsUsed = table.Column<string[]>(type: "text[]", nullable: true),
Blob2 = table.Column<string>(type: "JSONB", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_PendingTransactions", x => new { x.CryptoCode, x.TransactionId });
table.ForeignKey(
name: "FK_PendingTransactions_Stores_StoreId",
column: x => x.StoreId,
principalTable: "Stores",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_PendingTransactions_StoreId",
table: "PendingTransactions",
column: "StoreId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "PendingTransactions");
}
}
}

View File

@ -560,7 +560,7 @@ namespace BTCPayServer.Migrations
.HasColumnType("numeric");
b.Property<string>("Blob")
.HasColumnType("JSONB");
.HasColumnType("jsonb");
b.Property<string>("Currency")
.HasColumnType("text");
@ -637,6 +637,36 @@ namespace BTCPayServer.Migrations
b.ToTable("PayoutProcessors");
});
modelBuilder.Entity("BTCPayServer.Data.PendingTransaction", b =>
{
b.Property<string>("CryptoCode")
.HasColumnType("text");
b.Property<string>("TransactionId")
.HasColumnType("text");
b.Property<string>("Blob2")
.HasColumnType("JSONB");
b.Property<DateTimeOffset?>("Expiry")
.HasColumnType("timestamp with time zone");
b.Property<string[]>("OutpointsUsed")
.HasColumnType("text[]");
b.Property<int>("State")
.HasColumnType("integer");
b.Property<string>("StoreId")
.HasColumnType("text");
b.HasKey("CryptoCode", "TransactionId");
b.HasIndex("StoreId");
b.ToTable("PendingTransactions");
});
modelBuilder.Entity("BTCPayServer.Data.PlannedTransaction", b =>
{
b.Property<string>("Id")
@ -1324,6 +1354,16 @@ namespace BTCPayServer.Migrations
b.Navigation("Store");
});
modelBuilder.Entity("BTCPayServer.Data.PendingTransaction", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "Store")
.WithMany("PendingTransactions")
.HasForeignKey("StoreId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("Store");
});
modelBuilder.Entity("BTCPayServer.Data.PullPaymentData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
@ -1582,6 +1622,8 @@ namespace BTCPayServer.Migrations
b.Navigation("Payouts");
b.Navigation("PendingTransactions");
b.Navigation("PullPayments");
b.Navigation("Settings");

View File

@ -6,9 +6,10 @@
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.10.0" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="6.0.0" />
<PackageReference Include="NBitcoin" Version="7.0.37" />
<PackageReference Include="NBitcoin" Version="7.0.48" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="1.0.4" />
<PackageReference Include="System.Text.Json" Version="8.0.5" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="1.2.0" />
</ItemGroup>
<ItemGroup>

View File

@ -38,8 +38,6 @@ namespace BTCPayServer.Rating
ArgumentNullException.ThrowIfNull(str);
value = null;
str = str.Trim();
if (str.Length > 12)
return false;
var splitted = str.Split(new[] { '_', '-' }, StringSplitOptions.RemoveEmptyEntries);
if (splitted.Length == 2)
{

View File

@ -0,0 +1,38 @@
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Rating;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.Rates
{
public class BareBitcoinRateProvider : IRateProvider
{
private readonly HttpClient _httpClient;
public RateSourceInfo RateSourceInfo => new("barebitcoin", "Bare Bitcoin", "https://api.bb.no/v1/price/nok");
public BareBitcoinRateProvider(HttpClient httpClient)
{
_httpClient = httpClient ?? new HttpClient();
}
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{
using var response = await _httpClient.GetAsync(RateSourceInfo.Url, cancellationToken);
response.EnsureSuccessStatusCode();
var jobj = await response.Content.ReadAsAsync<JObject>(cancellationToken);
// Extract bid/ask prices from JSON response
var bid = (decimal)jobj["bid"];
var ask = (decimal)jobj["ask"];
// Create currency pair for BTC/NOK
var pair = new CurrencyPair("BTC", "NOK");
// Return single pair rate with bid/ask
return new[] { new PairRate(pair, new BidAsk(bid, ask)) };
}
}
}

View File

@ -0,0 +1,39 @@
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Rating;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.Rates
{
public class BitmyntRateProvider : IRateProvider
{
private readonly HttpClient _httpClient;
public RateSourceInfo RateSourceInfo => new("bitmynt", "Bitmynt", "https://ny.bitmynt.no/data/rates.json");
public BitmyntRateProvider(HttpClient httpClient)
{
_httpClient = httpClient ?? new HttpClient();
}
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{
using var response = await _httpClient.GetAsync(RateSourceInfo.Url, cancellationToken);
response.EnsureSuccessStatusCode();
var jobj = await response.Content.ReadAsAsync<JObject>(cancellationToken);
// Extract bid and ask prices from current_rate object
var currentRate = jobj["current_rate"];
var bid = currentRate["bid"].Value<decimal>();
var ask = currentRate["ask"].Value<decimal>();
// Create currency pair for BTC/NOK
var pair = new CurrencyPair("BTC", "NOK");
// Return single pair rate with bid/ask
return new[] { new PairRate(pair, new BidAsk(bid, ask)) };
}
}
}

View File

@ -2,11 +2,11 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.HostedServices;
using BTCPayServer.Hosting;
using BTCPayServer.Lightning;
using BTCPayServer.Models.AppViewModels;
@ -25,7 +25,7 @@ using Newtonsoft.Json.Linq;
using OpenQA.Selenium;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;
using PosViewType = BTCPayServer.Plugins.PointOfSale.PosViewType;
using WalletSettingsViewModel = BTCPayServer.Models.StoreViewModels.WalletSettingsViewModel;
namespace BTCPayServer.Tests
@ -94,16 +94,16 @@ namespace BTCPayServer.Tests
Assert.IsType<ViewResult>(response);
// Get enabled state from settings
response = controller.WalletSettings(user.StoreId, cryptoCode).GetAwaiter().GetResult();
response = await controller.WalletSettings(user.StoreId, cryptoCode);
var onchainSettingsModel = (WalletSettingsViewModel)Assert.IsType<ViewResult>(response).Model;
Assert.NotNull(onchainSettingsModel?.DerivationScheme);
Assert.True(onchainSettingsModel.Enabled);
// Disable wallet
onchainSettingsModel.Enabled = false;
response = controller.UpdateWalletSettings(onchainSettingsModel).GetAwaiter().GetResult();
response = await controller.UpdateWalletSettings(onchainSettingsModel);
Assert.IsType<RedirectToActionResult>(response);
response = controller.WalletSettings(user.StoreId, cryptoCode).GetAwaiter().GetResult();
response = await controller.WalletSettings(user.StoreId, cryptoCode);
onchainSettingsModel = (WalletSettingsViewModel)Assert.IsType<ViewResult>(response).Model;
Assert.NotNull(onchainSettingsModel?.DerivationScheme);
Assert.False(onchainSettingsModel.Enabled);
@ -125,7 +125,7 @@ namespace BTCPayServer.Tests
Assert.Equal("LTC", invoice.CryptoInfo[0].CryptoCode);
// Removing the derivation scheme, should redirect to store page
response = controller.ConfirmDeleteWallet(user.StoreId, cryptoCode).GetAwaiter().GetResult();
response = await controller.ConfirmDeleteWallet(user.StoreId, cryptoCode);
Assert.IsType<RedirectToActionResult>(response);
// Setting it again should show the confirmation page
@ -175,7 +175,7 @@ namespace BTCPayServer.Tests
Assert.Equal("ElectrumFile", settingsVm.Source);
// Now let's check that no data has been lost in the process
var store = tester.PayTester.StoreRepository.FindStore(storeId).GetAwaiter().GetResult();
var store = await tester.PayTester.StoreRepository.FindStore(storeId);
var handlers = tester.PayTester.GetService<PaymentMethodHandlerDictionary>();
var pmi = PaymentTypes.CHAIN.GetPaymentMethodId("BTC");
var onchainBTC = store.GetPaymentMethodConfig<DerivationSchemeSettings>(pmi, handlers);
@ -207,7 +207,7 @@ namespace BTCPayServer.Tests
Assert.Equal("paid", invoice.Status);
});
var wallet = tester.PayTester.GetController<UIWalletsController>();
var psbt = wallet.CreatePSBT(btcNetwork, onchainBTC,
var psbt = await wallet.CreatePSBT(btcNetwork, onchainBTC,
new WalletSendModel()
{
Outputs = new List<WalletSendModel.TransactionOutput>
@ -220,7 +220,7 @@ namespace BTCPayServer.Tests
}
},
FeeSatoshiPerByte = 1
}, default).GetAwaiter().GetResult();
}, default);
Assert.NotNull(psbt);
@ -441,136 +441,135 @@ namespace BTCPayServer.Tests
[Trait("Altcoins", "Altcoins")]
public async Task CanPayWithTwoCurrencies()
{
using (var tester = CreateServerTester())
using var tester = CreateServerTester();
tester.ActivateLTC();
await tester.StartAsync();
var user = tester.NewAccount();
await user.GrantAccessAsync();
user.RegisterDerivationScheme("BTC");
// First we try payment with a merchant having only BTC
var invoice = await user.BitPay.CreateInvoiceAsync(
new Invoice
{
Price = 5000.0m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
var cashCow = tester.ExplorerNode;
await cashCow.GenerateAsync(2); // get some money in case
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
var firstPayment = Money.Coins(0.04m);
await cashCow.SendToAddressAsync(invoiceAddress, firstPayment);
TestUtils.Eventually(() =>
{
tester.ActivateLTC();
await tester.StartAsync();
var user = tester.NewAccount();
await user.GrantAccessAsync();
user.RegisterDerivationScheme("BTC");
// First we try payment with a merchant having only BTC
var invoice = await user.BitPay.CreateInvoiceAsync(
new Invoice
{
Price = 5000.0m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.True(invoice.BtcPaid == firstPayment);
});
var cashCow = tester.ExplorerNode;
await cashCow.GenerateAsync(2); // get some money in case
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
var firstPayment = Money.Coins(0.04m);
await cashCow.SendToAddressAsync(invoiceAddress, firstPayment);
TestUtils.Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.True(invoice.BtcPaid == firstPayment);
});
Assert.Single(invoice.CryptoInfo); // Only BTC should be presented
Assert.Single(invoice.CryptoInfo); // Only BTC should be presented
var controller = tester.PayTester.GetController<UIInvoiceController>(null);
var checkout =
(Models.InvoicingModels.CheckoutModel)((JsonResult)controller.GetStatus(invoice.Id, null)
.GetAwaiter().GetResult()).Value;
Assert.Single(checkout.AvailablePaymentMethods);
Assert.Equal("BTC", checkout.PaymentMethodCurrency);
Assert.Single(invoice.PaymentCodes);
Assert.Single(invoice.SupportedTransactionCurrencies);
Assert.Single(invoice.SupportedTransactionCurrencies);
Assert.Single(invoice.PaymentSubtotals);
Assert.Single(invoice.PaymentTotals);
Assert.True(invoice.PaymentCodes.ContainsKey("BTC"));
Assert.True(invoice.SupportedTransactionCurrencies.ContainsKey("BTC"));
Assert.True(invoice.SupportedTransactionCurrencies["BTC"].Enabled);
Assert.True(invoice.PaymentSubtotals.ContainsKey("BTC"));
Assert.True(invoice.PaymentTotals.ContainsKey("BTC"));
//////////////////////
// Retry now with LTC enabled
user.RegisterDerivationScheme("LTC");
invoice = await user.BitPay.CreateInvoiceAsync(
new Invoice
{
Price = 5000.0m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
cashCow = tester.ExplorerNode;
invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
firstPayment = Money.Coins(0.04m);
await cashCow.SendToAddressAsync(invoiceAddress, firstPayment);
TestLogs.LogInformation("First payment sent to " + invoiceAddress);
TestUtils.Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.True(invoice.BtcPaid == firstPayment);
});
cashCow = tester.LTCExplorerNode;
var ltcCryptoInfo = invoice.CryptoInfo.FirstOrDefault(c => c.CryptoCode == "LTC");
Assert.NotNull(ltcCryptoInfo);
invoiceAddress = BitcoinAddress.Create(ltcCryptoInfo.Address, cashCow.Network);
var secondPayment = Money.Coins(decimal.Parse(ltcCryptoInfo.Due, CultureInfo.InvariantCulture));
await cashCow.GenerateAsync(4); // LTC is not worth a lot, so just to make sure we have money...
await cashCow.SendToAddressAsync(invoiceAddress, secondPayment);
TestLogs.LogInformation("Second payment sent to " + invoiceAddress);
TestUtils.Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal(Money.Zero, invoice.BtcDue);
var ltcPaid = invoice.CryptoInfo.First(c => c.CryptoCode == "LTC");
Assert.Equal(Money.Zero, ltcPaid.Due);
Assert.Equal(secondPayment, ltcPaid.CryptoPaid);
Assert.Equal("paid", invoice.Status);
Assert.False((bool)((JValue)invoice.ExceptionStatus).Value);
});
controller = tester.PayTester.GetController<UIInvoiceController>(null);
checkout = (Models.InvoicingModels.CheckoutModel)((JsonResult)controller.GetStatus(invoice.Id, "LTC")
var controller = tester.PayTester.GetController<UIInvoiceController>(null);
var checkout =
(Models.InvoicingModels.CheckoutModel)((JsonResult)controller.GetStatus(invoice.Id, null)
.GetAwaiter().GetResult()).Value;
Assert.Equal(2, checkout.AvailablePaymentMethods.Count);
Assert.Equal("LTC", checkout.PaymentMethodCurrency);
Assert.Single(checkout.AvailablePaymentMethods);
Assert.Equal("BTC", checkout.PaymentMethodCurrency);
Assert.Equal(2, invoice.PaymentCodes.Count());
Assert.Equal(2, invoice.SupportedTransactionCurrencies.Count());
Assert.Equal(2, invoice.SupportedTransactionCurrencies.Count());
Assert.Equal(2, invoice.PaymentSubtotals.Count());
Assert.Equal(2, invoice.PaymentTotals.Count());
Assert.True(invoice.PaymentCodes.ContainsKey("LTC"));
Assert.True(invoice.SupportedTransactionCurrencies.ContainsKey("LTC"));
Assert.True(invoice.SupportedTransactionCurrencies["LTC"].Enabled);
Assert.True(invoice.PaymentSubtotals.ContainsKey("LTC"));
Assert.True(invoice.PaymentTotals.ContainsKey("LTC"));
Assert.Single(invoice.PaymentCodes);
Assert.Single(invoice.SupportedTransactionCurrencies);
Assert.Single(invoice.SupportedTransactionCurrencies);
Assert.Single(invoice.PaymentSubtotals);
Assert.Single(invoice.PaymentTotals);
Assert.True(invoice.PaymentCodes.ContainsKey("BTC"));
Assert.True(invoice.SupportedTransactionCurrencies.ContainsKey("BTC"));
Assert.True(invoice.SupportedTransactionCurrencies["BTC"].Enabled);
Assert.True(invoice.PaymentSubtotals.ContainsKey("BTC"));
Assert.True(invoice.PaymentTotals.ContainsKey("BTC"));
//////////////////////
// Check if we can disable LTC
invoice = await user.BitPay.CreateInvoiceAsync(
new Invoice
// Retry now with LTC enabled
user.RegisterDerivationScheme("LTC");
invoice = await user.BitPay.CreateInvoiceAsync(
new Invoice
{
Price = 5000.0m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
cashCow = tester.ExplorerNode;
invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
firstPayment = Money.Coins(0.04m);
await cashCow.SendToAddressAsync(invoiceAddress, firstPayment);
TestLogs.LogInformation("First payment sent to " + invoiceAddress);
TestUtils.Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.True(invoice.BtcPaid == firstPayment);
});
cashCow = tester.LTCExplorerNode;
var ltcCryptoInfo = invoice.CryptoInfo.FirstOrDefault(c => c.CryptoCode == "LTC");
Assert.NotNull(ltcCryptoInfo);
invoiceAddress = BitcoinAddress.Create(ltcCryptoInfo.Address, cashCow.Network);
var secondPayment = Money.Coins(decimal.Parse(ltcCryptoInfo.Due, CultureInfo.InvariantCulture));
await cashCow.GenerateAsync(4); // LTC is not worth a lot, so just to make sure we have money...
await cashCow.SendToAddressAsync(invoiceAddress, secondPayment);
TestLogs.LogInformation("Second payment sent to " + invoiceAddress);
TestUtils.Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal(Money.Zero, invoice.BtcDue);
var ltcPaid = invoice.CryptoInfo.First(c => c.CryptoCode == "LTC");
Assert.Equal(Money.Zero, ltcPaid.Due);
Assert.Equal(secondPayment, ltcPaid.CryptoPaid);
Assert.Equal("paid", invoice.Status);
Assert.False((bool)((JValue)invoice.ExceptionStatus).Value);
});
controller = tester.PayTester.GetController<UIInvoiceController>(null);
checkout = (Models.InvoicingModels.CheckoutModel)((JsonResult)controller.GetStatus(invoice.Id, "LTC")
.GetAwaiter().GetResult()).Value;
Assert.Equal(2, checkout.AvailablePaymentMethods.Count);
Assert.Equal("LTC", checkout.PaymentMethodCurrency);
Assert.Equal(2, invoice.PaymentCodes.Count());
Assert.Equal(2, invoice.SupportedTransactionCurrencies.Count());
Assert.Equal(2, invoice.SupportedTransactionCurrencies.Count());
Assert.Equal(2, invoice.PaymentSubtotals.Count());
Assert.Equal(2, invoice.PaymentTotals.Count());
Assert.True(invoice.PaymentCodes.ContainsKey("LTC"));
Assert.True(invoice.SupportedTransactionCurrencies.ContainsKey("LTC"));
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
{
Price = 5000.0m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true,
SupportedTransactionCurrencies = new Dictionary<string, InvoiceSupportedTransactionCurrency>()
{
Price = 5000.0m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true,
SupportedTransactionCurrencies = new Dictionary<string, InvoiceSupportedTransactionCurrency>()
{
{"BTC", new InvoiceSupportedTransactionCurrency() {Enabled = true}}
}
}, Facade.Merchant);
{"BTC", new InvoiceSupportedTransactionCurrency() {Enabled = true}}
}
}, Facade.Merchant);
Assert.Single(invoice.CryptoInfo.Where(c => c.CryptoCode == "BTC"));
Assert.Empty(invoice.CryptoInfo.Where(c => c.CryptoCode == "LTC"));
}
Assert.Single(invoice.CryptoInfo, c => c.CryptoCode == "BTC");
Assert.DoesNotContain(invoice.CryptoInfo, c => c.CryptoCode == "LTC");
}
[Fact]
@ -745,7 +744,7 @@ noninventoryitem:
invoices = user.BitPay.GetInvoices();
Assert.Equal(2, invoices.Count(invoice => invoice.ItemCode.Equals("noninventoryitem")));
var inventoryItemInvoice =
Assert.Single(invoices.Where(invoice => invoice.ItemCode.Equals("inventoryitem")));
Assert.Single(invoices, invoice => invoice.ItemCode.Equals("inventoryitem"));
Assert.NotNull(inventoryItemInvoice);
//let's mark the inventoryitem invoice as invalid, this should return the item to back in stock
@ -759,39 +758,6 @@ noninventoryitem:
AppService.Parse(vmpos.Template).Single(item => item.Id == "inventoryitem").Inventory);
}, 10000);
//test payment methods option
vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
vmpos.Title = "hello";
vmpos.Currency = "BTC";
vmpos.Template = @"
btconly:
price: 1.0
title: good apple
payment_methods:
- BTC
normal:
price: 1.0";
vmpos.Template = AppService.SerializeTemplate(MigrationStartupTask.ParsePOSYML(vmpos.Template));
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, choiceKey: "btconly").Result);
Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, choiceKey: "normal").Result);
invoices = user.BitPay.GetInvoices();
var normalInvoice = invoices.Single(invoice => invoice.ItemCode == "normal");
var btcOnlyInvoice = invoices.Single(invoice => invoice.ItemCode == "btconly");
Assert.Single(btcOnlyInvoice.CryptoInfo);
Assert.Equal("BTC",
btcOnlyInvoice.CryptoInfo.First().CryptoCode);
Assert.Equal("BTC-CHAIN",
btcOnlyInvoice.CryptoInfo.First().PaymentType);
Assert.Equal(2, normalInvoice.CryptoInfo.Length);
Assert.Contains(
normalInvoice.CryptoInfo,
s => "BTC-CHAIN" == s.PaymentType && new[] { "BTC", "LTC" }.Contains(
s.CryptoCode));
//test topup option
vmpos.Template = @"
a:
@ -821,13 +787,13 @@ g:
vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
Assert.DoesNotContain("custom", vmpos.Template);
var items = AppService.Parse(vmpos.Template);
Assert.Contains(items, item => item.Id == "a" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Fixed);
Assert.Contains(items, item => item.Id == "b" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Fixed);
Assert.Contains(items, item => item.Id == "c" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Minimum);
Assert.Contains(items, item => item.Id == "d" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Fixed);
Assert.Contains(items, item => item.Id == "e" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Minimum);
Assert.Contains(items, item => item.Id == "f" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup);
Assert.Contains(items, item => item.Id == "g" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup);
Assert.Contains(items, item => item.Id == "a" && item.PriceType == AppItemPriceType.Fixed);
Assert.Contains(items, item => item.Id == "b" && item.PriceType == AppItemPriceType.Fixed);
Assert.Contains(items, item => item.Id == "c" && item.PriceType == AppItemPriceType.Minimum);
Assert.Contains(items, item => item.Id == "d" && item.PriceType == AppItemPriceType.Fixed);
Assert.Contains(items, item => item.Id == "e" && item.PriceType == AppItemPriceType.Minimum);
Assert.Contains(items, item => item.Id == "f" && item.PriceType == AppItemPriceType.Topup);
Assert.Contains(items, item => item.Id == "g" && item.PriceType == AppItemPriceType.Topup);
Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(app.Id, PosViewType.Static, choiceKey: "g").Result);
@ -837,5 +803,65 @@ g:
Assert.Equal("new", topupInvoice.Status);
}
}
[Fact]
[Trait("Integration", "Integration")]
public async Task CanUsePoSAppJsonEndpoint()
{
using var tester = CreateServerTester();
await tester.StartAsync();
var user = tester.NewAccount();
await user.GrantAccessAsync();
user.RegisterDerivationScheme("BTC");
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 = PointOfSaleAppType.AppType;
vm.AppName = "test";
vm.SelectedAppType = appType;
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 };
apps.HttpContext.SetAppData(appData);
pos.HttpContext.SetAppData(appData);
var vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
vmpos.Title = "App POS";
vmpos.Currency = "EUR";
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
// Failing requests
var (invoiceId1, error1) = await PosJsonRequest(tester, app.Id, "amount=-21&discount=10&tip=2");
Assert.Null(invoiceId1);
Assert.Equal("Negative amount is not allowed", error1);
var (invoiceId2, error2) = await PosJsonRequest(tester, app.Id, "amount=21&discount=-10&tip=-2");
Assert.Null(invoiceId2);
Assert.Equal("Negative tip or discount is not allowed", error2);
// Successful request
var (invoiceId3, error3) = await PosJsonRequest(tester, app.Id, "amount=21");
Assert.NotNull(invoiceId3);
Assert.Null(error3);
// Check generated invoice
var invoices = await user.BitPay.GetInvoicesAsync();
var invoice = invoices.First();
Assert.Equal(invoiceId3, invoice.Id);
Assert.Equal(21.00m, invoice.Price);
Assert.Equal("EUR", invoice.Currency);
}
private async Task<(string invoiceId, string error)> PosJsonRequest(ServerTester tester, string appId, string query)
{
var uriBuilder = new UriBuilder(tester.PayTester.ServerUri) { Path = $"/apps/{appId}/pos/light", Query = query };
var request = new HttpRequestMessage(HttpMethod.Post, uriBuilder.Uri);
request.Headers.Add("Accept", "application/json");
var response = await tester.PayTester.HttpClient.SendAsync(request);
var content = await response.Content.ReadAsStringAsync();
var json = JObject.Parse(content);
return (json["invoiceId"]?.Value<string>(), json["error"]?.Value<string>());
}
}
}

View File

@ -63,7 +63,6 @@ namespace BTCPayServer.Tests
//no tether on our regtest, lets create it and set it
var tether = tester.NetworkProvider.GetNetwork<ElementsBTCPayNetwork>("USDT");
var lbtc = tester.NetworkProvider.GetNetwork<ElementsBTCPayNetwork>("LBTC");
var etb = tester.NetworkProvider.GetNetwork<ElementsBTCPayNetwork>("ETB");
var issueAssetResult = await tester.LBTCExplorerNode.SendCommandAsync("issueasset", 100000, 0);
tether.AssetId = uint256.Parse(issueAssetResult.Result["asset"].ToString());
((ElementsBTCPayNetwork)tester.PayTester.GetService<BTCPayWalletProvider>().GetWallet("USDT").Network)
@ -71,15 +70,10 @@ namespace BTCPayServer.Tests
Assert.Equal(tether.AssetId, tester.NetworkProvider.GetNetwork<ElementsBTCPayNetwork>("USDT").AssetId);
Assert.Equal(tether.AssetId, ((ElementsBTCPayNetwork)tester.PayTester.GetService<BTCPayWalletProvider>().GetWallet("USDT").Network).AssetId);
var issueAssetResult2 = await tester.LBTCExplorerNode.SendCommandAsync("issueasset", 100000, 0);
etb.AssetId = uint256.Parse(issueAssetResult2.Result["asset"].ToString());
((ElementsBTCPayNetwork)tester.PayTester.GetService<BTCPayWalletProvider>().GetWallet("ETB").Network)
.AssetId = etb.AssetId;
user.RegisterDerivationScheme("LBTC");
user.RegisterDerivationScheme("USDT");
user.RegisterDerivationScheme("ETB");
//test: register 2 assets on the same elements network and make sure paying an invoice on one does not affect the other in any way
var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(0.1m, "BTC"));
@ -109,11 +103,7 @@ namespace BTCPayServer.Tests
Assert.Equal("paid", localInvoice.Status);
Assert.Single(localInvoice.CryptoInfo.Single(info => info.CryptoCode.Equals("USDT", StringComparison.InvariantCultureIgnoreCase)).Payments);
});
//test precision based on https://github.com/ElementsProject/elements/issues/805#issuecomment-601277606
var etbBip21 = new BitcoinUrlBuilder(invoice.CryptoInfo.Single(info => info.CryptoCode == "ETB").PaymentUrls.BIP21, etb.NBitcoinNetwork);
//precision = 2, 1ETB = 0.00000100
Assert.Equal(100, etbBip21.Amount.Satoshi);
var lbtcBip21 = new BitcoinUrlBuilder(invoice.CryptoInfo.Single(info => info.CryptoCode == "LBTC").PaymentUrls.BIP21, lbtc.NBitcoinNetwork);
//precision = 8, 0.1 = 0.1

View File

@ -39,13 +39,14 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.15" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.16" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.11" />
<PackageReference Include="Selenium.Support" Version="4.1.1" />
<PackageReference Include="Selenium.WebDriver" Version="4.22.0" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="128.0.6613.11900" />
<PackageReference Include="xunit" Version="2.6.6" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="133.0.6943.5300" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>

View File

@ -7,12 +7,14 @@ using System.Security.Claims;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Configuration;
using BTCPayServer.HostedServices;
using BTCPayServer.Hosting;
using BTCPayServer.Rating;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Mails;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Tests.Logging;
@ -27,7 +29,6 @@ using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using NBitcoin;
using NBXplorer;
using AuthenticationSchemes = BTCPayServer.Abstractions.Constants.AuthenticationSchemes;
namespace BTCPayServer.Tests
{

View File

@ -65,6 +65,11 @@ namespace BTCPayServer.Tests
Assert.Equal($"bitcoin:{address}", clipboard);
Assert.Equal($"bitcoin:{address.ToUpperInvariant()}", qrValue);
s.Driver.ElementDoesNotExist(By.Id("Lightning_BTC-CHAIN"));
// Contact option
var contactLink = s.Driver.FindElement(By.Id("ContactLink"));
Assert.Equal("Contact us", contactLink.Text);
Assert.Matches(supportUrl.Replace("{InvoiceId}", invoiceId), contactLink.GetAttribute("href"));
// Details should show exchange rate
s.Driver.ToggleCollapse("PaymentDetails");
@ -138,7 +143,6 @@ namespace BTCPayServer.Tests
Assert.Contains("resubmit a payment", expiredSection.Text);
Assert.DoesNotContain("This invoice expired with partial payment", expiredSection.Text);
});
Assert.True(s.Driver.ElementDoesNotExist(By.Id("ContactLink")));
Assert.True(s.Driver.ElementDoesNotExist(By.Id("ReceiptLink")));
Assert.Equal(storeUrl, s.Driver.FindElement(By.Id("StoreLink")).GetAttribute("href"));
@ -172,9 +176,6 @@ namespace BTCPayServer.Tests
Assert.Contains("This invoice expired with partial payment", expiredSection.Text);
Assert.DoesNotContain("resubmit a payment", expiredSection.Text);
});
var contactLink = s.Driver.FindElement(By.Id("ContactLink"));
Assert.Equal("Contact us", contactLink.Text);
Assert.Matches(supportUrl.Replace("{InvoiceId}", invoiceId), contactLink.GetAttribute("href"));
Assert.True(s.Driver.ElementDoesNotExist(By.Id("ReceiptLink")));
Assert.Equal(storeUrl, s.Driver.FindElement(By.Id("StoreLink")).GetAttribute("href"));
@ -243,7 +244,6 @@ namespace BTCPayServer.Tests
});
s.Driver.FindElement(By.Id("confetti"));
s.Driver.FindElement(By.Id("ReceiptLink"));
Assert.True(s.Driver.ElementDoesNotExist(By.Id("ContactLink")));
Assert.Equal(storeUrl, s.Driver.FindElement(By.Id("StoreLink")).GetAttribute("href"));
// BIP21

View File

@ -263,7 +263,7 @@ namespace BTCPayServer.Tests
});
TestLogs.LogInformation("Because UseAllStoreInvoices is true, let's make sure the invoice is tagged");
var invoiceEntity = tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id).GetAwaiter().GetResult();
var invoiceEntity = await tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id);
Assert.True(invoiceEntity.Version >= InvoiceEntity.InternalTagSupport_Version);
Assert.Contains(AppService.GetAppInternalTag(app.Id), invoiceEntity.InternalTags);
@ -281,7 +281,7 @@ namespace BTCPayServer.Tests
TransactionSpeed = "high",
FullNotifications = true
}, Facade.Merchant);
invoiceEntity = tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id).GetAwaiter().GetResult();
invoiceEntity = await tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id);
Assert.DoesNotContain(AppService.GetAppInternalTag(app.Id), invoiceEntity.InternalTags);
TestLogs.LogInformation("After turning setting a softcap, let's check that only actual payments are counted");

View File

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

View File

@ -19,6 +19,19 @@ namespace BTCPayServer.Tests
{
public static class Extensions
{
public static Task<NewTransactionEvent> WaitReceive(this NBXplorer.WebsocketNotificationSession notifications, DerivationStrategyBase target, Func<NewTransactionEvent, bool> predicate = null, CancellationToken cancellationToken = default)
=> WaitNext<NewTransactionEvent>(notifications, e => e.DerivationStrategy == target && (predicate is null || predicate(e)), cancellationToken);
public static async Task<TEvent> WaitNext<TEvent>(this NBXplorer.WebsocketNotificationSession notifications, Func<TEvent, bool> predicate, CancellationToken cancellationToken = default) where TEvent : NewEventBase
{
retry:
var evt = await notifications.NextEventAsync(cancellationToken);
if (evt is TEvent { } e)
{
if (predicate(e))
return e;
}
goto retry;
}
public static Task<KeyPathInformation> ReserveAddressAsync(this BTCPayWallet wallet, DerivationStrategyBase derivationStrategyBase)
{
return wallet.ReserveAddressAsync(null, derivationStrategyBase, "test");

View File

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection.Metadata;
using System.Runtime.CompilerServices;
using System.Security;
using System.Text;
@ -11,7 +10,6 @@ using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Configuration;
@ -20,40 +18,28 @@ using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.Hosting;
using BTCPayServer.JsonConverters;
using BTCPayServer.Lightning;
using BTCPayServer.Logging;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Plugins;
using BTCPayServer.Plugins.Bitcoin;
using BTCPayServer.Rating;
using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Fees;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Labels;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Validation;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBitcoin.Scripting.Parser;
using NBXplorer;
using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using Xunit;
using Xunit.Abstractions;
using StoreData = BTCPayServer.Data.StoreData;
namespace BTCPayServer.Tests
{
@ -117,11 +103,13 @@ namespace BTCPayServer.Tests
{
var compose1 = File.ReadAllText(Path.Combine(TestUtils.TryGetSolutionDirectoryInfo().FullName, "BTCPayServer.Tests", "docker-compose.yml"));
var compose2 = File.ReadAllText(Path.Combine(TestUtils.TryGetSolutionDirectoryInfo().FullName, "BTCPayServer.Tests", "docker-compose.altcoins.yml"));
var compose3 = File.ReadAllText(Path.Combine(TestUtils.TryGetSolutionDirectoryInfo().FullName, "BTCPayServer.Tests", "docker-compose.mutinynet.yml"));
var compose4 = File.ReadAllText(Path.Combine(TestUtils.TryGetSolutionDirectoryInfo().FullName, "BTCPayServer.Tests", "docker-compose.testnet.yml"));
List<DockerImage> GetImages(string content)
{
List<DockerImage> images = new List<DockerImage>();
foreach (var line in content.Split(new[] { "\n", "\r\n" }, StringSplitOptions.RemoveEmptyEntries))
var images = new List<DockerImage>();
foreach (var line in content.Split(["\n", "\r\n"], StringSplitOptions.RemoveEmptyEntries))
{
var l = line.Trim();
if (l.StartsWith("image:", StringComparison.OrdinalIgnoreCase))
@ -134,13 +122,15 @@ namespace BTCPayServer.Tests
var img1 = GetImages(compose1);
var img2 = GetImages(compose2);
var groups = img1.Concat(img2).GroupBy(g => g.Name);
var img3 = GetImages(compose3);
var img4 = GetImages(compose4);
var groups = img1.Concat(img2).Concat(img3).Concat(img4).GroupBy(g => g.Name);
foreach (var g in groups)
{
var tags = new HashSet<String>(g.Select(o => o.Tag));
var tags = new HashSet<string>(g.Select(o => o.Tag));
if (tags.Count != 1)
{
Assert.Fail($"All docker images '{g.Key}' in docker-compose.yml and docker-compose.altcoins.yml should have the same tags. (Found {string.Join(',', tags)})");
Assert.Fail($"All docker images '{g.Key}' across the docker-compose.yml files should have the same tags. (Found {string.Join(',', tags)})");
}
}
}
@ -188,10 +178,10 @@ namespace BTCPayServer.Tests
public void CanRandomizeByPercentage()
{
var generated = Enumerable.Range(0, 1000).Select(_ => MempoolSpaceFeeProvider.RandomizeByPercentage(100.0m, 10.0m)).ToArray();
Assert.Empty(generated.Where(g => g < 90m));
Assert.Empty(generated.Where(g => g > 110m));
Assert.NotEmpty(generated.Where(g => g < 91m));
Assert.NotEmpty(generated.Where(g => g > 109m));
Assert.DoesNotContain(generated, g => g < 90m);
Assert.DoesNotContain(generated, g => g > 110m);
Assert.Contains(generated, g => g < 91m);
Assert.Contains(generated, g => g > 109m);
}
private void CanParseDecimalsCore(string str, decimal expected)
@ -679,6 +669,26 @@ namespace BTCPayServer.Tests
Assert.Equal(utxo54, utxos[53]);
}
[Fact]
public void ResourceTrackerTest()
{
var tracker = new ResourceTracker<string>();
var t1 = tracker.StartTracking();
Assert.True(t1.TryTrack("1"));
Assert.False(t1.TryTrack("1"));
var t2 = tracker.StartTracking();
Assert.True(t2.TryTrack("2"));
Assert.False(t2.TryTrack("1"));
Assert.True(t1.Contains("1"));
Assert.True(t2.Contains("2"));
Assert.True(tracker.Contains("1"));
Assert.True(tracker.Contains("2"));
t1.Dispose();
Assert.False(tracker.Contains("1"));
Assert.True(tracker.Contains("2"));
Assert.True(t2.TryTrack("1"));
}
[Fact]
public void CanAcceptInvoiceWithTolerance()
{
@ -787,9 +797,9 @@ namespace BTCPayServer.Tests
}), BTCPayLogs);
await tor.Refresh();
Assert.Single(tor.Services.Where(t => t.ServiceType == TorServiceType.BTCPayServer));
Assert.Single(tor.Services.Where(t => t.ServiceType == TorServiceType.P2P));
Assert.Single(tor.Services.Where(t => t.ServiceType == TorServiceType.RPC));
Assert.Single(tor.Services, t => t.ServiceType == TorServiceType.BTCPayServer);
Assert.Single(tor.Services, t => t.ServiceType == TorServiceType.P2P);
Assert.Single(tor.Services, t => t.ServiceType == TorServiceType.RPC);
Assert.True(tor.Services.Count(t => t.ServiceType == TorServiceType.Other) > 1);
tor = new TorServices(CreateNetworkProvider(ChainName.Regtest),
@ -800,24 +810,24 @@ namespace BTCPayServer.Tests
}), BTCPayLogs);
await Task.WhenAll(tor.StartAsync(CancellationToken.None));
var btcpayS = Assert.Single(tor.Services.Where(t => t.ServiceType == TorServiceType.BTCPayServer));
var btcpayS = Assert.Single(tor.Services, t => t.ServiceType == TorServiceType.BTCPayServer);
Assert.Null(btcpayS.Network);
Assert.Equal("host.onion", btcpayS.OnionHost);
Assert.Equal(80, btcpayS.VirtualPort);
var p2p = Assert.Single(tor.Services.Where(t => t.ServiceType == TorServiceType.P2P));
var p2p = Assert.Single(tor.Services, t => t.ServiceType == TorServiceType.P2P);
Assert.NotNull(p2p.Network);
Assert.Equal("BTC", p2p.Network.CryptoCode);
Assert.Equal("host2.onion", p2p.OnionHost);
Assert.Equal(81, p2p.VirtualPort);
var rpc = Assert.Single(tor.Services.Where(t => t.ServiceType == TorServiceType.RPC));
var rpc = Assert.Single(tor.Services, t => t.ServiceType == TorServiceType.RPC);
Assert.NotNull(p2p.Network);
Assert.Equal("BTC", rpc.Network.CryptoCode);
Assert.Equal("host3.onion", rpc.OnionHost);
Assert.Equal(82, rpc.VirtualPort);
var unknown = Assert.Single(tor.Services.Where(t => t.ServiceType == TorServiceType.Other));
var unknown = Assert.Single(tor.Services, t => t.ServiceType == TorServiceType.Other);
Assert.Null(unknown.Network);
Assert.Equal("host4.onion", unknown.OnionHost);
Assert.Equal(83, unknown.VirtualPort);
@ -848,6 +858,13 @@ namespace BTCPayServer.Tests
Assert.IsType<MultisigDerivationStrategy>(((P2WSHDerivationStrategy)strategyBase).Inner);
Assert.Equal(expected, strategyBase.ToString());
foreach (var space in new[] { "\r\n", " ", "\t" })
{
var expectedWithNewLines = $"2-of-tpubDDXgATYzdQkHHhZZCMcNJj8BGDENvzMVou5v9NdxiP4rxDLj33nS233dGFW4htpVZSJ6zds9eVqAV9RyRHHiKtwQKX8eR4n4KN3Dwmj7A3h-{space}tpubDC8a54NFtQtMQAZ97VhoU9V6jVTvi9w4Y5SaAXJSBYETKg3AoX5CCKndznhPWxJUBToPCpT44s86QbKdGpKAnSjcMTGW4kE6UQ8vpBjcybW-tpubDChjnP9LXNrJp43biqjY7FH93wgRRNrNxB4Q8pH7PPRy8UPcH2S6V46WGVJ47zVGF7SyBJNCpnaogsFbsybVQckGtVhCkng3EtFn8qmxptS";
strategyBase = parser.Parse(expectedWithNewLines);
Assert.Equal(expected, strategyBase.ToString());
}
var inner = (MultisigDerivationStrategy)((P2WSHDerivationStrategy)strategyBase).Inner;
Assert.False(inner.IsLegacy);
Assert.Equal(3, inner.Keys.Count);
@ -864,7 +881,7 @@ namespace BTCPayServer.Tests
Assert.True(((DirectDerivationStrategy)strategyBase).Segwit);
// Failure cases
Assert.Throws<FormatException>(() => { parser.Parse("xpub 661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw"); }); // invalid format because of space
Assert.Throws<FormatException>(() => { parser.Parse("xpubZ661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw"); });
Assert.Throws<ParsingException>(() => { parser.ParseOutputDescriptor("invalid"); }); // invalid in general
Assert.Throws<ParsingException>(() => { parser.ParseOutputDescriptor("wpkh([8b60afd1/49h/0h/0h]xpub661MyMwAFXkMnyoBjyHndD3QwRbcGVBsTGeNZN6QGVHcfz4MPzBUxjSevweNFQx7SqmMHLdSA4FteGsRrEriu4pnVZMZWnruFFAYZATtcDw/0/*)#9x4vkw48"); }); // invalid checksum
}
@ -1644,7 +1661,7 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
testCases.ForEach(tuple =>
{
Assert.Equal(tuple.expectedOutput, UIInvoiceController.PosDataParser.ParsePosData(string.IsNullOrEmpty(tuple.input) ? null : JToken.Parse(tuple.input)));
Assert.Equal(tuple.expectedOutput, PosDataParser.ParsePosData(string.IsNullOrEmpty(tuple.input) ? null : JToken.Parse(tuple.input)));
});
}
[Fact]

View File

@ -0,0 +1,227 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.BIP78.Sender;
using BTCPayServer.Controllers;
using BTCPayServer.Events;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Views.Wallets;
using NBitcoin;
using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
using OpenQA.Selenium;
using Xunit;
using Xunit.Abstractions;
namespace BTCPayServer.Tests.FeatureTests;
[Collection(nameof(NonParallelizableCollectionDefinition))]
public class MultisigTests : UnitTestBase
{
public MultisigTests(ITestOutputHelper helper) : base(helper)
{
}
[Fact]
[Trait("Selenium", "Selenium")]
public async Task SignTestPSBT()
{
var cryptoCode = "BTC";
using var s = CreateSeleniumTester();
await s.StartAsync();
var network = s.Server.NetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
var resp1 = generateWalletResp("tprv8ZgxMBicQKsPeGSkDtxjScBmmHP4rfSEPkf1vNmoqt5QjPTco2zPd6UVWkJf2fU8gdKPYRdDMizxtMRqmpVpxsWuqRxVs2d5VsEhwxaK3h7",
"57b3f43a/84'/1'/0'", "tpubDCzBHRPRcv7Y3utw1hZVrCar21gsj8vsXcehAG4z3R4NnmdMAASQwYYxGBd2f4q5s5ZFGvQBBFs1jVcGsXYoSTA1YFQPwizjsQLU12ibLyu", network);
var resp2 = generateWalletResp("tprv8ZgxMBicQKsPeC6Xuw83UJHgjnszEUjwH9E5f5FZ3fHgJHBQApo8CmFCsowcdwbRM119UnTqSzVWUsWGtLsxc8wnZa5L8xmEsvEpiyRj4Js",
"ee7d36c4/84'/1'/0'", "tpubDCetxnEjn8HXA5NrDZbKKTUUYoWCVC2V3X7Kmh3o9UYTfh9c3wTPKyCyeUrLkQ8KHYptEsBoQq6AgqPZiW5neEgb2kjKEr41q1qSevoPFDM", network);
var resp3 = generateWalletResp("tprv8ZgxMBicQKsPekSniuKwLtXpB82dSDV8ZAK4uLUHxkiHWfDtR5yYwNZiicKdpT3UYwzTTMvXESCm45KyAiH7kiJY6yk51neC9ZvmwDpNsQh",
"6c014fb3/84'/1'/0'", "tpubDCaTgjJfS5UEim6h66VpQBEZ2Tj6hHk8TzvL81HygdW1M8vZCRhUZLNhb3WTimyP2XMQRA3QGZPwwxUsEFQYK4EoRUWTcb9oB237FJ112tN", network);
var multisigDerivationScheme = $"wsh(multi(2,[{resp1.AccountKeyPath}]{resp1.DerivationScheme}/0/*," +
$"[{resp2.AccountKeyPath}]{resp2.DerivationScheme}/0/*," +
$"[{resp3.AccountKeyPath}]{resp3.DerivationScheme}/0/*))";
var strategy = UIStoresController.ParseDerivationStrategy(multisigDerivationScheme, network);
strategy.Source = "ManualDerivationScheme";
var derivationScheme = strategy.AccountDerivation;
var testPSBT =
"cHNidP8BAIkCAAAAAQmiSunnaKN7F4Jv5uHROfYbIZOckCck/Wo7gAQmi9hfAAAAAAD9////AtgbZgAAAAAAIgAgWCUFlU9eWkyxn0l0yQxs2rXQZ7d9Ry8LaYECaVC0TUGAlpgAAAAAACIAIFZxT+UIdhHZC4qFPhPQ6IXdX+44HIxCYcoh/bNOhB0hAAAAAAABAStAAf8AAAAAACIAIL2DDkfKwKHxZj2EKxXUd4uwf0IvPaCxUtAPq9snpq9TAQDqAgAAAAABAVuHuou9E5y6zUJaUreQD0wUeiPnT2aY+YU7QaPJOiQCAAAAAAD9////AkAB/wAAAAAAIgAgvYMOR8rAofFmPYQrFdR3i7B/Qi89oLFS0A+r2yemr1PM5AYpAQAAABYAFIlFupZkD07+GRo24WRS3IFcf+EuAkcwRAIgGi9wAcTfc0d0+j+Vg82aYklXCUsPg+g3jS+PTBTSQwkCIAPh5CZF18DTBKqWU2qdhNCbZ8Tp/NCEHjLJRHcH0oluASECWnI1s9ozQRL2qbK6JbLHzj9LlU9Pras3nZfq/njBJwhwAAAAAQVpUiECMCCasr2FRmRMiWkM/l1iraFR18td5SZ2APyQiaI0yY8hA8K96vH64BelUJiEPGwM6UTwRSfAJUR2j8dkw7i31fFTIQMlHLlaAPxw3fl1vaM1EofIirt79MXOryM54zpHwu1GlVOuIgIDwr3q8frgF6VQmIQ8bAzpRPBFJ8AlRHaPx2TDuLfV8VNHMEQCIANnprskJz8oVsetqOEViHtzhmSG8c36r3zmUIHwIoOhAiAZ1jBqj40iu2S/nMfiGyuCC/jSiSGik7YVwiwN+bbxPAEiBgIwIJqyvYVGZEyJaQz+XWKtoVHXy13lJnYA/JCJojTJjxhXs/Q6VAAAgAEAAIAAAACAAAAAAAUAAAAiBgMlHLlaAPxw3fl1vaM1EofIirt79MXOryM54zpHwu1GlRhsAU+zVAAAgAEAAIAAAACAAAAAAAUAAAAiBgPCverx+uAXpVCYhDxsDOlE8EUnwCVEdo/HZMO4t9XxUxjufTbEVAAAgAEAAIAAAACAAAAAAAUAAAAAAQFpUiEDa/J6SaiRjP1jhq9jpNxFKovEuWBz28seNMvsn0JC/ZIhA7p3bS7vLYB5UxlNN6YqkEDITyaMlk/i450q6+4woveAIQPTchIOrd+TNGBOX6il1HRZnBndyRoUj/hahbjTaAGHglOuIgIDa/J6SaiRjP1jhq9jpNxFKovEuWBz28seNMvsn0JC/ZIYV7P0OlQAAIABAACAAAAAgAEAAAABAAAAIgIDundtLu8tgHlTGU03piqQQMhPJoyWT+LjnSrr7jCi94AY7n02xFQAAIABAACAAAAAgAEAAAABAAAAIgID03ISDq3fkzRgTl+opdR0WZwZ3ckaFI/4WoW402gBh4IYbAFPs1QAAIABAACAAAAAgAEAAAABAAAAAAEBaVIhA/fCRR3MWwCgNuXMvlWLonY+TurUKOHXOSHALCck62deIQPqeQXD8ws9SDEDXSyD6a3WFlIGH+gDUf2/xAfw8HxE8iEC3LBRJYYxRzIeg9NxLGvtfATvFaKsO9D7AUjoTLZzke5TriICAtywUSWGMUcyHoPTcSxr7XwE7xWirDvQ+wFI6Ey2c5HuGGwBT7NUAACAAQAAgAAAAIAAAAAADAAAACICA+p5BcPzCz1IMQNdLIPprdYWUgYf6ANR/b/EB/DwfETyGO59NsRUAACAAQAAgAAAAIAAAAAADAAAACICA/fCRR3MWwCgNuXMvlWLonY+TurUKOHXOSHALCck62deGFez9DpUAACAAQAAgAAAAIAAAAAADAAAAAA=";
var signedPsbt = SignWithSeed(testPSBT, derivationScheme, resp1);
s.TestLogs.LogInformation($"Signed PSBT: {signedPsbt}");
}
[Fact]
[Trait("Selenium", "Selenium")]
public async Task CanEnableAndUseMultisigWallet()
{
var cryptoCode = "BTC";
using var s = CreateSeleniumTester();
await s.StartAsync();
// var invoiceRepository = s.Server.PayTester.GetService<InvoiceRepository>();
s.RegisterNewUser(true);
var storeData = s.CreateNewStore();
var explorerProvider = s.Server.PayTester.GetService<ExplorerClientProvider>();
var client = explorerProvider.GetExplorerClient(cryptoCode);
var req = new GenerateWalletRequest { ScriptPubKeyType = ScriptPubKeyType.Segwit, SavePrivateKeys = true };
// var resp1 = await client.GenerateWalletAsync(req);
// s.TestLogs.LogInformation($"Created hot wallet 1: {resp1.DerivationScheme} | {resp1.AccountKeyPath} | {resp1.MasterHDKey.ToWif()}");
// var resp2 = await client.GenerateWalletAsync(req);
// s.TestLogs.LogInformation($"Created hot wallet 2: {resp2.DerivationScheme} | {resp2.AccountKeyPath} | {resp2.MasterHDKey.ToWif()}");
// var resp3 = await client.GenerateWalletAsync(req);
// s.TestLogs.LogInformation($"Created hot wallet 3: {resp3.DerivationScheme} | {resp3.AccountKeyPath} | {resp3.MasterHDKey.ToWif()}");
var network = s.Server.NetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
var resp1 = generateWalletResp("tprv8ZgxMBicQKsPeGSkDtxjScBmmHP4rfSEPkf1vNmoqt5QjPTco2zPd6UVWkJf2fU8gdKPYRdDMizxtMRqmpVpxsWuqRxVs2d5VsEhwxaK3h7",
"57b3f43a/84'/1'/0'", "tpubDCzBHRPRcv7Y3utw1hZVrCar21gsj8vsXcehAG4z3R4NnmdMAASQwYYxGBd2f4q5s5ZFGvQBBFs1jVcGsXYoSTA1YFQPwizjsQLU12ibLyu", network);
var resp2 = generateWalletResp("tprv8ZgxMBicQKsPeC6Xuw83UJHgjnszEUjwH9E5f5FZ3fHgJHBQApo8CmFCsowcdwbRM119UnTqSzVWUsWGtLsxc8wnZa5L8xmEsvEpiyRj4Js",
"ee7d36c4/84'/1'/0'", "tpubDCetxnEjn8HXA5NrDZbKKTUUYoWCVC2V3X7Kmh3o9UYTfh9c3wTPKyCyeUrLkQ8KHYptEsBoQq6AgqPZiW5neEgb2kjKEr41q1qSevoPFDM", network);
var resp3 = generateWalletResp("tprv8ZgxMBicQKsPekSniuKwLtXpB82dSDV8ZAK4uLUHxkiHWfDtR5yYwNZiicKdpT3UYwzTTMvXESCm45KyAiH7kiJY6yk51neC9ZvmwDpNsQh",
"6c014fb3/84'/1'/0'", "tpubDCaTgjJfS5UEim6h66VpQBEZ2Tj6hHk8TzvL81HygdW1M8vZCRhUZLNhb3WTimyP2XMQRA3QGZPwwxUsEFQYK4EoRUWTcb9oB237FJ112tN", network);
var multisigDerivationScheme = $"wsh(multi(2,[{resp1.AccountKeyPath}]{resp1.DerivationScheme}/0/*," +
$"[{resp2.AccountKeyPath}]{resp2.DerivationScheme}/0/*," +
$"[{resp3.AccountKeyPath}]{resp3.DerivationScheme}/0/*))";
var strategy = UIStoresController.ParseDerivationStrategy(multisigDerivationScheme, network);
strategy.Source = "ManualDerivationScheme";
var derivationScheme = strategy.AccountDerivation;
s.GoToWalletSettings();
s.Driver.FindElement(By.Id("ImportWalletOptionsLink")).Click();
s.Driver.FindElement(By.Id("ImportXpubLink")).Click();
s.Driver.FindElement(By.Id("DerivationScheme")).SendKeys(multisigDerivationScheme);
s.Driver.FindElement(By.Id("Continue")).Click();
s.Driver.FindElement(By.Id("Confirm")).Click();
s.TestLogs.LogInformation($"Multisig wallet setup: {multisigDerivationScheme}");
// enabling multisig
s.Driver.FindElement(By.Id("IsMultiSigOnServer")).Click();
s.Driver.FindElement(By.Id("DefaultIncludeNonWitnessUtxo")).Click();
s.Driver.FindElement(By.Id("SaveWalletSettings")).Click();
Assert.Contains("Wallet settings successfully updated.", s.FindAlertMessage().Text);
// fetch address from receive page
s.Driver.FindElement(By.Id("WalletNav-Receive")).Click();
var address = s.Driver.FindElement(By.Id("Address")).GetAttribute("data-text");
s.Driver.FindElement(By.XPath("//button[@value='fill-wallet']")).Click();
s.Driver.FindElement(By.Id("CancelWizard")).Click();
// we are creating a pending transaction
s.Driver.FindElement(By.Id("WalletNav-Send")).Click();
s.Driver.FindElement(By.Id("Outputs_0__DestinationAddress")).SendKeys(address);
var amount = "0.1";
s.Driver.FindElement(By.Id("Outputs_0__Amount")).SendKeys(amount);
s.Driver.FindElement(By.Id("CreatePendingTransaction")).Click();
// now clicking on View to sign transaction
await SignPendingTransactionWithKey(s, address, derivationScheme, resp1);
await SignPendingTransactionWithKey(s, address, derivationScheme, resp2);
// Broadcasting transaction and ensuring there is no longer broadcast button
s.Driver.WaitForElement(By.XPath("//a[text()='Broadcast']")).Click();
s.Driver.FindElement(By.Id("BroadcastTransaction")).Click();
Assert.Contains("Transaction broadcasted successfully", s.FindAlertMessage().Text);
s.Driver.AssertElementNotFound(By.XPath("//a[text()='Broadcast']"));
// Abort pending transaction flow
s.Driver.FindElement(By.Id("WalletNav-Send")).Click();
s.Driver.FindElement(By.Id("Outputs_0__DestinationAddress")).SendKeys(address);
s.Driver.FindElement(By.Id("Outputs_0__Amount")).SendKeys("0.2");
s.Driver.FindElement(By.Id("CreatePendingTransaction")).Click();
s.Driver.FindElement(By.XPath("//a[text()='Abort']")).Click();
s.Driver.FindElement(By.Id("ConfirmContinue")).Click();
Assert.Contains("Aborted Pending Transaction", s.FindAlertMessage().Text);
s.TestLogs.LogInformation($"Finished MultiSig Flow");
}
private async Task SignPendingTransactionWithKey(SeleniumTester s, string address,
DerivationStrategyBase derivationScheme, GenerateWalletResponse signingKey)
{
// getting to pending transaction page
s.Driver.WaitForElement(By.XPath("//a[text()='View']")).Click();
var transactionRow = s.Driver.FindElement(By.XPath($"//tr[td[text()='{address}']]"));
Assert.NotNull(transactionRow);
var signTransactionButton = s.Driver.FindElement(By.Id("SignTransaction"));
Assert.NotNull(signTransactionButton);
// fetching PSBT
s.Driver.FindElement(By.Id("PSBTOptionsExportHeader")).Click();
s.Driver.WaitForElement(By.Id("ShowRawVersion")).Click();
var psbt = s.Driver.WaitForElement(By.Id("psbt-base64")).Text;
while (string.IsNullOrEmpty(psbt))
{
psbt = s.Driver.FindElement(By.Id("psbt-base64")).Text;
}
// signing PSBT and entering it to submit
var signedPsbt = SignWithSeed(psbt, derivationScheme, signingKey);
s.Driver.FindElement(By.Id("PSBTOptionsImportHeader")).Click();
s.Driver.WaitForElement(By.Id("ImportedPSBT")).SendKeys(signedPsbt);
s.Driver.FindElement(By.Id("Decode")).Click();
}
private GenerateWalletResponse generateWalletResp(string tpriv, string keypath, string derivation, BTCPayNetwork network)
{
var key1 = new BitcoinExtKey(
ExtKey.Parse(tpriv, Network.RegTest),
Network.RegTest);
var parser = new DerivationSchemeParser(network);
var resp1 = new GenerateWalletResponse
{
MasterHDKey = key1,
DerivationScheme = parser.Parse(derivation),
AccountKeyPath = RootedKeyPath.Parse(keypath)
};
return resp1;
}
public string SignWithSeed(string psbtBase64, DerivationStrategyBase derivationStrategyBase,
GenerateWalletResponse resp)
{
var strMasterHdKey = resp.MasterHDKey;
var extKey = new BitcoinExtKey(strMasterHdKey, Network.RegTest);
var strKeypath = resp.AccountKeyPath.ToStringWithEmptyKeyPathAware();
RootedKeyPath rootedKeyPath = RootedKeyPath.Parse(strKeypath);
if (rootedKeyPath.MasterFingerprint != extKey.GetPublicKey().GetHDFingerPrint())
throw new Exception("Master fingerprint mismatch. Ensure the wallet matches the PSBT.");
// finished setting variables, now onto signing
var psbt = PSBT.Parse(psbtBase64, Network.RegTest);
// Sign the PSBT
extKey = extKey.Derive(rootedKeyPath.KeyPath);
psbt.Settings.SigningOptions = new SigningOptions();
var changed = psbt.PSBTChanged(() => psbt.SignAll(derivationStrategyBase, extKey, rootedKeyPath));
if (!changed)
throw new Exception("Failed to sign the PSBT. Ensure the inputs align with the account key path.");
// Return the updated and signed PSBT
return psbt.ToBase64();
}
}

View File

@ -17,11 +17,14 @@ using BTCPayServer.NTag424;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.PayoutProcessors;
using BTCPayServer.PayoutProcessors.Lightning;
using BTCPayServer.Plugins.PointOfSale.Controllers;
using BTCPayServer.Plugins.PointOfSale.Models;
using BTCPayServer.Rating;
using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Mails;
using BTCPayServer.Services.Notifications;
using BTCPayServer.Services.Notifications.Blobs;
using BTCPayServer.Services.Stores;
@ -317,6 +320,20 @@ namespace BTCPayServer.Tests
Assert.Empty(await client.GetFiles());
storeData = await client.GetStore(store.Id);
Assert.Null(storeData.LogoUrl);
// App Item Image
var app = await client.CreatePointOfSaleApp(store.Id, new PointOfSaleAppRequest { AppName = "Test App" });
await AssertValidationError(["file"],
async () => await client.UploadAppItemImage(app.Id, filePath, "text/csv")
);
var fileData = await client.UploadAppItemImage(app.Id, logoPath, "image/png");
Assert.Equal("logo.png", fileData.OriginalName);
files = await client.GetFiles();
Assert.Single(files);
await client.DeleteAppItemImage(app.Id, fileData.Id);
Assert.Empty(await client.GetFiles());
}
[Fact(Timeout = TestTimeout)]
@ -744,9 +761,9 @@ namespace BTCPayServer.Tests
await user.RegisterDerivationSchemeAsync("BTC");
var client = await user.CreateClient();
var item1 = new ViewPointOfSaleViewModel.Item { Id = "item1", Title = "Item 1", Price = 1, PriceType = ViewPointOfSaleViewModel.ItemPriceType.Fixed };
var item2 = new ViewPointOfSaleViewModel.Item { Id = "item2", Title = "Item 2", Price = 2, PriceType = ViewPointOfSaleViewModel.ItemPriceType.Fixed };
var item3 = new ViewPointOfSaleViewModel.Item { Id = "item3", Title = "Item 3", Price = 3, PriceType = ViewPointOfSaleViewModel.ItemPriceType.Fixed };
var item1 = new AppItem { Id = "item1", Title = "Item 1", Price = 1, PriceType = AppItemPriceType.Fixed };
var item2 = new AppItem { Id = "item2", Title = "Item 2", Price = 2, PriceType = AppItemPriceType.Fixed };
var item3 = new AppItem { Id = "item3", Title = "Item 3", Price = 3, PriceType = AppItemPriceType.Fixed };
var posItems = AppService.SerializeTemplate([item1, item2, item3]);
var posApp = await client.CreatePointOfSaleApp(user.StoreId, new PointOfSaleAppRequest { AppName = "test pos", Template = posItems, });
var crowdfundApp = await client.CreateCrowdfundApp(user.StoreId, new CrowdfundAppRequest { AppName = "test crowdfund" });
@ -984,7 +1001,7 @@ namespace BTCPayServer.Tests
Assert.Contains("ServerAdmin", admin.Roles);
Assert.NotNull(admin.Created);
Assert.True((DateTimeOffset.Now - admin.Created).Value.Seconds < 10);
// Creating a new user without proper creds is now impossible (unauthorized)
// Because if registration are locked and that an admin exists, we don't accept unauthenticated connection
var ex = await AssertAPIError("unauthenticated",
@ -1036,7 +1053,14 @@ namespace BTCPayServer.Tests
Password = "afewfoiewiou",
IsAdministrator = true
});
// Create user without password
await adminClient.CreateUser(new CreateApplicationUserRequest
{
Email = "nopassword@gmail.com"
});
// Regular user
var user1Acc = tester.NewAccount();
user1Acc.UserId = user1.Id;
user1Acc.IsAdmin = false;
@ -1399,10 +1423,17 @@ namespace BTCPayServer.Tests
Assert.Equal(0, card.Version);
var card1keys = new[] { card.K0, card.K1, card.K2, card.K3, card.K4 };
Assert.DoesNotContain(null, card1keys);
var card2 = await client.RegisterBoltcard(test4.Id, new RegisterBoltcardRequest()
{
UID = uid
});
Assert.Equal(0, card2.Version);
card2 = await client.RegisterBoltcard(test4.Id, new RegisterBoltcardRequest()
{
UID = uid,
OnExisting = OnExistingBehavior.UpdateVersion
});
Assert.Equal(1, card2.Version);
Assert.StartsWith("lnurlw://", card2.LNURLW);
Assert.EndsWith("/boltcard", card2.LNURLW);
@ -1507,7 +1538,7 @@ namespace BTCPayServer.Tests
});
}
[Fact]
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanProcessPayoutsExternally()
{
@ -2980,9 +3011,10 @@ namespace BTCPayServer.Tests
// check list for store with paid invoice
var merchantInvoices = await merchantClient.GetLightningInvoices(merchant.StoreId, "BTC");
merchantPendingInvoices = await merchantClient.GetLightningInvoices(merchant.StoreId, "BTC", true);
Assert.NotEmpty(merchantInvoices);
Assert.Empty(merchantPendingInvoices);
merchantPendingInvoices = await merchantClient.GetLightningInvoices(merchant.StoreId, "BTC", true);
Assert.True(merchantPendingInvoices.Length < merchantInvoices.Length);
Assert.All(merchantPendingInvoices, m => Assert.Equal(LightningInvoiceStatus.Unpaid, m.Status));
// if the test ran too many times the invoice might be on a later page
if (merchantInvoices.Length < 100)
Assert.Contains(merchantInvoices, i => i.Id == merchantInvoice.Id);
@ -3000,6 +3032,18 @@ namespace BTCPayServer.Tests
Assert.Single(info.NodeURIs);
Assert.NotEqual(0, info.BlockHeight);
// Disable for now see #6518
//// balance
//await TestUtils.EventuallyAsync(async () =>
//{
// var balance = await client.GetLightningNodeBalance(user.StoreId, "BTC");
// var localBalance = balance.OffchainBalance.Local.ToDecimal(LightMoneyUnit.BTC);
// var histogram = await client.GetLightningNodeHistogram(user.StoreId, "BTC");
// Assert.Equal(histogram.Balance, histogram.Series.Last());
// Assert.Equal(localBalance, histogram.Balance);
// Assert.Equal(localBalance, histogram.Series.Last());
//});
// As admin, can use the internal node through our store.
await user.MakeAdmin(true);
await user.RegisterInternalLightningNodeAsync("BTC");
@ -3021,6 +3065,10 @@ namespace BTCPayServer.Tests
client = await guest.CreateClient(Policies.CanUseLightningNodeInStore);
// Can use lightning node is only granted to store's owner
await AssertPermissionError("btcpay.store.canuselightningnode", () => client.GetLightningNodeInfo(user.StoreId, "BTC"));
// balance and histogram should not be accessible with view only clients
await AssertPermissionError("btcpay.store.canuselightningnode", () => client.GetLightningNodeBalance(user.StoreId, "BTC"));
await AssertPermissionError("btcpay.store.canuselightningnode", () => client.GetLightningNodeHistogram(user.StoreId, "BTC"));
}
[Fact(Timeout = 60 * 20 * 1000)]
@ -3046,7 +3094,7 @@ namespace BTCPayServer.Tests
new CreateInvoiceRequest
{
Currency = "USD",
Amount = 100,
Amount = 0.1m,
Checkout = new CreateInvoiceRequest.CheckoutOptions
{
PaymentMethods = new[] { "BTC-LN" },
@ -3268,6 +3316,12 @@ namespace BTCPayServer.Tests
await viewOnlyClient.SendHttpRequest($"api/v1/stores/{store.Id}/payment-methods/onchain/BTC/preview", new JObject() { ["config"] = xpub.ToString() }, HttpMethod.Post);
var method = await client.UpdateStorePaymentMethod(store.Id, "BTC-CHAIN", new UpdatePaymentMethodRequest() { Enabled = true, Config = JValue.CreateString(xpub.ToString())});
var method2 = await client.UpdateStorePaymentMethod(store.Id, "BTC-CHAIN", new UpdatePaymentMethodRequest() { Enabled = true, Config = new JObject() { ["derivationScheme"] = xpub.ToString(), ["label"] = "test", ["accountKeyPath"] = "aaaaaaaa/84'/0'/0'" } });
Assert.Equal("aaaaaaaa", method2.Config["accountKeySettings"][0]["rootFingerprint"].ToString());
Assert.Equal("84'/0'/0'", method2.Config["accountKeySettings"][0]["accountKeyPath"].ToString());
var method3 = await client.UpdateStorePaymentMethod(store.Id, "BTC-CHAIN", new UpdatePaymentMethodRequest() { Enabled = true, Config = new JObject() { ["derivationScheme"] = xpub.ToString() } });
Assert.Equal(method.ToJson(), method3.ToJson());
Assert.Equal(firstAddress, (await viewOnlyClient.PreviewStoreOnChainPaymentMethodAddresses(store.Id, "BTC")).Addresses.First().Address);
await AssertHttpError(403, async () =>
@ -3332,7 +3386,6 @@ namespace BTCPayServer.Tests
Assert.Equal(24, generateResponse.Mnemonic.Words.Length);
Assert.Equal(Wordlist.Japanese, generateResponse.Mnemonic.WordList);
}
[Fact(Timeout = 60 * 2 * 1000)]
@ -3536,8 +3589,7 @@ namespace BTCPayServer.Tests
});
var overview = await client.ShowOnChainWalletOverview(walletId.StoreId, walletId.CryptoCode);
Assert.Equal(0m, overview.Balance);
var fee = await client.GetOnChainFeeRate(walletId.StoreId, walletId.CryptoCode);
Assert.NotNull(fee.FeeRate);
@ -3583,6 +3635,17 @@ namespace BTCPayServer.Tests
overview = await client.ShowOnChainWalletOverview(walletId.StoreId, walletId.CryptoCode);
Assert.Equal(0.01m, overview.Balance);
// histogram should not be accessible with view only clients
await AssertHttpError(403, async () =>
{
await viewOnlyClient.GetOnChainWalletHistogram(walletId.StoreId, walletId.CryptoCode);
});
var histogram = await client.GetOnChainWalletHistogram(walletId.StoreId, walletId.CryptoCode);
Assert.Equal(histogram.Balance, histogram.Series.Last());
Assert.Equal(0.01m, histogram.Balance);
Assert.Equal(0.01m, histogram.Series.Last());
Assert.Equal(0, histogram.Series.First());
//the simplest request:
var nodeAddress = await tester.ExplorerNode.GetNewAddressAsync();
var createTxRequest = new CreateOnChainTransactionRequest()
@ -3774,7 +3837,6 @@ namespace BTCPayServer.Tests
await tester.WaitForEvent<NewBlockEvent>(async () =>
{
await tester.ExplorerNode.GenerateAsync(1);
}, bevent => bevent.PaymentMethodId == PaymentTypes.CHAIN.GetPaymentMethodId("BTC"));
@ -3812,7 +3874,7 @@ namespace BTCPayServer.Tests
void VerifyLightning(GenericPaymentMethodData[] methods)
{
var m = Assert.Single(methods.Where(m => m.PaymentMethodId == "BTC-LN"));
var m = Assert.Single(methods, m => m.PaymentMethodId == "BTC-LN");
Assert.Equal("Internal Node", m.Config["internalNodeRef"].Value<string>());
}
@ -3824,7 +3886,7 @@ namespace BTCPayServer.Tests
void VerifyOnChain(GenericPaymentMethodData[] dictionary)
{
var m = Assert.Single(methods.Where(m => m.PaymentMethodId == "BTC-CHAIN"));
var m = Assert.Single(methods, m => m.PaymentMethodId == "BTC-CHAIN");
var paymentMethodBaseData = Assert.IsType<JObject>(m.Config);
Assert.Equal(wallet.Config.AccountDerivation, paymentMethodBaseData["accountDerivation"].Value<string>());
}
@ -3930,7 +3992,12 @@ namespace BTCPayServer.Tests
var user = tester.NewAccount();
await user.GrantAccessAsync(true);
var client = await user.CreateClient(Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings);
var client = await user.CreateClient(Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings, Policies.CanModifyProfile);
await client.UpdateCurrentUser(new UpdateApplicationUserRequest
{
Name = "The Admin",
ImageUrl = "avatar.jpg"
});
var roles = await client.GetServerRoles();
Assert.Equal(4, roles.Count);
@ -3944,6 +4011,9 @@ namespace BTCPayServer.Tests
var storeUser = Assert.Single(users);
Assert.Equal(user.UserId, storeUser.UserId);
Assert.Equal(ownerRole.Id, storeUser.Role);
Assert.Equal(user.Email, storeUser.Email);
Assert.Equal("The Admin", storeUser.Name);
Assert.Equal("avatar.jpg", storeUser.ImageUrl);
var manager = tester.NewAccount();
await manager.GrantAccessAsync();
var employee = tester.NewAccount();
@ -3974,7 +4044,14 @@ namespace BTCPayServer.Tests
// add users to store
await client.AddStoreUser(user.StoreId, new StoreUserData { Role = managerRole.Id, UserId = manager.UserId });
await client.AddStoreUser(user.StoreId, new StoreUserData { Role = employeeRole.Id, UserId = employee.UserId });
await client.AddStoreUser(user.StoreId, new StoreUserData { Role = guestRole.Id, UserId = guest.UserId });
// add with email
await client.AddStoreUser(user.StoreId, new StoreUserData { Role = guestRole.Id, UserId = guest.Email });
// test unknown user
await AssertAPIError("user-not-found", async () => await client.AddStoreUser(user.StoreId, new StoreUserData { Role = managerRole.Id, UserId = "unknown" }));
await AssertAPIError("user-not-found", async () => await client.UpdateStoreUser(user.StoreId, "unknown", new StoreUserData { Role = ownerRole.Id }));
await AssertAPIError("user-not-found", async () => await client.RemoveStoreUser(user.StoreId, "unknown"));
//test no access to api for employee
await AssertPermissionError(Policies.CanViewStoreSettings, async () => await employeeClient.GetStore(user.StoreId));
@ -3995,9 +4072,14 @@ namespace BTCPayServer.Tests
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await managerClient.RemoveStoreUser(user.StoreId, user.UserId));
// updates
await client.UpdateStoreUser(user.StoreId, employee.UserId, new StoreUserData { Role = ownerRole.Id });
await employeeClient.GetStore(user.StoreId);
// remove
await client.RemoveStoreUser(user.StoreId, employee.UserId);
await AssertHttpError(403, async () => await employeeClient.GetStore(user.StoreId));
// test duplicate add
await client.AddStoreUser(user.StoreId, new StoreUserData { Role = ownerRole.Id, UserId = employee.UserId });
await AssertAPIError("duplicate-store-user-role", async () =>
await client.AddStoreUser(user.StoreId, new StoreUserData { Role = ownerRole.Id, UserId = employee.UserId }));
@ -4011,6 +4093,45 @@ namespace BTCPayServer.Tests
await AssertAPIError("store-user-role-orphaned", async () => await employeeClient.RemoveStoreUser(user.StoreId, employee.UserId));
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task ServerEmailTests()
{
using var tester = CreateServerTester();
await tester.StartAsync();
var admin = tester.NewAccount();
await admin.GrantAccessAsync(true);
var adminClient = await admin.CreateClient(Policies.Unrestricted);
var data = new EmailSettingsData
{
From = "admin@admin.com",
Login = "admin@admin.com",
Password = "admin@admin.com",
Port = 1234,
Server = "admin.com",
};
var serverEmailSettings = new ServerEmailSettingsData
{
EnableStoresToUseServerEmailSettings = false
};
await adminClient.UpdateServerEmailSettings(serverEmailSettings);
var s = await adminClient.GetServerEmailSettings();
// email password is masked and not returned from the server once set
serverEmailSettings.Password = null;
Assert.Equal(JsonConvert.SerializeObject(s), JsonConvert.SerializeObject(serverEmailSettings));
await AssertValidationError(new[] { nameof(EmailSettingsData.From) },
async () => await adminClient.UpdateServerEmailSettings(new ServerEmailSettingsData
{
From = "invalid"
}));
// NOTE: This email test fails silently in EmailSender.cs#31, can't test, but leaving for the future as reminder
//await adminClient.SendEmail(admin.StoreId,
// new SendEmailRequest { Body = "lol", Subject = "subj", Email = "to@example.org" });
}
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")]
@ -4034,6 +4155,8 @@ namespace BTCPayServer.Tests
};
await adminClient.UpdateStoreEmailSettings(admin.StoreId, data);
var s = await adminClient.GetStoreEmailSettings(admin.StoreId);
// email password is masked and not returned from the server once set
data.Password = null;
Assert.Equal(JsonConvert.SerializeObject(s), JsonConvert.SerializeObject(data));
await AssertValidationError(new[] { nameof(EmailSettingsData.From) },
async () => await adminClient.UpdateStoreEmailSettings(admin.StoreId,
@ -4120,6 +4243,11 @@ namespace BTCPayServer.Tests
Assert.True((await adminClient.GetUserByIdOrEmail(unapprovedUser.UserId)).Approved);
Assert.True((await unapprovedUserApiKeyClient.GetCurrentUser()).Approved);
Assert.True((await unapprovedUserBasicAuthClient.GetCurrentUser()).Approved);
var err = await AssertAPIError("invalid-state", async () =>
{
await adminClient.ApproveUser(unapprovedUser.UserId, true, CancellationToken.None);
});
Assert.Equal("User is already approved", err.APIError.Message);
// un-approve
Assert.True(await adminClient.ApproveUser(unapprovedUser.UserId, false, CancellationToken.None));
@ -4132,6 +4260,11 @@ namespace BTCPayServer.Tests
{
await unapprovedUserBasicAuthClient.GetCurrentUser();
});
err = await AssertAPIError("invalid-state", async () =>
{
await adminClient.ApproveUser(unapprovedUser.UserId, false, CancellationToken.None);
});
Assert.Equal("User is already unapproved", err.APIError.Message);
// reset policies to not require approval
await settings.UpdateSetting(new PoliciesSettings { LockSubscription = false, RequiresUserApproval = false });
@ -4148,10 +4281,11 @@ namespace BTCPayServer.Tests
Assert.Single(await adminClient.GetNotifications(false));
// try unapproving user which does not have the RequiresApproval flag
await AssertAPIError("invalid-state", async () =>
err = await AssertAPIError("invalid-state", async () =>
{
await adminClient.ApproveUser(newUser.UserId, false, CancellationToken.None);
});
Assert.Equal("Unapproving user failed: No approval required", err.APIError.Message);
}
[Fact(Timeout = 60 * 2 * 1000)]
@ -4171,13 +4305,17 @@ namespace BTCPayServer.Tests
await admin.GrantAccessAsync(true);
var adminClient = await admin.CreateClient(Policies.Unrestricted);
admin.RegisterLightningNode("BTC", LightningConnectionType.LndREST);
admin.RegisterLightningNode("BTC", LightningConnectionType.CLightning);
var payoutAmount = LightMoney.Satoshis(1000);
var inv = await tester.MerchantLnd.Client.CreateInvoice(payoutAmount, "Donation to merchant", TimeSpan.FromHours(1), default);
var resp = await tester.CustomerLightningD.Pay(inv.BOLT11);
Assert.Equal(PayResult.Ok, resp.Result);
var ppService = tester.PayTester.GetService<HostedServices.PullPaymentHostedService>();
var serializers = tester.PayTester.GetService<BTCPayNetworkJsonSerializerSettings>();
var store = tester.PayTester.GetService<StoreRepository>();
var dbContextFactory = tester.PayTester.GetService<Data.ApplicationDbContextFactory>();
Assert.True(await store.InternalNodePayoutAuthorized(admin.StoreId));
Assert.False(await store.InternalNodePayoutAuthorized("blah"));
await admin.MakeAdmin(false);
@ -4201,8 +4339,8 @@ namespace BTCPayServer.Tests
await TestUtils.EventuallyAsync(async () =>
{
var payoutC =
(await adminClient.GetStorePayouts(admin.StoreId, false)).Single(data => data.Id == payout.Id);
Assert.Equal(PayoutState.Completed, payoutC.State);
(await adminClient.GetStorePayouts(admin.StoreId, false)).SingleOrDefault(data => data.Id == payout.Id);
Assert.Equal(PayoutState.Completed, payoutC?.State);
});
payout = await adminClient.CreatePayout(admin.StoreId,
@ -4244,6 +4382,36 @@ namespace BTCPayServer.Tests
Destination = customerInvoice.BOLT11
});
Assert.Equal(payout2.OriginalAmount, new Money(100, MoneyUnit.Satoshi).ToDecimal(MoneyUnit.BTC));
// Checking if we can disable a payout...
var allLNPayouts = await ppService.GetPayouts(new ()
{
PayoutIds = new[] { payout2.Id },
Processor = LightningAutomatedPayoutSenderFactory.ProcessorName
});
Assert.NotEmpty(allLNPayouts);
var b = JsonConvert.DeserializeObject<Data.PayoutBlob>(allLNPayouts[0].Blob);
b.DisableProcessor(LightningAutomatedPayoutSenderFactory.ProcessorName);
Assert.Equal(1, b.IncrementErrorCount());
Assert.Equal(2, b.IncrementErrorCount());
allLNPayouts[0].Blob = JsonConvert.SerializeObject(b);
Assert.Equal(3, JsonConvert.DeserializeObject<Data.PayoutBlob>(allLNPayouts[0].Blob).IncrementErrorCount());
using var ctx = dbContextFactory.CreateContext();
var p = ctx.Payouts.Find(allLNPayouts[0].Id);
p.Blob = allLNPayouts[0].Blob;
await ctx.SaveChangesAsync();
var allLNPayouts2 = await ppService.GetPayouts(new()
{
PayoutIds = new[] { payout2.Id },
Processor = LightningAutomatedPayoutSenderFactory.ProcessorName
});
Assert.DoesNotContain(allLNPayouts[0].Id, allLNPayouts2.Select(a => a.Id));
allLNPayouts2 = await ppService.GetPayouts(new()
{
PayoutIds = new[] { payout2.Id },
Processor = "hello"
});
Assert.Contains(allLNPayouts[0].Id, allLNPayouts2.Select(a => a.Id));
}
[Fact(Timeout = 60 * 2 * 1000)]
@ -4312,8 +4480,8 @@ namespace BTCPayServer.Tests
payouts = await adminClient.GetStorePayouts(admin.StoreId);
Assert.Equal(3, payouts.Length);
Assert.Empty(payouts.Where(data => data.State == PayoutState.AwaitingApproval));
Assert.Empty(payouts.Where(data => data.PayoutAmount is null));
Assert.DoesNotContain(payouts, data => data.State == PayoutState.AwaitingApproval);
Assert.DoesNotContain(payouts, data => data.PayoutAmount is null);
Assert.Empty(await adminClient.ShowOnChainWalletTransactions(admin.StoreId, "BTC"));
@ -4356,7 +4524,7 @@ namespace BTCPayServer.Tests
{
Assert.Equal(2, (await adminClient.ShowOnChainWalletTransactions(admin.StoreId, "BTC")).Count());
payouts = await adminClient.GetStorePayouts(admin.StoreId);
Assert.Single(payouts.Where(data => data.State == PayoutState.InProgress));
Assert.Single(payouts, data => data.State == PayoutState.InProgress);
});
uint256 txid = null;
@ -4370,7 +4538,7 @@ namespace BTCPayServer.Tests
{
Assert.Equal(4, (await adminClient.ShowOnChainWalletTransactions(admin.StoreId, "BTC")).Count());
payouts = await adminClient.GetStorePayouts(admin.StoreId);
Assert.Empty(payouts.Where(data => data.State != PayoutState.InProgress));
Assert.DoesNotContain(payouts, data => data.State != PayoutState.InProgress);
});
// settings that were added later
@ -4436,7 +4604,7 @@ namespace BTCPayServer.Tests
payouts = await adminClient.GetStorePayouts(admin.StoreId);
try
{
Assert.Single(payouts.Where(data => data.State == PayoutState.InProgress && data.Id == payoutThatShouldBeProcessedStraightAway.Id));
Assert.Single(payouts, data => data.State == PayoutState.InProgress && data.Id == payoutThatShouldBeProcessedStraightAway.Id);
}
catch (SingleException)
{
@ -4480,7 +4648,7 @@ namespace BTCPayServer.Tests
await afterHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
payouts = await adminClient.GetStorePayouts(admin.StoreId);
Assert.Single(payouts.Where(data => data.State == PayoutState.AwaitingPayment && data.Id == payoutThatShouldNotBeProcessedStraightAway.Id));
Assert.Single(payouts, data => data.State == PayoutState.AwaitingPayment && data.Id == payoutThatShouldNotBeProcessedStraightAway.Id);
beforeHookTcs = new TaskCompletionSource();
afterHookTcs = new TaskCompletionSource();
@ -4513,7 +4681,7 @@ namespace BTCPayServer.Tests
await afterHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
payouts = await adminClient.GetStorePayouts(admin.StoreId);
Assert.Empty(payouts.Where(data => data.State != PayoutState.InProgress));
Assert.DoesNotContain(payouts, data => data.State != PayoutState.InProgress);
}
@ -4590,11 +4758,11 @@ namespace BTCPayServer.Tests
await client.AddOrUpdateOnChainWalletObject(admin.StoreId, "BTC", new AddOnChainWalletObjectRequest() { Type = "newtype", Id = test1.Id });
testObj = await client.GetOnChainWalletObject(admin.StoreId, "BTC", test);
Assert.Single(testObj.Links.Where(l => l.Id == "test1" && l.LinkData["testData"]?.Value<string>() == "lol"));
Assert.Single(testObj.Links.Where(l => l.Id == "test1" && l.ObjectData["testData"]?.Value<string>() == "test1"));
Assert.Single(testObj.Links, l => l.Id == "test1" && l.LinkData["testData"]?.Value<string>() == "lol");
Assert.Single(testObj.Links, l => l.Id == "test1" && l.ObjectData["testData"]?.Value<string>() == "test1");
testObj = await client.GetOnChainWalletObject(admin.StoreId, "BTC", test, false);
Assert.Single(testObj.Links.Where(l => l.Id == "test1" && l.LinkData["testData"]?.Value<string>() == "lol"));
Assert.Single(testObj.Links.Where(l => l.Id == "test1" && l.ObjectData is null));
Assert.Single(testObj.Links, l => l.Id == "test1" && l.LinkData["testData"]?.Value<string>() == "lol");
Assert.Single(testObj.Links, l => l.Id == "test1" && l.ObjectData is null);
async Task TestWalletRepository()
{
@ -4699,9 +4867,9 @@ clientBasic.UpdateStoreRateConfiguration(user.StoreId, new StoreRateConfiguratio
clientBasic.UpdateStoreRateConfiguration(user.StoreId, new StoreRateConfiguration() { IsCustomScript = false, PreferredSource = "coingeckoOOO", Spread = -1m }));
await AssertValidationError(new[] { "currencyPair" }, () =>
clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId, new StoreRateConfiguration() { IsCustomScript = false, PreferredSource = "coingecko" }, new[] { "BTC_USD_USD_BTC" }));
clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId, new StoreRateConfiguration() { IsCustomScript = false, PreferredSource = "coingecko" }, new[] { "BTCUSDUSDBTC" }));
await AssertValidationError(new[] { "PreferredSource", "currencyPair" }, () =>
clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId, new StoreRateConfiguration() { IsCustomScript = false, PreferredSource = "coingeckoOOO" }, new[] { "BTC_USD_USD_BTC" }));
clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId, new StoreRateConfiguration() { IsCustomScript = false, PreferredSource = "coingeckoOOO" }, new[] { "BTCUSDUSDBTC" }));
}
}
}

View File

@ -74,9 +74,8 @@ namespace BTCPayServer.Tests
tester.Driver.WaitForElement(By.Id("ConfirmInput")).SendKeys("DELETE");
tester.Driver.FindElement(By.Id("ConfirmContinue")).Click();
text = tester.Driver.PageSource;
Assert.DoesNotContain("Select-English (Custom)", text);
Assert.Contains("English (Custom) deleted", text);
Assert.Contains("Dictionary English (Custom) deleted", tester.FindAlertMessage().Text);
Assert.DoesNotContain("Select-English (Custom)", tester.Driver.PageSource);
}
[Fact(Timeout = TestTimeout)]

View File

@ -15,7 +15,7 @@ namespace BTCPayServer.Tests.Mocks
public string Action(UrlActionContext actionContext)
{
return $"{_BaseUrl}mock";
return $"/mock";
}
public string Content(string contentPath)

View File

@ -1,11 +1,11 @@
using System;
using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Hosting;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Plugins.Crowdfund.Models;
using BTCPayServer.Plugins.PointOfSale;
using BTCPayServer.Plugins.PointOfSale.Controllers;
using BTCPayServer.Plugins.PointOfSale.Models;
@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Mvc;
using Xunit;
using Xunit.Abstractions;
using static BTCPayServer.Tests.UnitTest1;
using PosViewType = BTCPayServer.Plugins.PointOfSale.PosViewType;
namespace BTCPayServer.Tests
{
@ -76,9 +77,8 @@ fruit tea:
Assert.Null( parsedDefault[0].BuyButtonText);
Assert.Equal( "~/img/pos-sample/green-tea.jpg" ,parsedDefault[0].Image);
Assert.Equal( 1 ,parsedDefault[0].Price);
Assert.Equal( ViewPointOfSaleViewModel.ItemPriceType.Fixed ,parsedDefault[0].PriceType);
Assert.Equal( AppItemPriceType.Fixed ,parsedDefault[0].PriceType);
Assert.Null( parsedDefault[0].AdditionalData);
Assert.Null( parsedDefault[0].PaymentMethods);
Assert.Equal( "Herbal Tea" ,parsedDefault[4].Title);
@ -87,9 +87,8 @@ fruit tea:
Assert.Null( parsedDefault[4].BuyButtonText);
Assert.Equal( "~/img/pos-sample/herbal-tea.jpg" ,parsedDefault[4].Image);
Assert.Equal( 1.8m ,parsedDefault[4].Price);
Assert.Equal( ViewPointOfSaleViewModel.ItemPriceType.Minimum ,parsedDefault[4].PriceType);
Assert.Equal( AppItemPriceType.Minimum ,parsedDefault[4].PriceType);
Assert.Null( parsedDefault[4].AdditionalData);
Assert.Null( parsedDefault[4].PaymentMethods);
}
[Fact]

View File

@ -404,10 +404,10 @@ namespace BTCPayServer.Tests
Assert.False(paymentValueRowColumn.Text.Contains("payjoin",
StringComparison.InvariantCultureIgnoreCase));
s.GoToWallet(receiverWalletId, WalletsNavPages.Transactions);
s.Driver.WaitForElement(By.CssSelector("#WalletTransactionsList tr"));
TestUtils.Eventually(() =>
{
s.GoToWallet(receiverWalletId, WalletsNavPages.Transactions);
s.Driver.WaitForElement(By.CssSelector("#WalletTransactionsList tr"));
Assert.Contains("payjoin", s.Driver.PageSource);
// Either the invoice id or the payjoin-exposed label, depending on the input having been used
Assert.Matches(new Regex($"({invoiceId}|payjoin-exposed)"), s.Driver.PageSource);
@ -426,7 +426,6 @@ namespace BTCPayServer.Tests
var notifications = await nbx.CreateWebsocketNotificationSessionAsync();
var alice = tester.NewAccount();
await alice.RegisterDerivationSchemeAsync("BTC", ScriptPubKeyType.Segwit, true);
await notifications.ListenDerivationSchemesAsync(new[] { alice.DerivationScheme });
BitcoinAddress address = null;
for (int i = 0; i < 5; i++)
@ -434,7 +433,7 @@ namespace BTCPayServer.Tests
address = (await nbx.GetUnusedAsync(alice.DerivationScheme, DerivationFeature.Deposit)).Address;
await tester.ExplorerNode.GenerateAsync(1);
tester.ExplorerNode.SendToAddress(address, Money.Coins(1.0m));
await notifications.NextEventAsync();
await notifications.WaitReceive(alice.DerivationScheme);
}
var paymentAddress = new Key().PubKey.GetAddress(ScriptPubKeyType.Legacy, Network.RegTest);
var otherAddress = new Key().PubKey.GetAddress(ScriptPubKeyType.Legacy, Network.RegTest);
@ -481,12 +480,12 @@ namespace BTCPayServer.Tests
var request = await fakeServer.GetNextRequest();
Assert.Equal("1", request.Request.Query["v"][0]);
Assert.Equal(changeIndex.ToString(), request.Request.Query["additionalfeeoutputindex"][0]);
Assert.Equal("1146", request.Request.Query["maxadditionalfeecontribution"][0]);
Assert.Equal("1853", request.Request.Query["maxadditionalfeecontribution"][0]);
TestLogs.LogInformation("The payjoin receiver tries to make us pay lots of fee");
var originalPSBT = await ParsePSBT(request);
var proposalTx = originalPSBT.GetGlobalTransaction();
proposalTx.Outputs[changeIndex].Value -= Money.Satoshis(1147);
proposalTx.Outputs[changeIndex].Value -= Money.Satoshis(1854);
await request.Response.WriteAsync(PSBT.FromTransaction(proposalTx, Network.RegTest).ToBase64(), Encoding.UTF8);
fakeServer.Done();
var ex = await Assert.ThrowsAsync<PayjoinSenderException>(async () => await requesting);
@ -569,10 +568,9 @@ namespace BTCPayServer.Tests
await notifications.DisposeAsync();
notifications = await nbx.CreateWebsocketNotificationSessionAsync();
await notifications.ListenDerivationSchemesAsync(new[] { bob.DerivationScheme });
address = (await nbx.GetUnusedAsync(bob.DerivationScheme, DerivationFeature.Deposit)).Address;
tester.ExplorerNode.SendToAddress(address, Money.Coins(1.1m));
await notifications.NextEventAsync();
await notifications.WaitReceive(bob.DerivationScheme);
await bob.ModifyOnchainPaymentSettings(p => p.PayJoinEnabled = true);
var invoice = bob.BitPay.CreateInvoice(
new Invoice { Price = 0.1m, Currency = "BTC", FullNotifications = true });
@ -603,7 +601,7 @@ namespace BTCPayServer.Tests
proposal = proposal.SignAll(derivationSchemeSettings.AccountDerivation, alice.GenerateWalletResponseV.AccountHDKey, signingAccount.GetRootedKeyPath());
proposal.Finalize();
await tester.ExplorerNode.SendRawTransactionAsync(proposal.ExtractTransaction());
await notifications.NextEventAsync();
await notifications.WaitReceive(bob.DerivationScheme);
TestLogs.LogInformation("Abusing minFeeRate should give not enough money error");
invoice = bob.BitPay.CreateInvoice(

View File

@ -643,7 +643,15 @@ retry:
GoToUrl(url);
Assert.DoesNotMatch("404 - Page not found</h", Driver.PageSource);
if (shouldHaveAccess)
{
Assert.DoesNotMatch("- Denied</h", Driver.PageSource);
// check associated link is active if present
var sidebarLink = Driver.FindElements(By.CssSelector($"#mainNav a[href=\"{url}\"]")).FirstOrDefault();
if (sidebarLink != null)
{
Assert.Contains("active", sidebarLink.GetAttribute("class"));
}
}
else
Assert.Contains("- Denied</h", Driver.PageSource);
}

View File

@ -32,6 +32,7 @@ using Microsoft.EntityFrameworkCore;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBitcoin.Payment;
using NBXplorer.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OpenQA.Selenium;
@ -95,7 +96,6 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.CssSelector("button[type='submit']")).Click();
Assert.Contains("Enter your email", s.Driver.PageSource);
s.Driver.FindElement(By.Name("buyerEmail")).SendKeys("aa@aa.com");
s.Driver.FindElement(By.CssSelector("input[type='submit']")).Click();
@ -114,7 +114,6 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("Title")).SendKeys("Pay123");
s.Driver.FindElement(By.Id("Amount")).SendKeys("700");
new SelectElement(s.Driver.FindElement(By.Id("FormId"))).SelectByValue("Email");
s.Driver.TakeScreenshot().SaveAsFile("C:\\Users\\NicolasDorier\\Downloads\\chromedriver-win64\\1.png");
s.ClickPagePrimary();
s.Driver.FindElement(By.XPath("//a[starts-with(@id, 'Edit-')]")).Click();
@ -385,10 +384,6 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("ConfirmPassword")).SendKeys("123456");
s.ClickPagePrimary();
Assert.Contains("Account successfully created.", s.FindAlertMessage().Text);
s.Driver.FindElement(By.Id("Email")).SendKeys(usr);
s.Driver.FindElement(By.Id("Password")).SendKeys("123456");
s.Driver.FindElement(By.Id("LoginButton")).Click();
// We should be logged in now
s.GoToHome();
@ -696,7 +691,7 @@ namespace BTCPayServer.Tests
// Store Emails without server fallback
s.GoToStore(StoreNavPages.Emails);
s.Driver.ElementDoesNotExist(By.Id("UseCustomSMTP"));
s.Driver.ElementDoesNotExist(By.Id("IsCustomSMTP"));
s.Driver.FindElement(By.Id("ConfigureEmailRules")).Click();
Assert.Contains("You need to configure email settings before this feature works", s.Driver.PageSource);
@ -711,12 +706,12 @@ namespace BTCPayServer.Tests
// Store Emails with server fallback
s.GoToStore(StoreNavPages.Emails);
Assert.False(s.Driver.FindElement(By.Id("UseCustomSMTP")).Selected);
Assert.False(s.Driver.FindElement(By.Id("IsCustomSMTP")).Selected);
s.Driver.FindElement(By.Id("ConfigureEmailRules")).Click();
Assert.DoesNotContain("You need to configure email settings before this feature works", s.Driver.PageSource);
s.GoToStore(StoreNavPages.Emails);
s.Driver.FindElement(By.Id("UseCustomSMTP")).Click();
s.Driver.FindElement(By.Id("IsCustomSMTP")).Click();
Thread.Sleep(250);
CanSetupEmailCore(s);
@ -737,7 +732,7 @@ namespace BTCPayServer.Tests
Assert.Contains("Store email rules saved", s.FindAlertMessage().Text);
s.GoToStore(StoreNavPages.Emails);
Assert.True(s.Driver.FindElement(By.Id("UseCustomSMTP")).Selected);
Assert.True(s.Driver.FindElement(By.Id("IsCustomSMTP")).Selected);
}
[Fact(Timeout = TestTimeout)]
@ -1244,15 +1239,17 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("Title")).SendKeys("Tea shop");
s.Driver.FindElement(By.CssSelector("label[for='DefaultView_Cart']")).Click();
s.Driver.FindElement(By.CssSelector(".template-item:nth-of-type(1)")).Click();
s.Driver.WaitUntilAvailable(By.Id("BuyButtonText"));
s.Driver.FindElement(By.Id("BuyButtonText")).SendKeys("Take my money");
s.Driver.FindElement(By.Id("EditorCategories-ts-control")).SendKeys("Drinks");
s.Driver.FindElement(By.CssSelector(".offcanvas-header button")).Click();
s.Driver.WaitUntilAvailable(By.Id("CodeTabButton"));
s.Driver.ScrollTo(By.Id("CodeTabButton"));
s.Driver.FindElement(By.Id("CodeTabButton")).Click();
var template = s.Driver.FindElement(By.Id("TemplateConfig")).GetAttribute("value");
Assert.Contains("\"buyButtonText\": \"Take my money\"", template);
Assert.Matches("\"categories\": \\[\r?\n\\s*\"Drinks\"\\s*\\]", template);
s.ClickPagePrimary();
Assert.Contains("App updated", s.FindAlertMessage().Text);
@ -1457,12 +1454,15 @@ namespace BTCPayServer.Tests
s.GoToUrl(editUrl);
s.Driver.ScrollTo(By.Id("btAddItem"));
s.Driver.FindElement(By.Id("btAddItem")).Click();
s.Driver.WaitUntilAvailable(By.Id("EditorTitle"));
s.Driver.FindElement(By.Id("EditorTitle")).SendKeys("Perk 1");
s.Driver.FindElement(By.Id("EditorAmount")).SendKeys("20");
// Test autogenerated ID
Assert.Equal("perk-1", s.Driver.FindElement(By.Id("EditorId")).GetAttribute("value"));
s.Driver.FindElement(By.Id("EditorId")).Clear();
s.Driver.FindElement(By.Id("EditorId")).SendKeys("Perk-1");
s.Driver.FindElement(By.CssSelector(".offcanvas-header button")).Click();
s.Driver.WaitUntilAvailable(By.Id("CodeTabButton"));
s.Driver.ScrollTo(By.Id("CodeTabButton"));
s.Driver.FindElement(By.Id("CodeTabButton")).Click();
var template = s.Driver.FindElement(By.Id("TemplateConfig")).GetAttribute("value");
@ -1874,28 +1874,20 @@ namespace BTCPayServer.Tests
Assert.Contains("test-label", s.Driver.PageSource);
});
// Let's try to remove a label
await TestUtils.EventuallyAsync(async () =>
{
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);
});
// Remove a label
s.Driver.WaitForElement(By.CssSelector("[data-value='test-label']")).Click();
await Task.Delay(500);
s.Driver.ExecuteJavaScript("var l=document.querySelector('[data-value=\"test-label\"]');l.click();l.nextSibling.dispatchEvent(new KeyboardEvent('keydown', {'key': 'Delete', keyCode: 8}));");
await Task.Delay(500);
await s.Driver.Navigate().RefreshAsync();
Assert.DoesNotContain("test-label", s.Driver.PageSource);
Assert.True(s.Driver.ElementDoesNotExist(By.Id("GoBack")));
//send money to addr and ensure it changed
var sess = await s.Server.ExplorerClient.CreateWebsocketNotificationSessionAsync();
await sess.ListenAllTrackedSourceAsync();
var nextEvent = sess.NextEventAsync();
await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(receiveAddr, Network.RegTest),
Money.Parse("0.1"));
await nextEvent;
await sess.WaitNext<NewTransactionEvent>(e => e.Outputs.FirstOrDefault()?.Address.ToString() == receiveAddr);
await Task.Delay(200);
s.Driver.Navigate().Refresh();
s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click();
@ -1905,6 +1897,7 @@ namespace BTCPayServer.Tests
// Check the label is applied to the tx
s.Driver.WaitWalletTransactionsLoaded();
// Sometimes this fails in local, but not CI
Assert.Equal("label2", s.Driver.FindElement(By.XPath("//*[@id=\"WalletTransactionsList\"]//*[contains(@class, 'transaction-label')]")).Text);
//change the wallet and ensure old address is not there and generating a new one does not result in the prev one
@ -1967,7 +1960,7 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("SignTransaction")).Click();
// Back button should lead back to the previous page inside the send wizard
var backUrl = s.Driver.FindElement(By.Id("GoBack")).GetAttribute("href");
Assert.EndsWith($"/send?returnUrl={walletTransactionUri.AbsolutePath}", backUrl);
Assert.EndsWith($"/send?returnUrl={Uri.EscapeDataString(walletTransactionUri.AbsolutePath)}", backUrl);
// Cancel button should lead to the page that referred to the send wizard
var cancelUrl = s.Driver.FindElement(By.Id("CancelWizard")).GetAttribute("href");
Assert.EndsWith(walletTransactionUri.AbsolutePath, cancelUrl);
@ -2181,6 +2174,55 @@ namespace BTCPayServer.Tests
Assert.Contains("PP1 Edited", s.Driver.PageSource);
}
[Fact]
[Trait("Selenium", "Selenium")]
public async Task CanUseAwaitProgressForInProgressPayout()
{
using var s = CreateSeleniumTester();
await s.StartAsync();
s.RegisterNewUser(true);
s.CreateNewStore();
s.GenerateWallet(isHotWallet: true);
await s.FundStoreWallet(denomination: 50.0m);
s.GoToStore(s.StoreId, StoreNavPages.PayoutProcessors);
s.Driver.FindElement(By.Id("Configure-BTC-CHAIN")).Click();
s.Driver.SetCheckbox(By.Id("ProcessNewPayoutsInstantly"), true);
s.ClickPagePrimary();
s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
s.ClickPagePrimary();
s.Driver.FindElement(By.Id("Name")).SendKeys("PP1");
s.Driver.FindElement(By.Id("Amount")).Clear();
s.Driver.FindElement(By.Id("Amount")).SendKeys("99.0");
s.Driver.SetCheckbox(By.Id("AutoApproveClaims"), true);
s.ClickPagePrimary();
s.Driver.FindElement(By.LinkText("View")).Click();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
var address = await s.Server.ExplorerNode.GetNewAddressAsync();
s.Driver.FindElement(By.Id("Destination")).SendKeys(address.ToString() + Keys.Enter);
s.GoToStore(s.StoreId, StoreNavPages.Payouts);
s.Driver.FindElement(By.Id("InProgress-view")).Click();
// Waiting for the payment processor to process the payment
int i = 0;
while (!s.Driver.PageSource.Contains("mass-action-select-all"))
{
s.Driver.Navigate().Refresh();
i++;
Thread.Sleep(1000);
if (i > 10)
break;
}
s.Driver.FindElement(By.ClassName("mass-action-select-all")).Click();
s.Driver.FindElement(By.Id("InProgress-mark-awaiting-payment")).Click();
s.Driver.FindElement(By.Id("AwaitingPayment-view")).Click();
Assert.Contains("PP1", s.Driver.PageSource);
}
[Fact]
[Trait("Selenium", "Selenium")]
[Trait("Lightning", "Lightning")]
@ -2909,6 +2951,16 @@ namespace BTCPayServer.Tests
// Unauthenticated user can't access recent transactions
s.GoToUrl(keypadUrl);
s.Driver.ElementDoesNotExist(By.Id("RecentTransactionsToggle"));
// But they can generate invoices
s.Driver.FindElement(By.CssSelector(".keypad [data-key='1']")).Click();
s.Driver.FindElement(By.CssSelector(".keypad [data-key='2']")).Click();
s.Driver.FindElement(By.CssSelector(".keypad [data-key='3']")).Click();
s.Driver.FindElement(By.Id("pay-button")).Click();
s.Driver.WaitUntilAvailable(By.Id("Checkout"));
s.Driver.FindElement(By.Id("DetailsToggle")).Click();
s.Driver.WaitForElement(By.Id("PaymentDetails-TotalFiat"));
Assert.Contains("1,23 €", s.Driver.FindElement(By.Id("PaymentDetails-TotalFiat")).Text);
}
[Fact]
@ -3783,7 +3835,7 @@ retry:
(_, string posId) = s.CreateApp("PointOfSale");
(_, string crowdfundId) = s.CreateApp("Crowdfund");
string GetStorePath(string subPath) => $"/stores/{storeId}/{subPath}";
string GetStorePath(string subPath) => $"/stores/{storeId}" + (string.IsNullOrEmpty(subPath) ? "" : $"/{subPath}");
// Owner access
s.AssertPageAccess(true, GetStorePath(""));

View File

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.Metrics;
using System.IO;
using System.Linq;
using System.Net.Http;
@ -15,8 +14,6 @@ using BTCPayServer.Client.Models;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Lightning;
using BTCPayServer.Lightning.CLightning;
using BTCPayServer.Models.AccountViewModels;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
@ -26,10 +23,8 @@ using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
using BTCPayServer.Tests.Logging;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.EntityFrameworkCore;
using NBitcoin;
using NBitcoin.DataEncoders;

View File

@ -208,6 +208,18 @@ namespace BTCPayServer.Tests
e => e.CurrencyPair == new CurrencyPair("BTC", "LBP") &&
e.BidAsk.Bid > 1.0m); // 1 BTC will always be more than 1 LBP (I hope)
}
else if (name == "bitmynt")
{
Assert.Contains(exchangeRates.ByExchange[name],
e => e.CurrencyPair == new CurrencyPair("BTC", "NOK") &&
e.BidAsk.Bid > 1.0m); // 1 BTC will always be more than 1 NOK
}
else if (name == "barebitcoin")
{
Assert.Contains(exchangeRates.ByExchange[name],
e => e.CurrencyPair == new CurrencyPair("BTC", "NOK") &&
e.BidAsk.Bid > 1.0m); // 1 BTC will always be more than 1 NOK
}
else
{
if (name == "kraken")
@ -234,7 +246,7 @@ namespace BTCPayServer.Tests
}
// Kraken emit one request only after first GetRates
factory.Providers["kraken"].GetRatesAsync(default).GetAwaiter().GetResult();
await factory.Providers["kraken"].GetRatesAsync(default);
var p = new KrakenExchangeRateProvider();
var rates = await p.GetRatesAsync(default);
@ -339,15 +351,15 @@ retry:
}
[Fact]
public void CanSolveTheDogesRatesOnKraken()
public async Task CanSolveTheDogesRatesOnKraken()
{
var factory = FastTests.CreateBTCPayRateFactory();
var fetcher = new RateFetcher(factory);
Assert.True(RateRules.TryParse("X_X=kraken(X_BTC) * kraken(BTC_X)", out var rule));
foreach (var pair in new[] { "DOGE_USD", "DOGE_CAD", "DASH_CAD", "DASH_USD", "DASH_EUR" })
foreach (var pair in new[] { "DOGE_USD", "DOGE_CAD" })
{
var result = fetcher.FetchRate(CurrencyPair.Parse(pair), rule, null, default).GetAwaiter().GetResult();
var result = await fetcher.FetchRate(CurrencyPair.Parse(pair), rule, null, default);
Assert.NotNull(result.BidAsk);
Assert.Empty(result.Errors);
}
@ -601,7 +613,7 @@ retry:
foreach (var rate in rates)
{
Assert.Single(rates.Where(r => r == rate));
Assert.Single(rates, r => r == rate);
}
}

View File

@ -38,10 +38,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.Rating;
using BTCPayServer.Security.Bitpay;
using BTCPayServer.Security.Greenfield;
using BTCPayServer.Services;
@ -70,7 +68,6 @@ using NBXplorer.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Schema;
using Npgsql;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;
@ -197,7 +194,7 @@ namespace BTCPayServer.Tests
[Fact]
[Trait("Integration", "Integration")]
public async void CanStoreArbitrarySettingsWithStore()
public async Task CanStoreArbitrarySettingsWithStore()
{
using var tester = CreateServerTester();
await tester.StartAsync();
@ -449,25 +446,25 @@ namespace BTCPayServer.Tests
Assert.IsType<ViewResult>(storeResponse);
Assert.IsType<ViewResult>(storeController.SetupLightningNode(user.StoreId, "BTC"));
storeController.SetupLightningNode(user.StoreId, new LightningNodeViewModel
await storeController.SetupLightningNode(user.StoreId, new LightningNodeViewModel
{
ConnectionString = $"type=charge;server={tester.MerchantCharge.Client.Uri.AbsoluteUri};allowinsecure=true",
SkipPortTest = true // We can't test this as the IP can't be resolved by the test host :(
}, "test", "BTC").GetAwaiter().GetResult();
}, "test", "BTC");
Assert.False(storeController.TempData.ContainsKey(WellKnownTempData.ErrorMessage));
storeController.TempData.Clear();
Assert.True(storeController.ModelState.IsValid);
Assert.IsType<RedirectToActionResult>(storeController.SetupLightningNode(user.StoreId,
Assert.IsType<RedirectToActionResult>(await storeController.SetupLightningNode(user.StoreId,
new LightningNodeViewModel
{
ConnectionString = $"type=charge;server={tester.MerchantCharge.Client.Uri.AbsoluteUri};allowinsecure=true"
}, "save", "BTC").GetAwaiter().GetResult());
}, "save", "BTC"));
// Make sure old connection string format does not work
Assert.IsType<RedirectToActionResult>(storeController.SetupLightningNode(user.StoreId,
Assert.IsType<RedirectToActionResult>(await storeController.SetupLightningNode(user.StoreId,
new LightningNodeViewModel { ConnectionString = tester.MerchantCharge.Client.Uri.AbsoluteUri },
"save", "BTC").GetAwaiter().GetResult());
"save", "BTC"));
storeResponse = storeController.LightningSettings(user.StoreId, "BTC");
var storeVm =
@ -841,6 +838,8 @@ namespace BTCPayServer.Tests
AssertSearchInvoice(acc, false, invoice.Id, "exceptionstatus:paidOver");
AssertSearchInvoice(acc, true, invoice.Id, "unusual:true");
AssertSearchInvoice(acc, false, invoice.Id, "unusual:false");
AssertSearchInvoice(acc, true, invoice.Id, "status:settled,exceptionstatus:paidPartial");
AssertSearchInvoice(acc, true, invoice.Id, "status:settled,status:invalid,exceptionstatus:paidPartial,exceptionstatus:paidOver");
var time = invoice.InvoiceTime;
AssertSearchInvoice(acc, true, invoice.Id, $"startdate:{time.ToString("yyyy-MM-dd HH:mm:ss")}");
@ -968,7 +967,6 @@ namespace BTCPayServer.Tests
using (var cts = new CancellationTokenSource(10000))
using (var listener = tester.ExplorerClient.CreateWebsocketNotificationSession())
{
listener.ListenAllDerivationSchemes();
var replaced = tester.ExplorerNode.SignRawTransaction(tx);
Thread.Sleep(1000); // Make sure the replacement has a different timestamp
var tx2 = tester.ExplorerNode.SendRawTransaction(replaced);
@ -1102,7 +1100,7 @@ namespace BTCPayServer.Tests
[Fact(Timeout = LongRunningTestTimeout)]
[Trait("Integration", "Integration")]
public async void CheckCORSSetOnBitpayAPI()
public async Task CheckCORSSetOnBitpayAPI()
{
using var tester = CreateServerTester();
await tester.StartAsync();
@ -1146,9 +1144,8 @@ namespace BTCPayServer.Tests
// Test request pairing code client side
var storeController = user.GetController<UIStoresController>();
storeController
.CreateToken(user.StoreId, new CreateTokenViewModel() { Label = "test2", StoreId = user.StoreId })
.GetAwaiter().GetResult();
await storeController
.CreateToken(user.StoreId, new CreateTokenViewModel() { Label = "test2", StoreId = user.StoreId });
Assert.NotNull(storeController.GeneratedPairingCode);
@ -1172,17 +1169,17 @@ namespace BTCPayServer.Tests
// Can generate API Key
var repo = tester.PayTester.GetService<TokenRepository>();
Assert.Empty(repo.GetLegacyAPIKeys(user.StoreId).GetAwaiter().GetResult());
Assert.IsType<RedirectToActionResult>(user.GetController<UIStoresController>()
.GenerateAPIKey(user.StoreId).GetAwaiter().GetResult());
Assert.Empty(await repo.GetLegacyAPIKeys(user.StoreId));
Assert.IsType<RedirectToActionResult>(await user.GetController<UIStoresController>()
.GenerateAPIKey(user.StoreId));
var apiKey = Assert.Single(repo.GetLegacyAPIKeys(user.StoreId).GetAwaiter().GetResult());
var apiKey = Assert.Single(await repo.GetLegacyAPIKeys(user.StoreId));
///////
// Generating a new one remove the previous
Assert.IsType<RedirectToActionResult>(user.GetController<UIStoresController>()
.GenerateAPIKey(user.StoreId).GetAwaiter().GetResult());
var apiKey2 = Assert.Single(repo.GetLegacyAPIKeys(user.StoreId).GetAwaiter().GetResult());
Assert.IsType<RedirectToActionResult>(await user.GetController<UIStoresController>()
.GenerateAPIKey(user.StoreId));
var apiKey2 = Assert.Single(await repo.GetLegacyAPIKeys(user.StoreId));
Assert.NotEqual(apiKey, apiKey2);
////////
@ -1196,7 +1193,7 @@ namespace BTCPayServer.Tests
var invoice = new Invoice() { Price = 5000.0m, Currency = "USD" };
message.Content = new StringContent(JsonConvert.SerializeObject(invoice), Encoding.UTF8,
"application/json");
var result = client.SendAsync(message).GetAwaiter().GetResult();
var result = await client.SendAsync(message);
result.EnsureSuccessStatusCode();
/////////////////////
@ -1210,7 +1207,7 @@ namespace BTCPayServer.Tests
mess.Headers.Add("x-identity",
"04b4d82095947262dd70f94c0a0e005ec3916e3f5f2181c176b8b22a52db22a8c436c4703f43a9e8884104854a11e1eb30df8fdf116e283807a1f1b8fe4c182b99");
mess.Method = HttpMethod.Get;
result = client.SendAsync(mess).GetAwaiter().GetResult();
result = await client.SendAsync(mess);
Assert.Equal(System.Net.HttpStatusCode.Unauthorized, result.StatusCode);
//
@ -1542,7 +1539,7 @@ namespace BTCPayServer.Tests
// We allow BTC and LN, but not BTC under 5 USD, so only LN should be in the invoice
var vm = await user.GetController<UIStoresController>().CheckoutAppearance().AssertViewModelAsync<CheckoutAppearanceViewModel>();
Assert.Equal(2, vm.PaymentMethodCriteria.Count);
var criteria = Assert.Single(vm.PaymentMethodCriteria.Where(m => m.PaymentMethod == btcMethod.ToString()));
var criteria = Assert.Single(vm.PaymentMethodCriteria, m => m.PaymentMethod == btcMethod.ToString());
Assert.Equal(btcMethod.ToString(), criteria.PaymentMethod);
criteria.Value = "5 USD";
criteria.Type = PaymentMethodCriteriaViewModel.CriteriaType.GreaterThan;
@ -2178,19 +2175,19 @@ namespace BTCPayServer.Tests
Assert.Equal(0, invoice.CryptoInfo[0].TxCount);
Assert.True(invoice.MinerFees.ContainsKey("BTC"));
Assert.Contains(Math.Round(invoice.MinerFees["BTC"].SatoshiPerBytes), new[] { 100.0m, 20.0m });
TestUtils.Eventually(() =>
await TestUtils.EventuallyAsync(async () =>
{
var textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery()
var textSearchResult = await tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery()
{
StoreId = new[] { user.StoreId },
TextSearch = invoice.OrderId
}).GetAwaiter().GetResult();
});
Assert.Single(textSearchResult);
textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery()
textSearchResult = await tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery()
{
StoreId = new[] { user.StoreId },
TextSearch = invoice.Id
}).GetAwaiter().GetResult();
});
Assert.Single(textSearchResult);
});
@ -2218,11 +2215,11 @@ namespace BTCPayServer.Tests
Assert.True(IsMapped(invoice, ctx));
cashCow.SendToAddress(invoiceAddress, firstPayment);
var invoiceEntity = repo.GetInvoice(invoice.Id, true).GetAwaiter().GetResult();
var invoiceEntity = await repo.GetInvoice(invoice.Id, true);
Money secondPayment = Money.Zero;
TestUtils.Eventually(() =>
await TestUtils.EventuallyAsync(async () =>
{
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("new", localInvoice.Status);
@ -2234,7 +2231,7 @@ namespace BTCPayServer.Tests
Assert.True(IsMapped(invoice, ctx));
Assert.True(IsMapped(localInvoice, ctx));
invoiceEntity = repo.GetInvoice(invoice.Id, true).GetAwaiter().GetResult();
invoiceEntity = await repo.GetInvoice(invoice.Id, true);
invoiceAddress = BitcoinAddress.Create(localInvoice.BitcoinAddress, cashCow.Network);
secondPayment = localInvoice.BtcDue;
});
@ -2277,18 +2274,18 @@ namespace BTCPayServer.Tests
var txId = await cashCow.SendToAddressAsync(invoiceAddress, invoice.BtcDue + Money.Coins(1));
TestUtils.Eventually(() =>
await TestUtils.EventuallyAsync(async () =>
{
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("paid", localInvoice.Status);
Assert.Equal(Money.Zero, localInvoice.BtcDue);
Assert.Equal("paidOver", (string)((JValue)localInvoice.ExceptionStatus).Value);
var textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery()
var textSearchResult = await tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery()
{
StoreId = new[] { user.StoreId },
TextSearch = txId.ToString()
}).GetAwaiter().GetResult();
});
Assert.Single(textSearchResult);
});
@ -2424,7 +2421,7 @@ namespace BTCPayServer.Tests
[Fact(Timeout = LongRunningTestTimeout)]
[Trait("Integration", "Integration")]
public async void CheckOnionlocationForNonOnionHtmlRequests()
public async Task CheckOnionlocationForNonOnionHtmlRequests()
{
using var tester = CreateServerTester();
await tester.StartAsync();
@ -3051,7 +3048,7 @@ namespace BTCPayServer.Tests
[Fact]
[Trait("Integration", "Integration")]
public async void CanUseLocalProviderFiles()
public async Task CanUseLocalProviderFiles()
{
using var tester = CreateServerTester();
await tester.StartAsync();
@ -3250,8 +3247,14 @@ namespace BTCPayServer.Tests
var date2018 = new DateTimeOffset(2018, 1, 1, 0, 0, 0, TimeSpan.Zero);
report = await GetReport(acc, new() { ViewName = "Payments", TimePeriod = new TimePeriod() { From = date2018, To = date2018 + TimeSpan.FromDays(365) } });
var invoiceIdIndex = report.GetIndex("InvoiceId");
var invoiceCurrencyAmountIndex = report.GetIndex("InvoiceCurrencyAmount");
var rateIndex = report.GetIndex("Rate");
var oldPaymentsCount = report.Data.Count(d => d[invoiceIdIndex].Value<string>() == "Q7RqoHLngK9svM4MgRyi9y");
Assert.Equal(9, oldPaymentsCount); // 11 payments, but 2 unaccounted
Assert.Single(report.Data, d =>
d[invoiceIdIndex].Value<string>() == "Q7RqoHLngK9svM4MgRyi9y" &&
GetAmount(rateIndex, d) == 6596.35m &&
GetAmount(invoiceCurrencyAmountIndex, d) == 1.18m);
var addr = await tester.ExplorerNode.GetNewAddressAsync();
// Two invoices get refunded
@ -3270,12 +3273,12 @@ namespace BTCPayServer.Tests
var fullyPaidIndex = report.GetIndex("FullyPaid");
var completedIndex = report.GetIndex("Completed");
var limitIndex = report.GetIndex("Limit");
var d = Assert.Single(report.Data.Where(d => d[report.GetIndex("InvoiceId")].Value<string>() == inv.Id));
var d = Assert.Single(report.Data, d => d[report.GetIndex("InvoiceId")].Value<string>() == inv.Id);
Assert.Equal(fullyPaid, (bool)d[fullyPaidIndex]);
Assert.Equal(currency, d[currencyIndex].Value<string>());
Assert.Equal(completed, (((JObject)d[completedIndex])["v"]).Value<decimal>());
Assert.Equal(awaiting, (((JObject)d[awaitingIndex])["v"]).Value<decimal>());
Assert.Equal(limit, (((JObject)d[limitIndex])["v"]).Value<decimal>());
Assert.Equal(completed, GetAmount(completedIndex, d));
Assert.Equal(awaiting, GetAmount(awaitingIndex, d));
Assert.Equal(limit, GetAmount(limitIndex, d));
}
await AssertData("USD", awaiting: 0.0m, limit: 10.0m, completed: 0.0m, fullyPaid: false);
@ -3296,6 +3299,12 @@ namespace BTCPayServer.Tests
}
}
private static decimal GetAmount(int idx, JArray d)
{
var jobj = (JObject)d[idx];
return Math.Round(jobj["v"].Value<decimal>(), jobj["d"].Value<int>());
}
private async Task<StoreReportResponse> GetReport(TestAccount acc, StoreReportRequest req)
{
var controller = acc.GetController<UIReportsController>();

View File

@ -360,7 +360,9 @@ retry:
{
await tester.StartAsync();
var engine = tester.PayTester.GetService<RazorProjectEngine>();
foreach (var file in soldir.EnumerateFiles("*.cshtml", SearchOption.AllDirectories))
var files = soldir.EnumerateFiles("*.cshtml", SearchOption.AllDirectories)
.Union(soldir.EnumerateFiles("*.razor", SearchOption.AllDirectories));
foreach (var file in files)
{
var filePath = file.FullName;
var txt = File.ReadAllText(file.FullName);
@ -375,7 +377,7 @@ retry:
}
}
defaultTranslatedKeys = defaultTranslatedKeys.Select(d => d.Trim()).Distinct().OrderBy(o => o).ToList();
defaultTranslatedKeys = defaultTranslatedKeys.Select(d => d.Trim().Replace("\r\n", "\n")).Distinct().OrderBy(o => o).ToList();
JObject obj = new JObject();
foreach (var v in defaultTranslatedKeys)
{

View File

@ -72,7 +72,7 @@ services:
- "sshd_datadir:/root/.ssh"
devlnd:
image: btcpayserver/bitcoin:26.0
image: btcpayserver/bitcoin:28.1
environment:
BITCOIN_NETWORK: regtest
BITCOIN_WALLETDIR: "/data/wallets"
@ -98,7 +98,7 @@ services:
custom:
nbxplorer:
image: nicolasdorier/nbxplorer:2.5.4
image: nicolasdorier/nbxplorer:2.5.22
restart: unless-stopped
ports:
- "32838:32838"
@ -134,7 +134,7 @@ services:
bitcoind:
restart: unless-stopped
image: btcpayserver/bitcoin:26.0
image: btcpayserver/bitcoin:28.1
environment:
BITCOIN_NETWORK: regtest
BITCOIN_WALLETDIR: "/data/wallets"
@ -162,7 +162,7 @@ services:
- "bitcoin_datadir:/data"
customer_lightningd:
image: btcpayserver/lightning:v24.05
image: btcpayserver/lightning:v24.08.2
stop_signal: SIGKILL
restart: unless-stopped
environment:
@ -180,6 +180,7 @@ services:
dev-bitcoind-poll=1
ports:
- "30992:9835" # api port
- "30892:9735" # server port
expose:
- "9735" # server port
- "9835" # api port
@ -190,7 +191,7 @@ services:
- bitcoind
merchant_lightningd:
image: btcpayserver/lightning:v24.05
image: btcpayserver/lightning:v24.08.2
stop_signal: SIGKILL
restart: unless-stopped
environment:
@ -208,6 +209,7 @@ services:
dev-bitcoind-poll=1
ports:
- "30993:9835" # api port
- "30893:9735" # server port
expose:
- "9735" # server port
- "9835" # api port
@ -227,7 +229,7 @@ services:
- "5432"
merchant_lnd:
image: btcpayserver/lnd:v0.18.3-beta
image: btcpayserver/lnd:v0.18.5-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -252,9 +254,12 @@ services:
no-rest-tls=1
ports:
- "35531:8080"
- "30894:9735"
- "53280:10009"
expose:
- "8080"
- "9735"
- "10009"
volumes:
- "merchant_lnd_datadir:/data"
- "bitcoin_datadir:/deps/.bitcoin"
@ -262,7 +267,7 @@ services:
- bitcoind
customer_lnd:
image: btcpayserver/lnd:v0.18.3-beta
image: btcpayserver/lnd:v0.18.5-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -287,8 +292,10 @@ services:
no-rest-tls=1
ports:
- "35532:8080"
- "30895:9735"
expose:
- "8080"
- "9735"
- "10009"
volumes:
- "customer_lnd_datadir:/root/.lnd"

View File

@ -0,0 +1,298 @@
version: "3"
# Run `docker-compose up dev` for bootstrapping your development environment
# Doing so will expose NBXplorer, Bitcoind RPC and postgres port to the host so that tests can Run,
# The Visual Studio launch setting `Docker-signet` is configured to use this environment.
services:
# The dev container is not actually used, it is just handy to run `docker-compose up dev` to start all services
dev:
image: alpine:3.7
command: [ "/bin/sh", "-c", "trap : TERM INT; while :; do echo Ready to code and debug like a rockstar!!!; sleep 2073600; done & wait" ]
depends_on:
- nbxplorer
- postgres
- customer_lightningd
- merchant_lightningd
- customer_lnd
- merchant_lnd
- sshd
- tor
sshd:
build:
context: .
dockerfile: sshd.Dockerfile
ports:
- "21622:22"
expose:
- 22
volumes:
- "sshd_datadir:/root/.ssh"
devlnd:
image: btcpayserver/mutinynet:c23afab47fbe
environment:
BITCOIN_NETWORK: signet
BITCOIN_WALLETDIR: "/data/wallets"
BITCOIN_EXTRA_ARGS: |
deprecatedrpc=signrawtransaction
connect=bitcoind:39388
fallbackfee=0.0002
rpcallowip=0.0.0.0/0
[signet]
signetchallenge=512102f7561d208dd9ae99bf497273e16f389bdbd6c4742ddb8e6b216e64fa2928ad8f51ae
addnode=45.79.52.207:38333
dnsseed=0
signetblocktime=30
depends_on:
- nbxplorer
- postgres
- customer_lnd
- merchant_lnd
selenium:
image: selenium/standalone-chrome:125.0
extra_hosts:
- "tests:172.23.0.18"
expose:
- "4444"
networks:
default:
custom:
nbxplorer:
image: nicolasdorier/nbxplorer:2.5.22
restart: unless-stopped
ports:
- "32838:32838"
expose:
- "32838"
environment:
NBXPLORER_NETWORK: signet
NBXPLORER_CHAINS: "btc"
NBXPLORER_BTCRPCURL: http://bitcoind:43782/
NBXPLORER_BTCNODEENDPOINT: bitcoind:39388
NBXPLORER_BTCRPCUSER: ceiwHEbqWI83
NBXPLORER_BTCRPCPASSWORD: DwubwWsoo3
NBXPLORER_BIND: 0.0.0.0:32838
NBXPLORER_MINGAPSIZE: 5
NBXPLORER_MAXGAPSIZE: 10
NBXPLORER_VERBOSE: 1
NBXPLORER_POSTGRES: User ID=postgres;Include Error Detail=true;Host=postgres;Port=5432;Database=nbxplorer_mutinynet
NBXPLORER_EXPOSERPC: 1
NBXPLORER_NOAUTH: 1
depends_on:
- bitcoind
bitcoind:
restart: unless-stopped
image: btcpayserver/mutinynet:c23afab47fbe
environment:
BITCOIN_NETWORK: signet
BITCOIN_WALLETDIR: "/data/wallets"
BITCOIN_EXTRA_ARGS: |-
rpcuser=ceiwHEbqWI83
rpcpassword=DwubwWsoo3
rpcport=43782
rpcbind=0.0.0.0:43782
rpcallowip=0.0.0.0/0
port=39388
whitelist=0.0.0.0/0
zmqpubrawblock=tcp://0.0.0.0:28332
zmqpubrawtx=tcp://0.0.0.0:28333
deprecatedrpc=signrawtransaction
fallbackfee=0.0002
[signet]
signetchallenge=512102f7561d208dd9ae99bf497273e16f389bdbd6c4742ddb8e6b216e64fa2928ad8f51ae
addnode=45.79.52.207:38333
dnsseed=0
signetblocktime=30
ports:
- "43782:43782"
- "39388:39388"
expose:
- "43782" # RPC
- "39388" # P2P
- "28332" # ZMQ
- "28333" # ZMQ
volumes:
- "bitcoin_datadir:/data"
customer_lightningd:
image: btcpayserver/lightning:v24.08.2
stop_signal: SIGKILL
restart: unless-stopped
environment:
EXPOSE_TCP: "true"
LIGHTNINGD_CHAIN: "btc"
LIGHTNINGD_NETWORK: "signet"
LIGHTNINGD_OPT: |
developer
bitcoin-datadir=/etc/bitcoin
bitcoin-rpcconnect=bitcoind
announce-addr=customer_lightningd:9735
log-level=debug
funding-confirms=1
dev-fast-gossip
dev-bitcoind-poll=1
ports:
- "30992:9835" # api port
- "30892:9735" # server port
expose:
- "9735" # server port
- "9835" # api port
volumes:
- "bitcoin_datadir:/etc/bitcoin"
- "customer_lightningd_datadir:/root/.lightning"
depends_on:
- bitcoind
merchant_lightningd:
image: btcpayserver/lightning:v24.08.2
stop_signal: SIGKILL
restart: unless-stopped
environment:
EXPOSE_TCP: "true"
LIGHTNINGD_CHAIN: "btc"
LIGHTNINGD_NETWORK: "signet"
LIGHTNINGD_OPT: |
developer
bitcoin-datadir=/etc/bitcoin
bitcoin-rpcconnect=bitcoind
announce-addr=merchant_lightningd:9735
funding-confirms=1
log-level=debug
dev-fast-gossip
dev-bitcoind-poll=1
ports:
- "30993:9835" # api port
- "30893:9735" # server port
expose:
- "9735" # server port
- "9835" # api port
volumes:
- "bitcoin_datadir:/etc/bitcoin"
- "merchant_lightningd_datadir:/root/.lightning"
depends_on:
- bitcoind
postgres:
image: postgres:13.13
environment:
POSTGRES_HOST_AUTH_METHOD: trust
ports:
- "39372:5432"
expose:
- "5432"
merchant_lnd:
image: btcpayserver/lnd:v0.18.5-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
LND_ENVIRONMENT: "signet"
LND_EXPLORERURL: "http://nbxplorer:32838/"
LND_REST_LISTEN_HOST: http://merchant_lnd:8080
LND_EXTRA_ARGS: |
restlisten=merchant_lnd:8080
rpclisten=127.0.0.1:10008
rpclisten=merchant_lnd:10009
bitcoin.node=bitcoind
bitcoind.rpchost=bitcoind:43782
bitcoind.rpcuser=ceiwHEbqWI83
bitcoind.rpcpass=DwubwWsoo3
bitcoind.zmqpubrawblock=tcp://bitcoind:28332
bitcoind.zmqpubrawtx=tcp://bitcoind:28333
externalip=merchant_lnd:9735
bitcoin.defaultchanconfs=1
no-macaroons=1
debuglevel=debug
trickledelay=1000
no-rest-tls=1
ports:
- "35531:8080"
- "30894:9735"
- "53280:10009"
expose:
- "8080"
- "9735"
- "10009"
volumes:
- "merchant_lnd_datadir:/data"
- "bitcoin_datadir:/deps/.bitcoin"
depends_on:
- bitcoind
customer_lnd:
image: btcpayserver/lnd:v0.18.5-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
LND_ENVIRONMENT: "signet"
LND_EXPLORERURL: "http://nbxplorer:32838/"
LND_REST_LISTEN_HOST: http://customer_lnd:8080
LND_EXTRA_ARGS: |
restlisten=customer_lnd:8080
rpclisten=127.0.0.1:10008
rpclisten=customer_lnd:10009
bitcoin.node=bitcoind
bitcoind.rpchost=bitcoind:43782
bitcoind.rpcuser=ceiwHEbqWI83
bitcoind.rpcpass=DwubwWsoo3
bitcoind.zmqpubrawblock=tcp://bitcoind:28332
bitcoind.zmqpubrawtx=tcp://bitcoind:28333
externalip=customer_lnd:9735
bitcoin.defaultchanconfs=1
no-macaroons=1
debuglevel=debug
trickledelay=1000
no-rest-tls=1
ports:
- "35532:8080"
- "30895:9735"
expose:
- "8080"
- "9735"
- "10009"
volumes:
- "customer_lnd_datadir:/root/.lnd"
- "bitcoin_datadir:/deps/.bitcoin"
depends_on:
- bitcoind
tor:
restart: unless-stopped
image: btcpayserver/tor:0.4.6.5
container_name: tor
environment:
TOR_PASSWORD: btcpayserver
ports:
- "9050:9050" # SOCKS
- "9051:9051" # Tor Control
volumes:
- "tor_datadir:/home/tor/.tor"
- "torrcdir:/usr/local/etc/tor"
- "tor_servicesdir:/var/lib/tor/hidden_services"
volumes:
sshd_datadir:
bitcoin_datadir:
elementsd_liquid_datadir:
customer_lightningd_datadir:
merchant_lightningd_datadir:
lightning_charge_datadir:
customer_lnd_datadir:
merchant_lnd_datadir:
tor_datadir:
torrcdir:
tor_servicesdir:
networks:
default:
driver: bridge
custom:
driver: bridge
ipam:
config:
- subnet: 172.23.0.0/16

View File

@ -0,0 +1,288 @@
version: "3"
# Run `docker-compose up dev` for bootstrapping your development environment
# Doing so will expose NBXplorer, Bitcoind RPC and postgres port to the host so that tests can Run,
# The Visual Studio launch setting `Docker-testnet` is configured to use this environment.
services:
# The dev container is not actually used, it is just handy to run `docker-compose up dev` to start all services
dev:
image: alpine:3.7
command: [ "/bin/sh", "-c", "trap : TERM INT; while :; do echo Ready to code and debug like a rockstar!!!; sleep 2073600; done & wait" ]
depends_on:
- nbxplorer
- postgres
- customer_lightningd
- merchant_lightningd
- customer_lnd
- merchant_lnd
- sshd
- tor
sshd:
build:
context: .
dockerfile: sshd.Dockerfile
ports:
- "21622:22"
expose:
- 22
volumes:
- "sshd_datadir:/root/.ssh"
devlnd:
image: btcpayserver/bitcoin:28.1
environment:
BITCOIN_NETWORK: testnet
BITCOIN_WALLETDIR: "/data/wallets"
BITCOIN_EXTRA_ARGS: |
deprecatedrpc=signrawtransaction
connect=bitcoind:39388
fallbackfee=0.0002
rpcallowip=0.0.0.0/0
depends_on:
- nbxplorer
- postgres
- customer_lnd
- merchant_lnd
selenium:
image: selenium/standalone-chrome:125.0
extra_hosts:
- "tests:172.23.0.18"
expose:
- "4444"
networks:
default:
custom:
nbxplorer:
image: nicolasdorier/nbxplorer:2.5.22
restart: unless-stopped
ports:
- "32838:32838"
expose:
- "32838"
environment:
NBXPLORER_NETWORK: testnet
NBXPLORER_CHAINS: "btc"
NBXPLORER_BTCRPCURL: http://bitcoind:43782/
NBXPLORER_BTCNODEENDPOINT: bitcoind:39388
NBXPLORER_BTCRPCUSER: ceiwHEbqWI83
NBXPLORER_BTCRPCPASSWORD: DwubwWsoo3
NBXPLORER_BIND: 0.0.0.0:32838
NBXPLORER_MINGAPSIZE: 5
NBXPLORER_MAXGAPSIZE: 10
NBXPLORER_VERBOSE: 1
NBXPLORER_POSTGRES: User ID=postgres;Include Error Detail=true;Host=postgres;Port=5432;Database=nbxplorer_testnet
NBXPLORER_EXPOSERPC: 1
NBXPLORER_NOAUTH: 1
depends_on:
- bitcoind
bitcoind:
restart: unless-stopped
image: btcpayserver/bitcoin:28.1
environment:
BITCOIN_NETWORK: testnet
BITCOIN_WALLETDIR: "/data/wallets"
BITCOIN_EXTRA_ARGS: |-
rpcuser=ceiwHEbqWI83
rpcpassword=DwubwWsoo3
rpcport=43782
rpcbind=0.0.0.0:43782
rpcallowip=0.0.0.0/0
port=39388
whitelist=0.0.0.0/0
zmqpubrawblock=tcp://0.0.0.0:28332
zmqpubrawtx=tcp://0.0.0.0:28333
deprecatedrpc=signrawtransaction
fallbackfee=0.0002
ports:
- "43782:43782"
- "39388:39388"
expose:
- "43782" # RPC
- "39388" # P2P
- "28332" # ZMQ
- "28333" # ZMQ
volumes:
- "bitcoin_datadir:/data"
customer_lightningd:
image: btcpayserver/lightning:v24.08.2
stop_signal: SIGKILL
restart: unless-stopped
environment:
EXPOSE_TCP: "true"
LIGHTNINGD_CHAIN: "btc"
LIGHTNINGD_NETWORK: "testnet"
LIGHTNINGD_OPT: |
developer
bitcoin-datadir=/etc/bitcoin
bitcoin-rpcconnect=bitcoind
announce-addr=customer_lightningd:9735
log-level=debug
funding-confirms=1
dev-fast-gossip
dev-bitcoind-poll=1
ports:
- "30992:9835" # api port
- "30892:9735" # server port
expose:
- "9735" # server port
- "9835" # api port
volumes:
- "bitcoin_datadir:/etc/bitcoin"
- "customer_lightningd_datadir:/root/.lightning"
depends_on:
- bitcoind
merchant_lightningd:
image: btcpayserver/lightning:v24.08.2
stop_signal: SIGKILL
restart: unless-stopped
environment:
EXPOSE_TCP: "true"
LIGHTNINGD_CHAIN: "btc"
LIGHTNINGD_NETWORK: "testnet"
LIGHTNINGD_OPT: |
developer
bitcoin-datadir=/etc/bitcoin
bitcoin-rpcconnect=bitcoind
announce-addr=merchant_lightningd:9735
funding-confirms=1
log-level=debug
dev-fast-gossip
dev-bitcoind-poll=1
ports:
- "30993:9835" # api port
- "30893:9735" # server port
expose:
- "9735" # server port
- "9835" # api port
volumes:
- "bitcoin_datadir:/etc/bitcoin"
- "merchant_lightningd_datadir:/root/.lightning"
depends_on:
- bitcoind
postgres:
image: postgres:13.13
environment:
POSTGRES_HOST_AUTH_METHOD: trust
ports:
- "39372:5432"
expose:
- "5432"
merchant_lnd:
image: btcpayserver/lnd:v0.18.5-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
LND_ENVIRONMENT: "testnet"
LND_EXPLORERURL: "http://nbxplorer:32838/"
LND_REST_LISTEN_HOST: http://merchant_lnd:8080
LND_EXTRA_ARGS: |
restlisten=merchant_lnd:8080
rpclisten=127.0.0.1:10008
rpclisten=merchant_lnd:10009
bitcoin.node=bitcoind
bitcoind.rpchost=bitcoind:43782
bitcoind.rpcuser=ceiwHEbqWI83
bitcoind.rpcpass=DwubwWsoo3
bitcoind.zmqpubrawblock=tcp://bitcoind:28332
bitcoind.zmqpubrawtx=tcp://bitcoind:28333
externalip=merchant_lnd:9735
bitcoin.defaultchanconfs=1
no-macaroons=1
debuglevel=debug
trickledelay=1000
no-rest-tls=1
ports:
- "35531:8080"
- "30894:9735"
- "53280:10009"
expose:
- "8080"
- "9735"
- "10009"
volumes:
- "merchant_lnd_datadir:/data"
- "bitcoin_datadir:/deps/.bitcoin"
depends_on:
- bitcoind
customer_lnd:
image: btcpayserver/lnd:v0.18.5-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
LND_ENVIRONMENT: "testnet"
LND_EXPLORERURL: "http://nbxplorer:32838/"
LND_REST_LISTEN_HOST: http://customer_lnd:8080
LND_EXTRA_ARGS: |
restlisten=customer_lnd:8080
rpclisten=127.0.0.1:10008
rpclisten=customer_lnd:10009
bitcoin.node=bitcoind
bitcoind.rpchost=bitcoind:43782
bitcoind.rpcuser=ceiwHEbqWI83
bitcoind.rpcpass=DwubwWsoo3
bitcoind.zmqpubrawblock=tcp://bitcoind:28332
bitcoind.zmqpubrawtx=tcp://bitcoind:28333
externalip=customer_lnd:9735
bitcoin.defaultchanconfs=1
no-macaroons=1
debuglevel=debug
trickledelay=1000
no-rest-tls=1
ports:
- "35532:8080"
- "30895:9735"
expose:
- "8080"
- "9735"
- "10009"
volumes:
- "customer_lnd_datadir:/root/.lnd"
- "bitcoin_datadir:/deps/.bitcoin"
depends_on:
- bitcoind
tor:
restart: unless-stopped
image: btcpayserver/tor:0.4.6.5
container_name: tor
environment:
TOR_PASSWORD: btcpayserver
ports:
- "9050:9050" # SOCKS
- "9051:9051" # Tor Control
volumes:
- "tor_datadir:/home/tor/.tor"
- "torrcdir:/usr/local/etc/tor"
- "tor_servicesdir:/var/lib/tor/hidden_services"
volumes:
sshd_datadir:
bitcoin_datadir:
elementsd_liquid_datadir:
customer_lightningd_datadir:
merchant_lightningd_datadir:
lightning_charge_datadir:
customer_lnd_datadir:
merchant_lnd_datadir:
tor_datadir:
torrcdir:
tor_servicesdir:
networks:
default:
driver: bridge
custom:
driver: bridge
ipam:
config:
- subnet: 172.23.0.0/16

View File

@ -69,7 +69,7 @@ services:
- "sshd_datadir:/root/.ssh"
devlnd:
image: btcpayserver/bitcoin:26.0
image: btcpayserver/bitcoin:28.1
environment:
BITCOIN_NETWORK: regtest
BITCOIN_WALLETDIR: "/data/wallets"
@ -95,7 +95,7 @@ services:
custom:
nbxplorer:
image: nicolasdorier/nbxplorer:2.5.4
image: nicolasdorier/nbxplorer:2.5.22
restart: unless-stopped
ports:
- "32838:32838"
@ -120,7 +120,7 @@ services:
bitcoind:
restart: unless-stopped
image: btcpayserver/bitcoin:26.0
image: btcpayserver/bitcoin:28.1
environment:
BITCOIN_NETWORK: regtest
BITCOIN_WALLETDIR: "/data/wallets"
@ -148,7 +148,7 @@ services:
- "bitcoin_datadir:/data"
customer_lightningd:
image: btcpayserver/lightning:v24.05
image: btcpayserver/lightning:v24.08.2
stop_signal: SIGKILL
restart: unless-stopped
environment:
@ -166,6 +166,7 @@ services:
dev-bitcoind-poll=1
ports:
- "30992:9835" # api port
- "30892:9735" # server port
expose:
- "9735" # server port
- "9835" # api port
@ -176,7 +177,7 @@ services:
- bitcoind
merchant_lightningd:
image: btcpayserver/lightning:v24.05
image: btcpayserver/lightning:v24.08.2
stop_signal: SIGKILL
restart: unless-stopped
environment:
@ -194,6 +195,7 @@ services:
dev-bitcoind-poll=1
ports:
- "30993:9835" # api port
- "30893:9735" # server port
expose:
- "9735" # server port
- "9835" # api port
@ -213,7 +215,7 @@ services:
- "5432"
merchant_lnd:
image: btcpayserver/lnd:v0.18.3-beta
image: btcpayserver/lnd:v0.18.5-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -239,6 +241,7 @@ services:
ports:
- "35531:8080"
- "53280:10009"
- "30894:9735"
expose:
- "8080"
- "9735"
@ -250,7 +253,7 @@ services:
- bitcoind
customer_lnd:
image: btcpayserver/lnd:v0.18.3-beta
image: btcpayserver/lnd:v0.18.5-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -275,6 +278,7 @@ services:
no-rest-tls=1
ports:
- "35532:8080"
- "30895:9735"
expose:
- "8080"
- "9735"

View File

@ -1,6 +1,5 @@
{
"maxParallelThreads": 4,
"longRunningTestSeconds": 60,
"diagnosticMessages": true,
"methodDisplay": "method"
}

View File

@ -8,6 +8,19 @@
<RunAnalyzersDuringLiveAnalysis>False</RunAnalyzersDuringLiveAnalysis>
<RunAnalyzersDuringBuild>False</RunAnalyzersDuringBuild>
</PropertyGroup>
<!-- Pre-compiling views should only be done for Release builds without dotnet watch or design time build .-->
<!-- Runtime compiling is only useful for debugging with hot reload of the views -->
<PropertyGroup Condition="'$(RazorCompileOnBuild)'=='' AND ('$(Configuration)' == 'Debug' OR '$(DotNetWatchBuild)' == 'true' OR '$(DesignTimeBuild)' == 'true')">
<RazorCompileOnBuild>false</RazorCompileOnBuild>
</PropertyGroup>
<PropertyGroup Condition="'$(RazorCompileOnBuild)'==''">
<RazorCompileOnBuild>true</RazorCompileOnBuild>
</PropertyGroup>
<PropertyGroup Condition="'$(RazorCompileOnBuild)' == 'true'">
<DefineConstants>$(DefineConstants);RAZOR_COMPILE_ON_BUILD</DefineConstants>
</PropertyGroup>
<ItemGroup>
<AssemblyAttribute Condition="'$(GitCommit)' != ''" Include="BTCPayServer.GitCommitAttribute">
<_Parameter1>$(GitCommit)</_Parameter1>
@ -37,19 +50,19 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="BTCPayServer.NTag424" Version="1.0.23" />
<PackageReference Include="BTCPayServer.NTag424" Version="1.0.25" />
<PackageReference Include="YamlDotNet" Version="8.0.0" />
<PackageReference Include="BIP78.Sender" Version="0.2.2" />
<PackageReference Include="BIP78.Sender" Version="0.2.4" />
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.2" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.6.0" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.6.9" />
<PackageReference Include="CsvHelper" Version="32.0.3" />
<PackageReference Include="Dapper" Version="2.1.35" />
<PackageReference Include="Fido2" Version="2.0.2" />
<PackageReference Include="Fido2.AspNet" Version="2.0.2" />
<PackageReference Include="LNURL" Version="0.0.34" />
<PackageReference Include="MailKit" Version="3.3.0" />
<PackageReference Include="Fido2" Version="3.0.1" />
<PackageReference Include="Fido2.AspNet" Version="3.0.1" />
<PackageReference Include="LNURL" Version="0.0.36" />
<PackageReference Include="MailKit" Version="4.8.0" />
<PackageReference Include="BTCPayServer.NETCore.Plugins.Mvc" Version="1.4.4" />
<PackageReference Include="QRCoder" Version="1.4.3" />
<PackageReference Include="QRCoder" Version="1.6.0" />
<PackageReference Include="System.IO.Pipelines" Version="8.0.0" />
<PackageReference Include="NBitpayClient" Version="1.0.0.39" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
@ -60,16 +73,15 @@
<PackageReference Include="Serilog.AspNetCore" Version="8.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.1-dev-00968" />
<PackageReference Include="SSH.NET" Version="2023.0.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="TwentyTwenty.Storage" Version="2.12.1" />
<PackageReference Include="TwentyTwenty.Storage.Amazon" Version="2.12.1" />
<PackageReference Include="TwentyTwenty.Storage.Azure" Version="2.12.1" />
<PackageReference Include="TwentyTwenty.Storage.Google" Version="2.12.1" />
<PackageReference Include="TwentyTwenty.Storage.Local" Version="2.12.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.7" />
<PackageReference Include="TwentyTwenty.Storage" Version="2.24.2" />
<PackageReference Include="TwentyTwenty.Storage.Amazon" Version="2.24.2" />
<PackageReference Include="TwentyTwenty.Storage.Azure" Version="2.24.2" />
<PackageReference Include="TwentyTwenty.Storage.Google" Version="2.24.2" />
<PackageReference Include="TwentyTwenty.Storage.Local" Version="2.24.2" />
<PackageReference Condition="'$(RazorCompileOnBuild)' == 'false'" Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.11" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.11" />
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="8.0.11" />
</ItemGroup>
<ItemGroup>

View File

@ -4,10 +4,12 @@
@using BTCPayServer.Services.Notifications;
@using Microsoft.AspNetCore.Identity;
@using Microsoft.AspNetCore.Routing;
@using Microsoft.Extensions.Localization
@implements IDisposable
@inject AuthenticationStateProvider _AuthenticationStateProvider
@inject NotificationManager _NotificationManager
@inject UserManager<ApplicationUser> _UserManager
@inject IStringLocalizer StringLocalizer
@inject IJSRuntime _JSRuntime
@inject LinkGenerator _LinkGenerator
@inject BTCPayServerOptions _BTCPayServerOptions
@ -16,13 +18,13 @@
<div id="Notifications">
@if (UnseenCount == "0")
{
<a href="@NotificationsUrl" id="NotificationsHandle" class="mainMenuButton" title="Notifications">
<a href="@NotificationsUrl" id="NotificationsHandle" class="mainMenuButton" title="@StringLocalizer["Notifications"]">
<Icon Symbol="nav-notifications" />
</a>
}
else
{
<button id="NotificationsHandle" class="mainMenuButton" title="Notifications" type="button" data-bs-toggle="dropdown">
<button id="NotificationsHandle" class="mainMenuButton" title="@StringLocalizer["Notifications"]" type="button" data-bs-toggle="dropdown">
<Icon Symbol="nav-notifications" />
<span class="badge rounded-pill bg-danger p-1 ms-1" id="NotificationsBadge">@UnseenCount</span>
</button>
@ -31,8 +33,8 @@
{
<div class="dropdown-menu text-center" id="NotificationsDropdown" aria-labelledby="NotificationsHandle">
<div class="d-flex gap-3 align-items-center justify-content-between py-3 px-4 border-bottom border-light">
<h5 class="m-0">Notifications</h5>
<a class="btn btn-link p-0" @onclick="MarkAllAsSeen" id="NotificationsMarkAllAsSeen">Mark all as seen</a>
<h5 class="m-0" text-translate="true">Notifications</h5>
<a class="btn btn-link p-0" @onclick="MarkAllAsSeen" id="NotificationsMarkAllAsSeen" text-translate="true">Mark all as seen</a>
</div>
<div id="NotificationsList" v-pre>
@foreach (var n in Last5)
@ -54,7 +56,7 @@
</div>
<div class="p-3">
<a href="@NotificationsUrl">View all</a>
<a href="@NotificationsUrl" text-translate="true">View all</a>
</div>
</div>
}

View File

@ -1,13 +1,13 @@
@using Microsoft.AspNetCore.Http
@inject IHttpContextAccessor HttpContextAccessor;
@inject IHttpContextAccessor HttpContextAccessor
@if (Users?.Any() is true)
{
<div @attributes="Attrs" class="@CssClass">
<label for="SignedInUser" class="form-label">Signed in user</label>
<label for="SignedInUser" class="form-label" text-translate="true">Signed in user</label>
<select id="SignedInUser" class="form-select" value="@_userId" @onchange="@(e => _userId = e.Value?.ToString())">
<option value="">None, just open the URL</option>
<option value="" text-translate="true">None, just open the URL</option>
@foreach (var u in Users)
{
<option value="@u.Key">@u.Value</option>

View File

@ -5,11 +5,13 @@
@using Microsoft.AspNetCore.Identity
@using Microsoft.AspNetCore.Mvc
@using Microsoft.AspNetCore.Routing
@using Microsoft.Extensions.Localization
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject UserManager<ApplicationUser> UserManager;
@inject UserLoginCodeService UserLoginCodeService;
@inject LinkGenerator LinkGenerator;
@inject IHttpContextAccessor HttpContextAccessor;
@inject UserManager<ApplicationUser> UserManager
@inject UserLoginCodeService UserLoginCodeService
@inject LinkGenerator LinkGenerator
@inject IHttpContextAccessor HttpContextAccessor
@inject IStringLocalizer StringLocalizer
@implements IDisposable
@if (!string.IsNullOrEmpty(_data))
@ -18,7 +20,7 @@
<div class="qr-container mb-2">
<QrCode Data="@_data" Size="Size"/>
</div>
<p class="text-center text-muted mb-1" id="progress">Valid for @_seconds seconds</p>
<p class="text-center text-muted mb-1" id="progress">@StringLocalizer["Valid for {0} seconds", _seconds]</p>
<div class="progress only-for-js" data-bs-toggle="tooltip" data-bs-placement="top">
<div class="progress-bar progress-bar-striped progress-bar-animated @(Percent < 15 ? "bg-warning" : null)" role="progressbar" style="width:@Percent%" id="progressbar"></div>
</div>

View File

@ -110,5 +110,10 @@ namespace BTCPayServer
{
return ColorTranslator.FromHtml(html);
}
public string ToHtml(Color color)
{
return ColorTranslator.ToHtml(color);
}
}
}

View File

@ -1,11 +1,5 @@
using System;
using System.Security.AccessControl;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewComponents;
using Microsoft.AspNetCore.Mvc.ViewFeatures;

View File

@ -1,6 +1,5 @@
using System.Collections.Generic;
using BTCPayServer.Client.Models;
using BTCPayServer.Services.Apps;
namespace BTCPayServer.Components.AppSales;

View File

@ -42,11 +42,19 @@ if (!window.appSales) {
render(data, period);
}
};
function addEventListeners() {
delegate('change', `#${id} [name="AppSalesPeriod-${appId}"]`, async e => {
const type = e.target.value;
await update(type);
});
}
delegate('change', `#${id} [name="AppSalesPeriod-${appId}"]`, async e => {
const type = e.target.value;
await update(type);
});
if (document.readyState === "loading") {
window.addEventListener("DOMContentLoaded", addEventListeners);
} else {
addEventListeners();
}
}
};
}

View File

@ -45,31 +45,31 @@
</li>
@if (ViewData.IsPageActive([StoreNavPages.General, StoreNavPages.Rates, StoreNavPages.CheckoutAppearance, StoreNavPages.Tokens, StoreNavPages.Users, StoreNavPages.Roles, StoreNavPages.Webhooks, StoreNavPages.PayoutProcessors, StoreNavPages.Emails, StoreNavPages.Forms]))
{
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyStoreSettings">
<li class="nav-item nav-item-sub" permission="@Policies.CanViewStoreSettings">
<a id="StoreNav-@(nameof(StoreNavPages.Rates))" class="nav-link @ViewData.ActivePageClass(StoreNavPages.Rates)" asp-controller="UIStores" asp-action="Rates" asp-route-storeId="@Model.Store.Id" text-translate="true">Rates</a>
</li>
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyStoreSettings">
<li class="nav-item nav-item-sub" permission="@Policies.CanViewStoreSettings">
<a id="StoreNav-@(nameof(StoreNavPages.CheckoutAppearance))" class="nav-link @ViewData.ActivePageClass(StoreNavPages.CheckoutAppearance)" asp-controller="UIStores" asp-action="CheckoutAppearance" asp-route-storeId="@Model.Store.Id" text-translate="true">Checkout Appearance</a>
</li>
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyStoreSettings">
<li class="nav-item nav-item-sub" permission="@Policies.CanViewStoreSettings">
<a id="StoreNav-@(nameof(StoreNavPages.Tokens))" class="nav-link @ViewData.ActivePageClass(StoreNavPages.Tokens)" asp-controller="UIStores" asp-action="ListTokens" asp-route-storeId="@Model.Store.Id" text-translate="true">Access Tokens</a>
</li>
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyStoreSettings">
<li class="nav-item nav-item-sub" permission="@Policies.CanViewStoreSettings">
<a id="StoreNav-@(nameof(StoreNavPages.Users))" class="nav-link @ViewData.ActivePageClass(StoreNavPages.Users)" asp-controller="UIStores" asp-action="StoreUsers" asp-route-storeId="@Model.Store.Id" text-translate="true">Users</a>
</li>
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyStoreSettings">
<li class="nav-item nav-item-sub" permission="@Policies.CanViewStoreSettings">
<a id="StoreNav-@(nameof(StoreNavPages.Roles))" class="nav-link @ViewData.ActivePageClass(StoreNavPages.Roles)" asp-controller="UIStores" asp-action="ListRoles" asp-route-storeId="@Model.Store.Id" text-translate="true">Roles</a>
</li>
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyStoreSettings">
<li class="nav-item nav-item-sub" permission="@Policies.CanViewStoreSettings">
<a id="StoreNav-@(nameof(StoreNavPages.Webhooks))" class="nav-link @ViewData.ActivePageClass(StoreNavPages.Webhooks)" asp-controller="UIStores" asp-action="Webhooks" asp-route-storeId="@Model.Store.Id" text-translate="true">Webhooks</a>
</li>
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyStoreSettings">
<li class="nav-item nav-item-sub" permission="@Policies.CanViewStoreSettings">
<a id="StoreNav-@(nameof(StoreNavPages.PayoutProcessors))" class="nav-link @ViewData.ActivePageClass(StoreNavPages.PayoutProcessors)" asp-controller="UIPayoutProcessors" asp-action="ConfigureStorePayoutProcessors" asp-route-storeId="@Model.Store.Id" text-translate="true">Payout Processors</a>
</li>
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyStoreSettings">
<li class="nav-item nav-item-sub" permission="@Policies.CanViewStoreSettings">
<a id="StoreNav-@(nameof(StoreNavPages.Emails))" class="nav-link @ViewData.ActivePageClass(StoreNavPages.Emails)" asp-controller="UIStores" asp-action="StoreEmailSettings" asp-route-storeId="@Model.Store.Id" text-translate="true">Emails</a>
</li>
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyStoreSettings">
<li class="nav-item nav-item-sub" permission="@Policies.CanViewStoreSettings">
<a id="StoreNav-@(nameof(StoreNavPages.Forms))" class="nav-link @ViewData.ActivePageClass(StoreNavPages.Forms)" asp-controller="UIForms" asp-action="FormsList" asp-route-storeId="@Model.Store.Id" text-translate="true">Forms</a>
</li>
}
@ -106,9 +106,14 @@
</li>
@if (ViewData.IsCategoryActive(typeof(WalletsNavPages), scheme.WalletId.ToString()) || ViewData.IsPageActive([WalletsNavPages.Settings], scheme.WalletId.ToString()) || ViewData.IsPageActive([StoreNavPages.OnchainSettings], categoryId))
{
<li class="nav-item nav-item-sub">
<a id="WalletNav-Send" class="nav-link @ViewData.ActivePageClass([WalletsNavPages.Send, WalletsNavPages.PSBT], scheme.WalletId.ToString())" asp-area="" asp-controller="UIWallets" asp-action="WalletSend" asp-route-walletId="@scheme.WalletId" text-translate="true">Send</a>
</li>
@if (!scheme.ReadonlyWallet)
{
<li class="nav-item nav-item-sub">
<a id="WalletNav-Send" class="nav-link @ViewData.ActivePageClass([WalletsNavPages.Send, WalletsNavPages.PSBT], scheme.WalletId.ToString())" asp-area="" asp-controller="UIWallets" asp-action="WalletSend" asp-route-walletId="@scheme.WalletId" text-translate="true">Send</a>
</li>
}
<li class="nav-item nav-item-sub">
<a id="WalletNav-Receive" class="nav-link @ViewData.ActivePageClass(WalletsNavPages.Receive, scheme.WalletId.ToString())" asp-area="" asp-controller="UIWallets" asp-action="WalletReceive" asp-route-walletId="@scheme.WalletId" text-translate="true">Receive</a>
</li>

View File

@ -1,3 +1,5 @@
@using System.Web
@using BTCPayServer.TagHelpers
@model BasePagingViewModel
@{
@ -85,10 +87,26 @@
{
// merge both, preferring the `query` properties in case of duplicate keys
query = query.Concat(Model.PaginationQuery)
.Where(e => e.Value != null)
.GroupBy(e => e.Key)
.ToDictionary(g => g.Key, g => g.First().Value);
}
return Url.Action(null, query);
return ReplaceQueryParameters(query);
}
string ReplaceQueryParameters(Dictionary<string, object> query)
{
var uri = new Uri(ViewContext.HttpContext.Request.GetCurrentUrlWithQueryString());
var queryParams = HttpUtility.ParseQueryString(uri.Query);
foreach (var (key, value) in query)
{
if (value != null) queryParams[key] = value?.ToString();
}
var uriBuilder = new UriBuilder(uri)
{
Query = queryParams.ToString()!
};
return uriBuilder.ToString();
}
}

View File

@ -1,7 +1,3 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Models;
using Microsoft.AspNetCore.Mvc;

View File

@ -1,5 +1,4 @@
using System;
using System.Drawing;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewComponents;

View File

@ -1,9 +1,13 @@
@using BTCPayServer.Abstractions.TagHelpers
@using BTCPayServer.Client.Models
@using BTCPayServer.TagHelpers
@using Microsoft.AspNetCore.Mvc.TagHelpers
@model BTCPayServer.Components.StoreLightningBalance.StoreLightningBalanceViewModel
@if(!Model.InitialRendering && Model.Balance == null)
@if (!Model.InitialRendering && Model.Balance == null)
{
return;
return;
}
<div id="StoreLightningBalance-@Model.Store.Id" class="widget store-lightning-balance">
<div id="StoreLightningBalance-@Model.StoreId" class="widget store-lightning-balance">
<div class="d-flex gap-3 align-items-center justify-content-between mb-2">
<h6 text-translate="true">Lightning Balance</h6>
@if (Model.CryptoCode != Model.DefaultCurrency && Model.Balance != null)
@ -128,13 +132,30 @@
</div>
}
</div>
@if (Model.Balance.OffchainBalance != null && Model.Balance.OnchainBalance != null)
{
<button class="d-inline-flex align-items-center btn btn-link text-primary fw-semibold p-0 mt-3 ms-n1" type="button" data-bs-toggle="collapse" data-bs-target=".balance-details" aria-expanded="false" aria-controls="balanceDetailsOffchain balanceDetailsOnchain">
<vc:icon symbol="caret-down"/>
<span class="ms-1" text-translate="true">Details</span>
</button>
}
<div class="d-flex flex-wrap align-items-center justify-content-between gap-3 @(Model.Series != null ? "my-3" : "mt-3")">
@if (Model.Balance.OffchainBalance != null && Model.Balance.OnchainBalance != null)
{
<button class="d-inline-flex align-items-center btn btn-link text-primary fw-semibold p-0 ms-n1" type="button" data-bs-toggle="collapse" data-bs-target=".balance-details" aria-expanded="false" aria-controls="balanceDetailsOffchain balanceDetailsOnchain">
<vc:icon symbol="caret-down" />
<span class="ms-1" text-translate="true">Details</span>
</button>
}
@if (Model.Series != null)
{
<div class="btn-group only-for-js mt-1" role="group" aria-label="Period">
<input type="radio" class="btn-check" name="StoreLightningBalancePeriod-@Model.StoreId" id="StoreLightningBalancePeriodWeek-@Model.StoreId" value="@HistogramType.Week" @(Model.Type == HistogramType.Week ? "checked" : "")>
<label class="btn btn-link" for="StoreLightningBalancePeriodWeek-@Model.StoreId">1W</label>
<input type="radio" class="btn-check" name="StoreLightningBalancePeriod-@Model.StoreId" id="StoreLightningBalancePeriodMonth-@Model.StoreId" value="@HistogramType.Month" @(Model.Type == HistogramType.Month ? "checked" : "")>
<label class="btn btn-link" for="StoreLightningBalancePeriodMonth-@Model.StoreId">1M</label>
<input type="radio" class="btn-check" name="StoreLightningBalancePeriod-@Model.StoreId" id="StoreLightningBalancePeriodYear-@Model.StoreId" value="@HistogramType.Year" @(Model.Type == HistogramType.Year ? "checked" : "")>
<label class="btn btn-link" for="StoreLightningBalancePeriodYear-@Model.StoreId">1Y</label>
</div>
}
</div>
<div class="ct-chart"></div>
<template>
@Safe.Json(Model)
</template>
}
else
{
@ -143,47 +164,18 @@
<span class="visually-hidden" text-translate="true">Loading...</span>
</div>
</div>
<script src="~/Components/StoreLightningBalance/Default.cshtml.js" asp-append-version="true"></script>
<script>
(async () => {
const url = @Safe.Json(Url.Action("LightningBalance", "UIStores", new { storeId = Model.Store.Id, cryptoCode = Model.CryptoCode }));
const storeId = @Safe.Json(Model.Store.Id);
const url = @Safe.Json(Model.DataUrl);
const storeId = @Safe.Json(Model.StoreId);
const response = await fetch(url);
if (response.ok) {
document.getElementById(`StoreLightningBalance-${storeId}`).outerHTML = await response.text();
const data = document.querySelector(`#StoreLightningBalance-${storeId} template`);
if (data) window.storeLightningBalance.dataLoaded(JSON.parse(data.innerHTML));
}
})();
</script>
}
</div>
<script>
(function () {
const storeId = @Safe.Json(Model.Store.Id);
const cryptoCode = @Safe.Json(Model.CryptoCode);
const defaultCurrency = @Safe.Json(Model.DefaultCurrency);
const divisibility = @Safe.Json(Model.CurrencyData.Divisibility);
const id = `StoreLightningBalance-${storeId}`;
const render = rate => {
const currency = rate ? defaultCurrency : cryptoCode;
document.querySelectorAll(`#${id} .currency`).forEach(c => c.innerText = currency)
document.querySelectorAll(`#${id} [data-balance]`).forEach(c => {
const value = Number.parseFloat(c.dataset.balance);
c.innerText = rate
? DashboardUtils.displayDefaultCurrency(value, rate, currency, divisibility)
: value
});
};
document.addEventListener('DOMContentLoaded', () => {
delegate('change', `#${id} .currency-toggle input`, async e => {
const { target } = e;
if (target.value === defaultCurrency) {
const rate = await DashboardUtils.fetchRate(`${cryptoCode}_${defaultCurrency}`);
if (rate) render(rate);
} else {
render(null);
}
});
});
})();
</script>

View File

@ -0,0 +1,93 @@
if (!window.storeLightningBalance) {
window.storeLightningBalance = {
dataLoaded (model) {
const { storeId, cryptoCode, defaultCurrency, currencyData: { divisibility } } = model;
const id = `StoreLightningBalance-${storeId}`;
const valueTransform = value => rate ? DashboardUtils.displayDefaultCurrency(value, rate, defaultCurrency, divisibility) : value
const labelCount = 6
const tooltip = Chartist.plugins.tooltip2({
template: '<div class="chartist-tooltip-value">{{value}}</div><div class="chartist-tooltip-line"></div>',
offset: {
x: 0,
y: -16
},
valueTransformFunction(value, label) {
return valueTransform(value) + ' ' + (rate ? defaultCurrency : cryptoCode)
}
})
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat
const dateFormatter = new Intl.DateTimeFormat('default', { month: 'short', day: 'numeric' })
const chartOpts = {
fullWidth: true,
showArea: true,
axisY: {
showLabel: false,
offset: 0
},
plugins: [tooltip]
};
const baseUrl = model.dataUrl;
let data = model;
let rate = null;
const render = data => {
let { series, labels } = data;
const currency = rate ? defaultCurrency : cryptoCode;
document.querySelectorAll(`#${id} .currency`).forEach(c => c.innerText = currency)
document.querySelectorAll(`#${id} [data-balance]`).forEach(c => {
c.innerText = valueTransform(c.dataset.balance)
});
if (!series) return;
const min = Math.min(...series);
const max = Math.max(...series);
const low = Math.max(min - ((max - min) / 5), 0);
const renderOpts = Object.assign({}, chartOpts, { low, axisX: {
labelInterpolationFnc(date, i) {
return i % labelEvery == 0 ? dateFormatter.format(new Date(date)) : null
}
} });
const pointCount = series.length;
const labelEvery = pointCount / labelCount;
const chart = new Chartist.Line(`#${id} .ct-chart`, {
labels: labels,
series: [series]
}, renderOpts);
};
const update = async type => {
const url = `${baseUrl}/${type}`;
const response = await fetch(url);
if (response.ok) {
data = await response.json();
render(data);
}
};
render(data);
function addEventListeners() {
delegate('change', `#${id} [name="StoreLightningBalancePeriod-${storeId}"]`, async e => {
const type = e.target.value;
await update(type);
})
delegate('change', `#${id} .currency-toggle input`, async e => {
const { target } = e;
if (target.value === defaultCurrency) {
rate = await DashboardUtils.fetchRate(`${cryptoCode}_${defaultCurrency}`);
if (rate) render(data);
} else {
rate = null;
render(data);
}
});
}
if (document.readyState === "loading") {
window.addEventListener("DOMContentLoaded", addEventListeners);
} else {
addEventListeners();
}
}
};
}

View File

@ -1,13 +1,11 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Configuration;
using BTCPayServer.Data;
using BTCPayServer.Lightning;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Security;
@ -18,11 +16,14 @@ using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using StoreData = BTCPayServer.Data.StoreData;
namespace BTCPayServer.Components.StoreLightningBalance;
public class StoreLightningBalance : ViewComponent
{
private const HistogramType DefaultType = HistogramType.Week;
private readonly StoreRepository _storeRepo;
private readonly CurrencyNameTable _currencies;
private readonly BTCPayServerOptions _btcpayServerOptions;
@ -32,6 +33,7 @@ public class StoreLightningBalance : ViewComponent
private readonly IOptions<ExternalServicesOptions> _externalServiceOptions;
private readonly IAuthorizationService _authorizationService;
private readonly PaymentMethodHandlerDictionary _handlers;
private readonly LightningHistogramService _lnHistogramService;
public StoreLightningBalance(
StoreRepository storeRepo,
@ -42,7 +44,8 @@ public class StoreLightningBalance : ViewComponent
IOptions<LightningNetworkOptions> lightningNetworkOptions,
IOptions<ExternalServicesOptions> externalServiceOptions,
IAuthorizationService authorizationService,
PaymentMethodHandlerDictionary handlers)
PaymentMethodHandlerDictionary handlers,
LightningHistogramService lnHistogramService)
{
_storeRepo = storeRepo;
_currencies = currencies;
@ -53,31 +56,32 @@ public class StoreLightningBalance : ViewComponent
_handlers = handlers;
_lightningClientFactory = lightningClientFactory;
_lightningNetworkOptions = lightningNetworkOptions;
_lnHistogramService = lnHistogramService;
}
public async Task<IViewComponentResult> InvokeAsync(StoreLightningBalanceViewModel vm)
public async Task<IViewComponentResult> InvokeAsync(StoreData store, string cryptoCode, bool initialRendering)
{
if (vm.Store == null)
throw new ArgumentNullException(nameof(vm.Store));
if (vm.CryptoCode == null)
throw new ArgumentNullException(nameof(vm.CryptoCode));
var defaultCurrency = store.GetStoreBlob().DefaultCurrency;
var vm = new StoreLightningBalanceViewModel
{
StoreId = store.Id,
CryptoCode = cryptoCode,
InitialRendering = initialRendering,
DefaultCurrency = defaultCurrency,
CurrencyData = _currencies.GetCurrencyData(defaultCurrency, true),
DataUrl = Url.Action("LightningBalanceDashboard", "UIStores", new { storeId = store.Id, cryptoCode })
};
vm.DefaultCurrency = vm.Store.GetStoreBlob().DefaultCurrency;
vm.CurrencyData = _currencies.GetCurrencyData(vm.DefaultCurrency, true);
if (vm.InitialRendering)
return View(vm);
try
{
var lightningClient = await GetLightningClient(vm.Store, vm.CryptoCode);
if (lightningClient == null)
{
vm.InitialRendering = false;
return View(vm);
}
var lightningClient = await GetLightningClient(store, vm.CryptoCode);
if (vm.InitialRendering)
return View(vm);
var balance = await lightningClient.GetBalance();
// balance
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var balance = await lightningClient.GetBalance(cts.Token);
vm.Balance = balance;
vm.TotalOnchain = balance.OnchainBalance != null
? (balance.OnchainBalance.Confirmed ?? 0L) + (balance.OnchainBalance.Reserved ?? 0L) +
@ -87,8 +91,16 @@ public class StoreLightningBalance : ViewComponent
? (balance.OffchainBalance.Opening ?? 0) + (balance.OffchainBalance.Local ?? 0) +
(balance.OffchainBalance.Closing ?? 0)
: null;
// histogram
var data = await _lnHistogramService.GetHistogram(lightningClient, DefaultType, cts.Token);
if (data != null)
{
vm.Type = data.Type;
vm.Series = data.Series;
vm.Labels = data.Labels;
}
}
catch (Exception ex) when (ex is NotImplementedException or NotSupportedException)
{
// not all implementations support balance fetching
@ -102,7 +114,7 @@ public class StoreLightningBalance : ViewComponent
return View(vm);
}
private async Task<ILightningClient> GetLightningClient(StoreData store, string cryptoCode )
private async Task<ILightningClient> GetLightningClient(StoreData store, string cryptoCode)
{
var network = _networkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
var id = PaymentTypes.LN.GetPaymentMethodId(cryptoCode);

View File

@ -1,4 +1,6 @@
using BTCPayServer.Data;
using System;
using System.Collections.Generic;
using BTCPayServer.Client.Models;
using BTCPayServer.Lightning;
using BTCPayServer.Services.Rates;
using NBitcoin;
@ -7,13 +9,17 @@ namespace BTCPayServer.Components.StoreLightningBalance;
public class StoreLightningBalanceViewModel
{
public string StoreId { get; set; }
public string CryptoCode { get; set; }
public string DefaultCurrency { get; set; }
public CurrencyData CurrencyData { get; set; }
public StoreData Store { get; set; }
public Money TotalOnchain { get; set; }
public LightMoney TotalOffchain { get; set; }
public LightningNodeBalance Balance { get; set; }
public string ProblemDescription { get; set; }
public bool InitialRendering { get; set; }
public bool InitialRendering { get; set; } = true;
public HistogramType Type { get; set; }
public IList<DateTimeOffset> Labels { get; set; }
public IList<decimal> Series { get; set; }
public string DataUrl { get; set; }
}

View File

@ -2,14 +2,14 @@
@if (Model.Services != null && Model.Services.Any())
{
<div id="StoreLightningServices-@Model.Store.Id" class="widget store-lightning-services">
<div id="StoreLightningServices-@Model.StoreId" class="widget store-lightning-services">
<header class="mb-4">
<h6 text-translate="true">Lightning Services</h6>
<a
asp-controller="UIPublicLightningNodeInfo"
asp-action="ShowLightningNodeInfo"app-top-items
asp-route-cryptoCode="@Model.CryptoCode"
asp-route-storeId="@Model.Store.Id"
asp-route-storeId="@Model.StoreId"
target="_blank"
id="PublicNodeInfo"
text-translate="true">

View File

@ -2,15 +2,16 @@ using System;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client;
using BTCPayServer.Configuration;
using BTCPayServer.Data;
using BTCPayServer.Lightning;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services;
using BTCPayServer.Services.Stores;
using BTCPayServer.Security;
using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
@ -20,24 +21,38 @@ public class StoreLightningServices : ViewComponent
{
private readonly BTCPayServerOptions _btcpayServerOptions;
private readonly BTCPayNetworkProvider _networkProvider;
private readonly IAuthorizationService _authorizationService;
private readonly PaymentMethodHandlerDictionary _handlers;
private readonly IOptions<LightningNetworkOptions> _lightningNetworkOptions;
private readonly IOptions<ExternalServicesOptions> _externalServiceOptions;
public StoreLightningServices(
BTCPayNetworkProvider networkProvider,
BTCPayServerOptions btcpayServerOptions,
IAuthorizationService authorizationService,
PaymentMethodHandlerDictionary handlers,
IOptions<LightningNetworkOptions> lightningNetworkOptions,
IOptions<ExternalServicesOptions> externalServiceOptions)
{
_networkProvider = networkProvider;
_btcpayServerOptions = btcpayServerOptions;
_lightningNetworkOptions = lightningNetworkOptions;
_externalServiceOptions = externalServiceOptions;
_authorizationService = authorizationService;
_handlers = handlers;
}
public IViewComponentResult Invoke(StoreLightningServicesViewModel vm)
public async Task<IViewComponentResult> InvokeAsync(StoreData store, string cryptoCode)
{
if (vm.Store == null)
throw new ArgumentNullException(nameof(vm.Store));
if (vm.CryptoCode == null)
throw new ArgumentNullException(nameof(vm.CryptoCode));
var vm = new StoreLightningServicesViewModel { StoreId = store.Id, CryptoCode = cryptoCode };
var id = PaymentTypes.LN.GetPaymentMethodId(cryptoCode);
var existing = store.GetPaymentMethodConfig<LightningPaymentMethodConfig>(id, _handlers);
if (existing?.IsInternalNode is true && _lightningNetworkOptions.Value.InternalLightningByCryptoCode.TryGetValue(cryptoCode, out _))
{
var result = await _authorizationService.AuthorizeAsync(HttpContext.User, null, new PolicyRequirement(Policies.CanUseInternalLightningNode));
vm.LightningNodeType = result.Succeeded ? LightningNodeType.Internal : null;
}
if (vm.LightningNodeType != LightningNodeType.Internal)
return View(vm);
if (!User.IsInRole(Roles.ServerAdmin))

View File

@ -8,8 +8,8 @@ namespace BTCPayServer.Components.StoreLightningServices;
public class StoreLightningServicesViewModel
{
public string StoreId { get; set; }
public string CryptoCode { get; set; }
public StoreData Store { get; set; }
public LightningNodeType LightningNodeType { get; set; }
public LightningNodeType? LightningNodeType { get; set; }
public List<AdditionalServiceViewModel> Services { get; set; }
}

View File

@ -1,7 +1,7 @@
@using BTCPayServer.Client
@model BTCPayServer.Components.StoreNumbers.StoreNumbersViewModel
<div class="widget store-numbers" id="StoreNumbers-@Model.Store.Id">
<div class="widget store-numbers" id="StoreNumbers-@Model.StoreId">
@if (Model.InitialRendering)
{
<div class="loading d-flex justify-content-center p-3">
@ -11,8 +11,8 @@
</div>
<script>
(async () => {
const url = @Safe.Json(Url.Action("StoreNumbers", "UIStores", new { storeId = Model.Store.Id, cryptoCode = Model.CryptoCode }));
const storeId = @Safe.Json(Model.Store.Id);
const url = @Safe.Json(Url.Action("StoreNumbers", "UIStores", new { storeId = Model.StoreId, cryptoCode = Model.CryptoCode }));
const storeId = @Safe.Json(Model.StoreId);
const response = await fetch(url);
if (response.ok) {
document.getElementById(`StoreNumbers-${storeId}`).outerHTML = await response.text();
@ -24,10 +24,10 @@
{
<div class="store-number">
<header>
<h6 text-translate="true">@ViewLocalizer["Paid invoices in the last {0} days", @Model.TimeframeDays]</h6>
<h6 text-translate="true">@ViewLocalizer["Paid invoices in the last {0} days", Model.TimeframeDays]</h6>
@if (Model.PaidInvoices > 0)
{
<a asp-controller="UIInvoice" asp-action="ListInvoices" asp-route-storeId="@Model.Store.Id" permission="@Policies.CanViewInvoices">View All</a>
<a asp-controller="UIInvoice" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" permission="@Policies.CanViewInvoices" text-translate="true">View All</a>
}
</header>
<div class="h3">@Model.PaidInvoices</div>
@ -35,7 +35,7 @@
<div class="store-number">
<header>
<h6 text-translate="true">Payouts Pending</h6>
<a asp-controller="UIStorePullPayments" asp-action="Payouts" asp-route-storeId="@Model.Store.Id" permission="@Policies.CanManagePullPayments" text-translate="true">Manage</a>
<a asp-controller="UIStorePullPayments" asp-action="Payouts" asp-route-storeId="@Model.StoreId" permission="@Policies.CanManagePullPayments" text-translate="true">Manage</a>
</header>
<div class="h3">@Model.PayoutsPending</div>
</div>

View File

@ -1,19 +1,12 @@
using System;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
using BTCPayServer.Components.StoreRecentTransactions;
using BTCPayServer.Data;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
using Dapper;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Npgsql;
using StoreData = BTCPayServer.Data.StoreData;
namespace BTCPayServer.Components.StoreNumbers;
@ -34,14 +27,15 @@ public class StoreNumbers : ViewComponent
_invoiceRepository = invoiceRepository;
}
public async Task<IViewComponentResult> InvokeAsync(StoreNumbersViewModel vm)
public async Task<IViewComponentResult> InvokeAsync(StoreData store, string cryptoCode, bool initialRendering)
{
if (vm.Store == null)
throw new ArgumentNullException(nameof(vm.Store));
if (vm.CryptoCode == null)
throw new ArgumentNullException(nameof(vm.CryptoCode));
vm.WalletId = new WalletId(vm.Store.Id, vm.CryptoCode);
var vm = new StoreNumbersViewModel
{
StoreId = store.Id,
CryptoCode = cryptoCode,
InitialRendering = initialRendering,
WalletId = new WalletId(store.Id, cryptoCode)
};
if (vm.InitialRendering)
return View(vm);
@ -50,12 +44,12 @@ public class StoreNumbers : ViewComponent
var offset = DateTimeOffset.Now.AddDays(-vm.TimeframeDays).ToUniversalTime();
vm.PaidInvoices = await _invoiceRepository.GetInvoiceCount(
new InvoiceQuery { StoreId = new [] { vm.Store.Id }, StartDate = offset, Status = new [] { "paid", "confirmed" } });
new InvoiceQuery { StoreId = [store.Id], StartDate = offset, Status = ["paid", "confirmed"] });
vm.PayoutsPending = await ctx.Payouts
.Where(p => p.PullPaymentData.StoreId == vm.Store.Id && !p.PullPaymentData.Archived && p.State == PayoutState.AwaitingApproval)
.Where(p => p.PullPaymentData.StoreId == store.Id && !p.PullPaymentData.Archived && p.State == PayoutState.AwaitingApproval)
.CountAsync();
vm.RefundsIssued = await ctx.Invoices
.Where(i => i.StoreData.Id == vm.Store.Id && !i.Archived && i.Created >= offset)
.Where(i => i.StoreData.Id == store.Id && !i.Archived && i.Created >= offset)
.SelectMany(i => i.Refunds)
.CountAsync();

View File

@ -1,10 +1,8 @@
using BTCPayServer.Data;
namespace BTCPayServer.Components.StoreNumbers;
public class StoreNumbersViewModel
{
public StoreData Store { get; set; }
public string StoreId { get; set; }
public WalletId WalletId { get; set; }
public int PayoutsPending { get; set; }
public int TimeframeDays { get; set; } = 7;

View File

@ -1,15 +1,14 @@
@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">
<div class="widget store-recent-invoices" id="StoreRecentInvoices-@Model.StoreId">
<header>
<h3 text-translate="true">Recent Invoices</h3>
@if (Model.Invoices.Any())
{
<a asp-controller="UIInvoice" asp-action="ListInvoices" asp-route-storeId="@Model.Store.Id" text-translate="true">View All</a>
<a asp-controller="UIInvoice" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" text-translate="true">View All</a>
}
</header>
@if (Model.InitialRendering)
@ -21,8 +20,8 @@
</div>
<script>
(async () => {
const url = @Safe.Json(Url.Action("RecentInvoices", "UIStores", new { storeId = Model.Store.Id, cryptoCode = Model.CryptoCode }));
const storeId = @Safe.Json(Model.Store.Id);
const url = @Safe.Json(Url.Action("RecentInvoices", "UIStores", new { storeId = Model.StoreId, cryptoCode = Model.CryptoCode }));
const storeId = @Safe.Json(Model.StoreId);
const response = await fetch(url);
if (response.ok) {
document.getElementById(`StoreRecentInvoices-${storeId}`).outerHTML = await response.text();
@ -68,7 +67,7 @@
<p class="text-secondary my-3" text-translate="true">
There are no recent invoices.
</p>
<a asp-controller="UIInvoice" asp-action="CreateInvoice" asp-route-storeId="@Model.Store.Id" class="fw-semibold" text-translate="true">
<a asp-controller="UIInvoice" asp-action="CreateInvoice" asp-route-storeId="@Model.StoreId" class="fw-semibold" text-translate="true">
Create Invoice
</a>
}

View File

@ -1,5 +1,3 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Data;
@ -9,7 +7,6 @@ using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
namespace BTCPayServer.Components.StoreRecentInvoices;
@ -35,12 +32,15 @@ public class StoreRecentInvoices : ViewComponent
_dbContextFactory = dbContextFactory;
}
public async Task<IViewComponentResult> InvokeAsync(StoreRecentInvoicesViewModel vm)
public async Task<IViewComponentResult> InvokeAsync(StoreData store, string cryptoCode, bool initialRendering)
{
if (vm.Store == null)
throw new ArgumentNullException(nameof(vm.Store));
if (vm.CryptoCode == null)
throw new ArgumentNullException(nameof(vm.CryptoCode));
var vm = new StoreRecentInvoicesViewModel
{
StoreId = store.Id,
CryptoCode = cryptoCode,
InitialRendering = initialRendering
};
if (vm.InitialRendering)
return View(vm);
@ -48,7 +48,7 @@ public class StoreRecentInvoices : ViewComponent
var invoiceEntities = await _invoiceRepo.GetInvoices(new InvoiceQuery
{
UserId = userId,
StoreId = new[] { vm.Store.Id },
StoreId = [store.Id],
IncludeArchived = false,
IncludeRefunds = true,
Take = 5

View File

@ -5,7 +5,7 @@ namespace BTCPayServer.Components.StoreRecentInvoices;
public class StoreRecentInvoicesViewModel
{
public StoreData Store { get; set; }
public string StoreId { get; set; }
public IList<StoreRecentInvoiceViewModel> Invoices { get; set; } = new List<StoreRecentInvoiceViewModel>();
public bool InitialRendering { get; set; }
public string CryptoCode { get; set; }

View File

@ -2,7 +2,7 @@
@inject DisplayFormatter DisplayFormatter
@model BTCPayServer.Components.StoreRecentTransactions.StoreRecentTransactionsViewModel
<div class="widget store-recent-transactions" id="StoreRecentTransactions-@Model.Store.Id">
<div class="widget store-recent-transactions" id="StoreRecentTransactions-@Model.StoreId">
<header>
<h3 text-translate="true">Recent Transactions</h3>
@if (Model.Transactions.Any())
@ -19,8 +19,8 @@
</div>
<script>
(async () => {
const url = @Safe.Json(Url.Action("RecentTransactions", "UIStores", new { storeId = Model.Store.Id, cryptoCode = Model.CryptoCode }));
const storeId = @Safe.Json(Model.Store.Id);
const url = @Safe.Json(Url.Action("RecentTransactions", "UIStores", new { storeId = Model.StoreId, cryptoCode = Model.CryptoCode }));
const storeId = @Safe.Json(Model.StoreId);
const response = await fetch(url);
if (response.ok) {
document.getElementById(`StoreRecentTransactions-${storeId}`).outerHTML = await response.text();
@ -36,7 +36,7 @@
<tr>
<th class="w-125px" text-translate="true">Date</th>
<th text-translate="true">Transaction</th>
<th text-translate="true">Labels</th>
<th style="min-width:125px" text-translate="true">Labels</th>
<th class="text-end" text-translate="true">Amount</th>
</tr>
</thead>
@ -44,11 +44,11 @@
@foreach (var tx in Model.Transactions)
{
<tr>
<td>@tx.Timestamp.ToTimeAgo()</td>
<td>
<td class="align-middle">@tx.Timestamp.ToTimeAgo()</td>
<td class="align-middle">
<vc:truncate-center text="@tx.Id" link="@tx.Link" classes="truncate-center-id" />
</td>
<td>
<td class="align-middle">
@if (tx.Labels.Any())
{
<div class="d-flex flex-wrap gap-2 align-items-center">
@ -70,18 +70,9 @@
}
</td>
@if (tx.Positive)
{
<td class="text-end text-success">
<span data-sensitive>@DisplayFormatter.Currency(tx.Balance, tx.Currency)</span>
</td>
}
else
{
<td class="text-end text-danger">
<span data-sensitive>@DisplayFormatter.Currency(tx.Balance, tx.Currency)</span>
</td>
}
<td class="align-middle text-end">
<span data-sensitive class="text-@(tx.Positive ? "success" : "danger")@(tx.IsConfirmed ? "" : " opacity-50")">@DisplayFormatter.Currency(tx.Balance, tx.Currency)</span>
</td>
</tr>
}
</tbody>

View File

@ -1,27 +1,16 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Data;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Labels;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
using Dapper;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using NBitcoin;
using NBXplorer;
using NBXplorer.Client;
using static BTCPayServer.Components.StoreRecentTransactions.StoreRecentTransactionsViewModel;
namespace BTCPayServer.Components.StoreRecentTransactions;
@ -47,26 +36,27 @@ public class StoreRecentTransactions : ViewComponent
_transactionLinkProviders = transactionLinkProviders;
}
public async Task<IViewComponentResult> InvokeAsync(StoreRecentTransactionsViewModel vm)
public async Task<IViewComponentResult> InvokeAsync(StoreData store, string cryptoCode, bool initialRendering)
{
if (vm.Store == null)
throw new ArgumentNullException(nameof(vm.Store));
if (vm.CryptoCode == null)
throw new ArgumentNullException(nameof(vm.CryptoCode));
vm.WalletId = new WalletId(vm.Store.Id, vm.CryptoCode);
var vm = new StoreRecentTransactionsViewModel
{
StoreId = store.Id,
CryptoCode = cryptoCode,
InitialRendering = initialRendering,
WalletId = new WalletId(store.Id, cryptoCode)
};
if (vm.InitialRendering)
return View(vm);
var derivationSettings = vm.Store.GetDerivationSchemeSettings(_handlers, vm.CryptoCode);
var derivationSettings = store.GetDerivationSchemeSettings(_handlers, vm.CryptoCode);
var transactions = new List<StoreRecentTransactionViewModel>();
if (derivationSettings?.AccountDerivation is not null)
{
var pmi = PaymentTypes.CHAIN.GetPaymentMethodId(vm.CryptoCode);
var network = ((IHasNetwork)_handlers[pmi]).Network;
var wallet = _walletProvider.GetWallet(network);
var allTransactions = await wallet.FetchTransactionHistory(derivationSettings.AccountDerivation, 0, 5, TimeSpan.FromDays(31.0), cancellationToken: this.HttpContext.RequestAborted);
var allTransactions = await wallet.FetchTransactionHistory(derivationSettings.AccountDerivation, 0, 5, TimeSpan.FromDays(31.0), cancellationToken: HttpContext.RequestAborted);
var walletTransactionsInfo = await _walletRepository.GetWalletTransactionsInfo(vm.WalletId, allTransactions.Select(t => t.TransactionId.ToString()).ToArray());
transactions = allTransactions

View File

@ -1,11 +1,10 @@
using System.Collections.Generic;
using BTCPayServer.Data;
namespace BTCPayServer.Components.StoreRecentTransactions;
public class StoreRecentTransactionsViewModel
{
public StoreData Store { get; set; }
public string StoreId { get; set; }
public IList<StoreRecentTransactionViewModel> Transactions { get; set; } = new List<StoreRecentTransactionViewModel>();
public WalletId WalletId { get; set; }
public bool InitialRendering { get; set; }

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