Compare commits

..

200 Commits

Author SHA1 Message Date
54dd1df55f Add ability to move a payout from InProgress to AwaitingPayment 2025-01-17 23:06:34 +09:00
6ae36825d5 Changelog 2.0.5 () 2024-12-23 23:07:53 +09:00
f1719ed3d2 Revert "Add All time to the Time filter, PlanB Assignment ()" ()
This reverts commit 79c5ff9ed00407cd08498a348d408e22885967bf.
2024-12-23 18:56:22 +09:00
79c5ff9ed0 Add All time to the Time filter, PlanB Assignment ()
* 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 () 2024-12-23 18:04:07 +09:00
08835895e9 Invoices: Allow admin to see invoices of users ()
* Invoices: Allow admin to see invoices of users

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

* Invoices: Allow admin to see invoices of users

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

* 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 ()
I wasn't able to reproduce the problem, but this most likely fixes  and .
2024-12-23 16:45:49 +09:00
4fb43cbbad Improve wording for setting up NBXplorer connection string () 2024-12-23 09:45:49 +09:00
adce1dffb1 Checkout: Add support link to footer ()
Closes .
2024-12-23 09:45:36 +09:00
0f049eee1b UI: Fix inconsistent responsiveness of labels ()
Fixes .
2024-12-23 09:26:07 +09:00
cfc2b9c236 Pull Payment: Minor improvements ()
- Changes the column title from "Refunded" to "Claimed"
- Adds "Copy Link" button to action column

Closes .
2024-12-23 09:25:27 +09:00
637c06fc01 Greenfield: Improve invoice receipt ()
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 ()
Fixes a regression introduced in : 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 () 2024-12-21 00:16:04 +09:00
44dc6499cd POS: Fix form redirect in conjunction with root path setting ()
* POS: Fix form redirect in conjunction with root path setting

Fixes  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 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 ()
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  and 
2024-12-18 09:41:22 -06:00
fe3bccf3ce Greenfield: Resolve store user's image URL
Amendment to .
2024-12-15 10:02:13 +01:00
7829a93251 POS: Create Invoice action optionally responds with JSON ()
* 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 () 2024-12-12 09:59:10 +09:00
00cc16455c App: Add events which the app subscribes to ()
* 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 () 2024-12-10 23:56:13 +09:00
b797cc9af8 Multisig/watchonly wallet transaction creation flow proof of concept () 2024-12-10 21:56:52 +09:00
cc915df10e Merge pull request from NicolasDorier/validatinglightningclient
Plugins: Add a way for LightningClient to be more customizable (Fix )
2024-12-10 19:01:09 +09:00
fe53424710 Merge pull request from NicolasDorier/globalcheckoutmodelext
Plugin: Add IGlobalCheckoutModelExtension
2024-12-10 18:59:46 +09:00
6cfd4cb633 Merge pull request 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 .

Fixes .
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 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 from dennisreimann/mwb-integration-docker
App: Docker Compose additions
2024-12-09 09:35:14 +09:00
61b882d426 fix: truncate center css for icons ()
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 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 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 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 from NicolasDorier/fix6437
Fix: Combination of status filters on invoices page causes 500 fatal server error (Fix )
2024-12-04 23:39:53 +09:00
a8d1f55544 Editor: Use offcanvas for all breakpoints ()
Avoids scrolling the editor out of the viewport. Closes .

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 ) 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 from NicolasDorier/payreqfix
Fix: Payment Requests should show payments of invalid invoices
2024-12-04 19:18:43 +09:00
e7b98fbae3 Merge pull request 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 from NicolasDorier/updatefido
Update Dependencies
2024-12-04 19:16:24 +09:00
eea9ba10fd Merge pull request 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 () () 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 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 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 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 ()
* 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 )
- 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 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 ()
Fixes 
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 () 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 ()
* 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 () 2024-11-23 10:52:23 +01:00
c6fc0302d2 fix: correct < plugin dependency implementation ()
was wrongly implemented using >=
2024-11-22 10:17:16 +01:00
843c813f86 Replace entity upsert in PlannedTransaction by plain SQL ()
* Replace entity upsert in PlannedTransaction by plain SQL

* Fix flaky test from 
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 ()
* 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 () 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 () 2024-11-14 13:14:36 +09:00
777748d79e UI: Fix escaped HTML tags in UTXO rescan message ()
Fixes .
2024-11-13 21:07:30 +09:00
d0f97e85d2 UI: Allow text break in labels to avoid horizontal scrolling () 2024-11-13 21:05:40 +09:00
2eff7523c3 UI: Fix mising navigation links for store managers () 2024-11-13 21:05:23 +09:00
12d4803c5c Fix: Incorrect calculation for crowdfund and payment request status () 2024-11-13 20:59:52 +09:00
fdbee350b8 Greenfield: Create Pull Request payoutMethods is now optional () 2024-11-13 18:31:55 +09:00
78f47516b8 Fix: Pay button shouldn't throw exception if currency isn't specific () () 2024-11-13 16:25:23 +09:00
088c0c7f85 UI: Do not escape apostrophe in custom server name ()
Fixes .
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 ()
Adjusts the POS buttons to match what we defined for the app in .
2024-11-13 07:50:15 +01:00
984475b9d0 Fix: Pull payment could get stuck in Pending mode () () 2024-11-13 15:30:41 +09:00
894f68f2ae Improve error messages for on-chain related greenfield operations ()
* 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 () () 2024-11-12 12:15:42 +09:00
e80296fa85 UI: Escape translated strings in JS ()
Fixes .
2024-11-12 11:16:56 +09:00
d0779b88e0 Fix: InvoiceCurrencyAmount and Rate columns in reports displays 0.00 () 2024-11-12 11:15:02 +09:00
acd7765159 fix: center qr code () 2024-11-12 10:43:00 +09:00
0a51031334 Docs: Improve invoice payemnt tolerance API docs ()
Fixes .
2024-11-12 09:59:17 +09:00
0f7c0341c5 Fix: Do not allow retry of payouts if they are non interactive (Boltcard) () 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 () 2024-11-09 23:09:31 +09:00
f5d26a1555 Store: Fix missing invitation email when adding new user ()
Fixes , 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 ()
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 ()
completion
2024-11-08 16:13:13 +09:00
a8581510e5 improve UX description ()
* 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 ()
Upload endpoints for app item images. Follow-up to .
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 () 2024-11-07 08:58:47 +09:00
641bdcff31 Histograms: Add Lightning data and API endpoints ()
* 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 ()
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 ()
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 ()
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 ()
* 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 ()
Regression from the translation PRs, in which the proper button ID was replaced. Fixes .
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 ) () 2024-11-04 13:05:10 +09:00
693eceb80f Reolve pull payment timezone () 2024-11-01 08:28:43 +09:00
7d8fc14159 fix: save proof blob if payout is in progress ()
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 () 2024-11-01 08:23:10 +09:00
e3ec07da76 Fix: Crowdfund page was crashing from 2.0.0 () () 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 ()
* 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 ()
* Add keypad icons

Closes .

* Keypad JS fixes
2024-10-29 23:44:37 +09:00
373b90e3b5 Liquid fixes ()
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 ()
* 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 ()
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 ()
* 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 ()
* 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 

* Account pages

* Notifications

* Placeholders

* Various pages and components

* Add more
2024-10-25 22:48:53 +09:00
e5611f9165 Fix tests () 2024-10-25 22:23:27 +09:00
540ad13265 Paging improvements ()
* 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 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 () 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 () 2024-10-20 00:08:28 +09:00
4bf0b79c2a Simple rename 2024-10-19 22:07:20 +09:00
cc0ea0b3f8 Refactor payouts processing () 2024-10-19 21:33:34 +09:00
62d765125d Toggle color fix 2024-10-18 15:14:30 +02:00
b5b45d9a27 Rename Transaction->Translation 2024-10-18 16:06:51 +09:00
8e098710c1 Require non interactivity for boltcard payments () 2024-10-18 14:09:41 +09:00
6dfb369b55 UI: Inactive toggle hover color fix () 2024-10-18 14:08:34 +09:00
817522ff97 refactor(checkout): displayed payment methods vue component ()
The displayed payment methods can change with updates aswell, so it
should be rendered as a vue component instead
2024-10-18 14:05:00 +09:00
8b5b90d247 refactor: make BeforeFetchingRates function public ()
This function is useful when a payment method wants to update its
payment prompt after receiving a partial payment, like ln does
2024-10-18 14:03:37 +09:00
b670097592 feat: provide store info to modify-lnurlp-request filter ()
adds store data to the filter using a new `StoreLNURLPayRequest` class
which simply adds a `Store` member.

closes: https://github.com/btcpayserver/btcpayserver/issues/6301
2024-10-18 14:03:07 +09:00
7b6a115adc [Greenfield] Select default payoutMethodId if none are selected in the Refund route () 2024-10-17 22:54:59 +09:00
77fba4aee3 Add more translations ()
* Newlines

* Dashboard

* Add more translations

* Moar

* Remove &nbsp; from translated texts

* Dictionary controller translations

* Batch 1 of controller updates

* Batch 2 of controller updates

* Component translations

* Batch 3 of controller updates

* Fixes
2024-10-17 22:51:40 +09:00
7e1712c8cd Merge pull request from NicolasDorier/fixmonero
Fix monero payments
2024-10-17 20:40:05 +09:00
b7affb1d34 Fix monero payments 2024-10-17 18:55:00 +09:00
d7fd90c4c3 Bump client lib 2024-10-17 16:41:36 +09:00
1d94782463 Merge pull request from NicolasDorier/fixelements
Fix elements payments
2024-10-16 22:34:32 +09:00
c7a05c3f09 Fix elements payments 2024-10-16 22:34:17 +09:00
2dc58a82b7 Item editor: Do not use only known props for diff ()
Brooke the file seller plugin which adds a property and this did not detect the item change
2024-10-16 14:25:06 +02:00
755dbbab00 fix server nav ui extensin () 2024-10-16 14:24:56 +02:00
b470fe22f1 Remove potential NRE 2024-10-16 17:04:27 +09:00
5b2560ddf7 Merge pull request from NicolasDorier/cleanupetw
Cleanup useless code
2024-10-16 16:38:15 +09:00
be429c527c Cleanup useless code 2024-10-16 16:25:16 +09:00
65fd537200 Merge pull request from btcpayserver/feat/lnd-0.18.3
Bumping LND to 0.18.3-beta
2024-10-15 18:49:41 -05:00
6e43c7f06f Bumping LND to 0.18.3-beta 2024-10-15 18:21:26 -05:00
618 changed files with 13426 additions and 7217 deletions
.circleci
.gitignore
BTCPayServer.Abstractions
BTCPayServer.Client
BTCPayServer.Common
BTCPayServer.Data
BTCPayServer.Rating
BTCPayServer.Tests
BTCPayServer
BTCPayServer.csproj
Blazor
ColorPalette.cs
Components
Configuration
Controllers
BitpayRateController.cs
GreenField
UIAccountController.csUIAppsController.Dashboard.csUIAppsController.csUIBoltcardController.csUIInvoiceController.UI.csUIInvoiceController.csUILNURLAuthController.csUILNURLController.csUIManageController.APIKeys.csUIManageController.Notifications.csUIManageController.csUINotificationsController.csUIPaymentRequestController.csUIPublicController.csUIPullPaymentController.Boltcard.csUIPullPaymentController.csUIReportsController.CheatMode.csUIReportsController.csUIServerController.Plugins.csUIServerController.Roles.csUIServerController.Storage.csUIServerController.Translations.csUIServerController.Users.csUIServerController.csUIStorePullPaymentsController.PullPayments.csUIStoresController.Dashboard.csUIStoresController.Email.csUIStoresController.Integrations.csUIStoresController.LightningLike.csUIStoresController.Onchain.csUIStoresController.Rates.csUIStoresController.Roles.csUIStoresController.Settings.csUIStoresController.Tokens.csUIStoresController.Users.csUIStoresController.csUIUserStoresController.csUIVaultController.csUIWalletsController.PSBT.csUIWalletsController.cs
Data
DerivationSchemeParser.csDerivationSchemeSettings.cs
Events
Extensions.cs
Extensions
Fido2
Filters
Forms
HostedServices
Hosting
Models
PaymentRequest
Payments
PayoutProcessors
Plugins
Program.cs
Properties
ResourceTracker.csRoles.cs
Security
Services
Storage/Services
FileService.cs
Providers/AzureBlobStorage/Configuration
Views
Shared
UIAccount
UIApps
UIError
UIFido2
UIForms
UIHome
UIInvoice
UILNURL
UILNURLAuth
UILightningAutomatedPayoutProcessors
UILightningLikePayout
UIManage
UIMoneroLikeStore
UINotifications
UIOnChainAutomatedPayoutProcessors
UIPaymentRequest
UIPayoutProcessors
UIPublic
UIPublicLightningNodeInfo
UIPullPayment
UIReports
UIServer
UIShopify
UIStorePullPayments
UIStores
UIUserStores
UIWallets
UIZcashLikeStore
wwwroot
checkout
crowdfund
img
js
locales
main
pos
swagger/v1
Build
Changelog.mdDockerfileREADME.md

@ -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:

@ -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

1
.gitignore vendored

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

@ -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" />

@ -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>

@ -10,7 +10,7 @@ public static class SetStatusMessageModelExtensions
{
public static void SetStatusSuccess(this ITempDataDictionary tempData, string statusMessage)
{
tempData.SetStatusMessageModel(new StatusMessageModel()
tempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Success,
Message = statusMessage
@ -34,19 +34,14 @@ public static class SetStatusMessageModelExtensions
tempData.TryGetValue("StatusMessageModel", out var model);
if (successMessage != null || errorMessage != null)
{
var parsedModel = new StatusMessageModel();
parsedModel.Message = (string)successMessage ?? (string)errorMessage;
if (successMessage != null)
var parsedModel = new StatusMessageModel
{
parsedModel.Severity = StatusMessageModel.StatusSeverity.Success;
}
else
{
parsedModel.Severity = StatusMessageModel.StatusSeverity.Error;
}
Message = (string)successMessage ?? (string)errorMessage,
Severity = successMessage != null ? StatusMessageModel.StatusSeverity.Success : StatusMessageModel.StatusSeverity.Error
};
return parsedModel;
}
else if (model != null && model is string str)
if (model is string str)
{
return JObject.Parse(str).ToObject<StatusMessageModel>();
}

@ -0,0 +1,139 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
using BTCPayServer.Lightning;
using NBitcoin;
namespace BTCPayServer.Client.App;
//methods available on the hub in the client
public interface IBTCPayAppHubClient
{
Task NotifyServerEvent(ServerEvent ev);
Task NotifyNetwork(string network);
Task NotifyServerNode(string nodeInfo);
Task TransactionDetected(TransactionDetectedRequest request);
Task NewBlock(string block);
Task StartListen(string key);
Task<LightningInvoice> CreateInvoice(string key, CreateLightningInvoiceRequest createLightningInvoiceRequest);
Task<LightningInvoice?> GetLightningInvoice(string key, uint256 paymentHash);
Task<LightningPayment?> GetLightningPayment(string key, uint256 paymentHash);
Task CancelInvoice(string key, uint256 paymentHash);
Task<List<LightningPayment>> GetLightningPayments(string key, ListPaymentsParams request);
Task<List<LightningInvoice>> GetLightningInvoices(string key, ListInvoicesParams request);
Task<PayResponse> PayInvoice(string key, string bolt11, long? amountMilliSatoshi);
Task MasterUpdated(long? deviceIdentifier);
Task<LightningNodeInformation> GetLightningNodeInfo(string key);
Task<LightningNodeBalance> GetLightningBalance(string key);
}
//methods available on the hub in the server
public interface IBTCPayAppHubServer
{
Task<bool> DeviceMasterSignal(long deviceIdentifier, bool active);
Task<Dictionary<string,string>> Pair(PairRequest request);
Task<AppHandshakeResponse> Handshake(AppHandshake request);
Task<bool> BroadcastTransaction(string tx);
Task<decimal> GetFeeRate(int blockTarget);
Task<BestBlockResponse> GetBestBlock();
Task<TxInfoResponse> FetchTxsAndTheirBlockHeads(string identifier, string[] txIds, string[] outpoints);
Task<string> DeriveScript(string identifier);
Task TrackScripts(string identifier, string[] scripts);
Task<string> UpdatePsbt(string[] identifiers, string psbt);
Task<CoinResponse[]> GetUTXOs(string[] identifiers);
Task<Dictionary<string, TxResp[]>> GetTransactions(string[] identifiers);
Task SendInvoiceUpdate(LightningInvoice lightningInvoice);
Task<long?> GetCurrentMaster();
}
public class ServerEvent
{
public string? Type { get; set; }
public string? StoreId { get; set; }
public string? UserId { get; set; }
public string? AppId { get; set; }
public string? InvoiceId { get; set; }
public string? Detail { get; set; }
}
public record TxResp
{
public TxResp(long confirmations, long? height, decimal balanceChange, DateTimeOffset timestamp, string transactionId)
{
Confirmations = confirmations;
Height = height;
BalanceChange = balanceChange;
Timestamp = timestamp;
TransactionId = transactionId;
}
public long Confirmations { get; set; }
public long? Height { get; set; }
public decimal BalanceChange { get; set; }
public DateTimeOffset Timestamp { get; set; }
public string TransactionId { get; set; }
public override string ToString()
{
return $"{{ Confirmations = {Confirmations}, Height = {Height}, BalanceChange = {BalanceChange}, Timestamp = {Timestamp}, TransactionId = {TransactionId} }}";
}
}
public class TransactionDetectedRequest
{
public string? Identifier { get; set; }
public string? TxId { get; set; }
public string[]? SpentScripts { get; set; }
public string[]? ReceivedScripts { get; set; }
public bool Confirmed { get; set; }
}
public class CoinResponse
{
public string? Identifier{ get; set; }
public bool Confirmed { get; set; }
public string? Script { get; set; }
public string? Outpoint { get; set; }
public decimal Value { get; set; }
public string? Path { get; set; }
}
public class TxInfoResponse
{
public Dictionary<string,TransactionResponse>? Txs { get; set; }
public Dictionary<string,string>? BlockHeaders { get; set; }
public Dictionary<string,int>? BlockHeghts { get; set; }
}
public class TransactionResponse
{
public string? BlockHash { get; set; }
public string? Transaction { get; set; }
}
public class BestBlockResponse
{
public string? BlockHash { get; set; }
public int BlockHeight { get; set; }
public string? BlockHeader { get; set; }
}
public class AppHandshake
{
public string[]? Identifiers { get; set; }
}
public class AppHandshakeResponse
{
//response about identifiers being tracked successfully
public string[]? IdentifiersAcknowledged { get; set; }
}
public class PairRequest
{
public Dictionary<string, string?> Derivations { get; set; } = new();
}

@ -0,0 +1,8 @@
#nullable enable
namespace BTCPayServer.Client.App.Models;
public class AcceptInviteRequest
{
public string? UserId { get; set; }
public string? Code { get; set; }
}

@ -0,0 +1,10 @@
#nullable enable
namespace BTCPayServer.Client.App.Models;
public class AcceptInviteResult
{
public string? Email { get; set; }
public bool? RequiresUserApproval { get; set; }
public bool? EmailHasBeenConfirmed { get; set; }
public string? PasswordSetCode { get; set; }
}

@ -0,0 +1,10 @@
using System;
namespace BTCPayServer.Client.App.Models;
public class AccessTokenResult
{
public string AccessToken { get; set; }
public string RefreshToken { get; set; }
public DateTimeOffset Expiry { get; set; }
}

@ -0,0 +1,13 @@
#nullable enable
namespace BTCPayServer.Client.App.Models;
public class AppInstanceInfo
{
public string BaseUrl { get; set; } = null!;
public string ServerName { get; set; } = null!;
public string? ContactUrl { get; set; }
public string? LogoUrl { get; set; }
public string? CustomThemeCssUrl { get; set; }
public string? CustomThemeExtension { get; set; }
public bool RegistrationEnabled { get; set; }
}

@ -0,0 +1,44 @@
#nullable enable
using System.Collections.Generic;
namespace BTCPayServer.Client.App.Models;
public class AppUserInfo
{
public string? UserId { get; set; }
public string? Email { get; set; }
public string? Name { get; set; }
public string? ImageUrl { get; set; }
public IEnumerable<string>? Roles { get; set; }
public IEnumerable<AppUserStoreInfo>? Stores { get; set; }
public void SetInfo(string email, string? name, string? imageUrl)
{
Email = email;
Name = name;
ImageUrl = imageUrl;
}
public static bool Equals(AppUserInfo? x, AppUserInfo? y)
{
if (ReferenceEquals(x, y)) return true;
if (ReferenceEquals(x, null)) return false;
if (ReferenceEquals(y, null)) return false;
if (x.GetType() != y.GetType()) return false;
return x.UserId == y.UserId && x.Email == y.Email &&
x.Name == y.Name && x.ImageUrl == y.ImageUrl &&
Equals(x.Roles, y.Roles) && Equals(x.Stores, y.Stores);
}
}
public class AppUserStoreInfo
{
public string Id { get; set; } = null!;
public string? Name { get; set; }
public string? LogoUrl { get; set; }
public string? RoleId { get; set; }
public string? PosAppId { get; set; }
public string? DefaultCurrency { get; set; }
public bool Archived { get; set; }
public IEnumerable<string>? Permissions { get; set; }
}

@ -0,0 +1,11 @@
#nullable enable
using System.Collections.Generic;
namespace BTCPayServer.Client.App.Models;
public class CreateStoreData
{
public string? DefaultCurrency { get; set; }
public string? RecommendedExchangeId { get; set; }
public Dictionary<string, string>? Exchanges { get; set; }
}

@ -16,7 +16,7 @@
<Platforms>AnyCPU</Platforms>
</PropertyGroup>
<PropertyGroup>
<Version Condition=" '$(Version)' == '' ">1.7.4</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.46" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>

@ -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);
}
}

@ -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)
{

@ -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)
{

@ -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)
{

@ -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);
}
}

@ -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);

@ -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; }
}

@ -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; }
}

@ -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; }
}

@ -37,7 +37,7 @@ public abstract class CrowdfundBaseData : AppBaseData
public class CrowdfundAppData : CrowdfundBaseData
{
public object? Perks { get; set; }
public AppItem[]? Perks { get; set; }
}
public class CrowdfundAppRequest : CrowdfundBaseData, IAppRequest

@ -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; }
}

@ -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; }

@ -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);
}
}

@ -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;
@ -31,7 +29,7 @@ public abstract class PointOfSaleBaseData : AppBaseData
public class PointOfSaleAppData : PointOfSaleBaseData
{
public object? Items { get; set; }
public AppItem[]? Items { get; set; }
}
public class PointOfSaleAppRequest : PointOfSaleBaseData, IAppRequest

@ -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

@ -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

@ -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;
}
}
}

@ -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;

@ -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
{

@ -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.6" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="2.0.1" />
</ItemGroup>
<ItemGroup>
<Folder Include="Altcoins\" />
</ItemGroup>
</Project>

@ -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();
}
}
}

@ -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;

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

@ -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);
}
}
}

@ -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" />

@ -92,7 +92,7 @@ namespace BTCPayServer.Data
blob.Remove("output");
blob.Remove("outpoint");
// Convert from sats to btc
if (cryptoData["value"] is not (null or { Type: JTokenType.Null }))
if (cryptoData["value"] is not (null or { Type: JTokenType.Null } or { Type: JTokenType.Object }))
{
var v = cryptoData["value"].Value<long>();
Amount = (decimal)v / (decimal)Money.COIN;
@ -103,7 +103,22 @@ namespace BTCPayServer.Data
blob.ConvertNumberToString("paymentMethodFee");
blob.Remove("networkFee");
blob.RemoveIfNull("paymentMethodFee");
}
}
// Liquid
else if (cryptoData["value"] is { Type: JTokenType.Object })
{
var v = cryptoData["value"]["value"].Value<long>();
var assetId = cryptoData["value"]["assetId"].Value<string>();
divisibility = GetDivisibility(assetId) ?? 8;
Amount = (decimal)v / (decimal)Math.Pow(10.0, divisibility);
cryptoData.Remove("value");
cryptoData["assetId"] = assetId;
blob["paymentMethodFee"] = blob["networkFee"];
blob.RemoveIfValue<decimal>("paymentMethodFee", 0.0m);
blob.ConvertNumberToString("paymentMethodFee");
blob.Remove("networkFee");
blob.RemoveIfNull("paymentMethodFee");
}
// Convert from millisats to btc
else if (cryptoData["amount"] is not (null or { Type: JTokenType.Null }))
{
@ -164,6 +179,17 @@ namespace BTCPayServer.Data
#pragma warning restore CS0618 // Type or member is obsolete
return true;
}
private int? GetDivisibility(string assetId) =>
assetId switch
{
"ce091c998b83c78bb71a632313ba3760f1763d9cfcffae02258ffa9865a37bd2" => 8,
"aa775044c32a7df391902b3659f46dfe004ccb2644ce2ddc7dba31e889391caf" => 2,
"0e99c1a6da379d1f4151fb9df90449d40d0608f6cb33a5bcbfc8c265f42bab0a" => 8,
"6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d" => 8,
_ => null,
};
[NotMapped]
public bool Migrated { get; set; }
[NotMapped]

@ -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");

@ -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; }
}

@ -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)
{

@ -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");
}
}
}

@ -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");

@ -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.46" />
<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>

@ -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)) };
}
}
}

@ -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)) };
}
}
}

@ -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>());
}
}
}

@ -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

@ -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="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>

@ -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
{

@ -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

@ -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");

@ -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/*

@ -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]

@ -17,6 +17,7 @@ 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.Services;
@ -317,6 +318,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 +759,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 +999,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 +1051,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 +1421,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);
@ -2980,9 +3009,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 +3030,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 +3063,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 +3092,7 @@ namespace BTCPayServer.Tests
new CreateInvoiceRequest
{
Currency = "USD",
Amount = 100,
Amount = 0.1m,
Checkout = new CreateInvoiceRequest.CheckoutOptions
{
PaymentMethods = new[] { "BTC-LN" },
@ -3536,8 +3582,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 +3628,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 +3830,6 @@ namespace BTCPayServer.Tests
await tester.WaitForEvent<NewBlockEvent>(async () =>
{
await tester.ExplorerNode.GenerateAsync(1);
}, bevent => bevent.PaymentMethodId == PaymentTypes.CHAIN.GetPaymentMethodId("BTC"));
@ -3812,7 +3867,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 +3879,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 +3985,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 +4004,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 +4037,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 +4065,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 }));
@ -4120,6 +4195,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 +4212,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 +4233,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 +4257,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 +4291,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 +4334,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 +4432,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 +4476,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 +4490,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 +4556,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 +4600,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 +4633,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 +4710,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()
{

@ -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)]

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

@ -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]

@ -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);

@ -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);
}

@ -95,7 +95,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();
@ -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();
@ -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,19 +1874,13 @@ 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
@ -2181,6 +2175,56 @@ 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.TakeScreenshot().SaveAsFile("C:\\Users\\NicolasDorier\\AppData\\Local\\Temp\\1109191644\\1.png");
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 +2953,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 +3837,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(""));

@ -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;

@ -727,5 +727,39 @@
},
"version": 2
}
},
{
"type": "payment",
"input": {
"output": null,
"version": 1,
"outpoint": null,
"accounted": true,
"cryptoCode": "XMR",
"networkFee": 0.0000000019,
"receivedTimeMs": 1705500405468,
"cryptoPaymentData": "{\"Amount\":62700000000,\"Address\":\"85CjjvQyW7PjNmiFRKZuEHKzZjiB3rjSu6n8zPzji4PtQxw1CyEY5H5FBge6GRUMJqR7FqsgBHU7H1FpEppvZXS6HGpFF6t\",\"SubaddressIndex\":23,\"SubaccountIndex\":0,\"BlockHeight\":3063946,\"ConfirmationCount\":10,\"TransactionId\":\"cc2e9ef03864c6af5e0d6f1c730ba142144f6588c50035b2996a59a6f3771b06\",\"LockTime\":0}",
"cryptoPaymentDataType": "MoneroLike"
},
"expected": {
"divisibility": 12,
"destination": "85CjjvQyW7PjNmiFRKZuEHKzZjiB3rjSu6n8zPzji4PtQxw1CyEY5H5FBge6GRUMJqR7FqsgBHU7H1FpEppvZXS6HGpFF6t",
"details": {
"blockHeight": 3063946,
"confirmationCount": 10,
"lockTime": 0,
"subaccountIndex": 0,
"subaddressIndex": 23,
"transactionId": "cc2e9ef03864c6af5e0d6f1c730ba142144f6588c50035b2996a59a6f3771b06"
},
"version": 2
},
"expectedProperties": {
"Amount": "0.0627",
"PaymentMethodId": "XMR-CHAIN",
"Currency": "XMR",
"Status": "Settled",
"Accounted": null
}
}
]

@ -1,5 +1,5 @@
Id,Blob,Created,ExceptionStatus,Status,StoreDataId,Archived,Blob2
Q7RqoHLngK9svM4MgRyi9y,\x1f8b0800000000000003c454cb76a24010dde72b7258cf180d08929d0a89899ae323ea4962163c0ae808ddd8344426c72f9bc57cd2fcc2340d31ea38b398cd2cfb5657d5add7fdf9fdc7fbd9f9b9845ce9ea5c1a6b9335e90db0dfd7936ca80cfd498ef45cfa52fc4818a1702bbec9893feb76d9ace3abd3d6e06e456dd76b2fece46eb8eee4d7032f9bae5f6fd44dbfb330ddd2995017a870c6691896f16200774442e4e41cae0b8c5a0cf8436dea6a4d56344d5135596e2ac286704690030f282abe349a724bd6e5a67c298cb089117746041fd815a5b2bb109304b1b6eb524812514347ef1bb2a6cd0686663e7a8b4779dcf3bd45eb5ae9bcb181e10f50c93ca6c44d1d768b3d422391817b172d2b2831880c489c229e0d4085478577890b7be51691823c418e1572d4b3c2043e60caabe29856ab578893520a58b4459a4d0d89a35bc1c54e73dec5534c84e5de8a8e520ad88c2c149ec0bb24c58ce6272c4f283e814e59399ddfe220762a48d5ebcb3f9b1a274ca380e08f24bbbaf9ec0c8b59fbdbc3d70965a20953566c8d2fbab589535b35dab61f78e9a4bb50c76b2af7bda9f18a56caca9ca1acbdf202e7a6375ccd4d50eca775539e8fbf69fa4a514c326e0e8d7870d7efb747fd66369faba38d366b4c641dc6c8c67660240b3d35c78dbe1be85dc38efcc9d7e7f832095ea4d38c1088457b5f4a9d87ee52ba5afe277a4b69fb71c1164b05270c6f5275370ec425e7cab6eb706ce511605660cf2fe575829762d7b243d85fe10a1e1e2e19475d44c161b3c9a0c818301627571717919530a0981f0767b342d8af39242ab9b0cd3588d3ad9762e0f150f784218f1f4d41b160c2685a26c57b8632c5a7b000cd80ce68789817610cace642446a36737875e5bf1aa17e99dfa179cc48b568d55df1c9ed1e7fd7a7f296cb9e0d8105a410bbf7edcee4014c4aefc60e3baa5860ffa454c279bbbb978860c4d59a77d7dce9e24e135bf54a130354487aa14855323898bf95f18916c3aeac3d2b090e7fc0860176c13d1ed2e76a40566dd0f1563d9010a88585f0356af5b3ed2f000000ffff030035140a5d88060000,2018-10-01 11:32:12+00,,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
Q7RqoHLngK9svM4MgRyi9y,\x1f8b080000000000000ac454cb76da400cfd97595302b1c1981d6012124c0e8f104e12b2f043b627d833663c767073f2655df493fa0bd50049714fbae9262b1f4ba3ab2be94abf7efc7c25d4275d3233e65b3eb2593836b362a24fc27949cd92d44826b9802bf544cbc2e5602097fdb0bde8d8d71be1fa416fe566d7936dbfbcb08362b17dbe6cefc6fdd5d0c7402e7c102a90e5718c3829803fe531f54ad26dd4887024906ebb65b6eb9a6e187adbd0b4965e2394159c7a704b1374375b5a4733b596765e23b04b290651ce4e7dbaae7c3ea43ca3b2e7fb02b24c71ed9b634b338ca56d19c3fb6075afcd4661b0ea5ce8fd17695ba14d91612ab89f7bf28a055c247b64d2c57648482cc83c44710104be539601f7e1bd94342a33ea3931e9064e9c810242c6a46bd4b12e2f1702982a922c171679ab11372fb111d5247bdb8d937c60ee0dc3c4a171c532e03993a2acd81e685af95fc87d2b4fa3a8ac861c1b73fe99b159314e23ce8e60481dbb6a39d2797f8103a5c5be868554f30b5595bb3477db56cf0da3209f0f56edd95668e360613dd38dbe192e69d1db049177399a6cee86a0bb0fdb967637fb6e981b5d1ff2596b62a5f6f578dc9b8e5bc5dd5d7bba3396cdb966c28cbacc8dac6c65e6c35973ec47e6c0729370feed313dcfa227f2091b0a6af4af6bd2bf1dac4977fd45d4d6e46dbf378ecc151f062ff80b3b0fd203d783fd2825a74c8049fc7f7cc29d802067bee3c6f021ad836972a20b013e15e0c9e5dc46f448ca34eb9e9d254e264130542926de5016d63d9e605eb9bb00b52c0d946680f1375cd200c5aba8605629724cc24e8c7bd8e3ca8228402c455cc943190359f721e175577a58c0e1599d8b10f379a24c253f88e6550d427dfeb5ebc7ea2720238e87e215151700ccf7af9b55eeffb3e108677f3103c44b38a378437124c38f0bf67ebd3a0d75bd22aa8eacba284770a5e3c3089c0227af0471f48c9c2cfae3859d04e683ffd7508fd281e2a0ac8ad26e790cc261ea5c35eb8db7df000000ffff0300d1083b8e01060000,2018-10-01 11:32:12+00,,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
Ka6GHBrFJPRwFRga1RD6Yz,\x1f8b0800000000000003c454cd769a4014dee7297258b74682a066a7427e24e6284a729a9fc5001798083364188826c727eba28fd457e830101353db45375dcef7cdbdf7bbbf3fbfff783d383c5470a09c1c2a3632cece87ec743c759e4f9d08a98e697c7b51be543f724e195cc86f5a1eb9a31177879131ef5d8e97cc0bc2c18d978f274fc3f5e96558ce9f1ecf8c953dbcb182da98b2009834264592d4fe3280604a13ecaf05dc9618431cc44337ba46abad6aed76db50b5635d72989414fbb0c069f545d5b59ed6eff4f4da10561916c698921d5eef367c0019cd311f0401833c973998b7a5e742ef7110a7fd65e9f62889c7b6f5726b777d3fc25c9fd5ca334683c2e71724a42c9511847555b24a1287d484dcaffc9d310072b80024cd1a724403f89073e52e5ee7d84789404394e4f00633915a558656fbb881fc823120b2388ae53a8a4037529157ac452df7e991cc154a3fc594b095229cecc147b4209cadf730b738db83ce79dda3dffc60becf4953f1e33f53ea1e6a1a53f216649bb7e8a08938fa384362a870298b30e7d5ec44b25aabacf00c73e045715838a31b63f6c4343b9c9b8f78d9595a2e2e07cb30f6cfce27cb6b0b3adeed93ae5dcf5ebafd65a763d1993e31b3cbb16d0fa6b65e5e5f1bd355d7551dad0f33ec112f36f39b7e61cd543b88fb23d34b23e7eb5d769cc70fca7e4518e4b8bdde2bc3c5e85e39b9ff4ff2ee95cddb1e235e484d049e95667b7cc86acd0db7ad7086d629105e61770ff58e4258900079097c9ce1069eec0e994003ccc0e7ae7359458c39cff293a3a314e51c1811db21d42c31895a3e4d6b2d7c750a7281dbf5e686c2d515e538145b5349ac947056d441c907a20ef17e5e8095c05c96ecc6c584006f0590d296c77d915dfdaf455954c7f7d93ae3b419b466af44e7b68fbf5fa97a99eb9a4d80c7b43a79af9b2d150238b5b5bac53e652cb17fba57d278b3dd9794122c6eb6a8aeb5bd8edbcbd8d79acb18e3eab05727a909063bfd47a5e868d5ec863d4779bcfb03561c4800c1e726bd8f0694cd047d9eaa054d8021222f9fda6a1f6c7e010000ffff0300ddecc9e08e060000,2018-10-01 11:54:10+00,,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
Q3kZ3F8cUD57WUqcc8QLs2,\x1f8b0800000000000003c4544972da4014ddfb1494d6099110a377806463a662b0c085f142c3176a23758b564b86b83859163952ae90564b96812259649365bff77ffff9fdfaf1f3fda654929023dd96a4a9ba5da9774ddbd06a8da5b1b3ede6741855a42fa945c408850761a6461ba3db654667539f3787fd2db51cb7bdb4a2fe68d739dc0ddd64be7bbdafef079da5ee64ce843a4085338e7d3ffb2f047026c447f681c3b2c0a8c9803f6af546bd2c2baa2ccb7545add404877042900d8f28484d949ada545bd566a32248d887883b2382cff85a23e71d08498458db71284491a861921876af3f1ec361bc5bb4d427dadb57bcfba145f4fd66fe36795a6599879438b1cd1eb04b68202270efb465694a0c020d223bfd6f64460c28260e94e6ccdc22bc11feb95597e327c5a7ff7a8708d9a6cf51d7f423f88029af916395b29c23764c2960d12449376612478f22332b3ef09e5ecb4b306333b80829603d30917f05ef9218337ab8c2ac507805e545b26bff7711bbf649def9ca9f29e50a35f108fe0852d4cd27a999cc3cdd25be5c28114d98b3748736a25bfb30b6ea5adbda786e3ceb2eebd31d5507ee5c7b45dbea563750d2deba9e7ddf1b6d173a54add5aea62ea6df1bad6db5aa93696da485c3fe60d09e0c6ac962519fec1b8632535b304516b63c2d5ab6627daa0c1cafd5d5ac6033fbfa1c5622ef45ba9e1102b176ef6ba9f3d85d4bb7ebff94de5a3a7edcb3c9629113863729bf221bc22ce79c2b3a1c9a8700304bb1e797ec56c18db1635a3e9cae700e8fce978ca30ea2603363364c237a8c85d1edb76f417134517633659b04592e6c7f07e290e54c1a5cfed59830e4f2a349534c336134ce82e213220bf129334013a006f5cfe3228c81951d0848d96236af2eb32b139addad64d343c848be68f95df1c9158fbfab5576cb59cf46c03c924adffbb1a05c8059e6ad14d845c502fb27dd12cec7e25e028211d76ede5dbd50c942215b6aae901e4a053e55a43c189ccddf4cf844d361e76ccf8cbc730bd833c00e389743fa5c0d48f20dbadcaa47e20335b1103ea52cdf1c7f030000ffff03003e6b8efb96060000,2018-10-01 11:54:32+00,,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
FSktP1Nrxu7arh7TUFAgTZ,\x1f8b0800000000000003c4554b72e33610ddcf295c5c27b23e16297a1599923c63591efd55f1781620d1143124011a0035545c3a59163952ae10008415d9a54caab2c992af5f773ff48f7ffefec7cb878b0b8760e7fac2192d52396d3df0aaf4104fbce56ad4df2e1f9d9f344348c6e193a175c47615047275b37517bdfbbb948738ee6f42713779bed98feee3dde2f9dbad5b8d6f36435c3b338e81d7ce41a922e59f2d50872e00f0946524da2b46d3601c49501f5dd76b377a6dbfedf7bc96e7f9c646e88e91089624d79456b7d3ebf8dd66a76b8c501544391346dfda7d6bc7503041641f630e4298e70c969bd093de7886d63de1a2ea7b67ddc28fb76595dcfdba1db9eeb296597086cb487ea231e3b9c9a0bc75f5b42409f90044a4e34d9090c029c370b1902825746bfc2d2b50b862d132cb2c5a247b41229429344699805798ab376afdcd66a369b1a8e41ca82993335ccd1d851e8cb6b0dcab7a9e53662c0f287f97d4c0c31c119d56c5d54d01fe0b54282f3268442c774e99012ba9e4fb33311e497106550f97e73206449e0b62bbd1fe6753eb8c699a30fa9ae45809d5dd0192e884ae5acec9ce946521f55c6d4dfdaaa20cdd413fdc2671390f36eeec9977c6f162f08da457e9704576fd344ea2db8f93743d84abf0f1b9db59cf7ef3fcf4ea6ac866ddc9a0b8bf1b8ffbd37177b75ebbd3ca5bb5e61d1f6624a46132101bbf1cce5a639cf8c120ccb7f39fbf146d917c75ce2b226046f1e5c9b959064fcef5d3ff24efc939bcae3b92a5d144e1bb63372b82a2d66c6dc70a17689f03951afbf2b5de5f884b8a5198c1e9585b78f26f63a778987088e46a7eaf89899485b8bebc3ce15dca04d154ec59597bc86a04765dcc77acb43d304962b55a5ab4d6267959cba027861fa4504b9985284a85ad09f01df015cf4ef96a852805d9c090b3462823558a9ad760bc5e7c27e2fb42323b95762d559b8f1f3f3e77f531a80b3c0199307d465f0e47530c30afbd5b47ec5d310cf69f0e9f713e1c972b6794a8ff803a69c3e3993d9e58bf6b4f6c42f4cf429f349b0cde0c0bdaa9f6ebc9b0d68f48246f195049a018f0fbfefd3d47b0b3e3f67e04972c038e687d391bcd0f87bf000000ffff030075db901fe2060000,2018-10-01 11:57:15+00,,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,

1 Id Blob Created ExceptionStatus Status StoreDataId Archived Blob2
2 Q7RqoHLngK9svM4MgRyi9y \x1f8b0800000000000003c454cb76a24010dde72b7258cf180d08929d0a89899ae323ea4962163c0ae808ddd8344426c72f9bc57cd2fcc2340d31ea38b398cd2cfb5657d5add7fdf9fdc7fbd9f9b9845ce9ea5c1a6b9335e90db0dfd7936ca80cfd498ef45cfa52fc4818a1702bbec9893feb76d9ace3abd3d6e06e456dd76b2fece46eb8eee4d7032f9bae5f6fd44dbfb330ddd2995017a870c6691896f16200774442e4e41cae0b8c5a0cf8436dea6a4d56344d5135596e2ac286704690030f282abe349a724bd6e5a67c298cb089117746041fd815a5b2bb109304b1b6eb524812514347ef1bb2a6cd0686663e7a8b4779dcf3bd45eb5ae9bcb181e10f50c93ca6c44d1d768b3d422391817b172d2b2831880c489c229e0d4085478577890b7be51691823c418e1572d4b3c2043e60caabe29856ab578893520a58b4459a4d0d89a35bc1c54e73dec5534c84e5de8a8e520ad88c2c149ec0bb24c58ce6272c4f283e814e59399ddfe220762a48d5ebcb3f9b1a274ca380e08f24bbbaf9ec0c8b59fbdbc3d70965a20953566c8d2fbab589535b35dab61f78e9a4bb50c76b2af7bda9f18a56caca9ca1acbdf202e7a6375ccd4d50eca775539e8fbf69fa4a514c326e0e8d7870d7efb747fd66369faba38d366b4c641dc6c8c67660240b3d35c78dbe1be85dc38efcc9d7e7f832095ea4d38c1088457b5f4a9d87ee52ba5afe277a4b69fb71c1164b05270c6f5275370ec425e7cab6eb706ce511605660cf2fe575829762d7b243d85fe10a1e1e2e19475d44c161b3c9a0c818301627571717919530a0981f0767b342d8af39242ab9b0cd3588d3ad9762e0f150f784218f1f4d41b160c2685a26c57b8632c5a7b000cd80ce68789817610cace642446a36737875e5bf1aa17e99dfa179cc48b568d55df1c9ed1e7fd7a7f296cb9e0d8105a410bbf7edcee4014c4aefc60e3baa5860ffa454c279bbbb978860c4d59a77d7dce9e24e135bf54a130354487aa14855323898bf95f18916c3aeac3d2b090e7fc0860176c13d1ed2e76a40566dd0f1563d9010a88585f0356af5b3ed2f000000ffff030035140a5d88060000 \x1f8b080000000000000ac454cb76da400cfd97595302b1c1981d6012124c0e8f104e12b2f043b627d833663c767073f2655df493fa0bd50049714fbae9262b1f4ba3ab2be94abf7efc7c25d4275d3233e65b3eb2593836b362a24fc27949cd92d44826b9802bf544cbc2e5602097fdb0bde8d8d71be1fa416fe566d7936dbfbcb08362b17dbe6cefc6fdd5d0c7402e7c102a90e5718c3829803fe531f54ad26dd4887024906ebb65b6eb9a6e187adbd0b4965e2394159c7a704b1374375b5a4733b596765e23b04b290651ce4e7dbaae7c3ea43ca3b2e7fb02b24c71ed9b634b338ca56d19c3fb6075afcd4661b0ea5ce8fd17695ba14d91612ab89f7bf28a055c247b64d2c57648482cc83c44710104be539601f7e1bd94342a33ea3931e9064e9c810242c6a46bd4b12e2f1702982a922c171679ab11372fb111d5247bdb8d937c60ee0dc3c4a171c532e03993a2acd81e685af95fc87d2b4fa3a8ac861c1b73fe99b159314e23ce8e60481dbb6a39d2797f8103a5c5be868554f30b5595bb3477db56cf0da3209f0f56edd95668e360613dd38dbe192e69d1db049177399a6cee86a0bb0fdb967637fb6e981b5d1ff2596b62a5f6f578dc9b8e5bc5dd5d7bba3396cdb966c28cbacc8dac6c65e6c35973ec47e6c0729370feed313dcfa227f2091b0a6af4af6bd2bf1dac4977fd45d4d6e46dbf378ecc151f062ff80b3b0fd203d783fd2825a74c8049fc7f7cc29d802067bee3c6f021ad836972a20b013e15e0c9e5dc46f448ca34eb9e9d254e264130542926de5016d63d9e605eb9bb00b52c0d946680f1375cd200c5aba8605629724cc24e8c7bd8e3ca8228402c455cc943190359f721e175577a58c0e1599d8b10f379a24c253f88e6550d427dfeb5ebc7ea2720238e87e215151700ccf7af9b55eeffb3e108677f3103c44b38a378437124c38f0bf67ebd3a0d75bd22aa8eacba284770a5e3c3089c0227af0471f48c9c2cfae3859d04e683ffd7508fd281e2a0ac8ad26e790cc261ea5c35eb8db7df000000ffff0300d1083b8e01060000 2018-10-01 11:32:12+00 expired 3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd f
3 Ka6GHBrFJPRwFRga1RD6Yz \x1f8b0800000000000003c454cd769a4014dee7297258b74682a066a7427e24e6284a729a9fc5001798083364188826c727eba28fd457e830101353db45375dcef7cdbdf7bbbf3fbfff783d383c5470a09c1c2a3632cece87ec743c759e4f9d08a98e697c7b51be543f724e195cc86f5a1eb9a31177879131ef5d8e97cc0bc2c18d978f274fc3f5e96558ce9f1ecf8c953dbcb182da98b2009834264592d4fe3280604a13ecaf05dc9618431cc44337ba46abad6aed76db50b5635d72989414fbb0c069f545d5b59ed6eff4f4da10561916c698921d5eef367c0019cd311f0401833c973998b7a5e742ef7110a7fd65e9f62889c7b6f5726b777d3fc25c9fd5ca334683c2e71724a42c9511847555b24a1287d484dcaffc9d310072b80024cd1a724403f89073e52e5ee7d84789404394e4f00633915a558656fbb881fc823120b2388ae53a8a4037529157ac452df7e991cc154a3fc594b095229cecc147b4209cadf730b738db83ce79dda3dffc60becf4953f1e33f53ea1e6a1a53f216649bb7e8a08938fa384362a870298b30e7d5ec44b25aabacf00c73e045715838a31b63f6c4343b9c9b8f78d9595a2e2e07cb30f6cfce27cb6b0b3adeed93ae5dcf5ebafd65a763d1993e31b3cbb16d0fa6b65e5e5f1bd355d7551dad0f33ec112f36f39b7e61cd543b88fb23d34b23e7eb5d769cc70fca7e4518e4b8bdde2bc3c5e85e39b9ff4ff2ee95cddb1e235e484d049e95667b7cc86acd0db7ad7086d629105e61770ff58e4258900079097c9ce1069eec0e994003ccc0e7ae7359458c39cff293a3a314e51c1811db21d42c31895a3e4d6b2d7c750a7281dbf5e686c2d515e538145b5349ac947056d441c907a20ef17e5e8095c05c96ecc6c584006f0590d296c77d915dfdaf455954c7f7d93ae3b419b466af44e7b68fbf5fa97a99eb9a4d80c7b43a79af9b2d150238b5b5bac53e652cb17fba57d278b3dd9794122c6eb6a8aeb5bd8edbcbd8d79acb18e3eab05727a909063bfd47a5e868d5ec863d4779bcfb03561c4800c1e726bd8f0694cd047d9eaa054d8021222f9fda6a1f6c7e010000ffff0300ddecc9e08e060000 2018-10-01 11:54:10+00 expired 3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd f
4 Q3kZ3F8cUD57WUqcc8QLs2 \x1f8b0800000000000003c4544972da4014ddfb1494d6099110a377806463a662b0c085f142c3176a23758b564b86b83859163952ae90564b96812259649365bff77ffff9fdfaf1f3fda654929023dd96a4a9ba5da9774ddbd06a8da5b1b3ede6741855a42fa945c408850761a6461ba3db654667539f3787fd2db51cb7bdb4a2fe68d739dc0ddd64be7bbdafef079da5ee64ce843a4085338e7d3ffb2f047026c447f681c3b2c0a8c9803f6af546bd2c2baa2ccb7545add404877042900d8f28484d949ada545bd566a32248d887883b2382cff85a23e71d08498458db71284491a861921876af3f1ec361bc5bb4d427dadb57bcfba145f4fd66fe36795a6599879438b1cd1eb04b68202270efb465694a0c020d223bfd6f64460c28260e94e6ccdc22bc11feb95597e327c5a7ff7a8708d9a6cf51d7f423f88029af916395b29c23764c2960d12449376612478f22332b3ef09e5ecb4b306333b80829603d30917f05ef9218337ab8c2ac507805e545b26bff7711bbf649def9ca9f29e50a35f108fe0852d4cd27a999cc3cdd25be5c28114d98b3748736a25bfb30b6ea5adbda786e3ceb2eebd31d5507ee5c7b45dbea563750d2deba9e7ddf1b6d173a54add5aea62ea6df1bad6db5aa93696da485c3fe60d09e0c6ac962519fec1b8632535b304516b63c2d5ab6627daa0c1cafd5d5ac6033fbfa1c5622ef45ba9e1102b176ef6ba9f3d85d4bb7ebff94de5a3a7edcb3c9629113863729bf221bc22ce79c2b3a1c9a8700304bb1e797ec56c18db1635a3e9cae700e8fce978ca30ea2603363364c237a8c85d1edb76f417134517633659b04592e6c7f07e290e54c1a5cfed59830e4f2a349534c336134ce82e213220bf129334013a006f5cfe3228c81951d0848d96236af2eb32b139addad64d343c848be68f95df1c9158fbfab5576cb59cf46c03c924adffbb1a05c8059e6ad14d845c502fb27dd12cec7e25e028211d76ede5dbd50c942215b6aae901e4a053e55a43c189ccddf4cf844d361e76ccf8cbc730bd833c00e389743fa5c0d48f20dbadcaa47e20335b1103ea52cdf1c7f030000ffff03003e6b8efb96060000 2018-10-01 11:54:32+00 expired 3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd f
5 FSktP1Nrxu7arh7TUFAgTZ \x1f8b0800000000000003c4554b72e33610ddcf295c5c27b23e16297a1599923c63591efd55f1781620d1143124011a0035545c3a59163952ae10008415d9a54caab2c992af5f773ff48f7ffefec7cb878b0b8760e7fac2192d52396d3df0aaf4104fbce56ad4df2e1f9d9f344348c6e193a175c47615047275b37517bdfbbb948738ee6f42713779bed98feee3dde2f9dbad5b8d6f36435c3b338e81d7ce41a922e59f2d50872e00f0946524da2b46d3601c49501f5dd76b377a6dbfedf7bc96e7f9c646e88e91089624d79456b7d3ebf8dd66a76b8c501544391346dfda7d6bc7503041641f630e4298e70c969bd093de7886d63de1a2ea7b67ddc28fb76595dcfdba1db9eeb296597086cb487ea231e3b9c9a0bc75f5b42409f90044a4e34d9090c029c370b1902825746bfc2d2b50b862d132cb2c5a247b41229429344699805798ab376afdcd66a369b1a8e41ca82993335ccd1d851e8cb6b0dcab7a9e53662c0f287f97d4c0c31c119d56c5d54d01fe0b54282f3268442c774e99012ba9e4fb33311e497106550f97e73206449e0b62bbd1fe6753eb8c699a30fa9ae45809d5dd0192e884ae5acec9ce946521f55c6d4dfdaaa20cdd413fdc2671390f36eeec9977c6f162f08da457e9704576fd344ea2db8f93743d84abf0f1b9db59cf7ef3fcf4ea6ac866ddc9a0b8bf1b8ffbd37177b75ebbd3ca5bb5e61d1f6624a46132101bbf1cce5a639cf8c120ccb7f39fbf146d917c75ce2b226046f1e5c9b959064fcef5d3ff24efc939bcae3b92a5d144e1bb63372b82a2d66c6dc70a17689f03951afbf2b5de5f884b8a5198c1e9585b78f26f63a778987088e46a7eaf89899485b8bebc3ce15dca04d154ec59597bc86a04765dcc77acb43d304962b55a5ab4d6267959cba027861fa4504b9985284a85ad09f01df015cf4ef96a852805d9c090b3462823558a9ad760bc5e7c27e2fb42323b95762d559b8f1f3f3e77f531a80b3c0199307d465f0e47530c30afbd5b47ec5d310cf69f0e9f713e1c972b6794a8ff803a69c3e3993d9e58bf6b4f6c42f4cf429f349b0cde0c0bdaa9f6ebc9b0d68f48246f195049a018f0fbfefd3d47b0b3e3f67e04972c038e687d391bcd0f87bf000000ffff030075db901fe2060000 2018-10-01 11:57:15+00 expired 3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd f

@ -9,3 +9,4 @@ dc130d025a4bd2e7ee83b7707d850e7c1a52872c222a5bc2e05d3357e79aa762-1,\x1f8b0800000
afc39884e024cbb3a48ca997b7e878b199d85fe97f90f94471364cc866ba83ad-1,\x1f8b080000000000000374904d6ac3301085f73945d03a14c992f593655cb20aa5942cbd1949a362d258c191534cc8c9bae8917a854a2e0ddd7476ef3de6d3e87d7d7c5e17cb25b9e070ee624fd64bb62a7a4087dd05fdbe3b6231eb9ad5a21246ce698fe93d0e872d968c3ed032d59cc4319d62d7a7ec13f09a5b90d269e1b8544c88600c55014d1dbc36c632a5515b651418ed4058ee6c25906a91258740d90ca6e44e1e672eb5402b47efc3f23e13351a270cc7bc2aa407164ce0ca04e1a9ac38b34885e65c4aabd50f0f9c8b639fd067641a469c4d374ca7149be8cbc7c866df903ff6334c47ecd3232428e9b5254dec43371c21e5ea9a426bc95ae59e562d79d96cb398c12d79baf795bddc57b676f80a6eca32c0db196fffbcb39f4ebf97ecba0392c5ed1b0000ffff030018cc54a1b1010000,Q7RqoHLngK9svM4MgRyi9y,t,,
6f2b8513ebfdb14b41d4de235f050cd028400cb01fde700d72c6bedc2ad8af1e-5,\x1f8b08000000000000037490bf6ac3301087f73c45d01cc249966425635c3285528a472ffa732a26b5151c39c5843c59873e525fa192434397de22ddef83efa4fbfefcba2e964b72c1e1dc869e6c977495fb012db6177475db610e85908a6d24c04c7b8c1f6138ee313358432e369330c65368fb987242517ba79876d6a0952503578243ea0d80e5c0944b2708e159e1d0714eb931d43b34b4508219e941cc62200ff378f752c719c2a368a937942b445158855e6967b8b05c0a2e9934c0b59585960c0b55d272a3cabb4f5b1bc63ea24b4aafdfcf38a776984e3154c1e59f915d5d913ff18b9e3aece3938e3ad36b43aad0fb76e8744cbbabb2ae215b5835e475b74fb7388c989ae7c7b6325d677ec0376da7d4ce936fff0ca9a7d3ef330eed11c9e2f6030000ffff0300b09cf40daf010000,Q7RqoHLngK9svM4MgRyi9y,f,,
3a6659e189f83f649c0010305def1b8fb78efdbe8c0ce44d56c8c22fff742b55-61,\x1f8b08000000000000037490bd6ec3201485f73c45c41c5580f973c6b8ca14555595d10be65e222b8d891c9cca8af2641dfa487d8582ab585d7a27ce39f001e7fbf3ebb6582ec915fb4b1b3ab25eb255d63d3a6caf08fbf684d994525121a85653da61fc08fd718b39a34f340f9f9230c47368bb987c22256fb4f09e7bc79d914a000aea8c6bd083c1461bcfd2524241593a5f3a250aef8d2991c952a9c21630812999c9c3c4d55615cad37998b625131eb8e5c080496c941525958c97b2708c838286022ac5b56060f42fcf3a17862e222464ec079c4cd78fe718aa00f96364b3afc81ffbd58e27ece2b38d36a7b79a54a1f36d7fb2315557655a4dd64c30b1aac9db669b84b7ef174cea652e2c99a9b064edf060ddf8d873ffe7a2fd787e3c65d71e912cee3f000000ffff0300a991d8c4b2010000,Q7RqoHLngK9svM4MgRyi9y,t,,
26c879f3d27a894a62f8730c84205ac9dec38b7bbc0a11ccc0c196d1259b25aa-1,\x1f8b08000000000000036c90cf6ed4301087dfc5e7086c27f19fdccaa2b0b4dbaa452b964ab94cec314d378943e204d26a9f8c038fc42be02eea01099f3c9faddfcc7cbf7ffe7a260b8e53e37b52b0848c68b059d0ee9b0eafa78804cfa9d69aa954c884f418befbf158229282bea109f173187cd3075210009eeb9ae796696198a1c63046a136b256b5492d1a9d03a72aa346a64e099e81d2122c77a9d4467141193d1ff237758e99fddcb6090163fcdc07b4a408e38c0931e33a04bff1364e4176eff61bf2ca6e61edb00fef21407c7aae88f1bd6bc60e425c70f3925291422415196b176f0eda096305d68e384d9154e4f3e5ae94878b61f1eeca680fdf862ffb3be5ecc5aacb9b0f87fce89e9aedfd63d64cf869fbb46c1fe67ab854dd74d5fd58efef4ad61c9c6bacce1eafa1bbc1beac486c70c4f516c2c3b9017d1b3da567bc403b6384715098260c1fedf98770944b8d1a2dcd585427a347ed7269a9a15c73953191096ab25a720e9866b904c6104daa98c9b9b0ff647325b253ac5bfc0a667d5dfaf43f65fb7578311a85ee9a2392d31f000000ffff03004a65a9b61e020000,Q7RqoHLngK9svM4MgRyi9y,t,,

1 Id Blob InvoiceDataId Accounted Blob2 Type
9 afc39884e024cbb3a48ca997b7e878b199d85fe97f90f94471364cc866ba83ad-1 \x1f8b080000000000000374904d6ac3301085f73945d03a14c992f593655cb20aa5942cbd1949a362d258c191534cc8c9bae8917a854a2e0ddd7476ef3de6d3e87d7d7c5e17cb25b9e070ee624fd64bb62a7a4087dd05fdbe3b6231eb9ad5a21246ce698fe93d0e872d968c3ed032d59cc4319d62d7a7ec13f09a5b90d269e1b8544c88600c55014d1dbc36c632a5515b651418ed4058ee6c25906a91258740d90ca6e44e1e672eb5402b47efc3f23e13351a270cc7bc2aa407164ce0ca04e1a9ac38b34885e65c4aabd50f0f9c8b639fd067641a469c4d374ca7149be8cbc7c866df903ff6334c47ecd3232428e9b5254dec43371c21e5ea9a426bc95ae59e562d79d96cb398c12d79baf795bddc57b676f80a6eca32c0db196fffbcb39f4ebf97ecba0392c5ed1b0000ffff030018cc54a1b1010000 Q7RqoHLngK9svM4MgRyi9y t
10 6f2b8513ebfdb14b41d4de235f050cd028400cb01fde700d72c6bedc2ad8af1e-5 \x1f8b08000000000000037490bf6ac3301087f73c45d01cc249966425635c3285528a472ffa732a26b5151c39c5843c59873e525fa192434397de22ddef83efa4fbfefcba2e964b72c1e1dc869e6c977495fb012db6177475db610e85908a6d24c04c7b8c1f6138ee313358432e369330c65368fb987242517ba79876d6a0952503578243ea0d80e5c0944b2708e159e1d0714eb931d43b34b4508219e941cc62200ff378f752c719c2a368a937942b445158855e6967b8b05c0a2e9934c0b59585960c0b55d272a3cabb4f5b1bc63ea24b4aafdfcf38a776984e3154c1e59f915d5d913ff18b9e3aece3938e3ad36b43aad0fb76e8744cbbabb2ae215b5835e475b74fb7388c989ae7c7b6325d677ec0376da7d4ce936fff0ca9a7d3ef330eed11c9e2f6030000ffff0300b09cf40daf010000 Q7RqoHLngK9svM4MgRyi9y f
11 3a6659e189f83f649c0010305def1b8fb78efdbe8c0ce44d56c8c22fff742b55-61 \x1f8b08000000000000037490bd6ec3201485f73c45c41c5580f973c6b8ca14555595d10be65e222b8d891c9cca8af2641dfa487d8582ab585d7a27ce39f001e7fbf3ebb6582ec915fb4b1b3ab25eb255d63d3a6caf08fbf684d994525121a85653da61fc08fd718b39a34f340f9f9230c47368bb987c22256fb4f09e7bc79d914a000aea8c6bd083c1461bcfd2524241593a5f3a250aef8d2991c952a9c21630812999c9c3c4d55615cad37998b625131eb8e5c080496c941525958c97b2708c838286022ac5b56060f42fcf3a17862e222464ec079c4cd78fe718aa00f96364b3afc81ffbd58e27ece2b38d36a7b79a54a1f36d7fb2315557655a4dd64c30b1aac9db669b84b7ef174cea652e2c99a9b064edf060ddf8d873ffe7a2fd787e3c65d71e912cee3f000000ffff0300a991d8c4b2010000 Q7RqoHLngK9svM4MgRyi9y t
12 26c879f3d27a894a62f8730c84205ac9dec38b7bbc0a11ccc0c196d1259b25aa-1 \x1f8b08000000000000036c90cf6ed4301087dfc5e7086c27f19fdccaa2b0b4dbaa452b964ab94cec314d378943e204d26a9f8c038fc42be02eea01099f3c9faddfcc7cbf7ffe7a260b8e53e37b52b0848c68b059d0ee9b0eafa78804cfa9d69aa954c884f418befbf158229282bea109f173187cd3075210009eeb9ae796696198a1c63046a136b256b5492d1a9d03a72aa346a64e099e81d2122c77a9d4467141193d1ff237758e99fddcb6090163fcdc07b4a408e38c0931e33a04bff1364e4176eff61bf2ca6e61edb00fef21407c7aae88f1bd6bc60e425c70f3925291422415196b176f0eda096305d68e384d9154e4f3e5ae94878b61f1eeca680fdf862ffb3be5ecc5aacb9b0f87fce89e9aedfd63d64cf869fbb46c1fe67ab854dd74d5fd58efef4ad61c9c6bacce1eafa1bbc1beac486c70c4f516c2c3b9017d1b3da567bc403b6384715098260c1fedf98770944b8d1a2dcd585427a347ed7269a9a15c73953191096ab25a720e9866b904c6104daa98c9b9b0ff647325b253ac5bfc0a667d5dfaf43f65fb7578311a85ee9a2392d31f000000ffff03004a65a9b61e020000 Q7RqoHLngK9svM4MgRyi9y t

@ -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);
}
}

@ -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 =
@ -648,7 +645,7 @@ namespace BTCPayServer.Tests
var store2 = acc.GetController<UIStoresController>();
await store2.Pair(pairingCode.ToString(), store2.CurrentStore.Id);
Assert.Contains(nameof(PairingResult.ReusedKey),
(string)store2.TempData[WellKnownTempData.ErrorMessage], StringComparison.CurrentCultureIgnoreCase);
store2.TempData[WellKnownTempData.ErrorMessage].ToString(), StringComparison.CurrentCultureIgnoreCase);
}
[Fact(Timeout = LongRunningTestTimeout * 2)]
@ -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")}");
@ -1102,7 +1101,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 +1145,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 +1170,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 +1194,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 +1208,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 +1540,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 +2176,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 +2216,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 +2232,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 +2275,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 +2422,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();
@ -2909,6 +2907,11 @@ namespace BTCPayServer.Tests
}
Assert.True(await invoiceMigrator.IsComplete());
});
var invoiceRepo = tester.PayTester.GetService<InvoiceRepository>();
var invoice = await invoiceRepo.GetInvoice("Q7RqoHLngK9svM4MgRyi9y");
var p = invoice.Payments.First(p => p.Id == "26c879f3d27a894a62f8730c84205ac9dec38b7bbc0a11ccc0c196d1259b25aa-1");
var details = p.GetDetails<BitcoinLikePaymentData>(handlers.GetBitcoinHandler("BTC"));
Assert.Equal("6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d", details.AssetId.ToString());
}
private static async Task RestartMigration(ServerTester tester)
@ -3046,7 +3049,7 @@ namespace BTCPayServer.Tests
[Fact]
[Trait("Integration", "Integration")]
public async void CanUseLocalProviderFiles()
public async Task CanUseLocalProviderFiles()
{
using var tester = CreateServerTester();
await tester.StartAsync();
@ -3245,8 +3248,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(8, oldPaymentsCount); // 10 payments, but 2 unaccounted
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
@ -3265,12 +3274,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);
@ -3291,6 +3300,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>();

@ -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)
{

@ -98,7 +98,7 @@ services:
custom:
nbxplorer:
image: nicolasdorier/nbxplorer:2.5.4
image: nicolasdorier/nbxplorer:2.5.16
restart: unless-stopped
ports:
- "32838:32838"
@ -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.1-beta
image: btcpayserver/lnd:v0.18.3-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.1-beta
image: btcpayserver/lnd:v0.18.3-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"

@ -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.16
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.3-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.3-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

@ -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:26.0
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.16
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:26.0
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.3-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.3-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

@ -95,7 +95,7 @@ services:
custom:
nbxplorer:
image: nicolasdorier/nbxplorer:2.5.4
image: nicolasdorier/nbxplorer:2.5.16
restart: unless-stopped
ports:
- "32838:32838"
@ -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.1-beta
image: btcpayserver/lnd:v0.18.3-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.1-beta
image: btcpayserver/lnd:v0.18.3-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"

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

@ -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="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,14 @@
<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" />
</ItemGroup>
<ItemGroup>

@ -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>
}

@ -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>

@ -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>

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

@ -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;

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

@ -2,23 +2,30 @@
@using BTCPayServer.Abstractions.TagHelpers
@using BTCPayServer.Plugins.Crowdfund
@model BTCPayServer.Components.AppSales.AppSalesViewModel
@{
var label = Model.AppType == CrowdfundAppType.AppType ? "Contributions" : "Sales";
}
<div id="AppSales-@Model.Id" class="widget app-sales">
<header class="mb-3">
<h3>@Model.Name @label</h3>
<h3>
@Model.Name
@if (Model.AppType == CrowdfundAppType.AppType)
{
<span text-translate="true">Contributions</span>
}
else
{
<span text-translate="true">Sales</span>
}
</h3>
@if (!string.IsNullOrEmpty(Model.AppUrl))
{
<a href="@Model.AppUrl">Manage</a>
<a href="@Model.AppUrl" text-translate="true">Manage</a>
}
</header>
@if (Model.InitialRendering)
{
<div class="loading d-flex justify-content-center p-3">
<div class="spinner-border text-light" role="status">
<span class="visually-hidden">Loading...</span>
<span class="visually-hidden" text-translate="true">Loading...</span>
</div>
</div>
<script src="~/Components/AppSales/Default.cshtml.js" asp-append-version="true"></script>
@ -39,7 +46,15 @@
{
<header class="mb-3">
<span>
<span class="sales-count">@Model.SalesCount</span> Total @label
<span class="sales-count">@Model.SalesCount</span>
@if (Model.AppType == CrowdfundAppType.AppType)
{
<span text-translate="true">Total Contributions</span>
}
else
{
<span text-translate="true">Total Sales</span>
}
</span>
<div class="btn-group only-for-js" role="group" aria-label="Filter">
<input type="radio" class="btn-check" name="AppSalesPeriod-@Model.Id" id="AppSalesPeriodWeek-@Model.Id" value="@AppSalesPeriod.Week" @(Model.Period == AppSalesPeriod.Week ? "checked" : "")>

@ -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();
}
}
};
}

@ -1,18 +1,24 @@
@using BTCPayServer.Plugins.Crowdfund
@model BTCPayServer.Components.AppTopItems.AppTopItemsViewModel
@{
var label = Model.AppType == CrowdfundAppType.AppType ? "contribution" : "sale";
}
<div id="AppTopItems-@Model.Id" class="widget app-top-items">
<header class="mb-3">
<h3>Top @(Model.AppType == CrowdfundAppType.AppType ? "Perks" : "Items")</h3>
<h3>
@if (Model.AppType == CrowdfundAppType.AppType)
{
<span text-translate="true">Top Perks</span>
}
else
{
<span text-translate="true">Top Items</span>
}
</h3>
</header>
@if (Model.InitialRendering)
{
<div class="loading d-flex justify-content-center p-3">
<div class="spinner-border text-light" role="status">
<span class="visually-hidden">Loading...</span>
<span class="visually-hidden" text-translate="true">Loading...</span>
</div>
</div>
<script src="~/Components/AppTopItems/Default.cshtml.js" asp-append-version="true"></script>
@ -45,7 +51,31 @@
@entry.Title
</span>
<span class="app-item-value" data-sensitive>
<span class="text-muted">@entry.SalesCount @($"{label}{(entry.SalesCount == 1 ? "" : "s")}"),</span>
<span class="text-muted">
@entry.SalesCount
@if (Model.AppType == CrowdfundAppType.AppType)
{
if (entry.SalesCount == 1)
{
<span text-translate="true">contribution</span>
}
else
{
<span text-translate="true">contributions</span>
}
}
else
{
if (entry.SalesCount == 1)
{
<span text-translate="true">sale</span>
}
else
{
<span text-translate="true">sales</span>
}
},
</span>
@entry.TotalFormatted
</span>
</div>
@ -55,7 +85,14 @@
else
{
<p class="text-secondary mt-3">
No @($"{label}s") have been made yet.
@if (Model.AppType == CrowdfundAppType.AppType)
{
<span text-translate="true">No contributions have been made yet.</span>
}
else
{
<span text-translate="true">No sales have been made yet.</span>
}
</p>
}
</div>

@ -10,7 +10,7 @@
<div class="d-inline-flex align-items-center gap-2">
@if (Model.IsArchived)
{
<span class="badge bg-warning">archived</span>
<span class="badge bg-warning" text-translate="true">archived</span>
}
<div class="badge badge-@badgeClass" data-invoice-state-badge="@Model.InvoiceId">
@if (canMark)
@ -21,13 +21,13 @@
<div class="dropdown-menu">
@if (Model.State.CanMarkInvalid())
{
<button type="button" class="dropdown-item lh-base" data-invoice-id="@Model.InvoiceId" data-new-state="invalid">
<button type="button" class="dropdown-item lh-base" data-invoice-id="@Model.InvoiceId" data-new-state="invalid" text-translate="true">
Mark as invalid
</button>
}
@if (Model.State.CanMarkComplete())
{
<button type="button" class="dropdown-item lh-base" data-invoice-id="@Model.InvoiceId" data-new-state="settled">
<button type="button" class="dropdown-item lh-base" data-invoice-id="@Model.InvoiceId" data-new-state="settled" text-translate="true">
Mark as settled
</button>
}
@ -62,6 +62,6 @@
}
@if (Model.HasRefund)
{
<span class="badge bg-warning">Refund</span>
<span class="badge bg-warning" text-translate="true">Refund</span>
}
</div>

@ -11,7 +11,7 @@
walletId = Model.WalletObjectId.WalletId
}): string.Empty;
}
<input id="@elementId" placeholder="Select labels" autocomplete="off" value="@string.Join(",", Model.SelectedLabels)"
<input id="@elementId" placeholder=@StringLocalizer["Select labels"] autocomplete="off" value="@string.Join(",", Model.SelectedLabels)"
class="only-for-js form-control label-manager ts-wrapper @(Model.DisplayInline ? "ts-inline" : "")"
data-fetch-url="@fetchUrl"
data-update-url="@updateUrl"

@ -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>
@ -322,8 +327,9 @@
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyServerSettings">
<a asp-controller="UIServer" id="SectionNav-@ServerNavPages.Files" class="nav-link @ViewData.ActivePageClass(ServerNavPages.Files)" asp-action="Files" text-translate="true">Files</a>
</li>
<vc:ui-extension-point location="server-nav" model="@Model"/>
}
<vc:ui-extension-point location="server-nav" model="@Model"/>
<li class="nav-item dropup">
<a class="nav-link @ViewData.ActivePageClass(ManageNavPages.Index)" role="button" data-bs-toggle="dropdown" data-bs-auto-close="outside" aria-expanded="false" id="Nav-Account">
<vc:icon symbol="nav-account"/>

@ -1,3 +1,5 @@
@using System.Web
@using BTCPayServer.TagHelpers
@model BasePagingViewModel
@{
@ -13,7 +15,7 @@
@if (Model.Skip > 0)
{
<li class="page-item">
<a class="page-link" tabindex="-1" href="@NavigatePages(-1, Model.Count)">Prev</a>
<a class="page-link" tabindex="-1" href="@NavigatePages(-1, Model.Count)" text-translate="true">Prev</a>
</li>
}
<li class="page-item disabled">
@ -35,7 +37,7 @@
@if ((Model.Total is null && Model.CurrentPageCount >= Model.Count) || (Model.Total is not null && Model.Total.Value > Model.Skip + Model.Count))
{
<li class="page-item">
<a class="page-link" href="@NavigatePages(1, Model.Count)">Next</a>
<a class="page-link" href="@NavigatePages(1, Model.Count)" text-translate="true">Next</a>
</li>
}
</ul>
@ -45,7 +47,7 @@
{
<ul class="pagination ms-auto">
<li class="page-item disabled">
<span class="page-link">Page Size</span>
<span class="page-link" text-translate="true">Page Size</span>
</li>
@foreach (var pageSize in pageSizeOptions)
{
@ -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();
}
}

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

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

@ -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)
@ -29,7 +33,7 @@
<div class="d-flex align-items-baseline gap-1">
<h3 class="d-inline-block me-1" data-balance="@Model.TotalOffchain" data-sensitive>@Model.TotalOffchain</h3>
<span class="text-secondary fw-semibold text-nowrap">
@ViewLocalizer["<span class=\"currency\">{0}</span> in channels", @Model.CryptoCode]
@ViewLocalizer["<span class=\"currency\">{0}</span> in channels", Model.CryptoCode]
</span>
</div>
@ -41,7 +45,7 @@
@Model.Balance.OffchainBalance.Opening
</span>
<span class="text-secondary text-nowrap">
@ViewLocalizer["<span class=\"currency\">{0}</span> opening channels", @Model.CryptoCode]
@ViewLocalizer["<span class=\"currency\">{0}</span> opening channels", Model.CryptoCode]
</span>
</div>
}
@ -52,7 +56,7 @@
@Model.Balance.OffchainBalance.Local
</span>
<span class="text-secondary text-nowrap">
@ViewLocalizer["<span class=\"currency\">{0}</span> local balance", @Model.CryptoCode]
@ViewLocalizer["<span class=\"currency\">{0}</span> local balance", Model.CryptoCode]
</span>
</div>
}
@ -63,7 +67,7 @@
@Model.Balance.OffchainBalance.Remote
</span>
<span class="text-secondary text-nowrap">
@ViewLocalizer["<span class=\"currency\">{0}</span> remote balance", @Model.CryptoCode]
@ViewLocalizer["<span class=\"currency\">{0}</span> remote balance", Model.CryptoCode]
</span>
</div>
}
@ -74,7 +78,7 @@
@Model.Balance.OffchainBalance.Closing
</span>
<span class="text-secondary text-nowrap">
@ViewLocalizer["<span class=\"currency\">{0}</span> closing channels", @Model.CryptoCode]
@ViewLocalizer["<span class=\"currency\">{0}</span> closing channels", Model.CryptoCode]
</span>
</div>
}
@ -87,7 +91,7 @@
<div class="d-flex align-items-baseline gap-1">
<h3 class="d-inline-block me-1" data-balance="@Model.TotalOnchain" data-sensitive>@Model.TotalOnchain</h3>
<span class="text-secondary fw-semibold text-nowrap">
@ViewLocalizer["<span class=\"currency\">{0}</span> on-chain", @Model.CryptoCode]
@ViewLocalizer["<span class=\"currency\">{0}</span> on-chain", Model.CryptoCode]
</span>
</div>
<div class="balance-details collapse" id="balanceDetailsOnchain">
@ -98,7 +102,7 @@
@Model.Balance.OnchainBalance.Confirmed
</span>
<span class="text-secondary text-nowrap">
@ViewLocalizer["<span class=\"currency\">{0}</span> confirmed", @Model.CryptoCode]
@ViewLocalizer["<span class=\"currency\">{0}</span> confirmed", Model.CryptoCode]
</span>
</div>
}
@ -109,7 +113,7 @@
@Model.Balance.OnchainBalance.Unconfirmed
</span>
<span class="text-secondary text-nowrap">
@ViewLocalizer["<span class=\"currency\">{0}</span> unconfirmed", @Model.CryptoCode]
@ViewLocalizer["<span class=\"currency\">{0}</span> unconfirmed", Model.CryptoCode]
</span>
</div>
}
@ -120,7 +124,7 @@
@Model.Balance.OnchainBalance.Reserved
</span>
<span class="text-secondary text-nowrap">
@ViewLocalizer["<span class=\"currency\">{0}</span> reserved", @Model.CryptoCode]
@ViewLocalizer["<span class=\"currency\">{0}</span> reserved", Model.CryptoCode]
</span>
</div>
}
@ -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>

@ -0,0 +1,94 @@
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 => {
const value = Number.parseFloat(c.dataset.balance);
c.innerText = valueTransform(value)
});
if (!series) return;
const min = Math.min(...series);
const max = Math.max(...series);
const low = Math.max(min - ((max - min) / 5), 0);
const renderOpts = Object.assign({}, chartOpts, { low, 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();
}
}
};
}

@ -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);

@ -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; }
}

@ -2,17 +2,17 @@
@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">
text-translate="true">
Node Info
</a>
</header>

@ -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))

@ -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; }
}

@ -1,18 +1,18 @@
@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">
<div class="spinner-border text-light" role="status">
<span class="visually-hidden">Loading...</span>
<span class="visually-hidden" text-translate="true">Loading...</span>
</div>
</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>

@ -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();

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