Compare commits

..

260 Commits

Author SHA1 Message Date
nicolas.dorier
38da96257f Fix test parallelization 2023-10-13 09:09:29 +09:00
Kukks
21f73d436a Fix form value setter 2023-10-12 13:24:22 +02:00
Andrew Camilleri
d58dde950e Fix pay report (#5388)
* Fix pay report

* Make sure we use 11 decimals in reports for lightning payments

---------

Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com>
2023-10-12 13:51:50 +09:00
d11n
8ac18b74df Checkout: Prevent re-rendering of payment details rows (#5392)
Potentially fixes #5390.
2023-10-12 09:35:47 +09:00
d11n
2846c38ff5 Invoice: Unify status display and functionality (#5360)
* Invoice: Unify status display and functionality

Consolidates the invoice status display and functionality (mark setted or invalid) across the dashboard, list and details pages.

* Test fix

---------

Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com>
2023-10-11 23:12:45 +09:00
nicolas.dorier
d44efce225 Simplify code 2023-10-11 21:49:51 +09:00
Andrew Camilleri
d3dca7e808 fix lq errors and tests (#5371)
* fix lq errors and tests

* more fixes

* more fixes

* fix

* fix xmr
2023-10-11 21:12:33 +09:00
d11n
41e3828eea Reporting: Improve rounding and display (#5363)
* Reporting: Improve rounding and display

* Fix test

* Refactor

---------

Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2023-10-11 20:48:40 +09:00
A. I. Oleynikov
9e76b4d28e Fix swagger (#5380) 2023-10-10 14:15:07 +09:00
d11n
ef03497350 Fix build warning (#5355)
Removes unused `string payoutSource` and shortens return with to ternary operator.
2023-10-10 12:30:48 +09:00
d11n
e5a2aeb145 Pull Payment: Add QR scanner for destination and infer payment method (#5358)
* Pull Payment: Add QR scanner for destination and infer payment method

Closes #4754.

* Test fix
2023-10-10 12:30:09 +09:00
d11n
229a4ea56c Invoice: Improve payment details (#5362)
* Invoice: Improve payment details

Clearer description and display, especially for overpayments. Closes #5207.

* Further refinements

* Test fix
2023-10-10 12:28:00 +09:00
Andrew Camilleri
f20e6d3768 Greenfield: allow delete user by email too (#5372) 2023-10-10 12:26:23 +09:00
d11n
1d210eb6e3 Crowdfund: Improve no perks case (#5378)
If there are no perks configured, do not display the perks sidebar and contribute custom amount directly, when the main CTA "Contribute" is clicked.

Before it opened a mopdal, where one had to select the only option (custom amount) manually — so this gets rid of the extra step.

Closes #5376.
2023-10-06 22:58:02 +09:00
Andrew Camilleri
d8422a979f Fix number of rates (#5365)
* Ripio had api changed
* Exchange rate host now requires an api key so removed
* Removed unused argoneum rate provider code
* switched cop and ugx to yadio
* bumped exchange sharp lib as poloniex api changed and rate source was not working
2023-10-06 16:08:50 +09:00
Andrew Camilleri
0cf6d39f02 If shitcoins are removed, dont try to hash its cryptocode for nbx (#5373) 2023-10-06 16:06:17 +09:00
Kukks
076c20a3b7 attempt to fix different casing in cryptocode of payments 2023-09-29 13:03:18 +02:00
d11n
0cfb0ba890 Email Rules: Require either recipients or customer email option (#5357) 2023-09-28 08:36:12 +02:00
nicolas.dorier
44a7e9387e bump 2023-09-27 17:02:49 +09:00
Kukks
e71954ee34 update lnurl 2023-09-27 09:13:12 +02:00
Nicolas Dorier
9cd9e84be6 Fix: After a while, a busy server would send error HTTP 500 (#5354)
This was due to Blazor which attempt to reconnect when the connection
is broken.

Before this, it would try again indefinitely, with this PR, it tries
only for around 3 minutes.

After this, the Blazor circuit should be dead anyway, so it's useless
to try again.
2023-09-27 16:05:57 +09:00
d11n
25af9c4227 Improve receipt info display (#5350)
* Improve receipt info display

Displays the info in correct order and adds optional info if tip was given with a percentage.

* Test fix

---------

Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com>
2023-09-26 22:50:04 +09:00
nicolas.dorier
72a99bf9a6 Recommend Yadio for ARS currency (see #5347) 2023-09-26 22:21:53 +09:00
nicolas.dorier
f1228523cb Try fix flackiness of CanUsePullPaymentsViaUI 2023-09-26 22:20:25 +09:00
nicolas.dorier
a45d368115 Use exchangeratehost as recommended rates for COP 2023-09-26 21:19:42 +09:00
Nicolas Dorier
16433dc183 Hide 'Connection established' when connection to server come back (#5352) 2023-09-26 16:40:02 +09:00
Nicolas Dorier
0a956fdc73 Remove some useless intermediary type from Rate Source (#5351) 2023-09-26 16:37:40 +09:00
nicolas.dorier
75396f491b Fix: Exchangerate.host falsly appear as Yadio in the UI (Fix #5347) 2023-09-26 14:45:46 +09:00
nicolas.dorier
66a064e78b Disable prism if old version 2023-09-22 23:43:06 +09:00
Nicolas Dorier
c4f8c4c7b4 Update changelog and bump (#5341)
* Update changelog and bump

* Update Changelog.md

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

* Update Changelog.md

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

* Update Changelog.md

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

* Update Changelog.md

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

* Update Changelog.md

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

---------

Co-authored-by: d11n <mail@dennisreimann.de>
2023-09-22 21:32:57 +09:00
Kukks
22bbafa659 bump lnurl 2023-09-22 12:22:46 +02:00
Nicolas Dorier
8cdfaba20c Fix: Revert to default block explorer button wasn't working (#5340)
* Fix: Revert to default block explorer button wasn't working

* Update BTCPayServer/Views/UIServer/Policies.cshtml

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

* Update BTCPayServer/Views/UIServer/Policies.cshtml

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

* Improve UI

---------

Co-authored-by: Andrew Camilleri <evilkukka@gmail.com>
Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2023-09-22 18:51:54 +09:00
d11n
7da82826fb API: Document payment method IDs (#5332)
* API: Document payment method IDs

This seems to be a source of confusion (see e.g. #5330), so I thought it'd be best to document the payment method IDs as an enum, so that we can refer to it in the several places they are used.

* Remove enum
2023-09-22 18:49:20 +09:00
d11n
9a46a64cad Test fixes (#5342)
* Test fixes

* Update BTCPayServer.Tests/ThirdPartyTests.cs

* Update BTCPayServer.Tests/ThirdPartyTests.cs
2023-09-22 11:48:59 +02:00
Andrew Camilleri
33198d693d Introduce archive pull payment permission and add Show QR code in view pull payment view (#5274)
* Introduce archive pull payment permission

* Add show qr option on pull payments

* Fix test

* update docs

* fix test

* Minor UI updates

* Update wording

---------

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2023-09-22 10:24:53 +02:00
d11n
eaeb7021d5 Fix POST redirect form submit (#5336)
The `submit()` method cannot be invoked on forms without a submit button. This changes it to call the method via the JS prototype, which can be seen as a workaround.

Checked this in Firefox and Chrome. Fixes #5335.
2023-09-22 15:03:57 +09:00
Nicolas Dorier
b443acd43b Fix: Transient error 500 when accessing the wallet page (#5326) (#5328) 2023-09-19 17:52:33 +09:00
Nicolas Dorier
616883648f Move bitcoin payment data specific stuff in NBXplorerListener (#5294) 2023-09-19 10:32:41 +09:00
d11n
7873f94848 Email Rules: Add default texts and document placeholders (#5314)
Applies default subject and body text on editing to simplify email rule setup. Once the text is edited manually, the defaus  aren't applied on switching the rule type.

Also documents the placeholders that can be used.
2023-09-19 10:10:36 +09:00
d11n
17d1832dad Payment Request: Add processing status for on-chain payments (#5309)
Closes #5297.
2023-09-19 10:10:13 +09:00
d11n
f034e2cd65 Wallet Receive: Update address formatting (#5313)
* Wallet Receive: Update address formatting

Closes #5311.

* Fix tests
2023-09-19 09:56:11 +09:00
Andrew Camilleri
19de73f9da Allow configuring nfc permission beforehand (#5319)
* Allow configuring nfc permission beforehand

* UI improvements

---------

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2023-09-19 09:55:47 +09:00
Andrew Camilleri
00acbccd7f Add payouts report (#5320) 2023-09-19 09:55:15 +09:00
d11n
77d8e202d3 Wallet: Delete custom labels (#5324)
* Tom Select improvements

* Wallet: Delete custom labels

Closes #5237.
2023-09-19 09:55:04 +09:00
Nicolas Dorier
44df8cf0c5 Rewrite the Notification dropdown with Blazor (#5325)
* Rewrite the Notification dropdown with Blazor

* Test fix

---------

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2023-09-18 10:55:05 +09:00
d11n
e694568674 Blazor Server: Improve wording (#5323) 2023-09-17 18:45:57 +02:00
d11n
163f805f3b Plugins: Add hook for resolving Lightning Address (#5322) 2023-09-14 12:53:48 +02:00
d11n
492512f527 Dashboard: Show revenue data for keypad (#5317)
* POS: Display fixes

* Dashboard: Fix Top Items component

* Dashboard: Fix App Sales component

* Dashboard: Show revenue data for keypad

Closes #5303.
2023-09-14 09:26:47 +09:00
Nicolas Dorier
73a4ac599c Add Blazor server (#5312)
* Add Blazor server

* Improve Blazor status UI

* Improve UX

---------

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2023-09-13 13:13:15 +09:00
d11n
4aedf76f1f Dashboard: Paid invoices in the last 7 days (#5316)
Adjust the prior number of transactions metric as discussed with @pavlenex. We now show the number of paid invoices instead of transactions, as this metric is more meaningful.

Closes #5300.
2023-09-13 09:02:02 +09:00
Nicolas Dorier
2d38113c66 Remove legacy confusing export (#5293) 2023-09-12 16:33:37 +09:00
d11n
445e1b7bd9 NFC: Fix error display (#5305)
Simple fix, the wrong variable was used. Fixes #5298.
2023-09-12 13:48:19 +09:00
d11n
019ac7ae31 Checkout: Cheating improvements (#5315)
Minor updates to the cheating options:
- Some browsers do not submit disabled fields, hence I made the amount field readonly in case of Lightning.
- Convert remaining amount when switching from onchain BTC to Lightning sats.
2023-09-12 13:48:01 +09:00
d11n
2b3b025bd8 Login: Re-add Remember Me button (#5307)
Closes #5302.
2023-09-12 12:16:37 +09:00
d11n
57bc90ad03 Archive stores and apps (#5296)
* Add flags and migration

* Archive store

* Archive apps
2023-09-11 09:59:17 +09:00
d11n
089e16020e Update LND image version (#5306)
See btcpayserver/btcpayserver-docker#828.
2023-09-10 10:07:44 +09:00
d11n
0c4f31794d Test fix: Update rate retrieval skipping parameters (#5308) 2023-09-09 09:46:09 +02:00
Kukks
cdffe9b355 Bump LNURL 2023-09-07 10:02:32 +02:00
nicolas.dorier
5a28cf9e87 Release new version of client 2023-09-06 08:21:46 +09:00
Nicolas Dorier
3b05de7f30 Fix: Crash caused by very old point of sales invoices (#5283) (#5291) 2023-09-05 15:32:49 +09:00
nicolas.dorier
79b2f1652b Changelog and bump 2023-09-02 23:22:59 +09:00
nicolas.dorier
b32e0e7cce Fix #5233: Error on the MigrationStartupTask 2023-09-02 23:12:37 +09:00
A. I. Oleynikov
1f9fbbee22 Update README (#5282)
Co-authored-by: A. I. Oleynikov <self@oleynikov.ai>
2023-09-01 16:03:51 +02:00
Vincent Bouzon
8c9f325c9f Display wallet balance in default currency (#5281)
* Display wallet balance in default currency

* Cleanups

---------

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2023-09-01 15:29:41 +09:00
d11n
9bf1e35bf4 Use _blank link targt for payment scheme links (#5284)
In addition to #5270. Fixes #5266.
2023-08-31 09:44:10 +09:00
nicolas.dorier
32e830a1c5 Fix slow 'Fasts' tests 2023-08-28 09:40:44 +09:00
nicolas.dorier
561bae071f Timeout CanGetRateCryptoCurrenciesByDefault 2023-08-26 21:13:55 +09:00
nicolas.dorier
08b6942c59 Bump, changelog 2023-08-26 21:03:45 +09:00
Andrew Camilleri
4564f9a46c Small improvements (#5273)
* BUmp LNURL

* Show app view link in nav when not enoguh permission to modify

* FIx permission misalignment on create pull payments

We have explicit permissions for pull payment creation, even allow them to be created through the invoices, but the create ui and cta were blocked behind  canmodify store permission.

* Make Ln address pass an invoiceId in the context to resolve breaking change
2023-08-26 20:50:07 +09:00
d11n
58a1c6d2c8 Parse POS string data for invoice details display (#5275)
* Parse POS string data for invoice details display

Fixes #5240.

* Improve POS data display
2023-08-26 20:48:48 +09:00
Kukks
97acec340c fix lnaddress nav item permission 2023-08-24 16:31:49 +02:00
nicolas.dorier
52790a6954 bump clightning 2023-08-24 20:59:15 +09:00
nicolas.dorier
af6249a741 bump lightning lib 2023-08-24 20:59:15 +09:00
d11n
17064ab3c8 POS: Unify item display in editor (#5272) 2023-08-24 08:51:22 +02:00
d11n
1487bf4ff5 Unset link targt for payment scheme links (#5270)
Potential fix for #5266 — see the discussion in that issue for details.

This change should be non-invasive, I tested the links in regular as well as modal mode and they worked in Firefox, Brave and Chrome.
2023-08-24 13:37:27 +09:00
d11n
e8c0858558 POS: Fix alignment of items in static view (#5271)
Items in static view weren't center aligned. This matches the classes in the cart view. Fixes #5230.
2023-08-23 11:11:41 +02:00
nicolas.dorier
56fa3fe8f2 Fix crash on /wallets/transactions with non zero skip parameter (Fix #5183) 2023-08-23 16:11:25 +09:00
nicolas.dorier
583813883c Simplified logic for receipt amount (#5197) 2023-08-23 10:43:34 +09:00
Andrew Camilleri
c69f95bdce Do not block payments on LN while syncing if it is not internal node (#5269) 2023-08-22 13:45:50 +02:00
Kukks
b3df403980 Fix LN payout manual payments UI crashing when payouts are not tied to pull payment 2023-08-15 15:11:04 +02:00
dstrukt
90ce75ee21 remove store ID from view request url (#5256) 2023-08-13 19:26:21 +02:00
Kukks
1c5fcfe094 bump v 2023-08-11 15:55:11 +02:00
Dennis Reimann
45c1fb42ee Changelog v1.11.2 2023-08-11 15:39:08 +02:00
d11n
64bd493996 POS: Unify item display (#5252)
Display unifications for static and cart view.
2023-08-11 15:37:43 +02:00
d11n
ec6029409e Improve invoice filter wording (#5251)
Closes #5250.
2023-08-11 15:08:44 +02:00
d11n
c0fc31c69a Improve invoices status filter (#5248) 2023-08-10 20:23:18 +02:00
d11n
b5d0188f21 Receipt improvements (#5239) 2023-08-10 13:57:54 +02:00
d11n
0ccbaf4bd6 Greenfield: Fix invoice lookup by capitalized status (#5245)
All statuses need to be lowercased before lookup, this wasn't the case for e.g. `Expired`.

Fixes #5244.
2023-08-10 13:34:09 +02:00
d11n
ed43fb2071 POS fixes (#5241) 2023-08-09 14:47:28 +02:00
d11n
d67ebd957e POS: Handle flexible price items in cart view (#5238) 2023-08-09 09:31:19 +02:00
Ikko Eltociear Ashimine
19d360a543 Fix: typo in InvoiceEntity.cs (#5236)
Minumum -> Minimum
2023-08-07 09:26:37 +02:00
d11n
7dc41ebcea Email Rules: Improve validation (#5234)
Came across this while testing things and the "Please fill all required fields before testing" message wasn't clear, because the required fields were not marked.

Co-authored-by: Andrew Camilleri <evilkukka@gmail.com>
2023-08-07 09:10:48 +02:00
d11n
1eb7c727f3 POS fixes (#5228) 2023-08-05 10:44:59 +02:00
evanc-ole
ede8171408 Checkout: Fix language select UI bug (#5229) 2023-08-04 07:44:50 +02:00
Kukks
2538f3d8f6 fix https://github.com/Kukks/BTCPayServerPlugins/issues/18 2023-08-03 20:48:46 +02:00
Pavlenex
ac64f5e395 Merge pull request #5227 from dennisreimann/supporters
Update supporters
2023-08-03 19:45:09 +02:00
Dennis Reimann
1a7a731b54 Update supporters
Improve colors and visual balance
2023-08-03 14:58:32 +02:00
ndeet
86f4d48bcb c-lightning to CLN; remove ptarmigan. (#5220) 2023-08-01 17:21:00 +03:00
Kukks
83536bee88 Fix BTG rate provider 2023-07-29 10:00:34 +02:00
Kukks
abfd6ea1dc update changelog and version 2023-07-29 09:48:47 +02:00
Kukks
688e873f7a fixes #5203 2023-07-29 09:15:12 +02:00
Kukks
c88df08350 fixes #5208 2023-07-29 09:15:11 +02:00
Kukks
82586590a7 potentially fixes #5203 2023-07-29 09:15:11 +02:00
Kukks
88c66f30f2 fixes #5204 2023-07-29 09:15:10 +02:00
Kukks
9132592717 fixes #5205 2023-07-29 09:15:10 +02:00
Kukks
c0ffab768a fix ident 2023-07-29 09:15:10 +02:00
dstrukt
69190081c8 ui+checkout: fix language cutoff bug (#5210) 2023-07-28 21:24:30 +02:00
Kukks
093206cf1e add changelog 2023-07-27 15:19:48 +02:00
Kukks
a0110b7570 Merge remote-tracking branch 'origin/feat/changelog-1.11' 2023-07-27 15:14:53 +02:00
pavlenex
6d65feca4c update changelog 2023-07-27 08:39:58 +02:00
Andrew Camilleri
95be0242b6 add opensats and update strike logo (#5202)
Co-authored-by: pavlenex <pavle@pavle.org>
2023-07-27 08:39:40 +02:00
rockstardev
79e121c3af Disabling playing of the invoice sound for existing stores 2023-07-26 10:42:00 -05:00
rockstardev
676ac2fe46 Changelog 1.11.0 2023-07-26 09:11:26 -05:00
rockstardev
8eabdab53a Preventing entering of negative tips and discounts in POS 2023-07-26 07:26:53 -05:00
rockstardev
957fb09ffc Reverting logic of how paid amount is displayed on the receipt 2023-07-26 07:26:32 -05:00
nicolas.dorier
4bffe117a9 Do not show cheatmode in release, fix warnigns 2023-07-25 10:50:34 +09:00
nicolas.dorier
05b01a13c8 Fix NRE error in PoS report 2023-07-24 23:20:17 +09:00
nicolas.dorier
08e21c1a5d Fix report view 2023-07-24 23:13:11 +09:00
nicolas.dorier
4d5245605d bump 2023-07-24 22:59:18 +09:00
d11n
453548d614 Checkout v2: Play sound when invoice is paid (#5113)
* Checkout v2: Play sound when invoice is paid

Closes #5085.

* Refactoring: Use low-level audio API to play the sound

Allows to play the sound regardless of browser permissions.

* Add audio file detection

* Use model state for file upload errors

* Add default sound and customizing option

* Fix mp3 detection

* Add sounds

* Update defaults

* Add nfcread and error sounds

* Improve label wording

* Replace sound

---------

Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2023-07-24 22:57:24 +09:00
Andrew Camilleri
95a0614ae1 Support accepting 0 amount bolt 11 invoices for payouts (#4014)
* Support accepting 0 amount bolt 11 invoices for payouts

* add test

* handle validation better

* fix case when we just want pp to provide amt

* Update BTCPayServer/HostedServices/PullPaymentHostedService.cs

* Update BTCPayServer/HostedServices/PullPaymentHostedService.cs

* Update BTCPayServer/Data/Payouts/LightningLike/UILightningLikePayoutController.cs

* Update UILightningLikePayoutController.cs

* fix null

* fix payments of payouts on cln

* add comment

* bump lightning lib

---------

Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com>
2023-07-24 20:40:26 +09:00
Andrew Camilleri
36ea17a6b7 Introduce Payout metadata for api and plugins (#5182)
* Introduce Payout metadata for api and plugins

* fix controller

* fix metadata requirement

* save an object

* pr changes
2023-07-24 18:37:18 +09:00
Nicolas Dorier
dc986959fd Add reporting feature (#5155)
* Add reporting feature

* Remove nodatime

* Add summaries

* work...

* Add chart title

* Fix error

* Allow to set hour in the field

* UI updates

* Fix fake data

* ViewDefinitions can be dynamic

* Add items sold

* Sticky table headers

* Update JS and remove jQuery usages

* JS click fix

* Handle tag all invoices for app

* fix dup row in items report

* Can cancel invoice request

* Add tests

* Fake data for items sold

* Rename Items to Products, improve navigation F5

* Use bordered table for summaries

---------

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2023-07-24 09:24:32 +09:00
d11n
845e2881fa POS Cart redesign (#5171)
* Move POS assets

* WIP

* Refactor into common Vue mixin

* Offcanvas updates

* Unifications across POS views

* POSData view fix

* Number and test fixes

* Update cart width

* Fix test

* More view unification

* Hide cart when emptied

* Validate cart

* Header improvement

* Increase remove icon size

* Animate add to cart action

* Offcanvas for mobile, sidebar for desktop

* ui+pos: updates icon size + badge + label

* Remove cart table headers

* Use same size for Cart and Shop headlines

* Update search placeholder

* Bump horizontal  input padding

* Increase sidebar width

* Bump badge font size

* Fix manipulating the quantity of line items

* Fix cart icon

* Update cart display

* updates empty button

* Rounded search input

* Remove cart button on desktop

* Fix dark accent color

* More accent fixes

* Fix plus/minus alignment

* Update BTCPayServer/Views/Shared/PointOfSale/Public/Cart.cshtml

* Apply suggestions from code review

---------

Co-authored-by: dstrukt <gfxdsign@gmail.com>
2023-07-22 21:15:41 +09:00
d11n
2e4be9310c Design system updates (#5186) 2023-07-21 09:27:37 +02:00
Nicolas Dorier
a2faa6fd59 Minor fixes (#5185) 2023-07-21 09:05:50 +02:00
Nicolas Dorier
0a78846e8d Stop using bitpay's CreateInvoice for non bitpay API usage (#5184) 2023-07-21 09:08:32 +09:00
Andrew Camilleri
4063a5aaee Quality of life improvements to payout processors (#5135)
* Quality of life improvements to payout processors

* Allows more fleixble intervals for payout processing from 10-60 mins to 1min-24hours(requested by users)
* Cancel ln payotus that expired (bolt11)
* Allow cancelling of ln payotus that have failed to be paid after x attempts
* Allow conifguring a threshold for when to process on-chain payouts (reduces fees)

# Conflicts:
#	BTCPayServer.Tests/SeleniumTests.cs

* Simplify the code

* switch to concurrent dictionary

* Allow ProcessNewPayoutsInstantly

* refactor plugin hook service to have events available and change processor hooks to actions with better args

* add procesor extended tests

* Update BTCPayServer.Tests/GreenfieldAPITests.cs

* fix concurrency issue

* Update BTCPayServer/PayoutProcessors/BaseAutomatedPayoutProcessor.cs

---------

Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2023-07-20 22:05:14 +09:00
d11n
b1c81b696f Generate unique order IDs for PoS and Crowdfund sales (#5127)
* Generate unique order IDs for PoS and Crowdfund sales

Part of #5054.

* Refactorings

* Updates

* Updates

* Refactoring

* Remove search by AdditionalSearchTerm

* Implement appid

---------

Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2023-07-20 16:03:39 +09:00
d11n
0017f236a7 Improve create first store view (#5181)
* Improve create first store view

Closes #5008.

* Fix tests
2023-07-19 22:21:16 +09:00
Andrew Camilleri
19d5e64063 Form invoice amount adjusters (#5158)
* Fix constant fields being editable on UI

* fix redirect to checkout if invoice is settled (redirect to receipt instead)

* enhance: make mirror field type able to map values

* Introduce invoice amount adjustment fields for form

* Integrate invoice amount adjustment fields for form on pos

* Support mirror in editor

* Indicate when special field names are used

* polsih mirror view and name suggestions for fields

* clarify

* hide hidden field from ui

* Minor adjustmentts

* Improve mirror field editing

---------

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2023-07-19 18:54:51 +09:00
Nicolas Dorier
22435a2bf5 Refactor logic for calculating due amount of invoices (#5174)
* Refactor logic for calculating due amount of invoices

* Remove Money type from the accounting

* Fix tests

* Fix a corner case

* fix bug

* Rename PaymentCurrency to Currency

* Fix bug

* Rename PaymentCurrency -> Currency

* Payment objects should have access to the InvoiceEntity

* Set Currency USD in tests

* Simplify some code

* Remove useless code

* Simplify code, kukks comment
2023-07-19 18:47:32 +09:00
Andrew Camilleri
a7def63137 fix pos item topups lnurl (#5172)
fixes #5170
2023-07-17 13:08:41 +02:00
Kukks
3703a170e7 try fix migration for pos yml 2023-07-13 14:59:18 +02:00
Deverick
73fbfbd7cb Add support for Monero RPC authentication (#5157)
Co-authored-by: Andrew Camilleri <evilkukka@gmail.com>
2023-07-13 12:24:08 +02:00
Dennis Reimann
acae3b8753 Refactoring 2023-07-13 12:17:41 +02:00
Kukks
a618f901fc Support NFC on modal 2023-07-13 12:17:41 +02:00
Andrew Camilleri
6d4918f0ab Update ViewPullPayment.cshtml 2023-07-13 12:17:01 +02:00
Kukks
7f2c4d2e7a add extension point for pull payment view 2023-07-13 12:17:01 +02:00
Lee Salminen
fd6d361e1a CheckoutV2: When WebSocket disconnects, we should continue polling via XHR (#5165)
* When WebSocket disconnects, we should continue polling via XHR

* Update BTCPayServer/wwwroot/checkout-v2/checkout.js

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

---------

Co-authored-by: d11n <mail@dennisreimann.de>
2023-07-11 21:56:13 +02:00
nicolas.dorier
b5f0924651 Serialize PosAppCartItem.value as decimal instead of string 2023-07-11 15:49:16 +09:00
d11n
1600dd4759 POS: Backwards-compatible price parsing (#5163)
* POS: Backwards-compatible price parsing

Fixes #5159 and a regression introduced in bbff9710bf: The price in posData needs to be parsed in a backwards-compatible manner, as the old format of price as an object exists in the invoice metadata.

* Test corner cases

---------

Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2023-07-11 15:32:01 +09:00
d11n
c777746b69 Custom Forms: Allow HTML in labels and help text (#5136)
* Custom Forms: Allow HTML in labels and help text

Fixes #5003.

* Vue: Sanitize labels and helper text input

* Form editor: Fix blur on input for select option values

---------

Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com>
2023-07-11 13:02:02 +09:00
nicolas.dorier
9f5466a41f Make sure CheckJsContent run as part of CI, and ignore end of line differences 2023-07-11 09:41:28 +09:00
Dennis Reimann
4d1e4801bf Dark theme color fix 2023-07-10 11:33:39 +02:00
Andrew Camilleri
5e469ff9c0 Improve rates (#5166)
* Removes Chaincoin shitcoin which is so dead even its website is gone
* Add ExchangeRateHost and FreeCurrencyRates as new rate providers
* Add recommended rate providers for UGX and RSD
* Fix BTX rate by switching to graviex
* Fix BTC rate by switching to exmo
* Fix LCAD rate script
2023-07-10 17:31:48 +09:00
d11n
2f3eedea5b Invoice lists: Show icons for payment methods (#5137) 2023-07-08 17:33:13 +02:00
rockstardev
5c5d6dc1e2 Bumping LND to 0.16.4-beta 2023-07-08 08:22:42 -05:00
Andrew Camilleri
fbe31ce64f Support LNURL in pay button (#5107)
* Support LNURL in pay button

* UI updates

* Cleanups

---------

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2023-07-06 10:12:31 +02:00
Chukwuleta Tobechi
0b082138c8 Payment Requests: List view improvements (#5065)
* List invoice checkbox variant

* Remove custom css

* Improve payment requests list view

* Improve Payment Requests List View

* List invoice checkbox variant

* Remove custom css

* Improve payment requests list view

* Improve Payment Requests List View

* Update payment request (name link leads to view not edit)

* Refactoring

---------

Co-authored-by: d11n <mail@dennisreimann.de>
2023-07-06 10:02:23 +02:00
d11n
966e598f10 Apps: Add direct file upload in item editor (#5140) 2023-07-06 11:01:36 +09:00
d11n
e998340387 POS: Account for custom amount in cart view (#5151)
* Add failing test

* Account for custom amount

* Test fix
2023-07-05 17:23:15 +09:00
Aaron Dewes
f6b27cc5f9 Compare domains in lowercase
Domains are case-insensitive, so this comparision should be too.

I encountered this issue with a Citadel user who accidentially named their domain an uppercase name (Pay.example.com), but browsers automatically converted it to pay.example.com
2023-07-03 08:49:16 +02:00
Nicolas Dorier
f3dbf1e139 Allow browser to access LND config (#5128) 2023-06-30 15:08:23 +09:00
d11n
627d84fc91 Update to Bootstrap v5.3 (#5132)
Based on btcpayserver/btcpayserver-design#63
2023-06-30 09:21:27 +09:00
Nicolas Dorier
8cde8c01df Add category feature to the PoS with Cart (#5078)
* Add grouping feature to the PoS with Cart

* Improve UI

* Rename groups to categories

* Make it easier to select categories of the items

* Refactor TemplateEditor, use TomSelect for categories

* Prevent Vue code insertion

* Prevent empty categories

* Add label ids

* Add test case

---------

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2023-06-30 09:13:15 +09:00
nicolas.dorier
983b8c1f54 Fix changelog 2023-06-27 21:32:05 +09:00
Nicolas Dorier
d666d8ea1a Changelog 1.10.3 (#5125)
* Changelog 1.10.3

* Apply suggestions from code review

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

* Apply suggestions from code review

* Update changelog

---------

Co-authored-by: Andrew Camilleri <evilkukka@gmail.com>
Co-authored-by: d11n <mail@dennisreimann.de>
2023-06-27 21:23:56 +09:00
Dennis Reimann
3ed81c3a78 Greenfield: Fix missing default currency in stores API
Docs mention it should be present, but it wasn't. Fixes #5123.
2023-06-27 12:52:24 +02:00
nicolas.dorier
4afec2e2b6 Fix: Using lnaddresses on Nostr should not result in lots of invoice being created 2023-06-27 12:50:24 +02:00
d11n
db83d238d5 Crowdfund: Fix JS errors in empty state (#5121)
An empty crfowdfund with the default perk had JS errors.
2023-06-27 09:42:18 +09:00
rockstardev
fdcf7b3b7a Bumping LND to 0.16.3-beta (#5124) 2023-06-27 09:06:31 +09:00
Nicolas Dorier
53aafcf86b Fix: The current preimage of a invoice's lightning payment method should be available via API (#5111) 2023-06-23 19:12:11 +09:00
d11n
aec84f6d67 Dashboard: Limit "Top Items" to five (#5110)
Feedback we got at BTCPrague: Do not show more than five items in the top list, because otherwise the list can get very long if there's a POS with many items.
2023-06-23 11:31:05 +02:00
d11n
01e9f82d24 Policies: Update wording to fit API keys and Roles (#5106)
* Policies: Update wording to fit API keys and Roles

Closes #5021.

* API keys: Improve spacing
2023-06-22 10:37:30 +02:00
Nicolas Dorier
2eff45e65c Ajaxify the wallet transaction list to avoid timeout (Fix #4987) (#5100)
* Ajaxify the wallet transaction list to avoid timeout (Fix #4987)

* Add cancellation to request to wallet transactions

* Fix tests

* Improve empty state

* Cleanups

---------

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2023-06-22 16:09:53 +09:00
d11n
13203c3e2b Receipt improvements (#5077)
* Remove Order ID link

* Add separate print version for receipt

* Fix POS number handling and add keypad test

Fixes #5056.

* Add formatting function

* Remove OrderUrl for POS, bring back order link for receipt

* Update BTCPayServer/Plugins/PointOfSale/Controllers/UIPointOfSaleController.cs
2023-06-22 15:57:29 +09:00
d11n
82c5e0e43d Dashboard: Make invoice badges consistent with those on invoices list (#5108)
Closes #4969.
2023-06-22 15:47:12 +09:00
d11n
a1575f404b Invoices: Fix search box shrinking too small (#5105)
Fixes #5099.
2023-06-21 09:52:42 +02:00
Dennis Reimann
e1509506dc Upgrade Bootstrap-Vue and fix tooltip positioning
Fixes #4956.
2023-06-21 08:31:13 +02:00
nicolas.dorier
0c1d0d7b05 Fix: formResponse and formId missing from API's GetPaymentRequest route 2023-06-21 12:47:21 +09:00
Nicolas Dorier
ad70856af0 Fix: LN payments failed to be detected on litd (#5104) 2023-06-21 12:15:46 +09:00
nicolas.dorier
8615f120ce Fix tests 2023-06-20 22:37:05 +09:00
d11n
0d0477d661 Lightning: Relax GetInfo constraint for LNDhub connections (#5083)
* Lightning: Relax GetInfo constraint for LNDhub connections

The LNDhub-compatible implementation by LNbits does not support the `GetInfo` call for all their funding sources — see lnbits/lnbits#1182. By catching that exception in combination with the `LndHubLightningClient`, we give people the ability to still use their LNbits-based LNDhub as a Lightning node.

Fixes #4482.

* Update approach to handling unsupported GetInfo calls
2023-06-20 17:28:16 +09:00
Andrew Camilleri
b31dc30878 Make file management UI more useful (#5081)
* Make file management UI more useful

* Simplify markup

* Move file info to top

---------

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2023-06-20 08:58:28 +02:00
nicolas.dorier
6e392f4cfb After changing PoS items in UpdatePoS ident the JSON template 2023-06-19 14:44:12 +09:00
nicolas.dorier
cc3bdc331e Fix build 2023-06-16 23:19:47 +09:00
nicolas.dorier
76faf77a1c Fix keypad view broken by previous commit 2023-06-16 23:18:47 +09:00
Andrew Camilleri
d8c0e5bf3a Add extension point to template editor (#5080) 2023-06-16 23:05:49 +09:00
d11n
28c4c320cc Checkout v2: Add return link in processing state (#5075)
* Checkout v2: Add return link in processing state

* Update copy text position
2023-06-16 23:05:08 +09:00
Nicolas Dorier
e81403ec3f Fix: Applying a discount in PoS with cart wasn't working (#5079) 2023-06-16 23:02:14 +09:00
d11n
f11424f73a Pull Payment: Support LNURL Withdraw with SATS denomination (#5041)
* Pull Payment: Support LNURL Withdraw with SATS denomination

* Refactor and add tests
2023-06-16 10:56:17 +09:00
ndeet
fa8b977016 Remove id from create webhook endpoint; fix consistency. (#5045) 2023-06-16 10:53:41 +09:00
d11n
d181846339 Refund: Fix overpaid option (#5076)
Closes #5066.
2023-06-16 10:52:52 +09:00
Nicolas Dorier
1956919886 Do not crash when an invoice have an amount that is too big (#5070) 2023-06-16 10:47:58 +09:00
Dennis Reimann
0f66498965 NFC: Do not start scanning if unsupported
Fixes #5067.
2023-06-14 09:14:09 +02:00
Nicolas Dorier
918cd152b1 Fix: Incorrect rounding in the receipt of PoS invoice (fix #5071) (#5072) 2023-06-13 20:34:21 +02:00
d11n
d3222df396 Fix build warnings (#5069)
Fixes these two:

```
/source/BTCPayServer/Hosting/MigrationStartupTask.cs(643,49): warning CS0168: The variable 'items' is declared but never used [/source/BTCPayServer/BTCPayServer.csproj]
/source/BTCPayServer/Hosting/MigrationStartupTask.cs(644,24): warning CS0168: The variable 'newTemplate' is declared but never used [/source/BTCPayServer/BTCPayServer.csproj]
```
2023-06-13 20:46:44 +09:00
d11n
a84ffd8c7e Crowdfund: Fix null pointer exception for topup type (missing price) (#5068)
Items with type topup have a price = null and hence not even the property set (ignored in JSON). This needs to be handled in the temlate, otherwise this exception occurs:

```
An unhandled exception was thrown by the application.
System.InvalidOperationException: Nullable object must have a value.
   at AspNetCoreGeneratedDocument.Views_Shared_Crowdfund_Public_ContributeForm.<>c__DisplayClass24_0.<<ExecuteAsync>b__0>d.MoveNext()
```
2023-06-13 20:46:27 +09:00
Kukks
6d0f9120b8 prep for 1.10.2 2023-06-07 18:02:51 +02:00
Kukks
aafb4a7f2a Fix stale invoice api for settle invoice
fixes #5049
2023-06-07 17:57:03 +02:00
nicolas.dorier
ae432ff237 Fix: Crash on migation of old instances (Fix #5051) 2023-06-07 10:20:39 +02:00
Dennis Reimann
cdc318c71a Pay Button: Fix circular reference when serializing JSON
When apps were set, the `GetAllApps` included the store data, which led to a circular reference when serializing the JSON. That data isn't necessary here, so we can just drop it before rendering.

Fixes #5038.
2023-06-05 12:35:06 +02:00
Dennis Reimann
94d1cec8a9 Hide Sensitive Info: Fix script location
The script snippet needs to be located outside of the theme if-conditions, otherwise it only works if no custom theme is applied.
2023-06-05 12:34:54 +02:00
nicolas.dorier
c0bc19ea59 Update Changelog 2023-06-02 18:21:57 +09:00
nicolas.dorier
6f07714cd9 Language update 2023-06-02 18:18:10 +09:00
nicolas.dorier
a9d2cac23c bump 1.10.1 2023-06-02 18:15:56 +09:00
Nicolas Dorier
693b46126b Bump Bitcoin core to 25.0 (#5032) 2023-06-02 16:41:35 +09:00
Kukks
bbff9710bf fix cart + form combination bug fixes #5031 2023-06-02 09:34:55 +02:00
nicolas.dorier
358e122775 Fix tests 2023-06-01 22:17:42 +09:00
Andrew Camilleri
3818468932 Pluginify on chain wallet setup (#4999)
* Pluginify on chain wallet setup

This PR fixes a few logical points in the wallet setup flow to allow more extensive plugin flexibility; It also fixes an issue when building plugins that requires an Altcoin config profile. Here is an example showcasing the Liquid+ plugin using this to enforce that it is a hot wallet (a requirement it has) and that import to RPC is always set, and a new option that is used to configure the wallet further https://i.imgur.com/pDPQ73v.gif

* Update BTCPayServer/Controllers/UIStoresController.Onchain.cs

* update nbx
2023-06-01 21:18:28 +09:00
Kukks
3d2554fbe1 Make role name show uneditable when not creating 2023-05-31 15:49:34 +02:00
Kukks
4309603317 Hide topup items from cart 2023-05-31 15:49:34 +02:00
Dennis Reimann
f733c9ea77 Form Builder: Improve wording
Element -> Field. Something bas and I came across while reviewing the blog post.
2023-05-31 14:57:11 +02:00
Kukks
775ee01171 fix store role deletion fixes #5027 2023-05-31 13:42:38 +02:00
nicolas.dorier
33ec790137 Changelog 2023-05-31 11:50:10 +09:00
d11n
0c575c888c Remove payment requirement for marking expired invoices (#5006)
* Remove payment requirement for marking expired invoices

Allows to manually mark expired invoices, regardless of registered payments. See dennisreimann/btcpayserver-plugin-lnbank#34 for context, in which BTCPay Server sometimes did not register payments that were received to a LNbank wallet (this got fixed in btcpayserver/BTCPayServer.Lightning#129)

* Refactor conditions for better readability
2023-05-31 11:49:01 +09:00
nicolas.dorier
24f7e51e3a Small adjustements 2023-05-31 11:27:03 +09:00
Nicolas Dorier
0a0cf97c55 Do not cleanup unreachable stores (#5025) 2023-05-31 11:22:37 +09:00
Dennis Reimann
16b988d097 UI: Only display applicable refund options
Fixes #5019.
2023-05-30 12:51:51 +02:00
Dennis Reimann
5edc0ff6ef UI: Fix visual bug with Hide Sensitive Info
Fixes #5011
2023-05-30 11:20:58 +02:00
Dennis Reimann
375b96e508 UI: Center-align recovery phrase
Fixes #5007.
2023-05-30 11:19:16 +02:00
Dennis Reimann
1e72b12074 UI: Store selector link distinguishes between owner and user
The `IsOwner` property went missing with #4940, so everyone landed on the invoices list when switching stores. This brings back the original behaviour of linking to the Dashboard, if the user has the permission to access it.

Fixes #5015.
2023-05-30 11:18:34 +02:00
Kukks
4a6d52f78e Fix crowdfund perk support fixes #5013 2023-05-30 10:34:48 +02:00
Kukks
35dd580e74 Fix cart view and provide better naming for default items fixes #5012 2023-05-30 10:05:31 +02:00
Kukks
79836ef1de make free invoices from pos redirect to receipt and make receipt reload fast on such case 2023-05-30 10:04:23 +02:00
Kukks
8cb06f9c6c fix user store reole setting fixes #5010 2023-05-30 09:37:14 +02:00
nicolas.dorier
215a36e7a9 Fix: Some multi path payment on LND wouldn't be detected 2023-05-30 12:26:30 +09:00
Nicolas Dorier
247f6b86a5 Changelog 1.10.0 (#5004) 2023-05-29 16:24:27 +09:00
d11n
a9d42f1e6a Add What's New in v1.10.0 (#4992)
* Add What's New in v1.10.0

* Update BTCPayServer/Views/UIStores/Dashboard.cshtml

Co-authored-by: B <102448109+Bas02@users.noreply.github.com>

---------

Co-authored-by: B <102448109+Bas02@users.noreply.github.com>
2023-05-29 16:08:46 +09:00
nicolas.dorier
4e03c2523a Prune webhook data from database 2023-05-29 09:02:47 +02:00
nicolas.dorier
418b476725 Bug fix on StoreRoleId parsing 2023-05-27 12:51:48 +09:00
Andrew Camilleri
783e4ccb35 Store Custom Roles (#4940) 2023-05-26 23:49:32 +09:00
nicolas.dorier
6b7fb55658 Fix: Payment not marked as settled even if the payment is successful with LNBank
Fix https://github.com/dennisreimann/btcpayserver-plugin-lnbank/issues/33
2023-05-25 21:09:13 +09:00
Nicolas Dorier
3d5361cd11 [Bug] If a altcoins is disabled from BTCPay and payout processor is used, it would crash at restart (#4997)
Co-authored-by: Andrew Camilleri <evilkukka@gmail.com>
2023-05-25 12:42:23 +02:00
nicolas.dorier
2c4349c630 Test concurrent payment of lightning invoices 2023-05-25 18:41:17 +09:00
d11n
3589417b58 Form Editor: Minor wording adjustments (#4998) 2023-05-25 08:51:03 +02:00
d11n
55203e0b64 Dashboard: Fix SATS denomination display (#4994)
When the default currency of the store is SATS, the display was broken.
2023-05-25 10:08:00 +09:00
nicolas.dorier
a918288e3b Fix codeql config to not scan vendor js, add it to solution 2023-05-23 10:38:59 +09:00
nicolas.dorier
e183138d2c Remove vendor js from codeql scan 2023-05-23 10:07:08 +09:00
Zaxounette
d3e42862ed Create codeql.yml (#4821)
Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com>
2023-05-23 09:23:24 +09:00
Andrew Camilleri
8860eec254 Switch Apps to json not YML (#4792) 2023-05-23 09:18:57 +09:00
Kukks
97e7e60cea Add minrelayfee to payjoin request
fixes #4689
2023-05-22 14:56:08 +02:00
d11n
44aaf7acbb Form editor (#4968)
Co-authored-by: dstrukt <gfxdsign@gmail.com>
2023-05-22 13:30:28 +02:00
Nicolas Dorier
9b721fae27 Better handle postgres requests for wallet objects (#4985) 2023-05-20 23:26:16 +09:00
Nicolas Dorier
c3f412e3bb Bump tests to Bitcoin Core 24.1 (#4988) 2023-05-20 21:38:39 +09:00
nicolas.dorier
ee738a29f0 Stop spamming logs with event aggregator logging 2023-05-19 15:24:20 +09:00
d11n
6c6544bf9b Improve invoice filtering UI (#4914)
* Improve invoice filtering UI

Closes #3664.

* UI updates

* Add app filter

* Add indicator for active filters

* updates text

* Improve selected filter display

* Apply suggestions from code review

---------

Co-authored-by: dstrukt <gfxdsign@gmail.com>
2023-05-19 10:42:09 +09:00
Nicolas Dorier
3d57b944ca Fix a bunch of minor bugs (#4983) 2023-05-19 08:41:21 +09:00
Nicolas Dorier
acf003b1b4 Do not generate new address when a new payment is detected (#4984)
* Do not generate new address when a new payment is detected

* Update BTCPayServer.Tests/UnitTest1.cs

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

---------

Co-authored-by: Andrew Camilleri <evilkukka@gmail.com>
2023-05-18 16:53:01 +09:00
d11n
52e108d32f Minor design system updates (#4973)
- Update Manage Plugins icon
- Add ESC to supporters sprite
- Update body-text-active variable
2023-05-17 10:19:26 +02:00
Nicolas Dorier
7b96f96025 bump clightning (#4970)
* bump clightning

* Remove Lightning Charge from our tests
2023-05-16 09:17:21 +09:00
Kukks
8db5e7e043 Plugins: Allow payout processors to signal they cannot be removed through common UI 2023-05-15 09:49:13 +02:00
d11n
25fb5c1293 Checkout v2: Improve expired paid partial state (#4827)
Co-authored-by: Andrew Camilleri <evilkukka@gmail.com>
2023-05-11 10:38:40 +02:00
dstrukt
37f0498def adds payouts settings button (#3857)
Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2023-05-11 10:37:28 +02:00
d11n
02110f93d7 Hide sensitive info (#4966) 2023-05-11 10:35:51 +02:00
d11n
195dfc2c47 Refund updates (#4934) 2023-05-11 10:33:33 +02:00
d11n
541b6cf9eb Improve create first store case (#4951) 2023-05-10 11:18:29 +02:00
d11n
2c26b77afc Forms: Add multiline input (#4942) 2023-05-10 11:14:19 +02:00
nicolas.dorier
99bcec5597 bump nbx 2023-05-09 22:06:23 +09:00
Andrew Camilleri
781190a65d Bump to 1.9.3 (#4955)
* Bump to 1.9.3

* Apply suggestions from code review

* Update Changelog.md

* Update Changelog.md

* Update Changelog.md

* Update Changelog.md

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

* Update Changelog.md

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

* Update Changelog.md

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

---------

Co-authored-by: d11n <mail@dennisreimann.de>
2023-05-08 21:30:43 +09:00
d11n
3763480280 NFC: Handle HTTP-related exceptions (#4965) 2023-05-08 12:11:42 +02:00
Andrew Camilleri
6fad5ebedb Do not crash checkout when attempting lnurl checkout through non secure page (#4964)
Co-authored-by: d11n <mail@dennisreimann.de>
2023-05-08 12:09:48 +02:00
Andrew Camilleri
0690194aa1 Fix posdata with primitive array (#4954)
Co-authored-by: d11n <mail@dennisreimann.de>
2023-05-08 11:02:13 +02:00
nicolas.dorier
03b94e2be3 Minor refactoring about DefaultPaymentMethod 2023-05-08 09:14:58 +09:00
d11n
18e34b3cbe Checkout v2: Improve truncation of displayed addresses (#4924) 2023-05-05 10:00:55 +02:00
d11n
a0bb3ace61 LN Settings: Show only node host name (#4927) 2023-05-05 09:59:33 +02:00
Dennis Reimann
920ad67633 Rates: Fix advanced rules example formatting
Fixes #4920.
2023-05-05 09:58:42 +02:00
Dennis Reimann
8b8f72129c Crowdfund: Fix redirect URL fallback
As the request for invoice creation is issued via web socket, the display URL ends up being the hob connection URL. This replaces it with the actual app URL and fixes #4930.
2023-05-05 09:57:44 +02:00
Dennis Reimann
b9b11e722c Greenfield: Apply store default payment method on invoice creation
Fixes #4947.
2023-05-05 09:56:23 +02:00
480 changed files with 16534 additions and 24702 deletions

2
.github/codeql/codeql-config.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
paths-ignore:
- 'BTCPayServer/wwwroot/vendor/**/*.js'

80
.github/workflows/codeql.yml vendored Normal file
View File

@@ -0,0 +1,80 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
# Allow running tests manually. Usefull if scan failure, or need to rescan before next scheduled date.
workflow_dispatch:
# We scan only on a schedule for now, can uncomment the following to scan on commit or PR merge later on if deemed appropriate.
# push:
# branches: [ "master" ]
# pull_request:
# branches: [ "master" ]
schedule:
# Scan every Monday 06:00 UTC.
- cron: '0 6 * * 1'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'javascript', 'csharp' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Use only 'java' to analyze code written in Java, Kotlin or both
# Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
config-file: ./.github/codeql/codeql-config.yml
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
category: "/language:${{matrix.language}}"

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.Data.Common;
using BTCPayServer.Abstractions.Models; using BTCPayServer.Abstractions.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata;
@@ -21,7 +22,6 @@ namespace BTCPayServer.Abstractions.Contracts
} }
public abstract T CreateContext(); public abstract T CreateContext();
class CustomNpgsqlMigrationsSqlGenerator : NpgsqlMigrationsSqlGenerator class CustomNpgsqlMigrationsSqlGenerator : NpgsqlMigrationsSqlGenerator
{ {
#pragma warning disable EF1001 // Internal EF Core API usage. #pragma warning disable EF1001 // Internal EF Core API usage.

View File

@@ -1,3 +1,4 @@
using System;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace BTCPayServer.Abstractions.Contracts namespace BTCPayServer.Abstractions.Contracts
@@ -6,5 +7,8 @@ namespace BTCPayServer.Abstractions.Contracts
{ {
Task ApplyAction(string hook, object args); Task ApplyAction(string hook, object args);
Task<object> ApplyFilter(string hook, object args); Task<object> ApplyFilter(string hook, object args);
event EventHandler<(string hook, object args)> ActionInvoked;
event EventHandler<(string hook, object args)> FilterInvoked;
} }
} }

View File

@@ -69,7 +69,6 @@ public class Form
if (!nameReturned.Add(fullName)) if (!nameReturned.Add(fullName))
{ {
errors.Add($"Form contains duplicate field names '{fullName}'"); errors.Add($"Form contains duplicate field names '{fullName}'");
continue;
} }
} }
return errors.Count == 0; return errors.Count == 0;
@@ -86,18 +85,13 @@ public class Form
thisPath.Add(field.Name); thisPath.Add(field.Name);
yield return (thisPath, field); yield return (thisPath, field);
} }
foreach (var child in field.Fields)
{
if (field.Constant)
child.Constant = true;
foreach (var descendant in GetAllFieldsCore(thisPath, field.Fields)) foreach (var descendant in GetAllFieldsCore(thisPath, field.Fields))
{ {
descendant.Field.Constant = field.Constant || descendant.Field.Constant;
yield return descendant; yield return descendant;
} }
} }
} }
}
public void ApplyValuesFromForm(IEnumerable<KeyValuePair<string, StringValues>> form) public void ApplyValuesFromForm(IEnumerable<KeyValuePair<string, StringValues>> form)
{ {
@@ -111,31 +105,7 @@ public class Form
} }
} }
public void SetValues(JObject values)
{
var fields = GetAllFields().ToDictionary(k => k.FullName, k => k.Field);
SetValues(fields, new List<string>(), values);
}
private void SetValues(Dictionary<string, Field> fields, List<string> path, JObject values)
{
foreach (var prop in values.Properties())
{
List<string> propPath = new List<string>(path.Count + 1);
propPath.AddRange(path);
propPath.Add(prop.Name);
if (prop.Value.Type == JTokenType.Object)
{
SetValues(fields, propPath, (JObject)prop.Value);
}
else if (prop.Value.Type == JTokenType.String)
{
var fullName = string.Join('_', propPath.Where(s => !string.IsNullOrEmpty(s)));
if (fields.TryGetValue(fullName, out var f) && !f.Constant)
f.Value = prop.Value.Value<string>();
}
}
}
} }

View File

@@ -1,3 +1,4 @@
using System.Web;
using Ganss.XSS; using Ganss.XSS;
using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Rendering;
@@ -22,6 +23,11 @@ namespace BTCPayServer.Abstractions.Services
return _htmlHelper.Raw(_htmlSanitizer.Sanitize(value)); return _htmlHelper.Raw(_htmlSanitizer.Sanitize(value));
} }
public IHtmlContent RawEncode(string value)
{
return _htmlHelper.Raw(HttpUtility.HtmlEncode(_htmlSanitizer.Sanitize(value)));
}
public IHtmlContent Json(object model) public IHtmlContent Json(object model)
{ {
return _htmlHelper.Raw(_jsonHelper.Serialize(model)); return _htmlHelper.Raw(_jsonHelper.Serialize(model));

View File

@@ -6,31 +6,33 @@ using Microsoft.Extensions.Logging;
namespace BTCPayServer.Abstractions.TagHelpers; namespace BTCPayServer.Abstractions.TagHelpers;
[HtmlTargetElement(Attributes = nameof(Permission))] [HtmlTargetElement(Attributes = "[permission]")]
[HtmlTargetElement(Attributes = "[not-permission]" )]
public class PermissionTagHelper : TagHelper public class PermissionTagHelper : TagHelper
{ {
private readonly IAuthorizationService _authorizationService; private readonly IAuthorizationService _authorizationService;
private readonly IHttpContextAccessor _httpContextAccessor; private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ILogger<PermissionTagHelper> _logger;
public PermissionTagHelper(IAuthorizationService authorizationService, IHttpContextAccessor httpContextAccessor, ILogger<PermissionTagHelper> logger) public PermissionTagHelper(IAuthorizationService authorizationService, IHttpContextAccessor httpContextAccessor)
{ {
_authorizationService = authorizationService; _authorizationService = authorizationService;
_httpContextAccessor = httpContextAccessor; _httpContextAccessor = httpContextAccessor;
_logger = logger;
} }
public string Permission { get; set; } public string Permission { get; set; }
public string NotPermission { get; set; }
public string PermissionResource { get; set; } public string PermissionResource { get; set; }
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{ {
if (string.IsNullOrEmpty(Permission)) if (string.IsNullOrEmpty(Permission) && string.IsNullOrEmpty(NotPermission))
return; return;
if (_httpContextAccessor.HttpContext is null) if (_httpContextAccessor.HttpContext is null)
return; return;
var key = $"{Permission}_{PermissionResource}"; var expectedResult = !string.IsNullOrEmpty(Permission);
var key = $"{Permission??NotPermission}_{PermissionResource}";
if (!_httpContextAccessor.HttpContext.Items.TryGetValue(key, out var o) || if (!_httpContextAccessor.HttpContext.Items.TryGetValue(key, out var o) ||
o is not AuthorizationResult res) o is not AuthorizationResult res)
{ {
@@ -39,7 +41,7 @@ public class PermissionTagHelper : TagHelper
Permission); Permission);
_httpContextAccessor.HttpContext.Items.Add(key, res); _httpContextAccessor.HttpContext.Items.Add(key, res);
} }
if (!res.Succeeded) if (expectedResult != res.Succeeded)
{ {
output.SuppressOutput(); output.SuppressOutput();
} }

View File

@@ -12,9 +12,11 @@
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
<RepositoryUrl>https://github.com/btcpayserver/btcpayserver</RepositoryUrl> <RepositoryUrl>https://github.com/btcpayserver/btcpayserver</RepositoryUrl>
<RepositoryType>git</RepositoryType> <RepositoryType>git</RepositoryType>
<Configurations>Debug;Release;Altcoins-Debug;Altcoins-Release</Configurations>
<Platforms>AnyCPU</Platforms>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<Version Condition=" '$(Version)' == '' ">1.7.2</Version> <Version Condition=" '$(Version)' == '' ">1.7.3</Version>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' "> <PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<PublishRepositoryUrl>true</PublishRepositoryUrl> <PublishRepositoryUrl>true</PublishRepositoryUrl>
@@ -30,7 +32,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.3.21" /> <PackageReference Include="BTCPayServer.Lightning.Common" Version="1.3.21" />
<PackageReference Include="NBitcoin" Version="7.0.24" /> <PackageReference Include="NBitcoin" Version="7.0.24" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="icon.png" Pack="true" PackagePath="\" /> <None Include="icon.png" Pack="true" PackagePath="\" />

View File

@@ -55,7 +55,7 @@ namespace BTCPayServer.Client
} }
public virtual async Task<IEnumerable<OnChainWalletTransactionData>> ShowOnChainWalletTransactions( public virtual async Task<IEnumerable<OnChainWalletTransactionData>> ShowOnChainWalletTransactions(
string storeId, string cryptoCode, TransactionStatus[] statusFilter = null, string labelFilter = null, string storeId, string cryptoCode, TransactionStatus[] statusFilter = null, string labelFilter = null, int skip = 0,
CancellationToken token = default) CancellationToken token = default)
{ {
var query = new Dictionary<string, object>(); var query = new Dictionary<string, object>();
@@ -67,6 +67,10 @@ namespace BTCPayServer.Client
{ {
query.Add(nameof(labelFilter), labelFilter); query.Add(nameof(labelFilter), labelFilter);
} }
if (skip != 0)
{
query.Add(nameof(skip), skip);
}
var response = var response =
await _httpClient.SendAsync( await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/transactions", query), token); CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/transactions", query), token);

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Client.Models; using BTCPayServer.Client.Models;
@@ -11,5 +12,11 @@ namespace BTCPayServer.Client
var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/server/info"), token); var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/server/info"), token);
return await HandleResponse<ServerInfoData>(response); return await HandleResponse<ServerInfoData>(response);
} }
public virtual async Task<List<RoleData>> GetServerRoles(CancellationToken token = default)
{
using var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/server/roles"), token);
return await HandleResponse<List<RoleData>>(response);
}
} }
} }

View File

@@ -9,6 +9,13 @@ namespace BTCPayServer.Client
{ {
public partial class BTCPayServerClient public partial class BTCPayServerClient
{ {
public virtual async Task<List<RoleData>> GetStoreRoles(string storeId,
CancellationToken token = default)
{
using var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/roles"), token);
return await HandleResponse<List<RoleData>>(response);
}
public virtual async Task<IEnumerable<StoreUserData>> GetStoreUsers(string storeId, public virtual async Task<IEnumerable<StoreUserData>> GetStoreUsers(string storeId,
CancellationToken token = default) CancellationToken token = default)
{ {

View File

@@ -51,7 +51,8 @@ namespace BTCPayServer.Client
{ {
if (message.StatusCode == System.Net.HttpStatusCode.UnprocessableEntity) if (message.StatusCode == System.Net.HttpStatusCode.UnprocessableEntity)
{ {
var err = JsonConvert.DeserializeObject<Models.GreenfieldValidationError[]>(await message.Content.ReadAsStringAsync()); var aa = await message.Content.ReadAsStringAsync();
var err = JsonConvert.DeserializeObject<Models.GreenfieldValidationError[]>(aa);
throw new GreenfieldValidationException(err); throw new GreenfieldValidationException(err);
} }
if (message.StatusCode == System.Net.HttpStatusCode.Forbidden) if (message.StatusCode == System.Net.HttpStatusCode.Forbidden)

View File

@@ -37,6 +37,7 @@ namespace BTCPayServer.Client.Models
public string RedirectUrl { get; set; } = null; public string RedirectUrl { get; set; } = null;
public bool? RedirectAutomatically { get; set; } = null; public bool? RedirectAutomatically { get; set; } = null;
public bool? RequiresRefundEmail { get; set; } = null; public bool? RequiresRefundEmail { get; set; } = null;
public bool? Archived { get; set; } = null;
public string FormId { get; set; } = null; public string FormId { get; set; } = null;
public string EmbeddedCSS { get; set; } = null; public string EmbeddedCSS { get; set; } = null;
public CheckoutType? CheckoutType { get; set; } = null; public CheckoutType? CheckoutType { get; set; } = null;
@@ -78,6 +79,7 @@ namespace BTCPayServer.Client.Models
public bool? DisplayPerksValue { get; set; } = null; public bool? DisplayPerksValue { get; set; } = null;
public bool? DisplayPerksRanking { get; set; } = null; public bool? DisplayPerksRanking { get; set; } = null;
public bool? SortPerksByPopularity { get; set; } = null; public bool? SortPerksByPopularity { get; set; } = null;
public bool? Archived { get; set; } = null;
public string[] Sounds { get; set; } = null; public string[] Sounds { get; set; } = null;
public string[] AnimationColors { get; set; } = null; public string[] AnimationColors { get; set; } = null;
} }

View File

@@ -1,8 +1,11 @@
#nullable enable #nullable enable
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client.Models; namespace BTCPayServer.Client.Models;
public class CreatePayoutThroughStoreRequest : CreatePayoutRequest public class CreatePayoutThroughStoreRequest : CreatePayoutRequest
{ {
public string? PullPaymentId { get; set; } public string? PullPaymentId { get; set; }
public bool? Approved { get; set; } public bool? Approved { get; set; }
public JObject? Metadata { get; set; }
} }

View File

@@ -1,5 +1,4 @@
namespace BTCPayServer.Client.Models namespace BTCPayServer.Client.Models;
{
public enum InvoiceExceptionStatus public enum InvoiceExceptionStatus
{ {
None, None,
@@ -9,4 +8,5 @@ namespace BTCPayServer.Client.Models
Invalid, Invalid,
PaidOver PaidOver
} }
}

View File

@@ -10,4 +10,9 @@ public class LightningAutomatedPayoutSettings
[JsonConverter(typeof(TimeSpanJsonConverter.Seconds))] [JsonConverter(typeof(TimeSpanJsonConverter.Seconds))]
public TimeSpan IntervalSeconds { get; set; } public TimeSpan IntervalSeconds { get; set; }
public int? CancelPayoutAfterFailures { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
public bool ProcessNewPayoutsInstantly { get; set; }
} }

View File

@@ -12,4 +12,8 @@ public class OnChainAutomatedPayoutSettings
public TimeSpan IntervalSeconds { get; set; } public TimeSpan IntervalSeconds { get; set; }
public int? FeeBlockTarget { get; set; } public int? FeeBlockTarget { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
public decimal Threshold { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
public bool ProcessNewPayoutsInstantly { get; set; }
} }

View File

@@ -7,7 +7,7 @@ namespace BTCPayServer.Client.Models
public class PaymentRequestData : PaymentRequestBaseData public class PaymentRequestData : PaymentRequestBaseData
{ {
[JsonConverter(typeof(StringEnumConverter))] [JsonConverter(typeof(StringEnumConverter))]
public PaymentRequestData.PaymentRequestStatus Status { get; set; } public PaymentRequestStatus Status { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))] [JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
public DateTimeOffset CreatedTime { get; set; } public DateTimeOffset CreatedTime { get; set; }
public string Id { get; set; } public string Id { get; set; }
@@ -16,7 +16,8 @@ namespace BTCPayServer.Client.Models
{ {
Pending = 0, Pending = 0,
Completed = 1, Completed = 1,
Expired = 2 Expired = 2,
Processing = 3
} }
} }
} }

View File

@@ -31,5 +31,6 @@ namespace BTCPayServer.Client.Models
public PayoutState State { get; set; } public PayoutState State { get; set; }
public int Revision { get; set; } public int Revision { get; set; }
public JObject PaymentProof { get; set; } public JObject PaymentProof { get; set; }
public JObject Metadata { get; set; }
} }
} }

View File

@@ -9,6 +9,8 @@ namespace BTCPayServer.Client.Models
public string AppType { get; set; } public string AppType { get; set; }
public string Name { get; set; } public string Name { get; set; }
public string StoreId { get; set; } public string StoreId { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public bool? Archived { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))] [JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
public DateTimeOffset Created { get; set; } public DateTimeOffset Created { get; set; }
} }

View File

@@ -9,6 +9,7 @@ namespace BTCPayServer.Client.Models
{ {
RateThen, RateThen,
CurrentRate, CurrentRate,
OverpaidAmount,
Fiat, Fiat,
Custom Custom
} }
@@ -18,8 +19,13 @@ namespace BTCPayServer.Client.Models
public string? Name { get; set; } = null; public string? Name { get; set; } = null;
public string? PaymentMethod { get; set; } public string? PaymentMethod { get; set; }
public string? Description { get; set; } = null; public string? Description { get; set; } = null;
[JsonConverter(typeof(StringEnumConverter))] [JsonConverter(typeof(StringEnumConverter))]
public RefundVariant? RefundVariant { get; set; } public RefundVariant? RefundVariant { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal SubtractPercentage { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))] [JsonConverter(typeof(NumericStringJsonConverter))]
public decimal? CustomAmount { get; set; } public decimal? CustomAmount { get; set; }
public string? CustomCurrency { get; set; } public string? CustomCurrency { get; set; }

View File

@@ -16,6 +16,8 @@ namespace BTCPayServer.Client.Models
public string Website { get; set; } public string Website { get; set; }
public string SupportUrl { get; set; }
[JsonConverter(typeof(TimeSpanJsonConverter.Seconds))] [JsonConverter(typeof(TimeSpanJsonConverter.Seconds))]
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)] [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public TimeSpan InvoiceExpiration { get; set; } = TimeSpan.FromMinutes(15); public TimeSpan InvoiceExpiration { get; set; } = TimeSpan.FromMinutes(15);
@@ -43,6 +45,9 @@ namespace BTCPayServer.Client.Models
public bool LazyPaymentMethods { get; set; } public bool LazyPaymentMethods { get; set; }
public bool RedirectAutomatically { get; set; } public bool RedirectAutomatically { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public bool Archived { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)] [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public bool ShowRecommendedFee { get; set; } = true; public bool ShowRecommendedFee { get; set; } = true;
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)] [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]

View File

@@ -1,3 +1,5 @@
using System.Collections.Generic;
namespace BTCPayServer.Client.Models namespace BTCPayServer.Client.Models
{ {
public class StoreData : StoreBaseData public class StoreData : StoreBaseData
@@ -17,4 +19,12 @@ namespace BTCPayServer.Client.Models
public string Role { get; set; } public string Role { get; set; }
} }
public class RoleData
{
public string Id { get; set; }
public List<string> Permissions { get; set; }
public string Role { get; set; }
public bool IsServerRole { get; set; }
}
} }

View File

@@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using BTCPayServer.JsonConverters;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client.Models;
public class StoreReportRequest
{
public string ViewName { get; set; }
public TimePeriod TimePeriod { get; set; }
}
public class StoreReportResponse
{
public class Field
{
public Field()
{
}
public Field(string name, string type)
{
Name = name;
Type = type;
}
public string Name { get; set; }
public string Type { get; set; }
}
public IList<Field> Fields { get; set; } = new List<Field>();
public List<JArray> Data { get; set; }
public DateTimeOffset From { get; set; }
public DateTimeOffset To { get; set; }
public List<ChartDefinition> Charts { get; set; }
public int GetIndex(string fieldName)
{
return Fields.ToList().FindIndex(f => f.Name == fieldName);
}
}
public class ChartDefinition
{
public string Name { get; set; }
public List<string> Groups { get; set; } = new List<string>();
public List<string> Totals { get; set; } = new List<string>();
public bool HasGrandTotal { get; set; }
public List<string> Aggregates { get; set; } = new List<string>();
public List<string> Filters { get; set; } = new List<string>();
}
public class TimePeriod
{
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
public DateTimeOffset? From { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
public DateTimeOffset? To { get; set; }
}

View File

@@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace BTCPayServer.Client.Models
{
public class StoreReportsResponse
{
public string ViewName { get; set; }
public StoreReportResponse.Field[] Fields
{
get;
set;
}
}
}

View File

@@ -51,6 +51,10 @@ namespace BTCPayServer.Client.Models
public DateTimeOffset Timestamp { get; set; } public DateTimeOffset Timestamp { get; set; }
[JsonExtensionData] [JsonExtensionData]
public IDictionary<string, JToken> AdditionalData { get; set; } public IDictionary<string, JToken> AdditionalData { get; set; }
public bool IsPruned()
{
return DeliveryId is null;
}
public T ReadAs<T>() public T ReadAs<T>()
{ {
var str = JsonConvert.SerializeObject(this, DefaultSerializerSettings); var str = JsonConvert.SerializeObject(this, DefaultSerializerSettings);

View File

@@ -1,6 +1,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using System.Linq.Expressions;
namespace BTCPayServer.Client namespace BTCPayServer.Client
{ {
@@ -31,6 +33,7 @@ namespace BTCPayServer.Client
public const string CanManageUsers = "btcpay.server.canmanageusers"; public const string CanManageUsers = "btcpay.server.canmanageusers";
public const string CanDeleteUser = "btcpay.user.candeleteuser"; public const string CanDeleteUser = "btcpay.user.candeleteuser";
public const string CanManagePullPayments = "btcpay.store.canmanagepullpayments"; public const string CanManagePullPayments = "btcpay.store.canmanagepullpayments";
public const string CanArchivePullPayments = "btcpay.store.canarchivepullpayments";
public const string CanCreatePullPayments = "btcpay.store.cancreatepullpayments"; public const string CanCreatePullPayments = "btcpay.store.cancreatepullpayments";
public const string CanCreateNonApprovedPullPayments = "btcpay.store.cancreatenonapprovedpullpayments"; public const string CanCreateNonApprovedPullPayments = "btcpay.store.cancreatenonapprovedpullpayments";
public const string CanViewCustodianAccounts = "btcpay.store.canviewcustodianaccounts"; public const string CanViewCustodianAccounts = "btcpay.store.canviewcustodianaccounts";
@@ -67,6 +70,7 @@ namespace BTCPayServer.Client
yield return CanViewLightningInvoiceInStore; yield return CanViewLightningInvoiceInStore;
yield return CanCreateLightningInvoiceInStore; yield return CanCreateLightningInvoiceInStore;
yield return CanManagePullPayments; yield return CanManagePullPayments;
yield return CanArchivePullPayments;
yield return CanCreatePullPayments; yield return CanCreatePullPayments;
yield return CanCreateNonApprovedPullPayments; yield return CanCreateNonApprovedPullPayments;
yield return CanViewCustodianAccounts; yield return CanViewCustodianAccounts;
@@ -134,7 +138,7 @@ namespace BTCPayServer.Client
{ {
static Permission() static Permission()
{ {
Init(); PolicyMap = Init();
} }
public static Permission Create(string policy, string scope = null) public static Permission Create(string policy, string scope = null)
@@ -235,11 +239,13 @@ namespace BTCPayServer.Client
return subPolicies.Contains(subpolicy) || subPolicies.Any(s => ContainsPolicy(s, subpolicy)); return subPolicies.Contains(subpolicy) || subPolicies.Any(s => ContainsPolicy(s, subpolicy));
} }
private static Dictionary<string, HashSet<string>> PolicyMap = new(); public static ReadOnlyDictionary<string, HashSet<string>> PolicyMap { get; private set; }
private static void Init()
private static ReadOnlyDictionary<string, HashSet<string>> Init()
{ {
PolicyHasChild(Policies.CanModifyStoreSettings, var policyMap = new Dictionary<string, HashSet<string>>();
PolicyHasChild(policyMap, Policies.CanModifyStoreSettings,
Policies.CanManageCustodianAccounts, Policies.CanManageCustodianAccounts,
Policies.CanManagePullPayments, Policies.CanManagePullPayments,
Policies.CanModifyInvoices, Policies.CanModifyInvoices,
@@ -248,25 +254,42 @@ namespace BTCPayServer.Client
Policies.CanModifyPaymentRequests, Policies.CanModifyPaymentRequests,
Policies.CanUseLightningNodeInStore); Policies.CanUseLightningNodeInStore);
PolicyHasChild(Policies.CanManageUsers, Policies.CanCreateUser); PolicyHasChild(policyMap,Policies.CanManageUsers, Policies.CanCreateUser);
PolicyHasChild(Policies.CanManagePullPayments, Policies.CanCreatePullPayments); PolicyHasChild(policyMap,Policies.CanManagePullPayments, Policies.CanCreatePullPayments, Policies.CanArchivePullPayments);
PolicyHasChild(Policies.CanCreatePullPayments, Policies.CanCreateNonApprovedPullPayments); PolicyHasChild(policyMap,Policies.CanCreatePullPayments, Policies.CanCreateNonApprovedPullPayments);
PolicyHasChild(Policies.CanModifyPaymentRequests, Policies.CanViewPaymentRequests); PolicyHasChild(policyMap,Policies.CanModifyPaymentRequests, Policies.CanViewPaymentRequests);
PolicyHasChild(Policies.CanModifyProfile, Policies.CanViewProfile); PolicyHasChild(policyMap,Policies.CanModifyProfile, Policies.CanViewProfile);
PolicyHasChild(Policies.CanUseLightningNodeInStore, Policies.CanViewLightningInvoiceInStore, Policies.CanCreateLightningInvoiceInStore); PolicyHasChild(policyMap,Policies.CanUseLightningNodeInStore, Policies.CanViewLightningInvoiceInStore, Policies.CanCreateLightningInvoiceInStore);
PolicyHasChild(Policies.CanManageNotificationsForUser, Policies.CanViewNotificationsForUser); PolicyHasChild(policyMap,Policies.CanManageNotificationsForUser, Policies.CanViewNotificationsForUser);
PolicyHasChild(Policies.CanModifyServerSettings, PolicyHasChild(policyMap,Policies.CanModifyServerSettings,
Policies.CanUseInternalLightningNode, Policies.CanUseInternalLightningNode,
Policies.CanManageUsers); Policies.CanManageUsers);
PolicyHasChild(Policies.CanUseInternalLightningNode, Policies.CanCreateLightningInvoiceInternalNode, Policies.CanViewLightningInvoiceInternalNode); PolicyHasChild(policyMap, Policies.CanUseInternalLightningNode, Policies.CanCreateLightningInvoiceInternalNode, Policies.CanViewLightningInvoiceInternalNode);
PolicyHasChild(Policies.CanManageCustodianAccounts, Policies.CanViewCustodianAccounts); PolicyHasChild(policyMap, Policies.CanManageCustodianAccounts, Policies.CanViewCustodianAccounts);
PolicyHasChild(Policies.CanModifyInvoices, Policies.CanViewInvoices, Policies.CanCreateInvoice, Policies.CanCreateLightningInvoiceInStore); PolicyHasChild(policyMap, Policies.CanModifyInvoices, Policies.CanViewInvoices, Policies.CanCreateInvoice, Policies.CanCreateLightningInvoiceInStore);
PolicyHasChild(Policies.CanViewStoreSettings, Policies.CanViewInvoices, Policies.CanViewPaymentRequests); PolicyHasChild(policyMap, Policies.CanViewStoreSettings, Policies.CanViewInvoices, Policies.CanViewPaymentRequests);
var missingPolicies = Policies.AllPolicies.ToHashSet();
//recurse through the tree to see which policies are not included in the tree
foreach (var policy in policyMap)
{
missingPolicies.Remove(policy.Key);
foreach (var subPolicy in policy.Value)
{
missingPolicies.Remove(subPolicy);
}
} }
private static void PolicyHasChild(string policy, params string[] subPolicies) foreach (var missingPolicy in missingPolicies)
{ {
if (PolicyMap.TryGetValue(policy, out var existingSubPolicies)) policyMap.Add(missingPolicy, new HashSet<string>());
}
return new ReadOnlyDictionary<string, HashSet<string>>(policyMap);
}
private static void PolicyHasChild(Dictionary<string, HashSet<string>>policyMap, string policy, params string[] subPolicies)
{
if (policyMap.TryGetValue(policy, out var existingSubPolicies))
{ {
foreach (string subPolicy in subPolicies) foreach (string subPolicy in subPolicies)
{ {
@@ -275,7 +298,7 @@ namespace BTCPayServer.Client
} }
else else
{ {
PolicyMap.Add(policy, subPolicies.ToHashSet()); policyMap.Add(policy, subPolicies.ToHashSet());
} }
} }

View File

@@ -16,7 +16,7 @@ namespace BTCPayServer
DefaultRateRules = new[] DefaultRateRules = new[]
{ {
"BTG_X = BTG_BTC * BTC_X", "BTG_X = BTG_BTC * BTC_X",
"BTG_BTC = bitfinex(BTG_BTC)", "BTG_BTC = gate(BTG_BTC)",
}, },
CryptoImagePath = "imlegacy/btg.svg", CryptoImagePath = "imlegacy/btg.svg",
LightningImagePath = "imlegacy/btg-lightning.svg", LightningImagePath = "imlegacy/btg-lightning.svg",

View File

@@ -17,7 +17,7 @@ namespace BTCPayServer
DefaultRateRules = new[] DefaultRateRules = new[]
{ {
"BTX_X = BTX_BTC * BTC_X", "BTX_X = BTX_BTC * BTC_X",
"BTX_BTC = hitbtc(BTX_BTC)" "BTX_BTC = graviex(BTX_BTC)"
}, },
CryptoImagePath = "imlegacy/bitcore.svg", CryptoImagePath = "imlegacy/bitcore.svg",
LightningImagePath = "imlegacy/bitcore-lightning.svg", LightningImagePath = "imlegacy/bitcore-lightning.svg",

View File

@@ -1,32 +0,0 @@
using NBitcoin;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
public void InitChaincoin()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("CHC");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Chaincoin",
BlockExplorerLink = NetworkType == ChainName.Mainnet
? "https://explorer.chaincoin.org/Explorer/Transaction/{0}"
: "https://test.explorer.chaincoin.org/Explorer/Transaction/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
DefaultRateRules = new[]
{
"CHC_X = CHC_BTC * BTC_X",
"CHC_BTC = txbit(CHC_X)"
},
CryptoImagePath = "imlegacy/chaincoin.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
//https://github.com/satoshilabs/slips/blob/master/slip-0044.md
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("711'")
: new KeyPath("1'")
});
}
}
}

View File

@@ -19,7 +19,7 @@ namespace BTCPayServer
"USDT_X = USDT_BTC * BTC_X", "USDT_X = USDT_BTC * BTC_X",
"USDT_BTC = bitfinex(UST_BTC)", "USDT_BTC = bitfinex(UST_BTC)",
}, },
AssetId = new uint256("ce091c998b83c78bb71a632313ba3760f1763d9cfcffae02258ffa9865a37bd2"), AssetId = NetworkType == ChainName.Regtest? null: new uint256("ce091c998b83c78bb71a632313ba3760f1763d9cfcffae02258ffa9865a37bd2"),
DisplayName = "Liquid Tether", DisplayName = "Liquid Tether",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://liquid.network/tx/{0}" : "https://liquid.network/testnet/tx/{0}", BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://liquid.network/tx/{0}" : "https://liquid.network/testnet/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork, NBXplorerNetwork = nbxplorerNetwork,
@@ -42,7 +42,7 @@ namespace BTCPayServer
"ETB_BTC = bitpay(ETB_BTC)" "ETB_BTC = bitpay(ETB_BTC)"
}, },
Divisibility = 2, Divisibility = 2,
AssetId = new uint256("aa775044c32a7df391902b3659f46dfe004ccb2644ce2ddc7dba31e889391caf"), AssetId = NetworkType == ChainName.Regtest? null: new uint256("aa775044c32a7df391902b3659f46dfe004ccb2644ce2ddc7dba31e889391caf"),
DisplayName = "Ethiopian Birr", DisplayName = "Ethiopian Birr",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://liquid.network/tx/{0}" : "https://liquid.network/testnet/tx/{0}", BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://liquid.network/tx/{0}" : "https://liquid.network/testnet/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork, NBXplorerNetwork = nbxplorerNetwork,
@@ -63,8 +63,9 @@ namespace BTCPayServer
"LCAD_CAD = 1", "LCAD_CAD = 1",
"LCAD_X = CAD_BTC * BTC_X", "LCAD_X = CAD_BTC * BTC_X",
"LCAD_BTC = bylls(CAD_BTC)", "LCAD_BTC = bylls(CAD_BTC)",
"CAD_BTC = LCAD_BTC"
}, },
AssetId = new uint256("0e99c1a6da379d1f4151fb9df90449d40d0608f6cb33a5bcbfc8c265f42bab0a"), AssetId = NetworkType == ChainName.Regtest? null: new uint256("0e99c1a6da379d1f4151fb9df90449d40d0608f6cb33a5bcbfc8c265f42bab0a"),
DisplayName = "Liquid CAD", DisplayName = "Liquid CAD",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://liquid.network/tx/{0}" : "https://liquid.network/testnet/tx/{0}", BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://liquid.network/tx/{0}" : "https://liquid.network/testnet/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork, NBXplorerNetwork = nbxplorerNetwork,

View File

@@ -1,4 +1,5 @@
#if ALTCOINS #if ALTCOINS
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using BTCPayServer.Common; using BTCPayServer.Common;
@@ -18,7 +19,8 @@ namespace BTCPayServer
NewTransactionEvent evtOutputs) NewTransactionEvent evtOutputs)
{ {
return evtOutputs.Outputs.Where(output => return evtOutputs.Outputs.Where(output =>
output.Value is AssetMoney assetMoney && assetMoney.AssetId == AssetId).Select(output => (output.Value is not AssetMoney && NetworkCryptoCode.Equals(evtOutputs.CryptoCode, StringComparison.InvariantCultureIgnoreCase)) ||
(output.Value is AssetMoney assetMoney && assetMoney.AssetId == AssetId)).Select(output =>
{ {
var outpoint = new OutPoint(evtOutputs.TransactionData.TransactionHash, output.Index); var outpoint = new OutPoint(evtOutputs.TransactionData.TransactionHash, output.Index);
return (output, outpoint); return (output, outpoint);
@@ -34,12 +36,12 @@ namespace BTCPayServer
output.Value is AssetMoney assetMoney && assetMoney.AssetId == AssetId)); output.Value is AssetMoney assetMoney && assetMoney.AssetId == AssetId));
} }
public override PaymentUrlBuilder GenerateBIP21(string cryptoInfoAddress, Money cryptoInfoDue) public override PaymentUrlBuilder GenerateBIP21(string cryptoInfoAddress, decimal? cryptoInfoDue)
{ {
//precision 0: 10 = 0.00000010 //precision 0: 10 = 0.00000010
//precision 2: 10 = 0.00001000 //precision 2: 10 = 0.00001000
//precision 8: 10 = 10 //precision 8: 10 = 10
var money = cryptoInfoDue is null ? null : new Money(cryptoInfoDue.ToDecimal(MoneyUnit.BTC) / decimal.Parse("1".PadRight(1 + 8 - Divisibility, '0')), MoneyUnit.BTC); var money = cryptoInfoDue / (decimal)Math.Pow(10, 8 - Divisibility);
var builder = base.GenerateBIP21(cryptoInfoAddress, money); var builder = base.GenerateBIP21(cryptoInfoAddress, money);
builder.QueryParams.Add("assetid", AssetId.ToString()); builder.QueryParams.Add("assetid", AssetId.ToString());
return builder; return builder;

View File

@@ -45,10 +45,10 @@ namespace BTCPayServer.Services.Altcoins.Monero.RPC
httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Basic", httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Basic",
Convert.ToBase64String(Encoding.Default.GetBytes($"{_username}:{_password}"))); Convert.ToBase64String(Encoding.Default.GetBytes($"{_username}:{_password}")));
var rawResult = await _httpClient.SendAsync(httpRequest, cts); HttpResponseMessage rawResult = await _httpClient.SendAsync(httpRequest, cts);
var rawJson = await rawResult.Content.ReadAsStringAsync();
rawResult.EnsureSuccessStatusCode(); rawResult.EnsureSuccessStatusCode();
var rawJson = await rawResult.Content.ReadAsStringAsync();
JsonRpcResult<TResponse> response; JsonRpcResult<TResponse> response;
try try
{ {

View File

@@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using BTCPayServer.Common; using BTCPayServer.Common;
@@ -87,13 +88,13 @@ namespace BTCPayServer
}); });
} }
public virtual PaymentUrlBuilder GenerateBIP21(string cryptoInfoAddress, Money cryptoInfoDue) public virtual PaymentUrlBuilder GenerateBIP21(string cryptoInfoAddress, decimal? cryptoInfoDue)
{ {
var builder = new PaymentUrlBuilder(this.NBitcoinNetwork.UriScheme); var builder = new PaymentUrlBuilder(this.NBitcoinNetwork.UriScheme);
builder.Host = cryptoInfoAddress; builder.Host = cryptoInfoAddress;
if (cryptoInfoDue != null && cryptoInfoDue != Money.Zero) if (cryptoInfoDue is not null && cryptoInfoDue.Value != 0.0m)
{ {
builder.QueryParams.Add("amount", cryptoInfoDue.ToString(false, true)); builder.QueryParams.Add("amount", cryptoInfoDue.Value.ToString(CultureInfo.InvariantCulture));
} }
return builder; return builder;
} }

View File

@@ -56,7 +56,6 @@ namespace BTCPayServer
InitViacoin(); InitViacoin();
InitMonero(); InitMonero();
InitZcash(); InitZcash();
InitChaincoin();
// InitArgoneum();//their rate source is down 9/15/20. // InitArgoneum();//their rate source is down 9/15/20.
// InitMonetaryUnit(); Not supported from Bittrex from 11/23/2022, dead shitcoin // InitMonetaryUnit(); Not supported from Bittrex from 11/23/2022, dead shitcoin

View File

@@ -4,7 +4,7 @@
<ItemGroup> <ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" /> <FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="NBXplorer.Client" Version="4.2.3" /> <PackageReference Include="NBXplorer.Client" Version="4.2.5" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="2.0.1" /> <PackageReference Include="NicolasDorier.StandardConfiguration" Version="2.0.1" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition="'$(Altcoins)' != 'true'"> <ItemGroup Condition="'$(Altcoins)' != 'true'">

View File

@@ -64,6 +64,7 @@ namespace BTCPayServer.Data
public DbSet<U2FDevice> U2FDevices { get; set; } public DbSet<U2FDevice> U2FDevices { get; set; }
public DbSet<Fido2Credential> Fido2Credentials { get; set; } public DbSet<Fido2Credential> Fido2Credentials { get; set; }
public DbSet<UserStore> UserStore { get; set; } public DbSet<UserStore> UserStore { get; set; }
public DbSet<StoreRole> StoreRoles { get; set; }
[Obsolete] [Obsolete]
public DbSet<WalletData> Wallets { get; set; } public DbSet<WalletData> Wallets { get; set; }
public DbSet<WalletObjectData> WalletObjects { get; set; } public DbSet<WalletObjectData> WalletObjects { get; set; }
@@ -129,6 +130,7 @@ namespace BTCPayServer.Data
PayoutProcessorData.OnModelCreating(builder, Database); PayoutProcessorData.OnModelCreating(builder, Database);
WebhookData.OnModelCreating(builder, Database); WebhookData.OnModelCreating(builder, Database);
FormData.OnModelCreating(builder, Database); FormData.OnModelCreating(builder, Database);
StoreRole.OnModelCreating(builder, Database);
if (Database.IsSqlite() && !_designTime) if (Database.IsSqlite() && !_designTime)

View File

@@ -1,3 +1,6 @@
using System;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Models; using BTCPayServer.Abstractions.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;

View File

@@ -14,6 +14,7 @@ namespace BTCPayServer.Data
public DateTimeOffset Created { get; set; } public DateTimeOffset Created { get; set; }
public bool TagAllInvoices { get; set; } public bool TagAllInvoices { get; set; }
public string Settings { get; set; } public string Settings { get; set; }
public bool Archived { get; set; }
internal static void OnModelCreating(ModelBuilder builder) internal static void OnModelCreating(ModelBuilder builder)
{ {

View File

@@ -1,6 +1,8 @@
using System; using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
namespace BTCPayServer.Data; namespace BTCPayServer.Data;
@@ -41,4 +43,7 @@ public class LightningAddressDataBlob
public decimal? Max { get; set; } public decimal? Max { get; set; }
public JObject InvoiceMetadata { get; set; } public JObject InvoiceMetadata { get; set; }
[JsonExtensionData] public Dictionary<string, JToken> AdditionalData { get; set; }
} }

View File

@@ -8,6 +8,7 @@ namespace BTCPayServer.Data;
public class AutomatedPayoutBlob public class AutomatedPayoutBlob
{ {
public TimeSpan Interval { get; set; } = TimeSpan.FromHours(1); public TimeSpan Interval { get; set; } = TimeSpan.FromHours(1);
public bool ProcessNewPayoutsInstantly { get; set; }
} }
public class PayoutProcessorData : IHasBlobUntyped public class PayoutProcessorData : IHasBlobUntyped
{ {

View File

@@ -1,13 +1,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text; using System.Text;
using BTCPayServer.Client;
using BTCPayServer.Client.Models; using BTCPayServer.Client.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using PayoutProcessorData = BTCPayServer.Data.PayoutProcessorData;
namespace BTCPayServer.Data namespace BTCPayServer.Data
{ {
@@ -37,8 +34,6 @@ namespace BTCPayServer.Data
public byte[] StoreCertificate { get; set; } public byte[] StoreCertificate { get; set; }
[NotMapped] public string Role { get; set; }
public string StoreBlob { get; set; } public string StoreBlob { get; set; }
[Obsolete("Use GetDefaultPaymentId instead")] [Obsolete("Use GetDefaultPaymentId instead")]
@@ -52,6 +47,8 @@ namespace BTCPayServer.Data
public IEnumerable<CustodianAccountData> CustodianAccounts { get; set; } public IEnumerable<CustodianAccountData> CustodianAccounts { get; set; }
public IEnumerable<StoreSettingData> Settings { get; set; } public IEnumerable<StoreSettingData> Settings { get; set; }
public IEnumerable<FormData> Forms { get; set; } public IEnumerable<FormData> Forms { get; set; }
public IEnumerable<StoreRole> StoreRoles { get; set; }
public bool Archived { get; set; }
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade) internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
{ {

View File

@@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Newtonsoft.Json;
namespace BTCPayServer.Data;
public class StoreRole
{
public string Id { get; set; }
public string StoreDataId { get; set; }
public string Role { get; set; }
public List<string> Permissions { get; set; }
public List<UserStore> Users { get; set; }
public StoreData StoreData { get; set; }
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
{
builder.Entity<StoreRole>(entity =>
{
entity.HasOne(e => e.StoreData)
.WithMany(s => s.StoreRoles)
.HasForeignKey(e => e.StoreDataId)
.OnDelete(DeleteBehavior.Cascade)
.IsRequired(false);
entity.HasIndex(entity => new {entity.StoreDataId, entity.Role}).IsUnique();
});
if (!databaseFacade.IsNpgsql())
{
builder.Entity<StoreRole>()
.Property(o => o.Permissions)
.HasConversion(
v => JsonConvert.SerializeObject(v),
v => JsonConvert.DeserializeObject<List<string>>(v)?? new List<string>(),
new ValueComparer<List<string>>(
(c1, c2) => c1 ==c2 || c1 != null && c2 != null && c1.SequenceEqual(c2),
c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
c => c.ToList()));
}
}
}

View File

@@ -1,3 +1,4 @@
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace BTCPayServer.Data namespace BTCPayServer.Data
@@ -9,7 +10,10 @@ namespace BTCPayServer.Data
public string StoreDataId { get; set; } public string StoreDataId { get; set; }
public StoreData StoreData { get; set; } public StoreData StoreData { get; set; }
public string Role { get; set; } [Column("Role")]
public string StoreRoleId { get; set; }
public StoreRole StoreRole { get; set; }
internal static void OnModelCreating(ModelBuilder builder) internal static void OnModelCreating(ModelBuilder builder)
@@ -32,6 +36,10 @@ namespace BTCPayServer.Data
.HasOne(pt => pt.StoreData) .HasOne(pt => pt.StoreData)
.WithMany(t => t.UserStores) .WithMany(t => t.UserStores)
.HasForeignKey(pt => pt.StoreDataId); .HasForeignKey(pt => pt.StoreDataId);
builder.Entity<UserStore>().HasOne(e => e.StoreRole)
.WithMany(role => role.Users)
.HasForeignKey(e => e.StoreRoleId);
} }
} }
} }

View File

@@ -5,7 +5,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
namespace BTCPayServer.Data namespace BTCPayServer.Data
{ {
public class WebhookDeliveryData : IHasBlobUntyped public class WebhookDeliveryData
{ {
[Key] [Key]
[MaxLength(25)] [MaxLength(25)]
@@ -17,10 +17,8 @@ namespace BTCPayServer.Data
[Required] [Required]
public DateTimeOffset Timestamp { get; set; } public DateTimeOffset Timestamp { get; set; }
[Obsolete("Use Blob2 instead")] public string Blob { get; set; }
public byte[] Blob { get; set; } public bool Pruned { get; set; }
public string Blob2 { get; set; }
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade) internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
{ {
@@ -28,11 +26,11 @@ namespace BTCPayServer.Data
.HasOne(o => o.Webhook) .HasOne(o => o.Webhook)
.WithMany(a => a.Deliveries).OnDelete(DeleteBehavior.Cascade); .WithMany(a => a.Deliveries).OnDelete(DeleteBehavior.Cascade);
builder.Entity<WebhookDeliveryData>().HasIndex(o => o.WebhookId); builder.Entity<WebhookDeliveryData>().HasIndex(o => o.WebhookId);
builder.Entity<WebhookDeliveryData>().HasIndex(o => o.Timestamp);
if (databaseFacade.IsNpgsql()) if (databaseFacade.IsNpgsql())
{ {
builder.Entity<WebhookDeliveryData>() builder.Entity<WebhookDeliveryData>()
.Property(o => o.Blob2) .Property(o => o.Blob)
.HasColumnType("JSONB"); .HasColumnType("JSONB");
} }
} }

View File

@@ -0,0 +1,106 @@
using System;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using NBitcoin;
using Newtonsoft.Json;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20230504125505_StoreRoles")]
public partial class StoreRoles : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
var permissionsType = migrationBuilder.IsNpgsql() ? "TEXT[]" : "TEXT";
migrationBuilder.CreateTable(
name: "StoreRoles",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
StoreDataId = table.Column<string>(type: "TEXT", nullable: true),
Role = table.Column<string>(type: "TEXT", nullable: false),
Permissions = table.Column<string>(type: permissionsType, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_StoreRoles", x => x.Id);
table.ForeignKey(
name: "FK_StoreRoles_Stores_StoreDataId",
column: x => x.StoreDataId,
principalTable: "Stores",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_StoreRoles_StoreDataId_Role",
table: "StoreRoles",
columns: new[] { "StoreDataId", "Role" },
unique: true);
object GetPermissionsData(string[] permissions)
{
if (migrationBuilder.IsNpgsql())
return permissions;
return JsonConvert.SerializeObject(permissions);
}
migrationBuilder.InsertData(
"StoreRoles",
columns: new[] { "Id", "Role", "Permissions" },
columnTypes: new[] { "TEXT", "TEXT", permissionsType },
values: new object[,]
{
{
"Owner", "Owner", GetPermissionsData(new[]
{
"btcpay.store.canmodifystoresettings",
"btcpay.store.cantradecustodianaccount",
"btcpay.store.canwithdrawfromcustodianaccount",
"btcpay.store.candeposittocustodianaccount"
})
},
{
"Guest", "Guest", GetPermissionsData(new[]
{
"btcpay.store.canviewstoresettings",
"btcpay.store.canmodifyinvoices",
"btcpay.store.canviewcustodianaccounts",
"btcpay.store.candeposittocustodianaccount"
})
}
});
if (this.SupportAddForeignKey(migrationBuilder.ActiveProvider))
{
migrationBuilder.AddForeignKey(
name: "FK_UserStore_StoreRoles_Role",
table: "UserStore",
column: "Role",
principalTable: "StoreRoles",
principalColumn: "Id");
}
}
protected override void Down(MigrationBuilder migrationBuilder)
{
if (this.SupportDropForeignKey(migrationBuilder.ActiveProvider))
{
migrationBuilder.DropForeignKey(
name: "FK_UserStore_StoreRoles_Role",
table: "UserStore");
}
migrationBuilder.DropTable(
name: "StoreRoles");
}
}
}

View File

@@ -0,0 +1,83 @@
using System;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using NBitcoin;
using Newtonsoft.Json;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20230529135505_WebhookDeliveriesCleanup")]
public partial class WebhookDeliveriesCleanup : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
if (migrationBuilder.IsNpgsql())
{
migrationBuilder.Sql("DROP TABLE IF EXISTS \"InvoiceWebhookDeliveries\", \"WebhookDeliveries\";");
migrationBuilder.CreateTable(
name: "WebhookDeliveries",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
WebhookId = table.Column<string>(type: "TEXT", nullable: false),
Timestamp = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
Pruned = table.Column<bool>(type: "BOOLEAN", nullable: false),
Blob = table.Column<string>(type: "JSONB", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_WebhookDeliveries", x => x.Id);
table.ForeignKey(
name: "FK_WebhookDeliveries_Webhooks_WebhookId",
column: x => x.WebhookId,
principalTable: "Webhooks",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_WebhookDeliveries_WebhookId",
table: "WebhookDeliveries",
column: "WebhookId");
migrationBuilder.Sql("CREATE INDEX \"IX_WebhookDeliveries_Timestamp\" ON \"WebhookDeliveries\"(\"Timestamp\") WHERE \"Pruned\" IS FALSE");
migrationBuilder.CreateTable(
name: "InvoiceWebhookDeliveries",
columns: table => new
{
InvoiceId = table.Column<string>(type: "TEXT", nullable: false),
DeliveryId = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_InvoiceWebhookDeliveries", x => new { x.InvoiceId, x.DeliveryId });
table.ForeignKey(
name: "FK_InvoiceWebhookDeliveries_WebhookDeliveries_DeliveryId",
column: x => x.DeliveryId,
principalTable: "WebhookDeliveries",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_InvoiceWebhookDeliveries_Invoices_InvoiceId",
column: x => x.InvoiceId,
principalTable: "Invoices",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
}
}
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

View File

@@ -0,0 +1,39 @@
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20230906135844_AddArchivedFlagForStoresAndApps")]
public partial class AddArchivedFlagForStoresAndApps : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "Archived",
table: "Stores",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "Archived",
table: "Apps",
nullable: false,
defaultValue: false);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Archived",
table: "Stores");
migrationBuilder.DropColumn(
name: "Archived",
table: "Apps");
}
}
}

View File

@@ -79,6 +79,9 @@ namespace BTCPayServer.Migrations
b.Property<string>("AppType") b.Property<string>("AppType")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<bool>("Archived")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Created") b.Property<DateTimeOffset>("Created")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
@@ -214,56 +217,6 @@ namespace BTCPayServer.Migrations
b.ToTable("CustodianAccount"); b.ToTable("CustodianAccount");
}); });
modelBuilder.Entity("BTCPayServer.Data.FormData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Config")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<bool>("Public")
.HasColumnType("INTEGER");
b.Property<string>("StoreId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("StoreId");
b.ToTable("Forms");
});
modelBuilder.Entity("BTCPayServer.Data.PayoutProcessorData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<byte[]>("Blob")
.HasColumnType("BLOB");
b.Property<string>("PaymentMethod")
.HasColumnType("TEXT");
b.Property<string>("Processor")
.HasColumnType("TEXT");
b.Property<string>("StoreId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("StoreId");
b.ToTable("PayoutProcessors");
});
modelBuilder.Entity("BTCPayServer.Data.Fido2Credential", b => modelBuilder.Entity("BTCPayServer.Data.Fido2Credential", b =>
{ {
b.Property<string>("Id") b.Property<string>("Id")
@@ -292,6 +245,31 @@ namespace BTCPayServer.Migrations
b.ToTable("Fido2Credentials"); b.ToTable("Fido2Credentials");
}); });
modelBuilder.Entity("BTCPayServer.Data.FormData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Config")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<bool>("Public")
.HasColumnType("INTEGER");
b.Property<string>("StoreId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("StoreId");
b.ToTable("Forms");
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b => modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
{ {
b.Property<string>("Id") b.Property<string>("Id")
@@ -655,6 +633,34 @@ namespace BTCPayServer.Migrations
b.ToTable("Payouts"); b.ToTable("Payouts");
}); });
modelBuilder.Entity("BTCPayServer.Data.PayoutProcessorData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<byte[]>("Blob")
.HasColumnType("BLOB");
b.Property<string>("Blob2")
.HasColumnType("TEXT");
b.Property<string>("PaymentMethod")
.HasColumnType("TEXT");
b.Property<string>("Processor")
.HasColumnType("TEXT");
b.Property<string>("StoreId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("StoreId");
b.ToTable("PayoutProcessors");
});
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b => modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
{ {
b.Property<string>("Id") b.Property<string>("Id")
@@ -748,6 +754,9 @@ namespace BTCPayServer.Migrations
b.Property<string>("Id") b.Property<string>("Id")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<bool>("Archived")
.HasColumnType("INTEGER");
b.Property<string>("DefaultCrypto") b.Property<string>("DefaultCrypto")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
@@ -802,6 +811,28 @@ namespace BTCPayServer.Migrations
b.ToTable("Files"); b.ToTable("Files");
}); });
modelBuilder.Entity("BTCPayServer.Data.StoreRole", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<string>("Permissions")
.HasColumnType("TEXT");
b.Property<string>("Role")
.HasColumnType("TEXT");
b.Property<string>("StoreDataId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("StoreDataId", "Role")
.IsUnique();
b.ToTable("StoreRoles");
});
modelBuilder.Entity("BTCPayServer.Data.StoreSettingData", b => modelBuilder.Entity("BTCPayServer.Data.StoreSettingData", b =>
{ {
b.Property<string>("StoreId") b.Property<string>("StoreId")
@@ -878,13 +909,16 @@ namespace BTCPayServer.Migrations
b.Property<string>("StoreDataId") b.Property<string>("StoreDataId")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<string>("Role") b.Property<string>("StoreRoleId")
.HasColumnType("TEXT"); .HasColumnType("TEXT")
.HasColumnName("Role");
b.HasKey("ApplicationUserId", "StoreDataId"); b.HasKey("ApplicationUserId", "StoreDataId");
b.HasIndex("StoreDataId"); b.HasIndex("StoreDataId");
b.HasIndex("StoreRoleId");
b.ToTable("UserStore"); b.ToTable("UserStore");
}); });
@@ -991,12 +1025,12 @@ namespace BTCPayServer.Migrations
.HasMaxLength(25) .HasMaxLength(25)
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<byte[]>("Blob") b.Property<string>("Blob")
.HasColumnType("BLOB");
b.Property<string>("Blob2")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<bool>("Pruned")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Timestamp") b.Property<DateTimeOffset>("Timestamp")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
@@ -1007,6 +1041,8 @@ namespace BTCPayServer.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("Timestamp");
b.HasIndex("WebhookId"); b.HasIndex("WebhookId");
b.ToTable("WebhookDeliveries"); b.ToTable("WebhookDeliveries");
@@ -1188,26 +1224,6 @@ namespace BTCPayServer.Migrations
b.Navigation("StoreData"); b.Navigation("StoreData");
}); });
modelBuilder.Entity("BTCPayServer.Data.FormData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "Store")
.WithMany("Forms")
.HasForeignKey("StoreId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("Store");
});
modelBuilder.Entity("BTCPayServer.Data.PayoutProcessorData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "Store")
.WithMany("PayoutProcessors")
.HasForeignKey("StoreId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("Store");
});
modelBuilder.Entity("BTCPayServer.Data.Fido2Credential", b => modelBuilder.Entity("BTCPayServer.Data.Fido2Credential", b =>
{ {
b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser") b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser")
@@ -1218,6 +1234,16 @@ namespace BTCPayServer.Migrations
b.Navigation("ApplicationUser"); b.Navigation("ApplicationUser");
}); });
modelBuilder.Entity("BTCPayServer.Data.FormData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "Store")
.WithMany("Forms")
.HasForeignKey("StoreId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("Store");
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b => modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
{ {
b.HasOne("BTCPayServer.Data.StoreData", "StoreData") b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
@@ -1343,6 +1369,16 @@ namespace BTCPayServer.Migrations
b.Navigation("StoreData"); b.Navigation("StoreData");
}); });
modelBuilder.Entity("BTCPayServer.Data.PayoutProcessorData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "Store")
.WithMany("PayoutProcessors")
.HasForeignKey("StoreId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("Store");
});
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b => modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
{ {
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
@@ -1392,6 +1428,16 @@ namespace BTCPayServer.Migrations
b.Navigation("ApplicationUser"); b.Navigation("ApplicationUser");
}); });
modelBuilder.Entity("BTCPayServer.Data.StoreRole", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("StoreRoles")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("StoreData");
});
modelBuilder.Entity("BTCPayServer.Data.StoreSettingData", b => modelBuilder.Entity("BTCPayServer.Data.StoreSettingData", b =>
{ {
b.HasOne("BTCPayServer.Data.StoreData", "Store") b.HasOne("BTCPayServer.Data.StoreData", "Store")
@@ -1446,9 +1492,15 @@ namespace BTCPayServer.Migrations
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.HasOne("BTCPayServer.Data.StoreRole", "StoreRole")
.WithMany("Users")
.HasForeignKey("StoreRoleId");
b.Navigation("ApplicationUser"); b.Navigation("ApplicationUser");
b.Navigation("StoreData"); b.Navigation("StoreData");
b.Navigation("StoreRole");
}); });
modelBuilder.Entity("BTCPayServer.Data.WalletObjectLinkData", b => modelBuilder.Entity("BTCPayServer.Data.WalletObjectLinkData", b =>
@@ -1606,9 +1658,16 @@ namespace BTCPayServer.Migrations
b.Navigation("Settings"); b.Navigation("Settings");
b.Navigation("StoreRoles");
b.Navigation("UserStores"); b.Navigation("UserStores");
}); });
modelBuilder.Entity("BTCPayServer.Data.StoreRole", b =>
{
b.Navigation("Users");
});
modelBuilder.Entity("BTCPayServer.Data.WalletData", b => modelBuilder.Entity("BTCPayServer.Data.WalletData", b =>
{ {
b.Navigation("WalletTransactions"); b.Navigation("WalletTransactions");

View File

@@ -1,37 +0,0 @@
using System;
namespace BTCPayServer.Rating
{
public enum RateSource
{
Coingecko,
Direct
}
public class AvailableRateProvider
{
public string Name { get; }
public string Url { get; }
public string Id { get; }
public RateSource Source { get; }
public AvailableRateProvider(string id, string name, string url) : this(id, name, url, RateSource.Direct)
{
}
public AvailableRateProvider(string id, string name, string url, RateSource source)
{
Id = id;
Name = name;
Url = url;
Source = source;
}
public string DisplayName =>
Source switch
{
RateSource.Direct => Name,
RateSource.Coingecko => $"{Name} (via CoinGecko)",
_ => throw new NotSupportedException(Source.ToString())
};
}
}

View File

@@ -7,8 +7,8 @@
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1" /> <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.9" /> <PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.9" />
<PackageReference Include="NBitcoin" Version="7.0.24" /> <PackageReference Include="NBitcoin" Version="7.0.24" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="1.0.2" /> <PackageReference Include="DigitalRuby.ExchangeSharp" Version="1.0.4" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -20,13 +20,13 @@ namespace BTCPayServer.Services.Rates
} }
public class CurrencyNameTable public class CurrencyNameTable
{ {
public static CurrencyNameTable Instance = new CurrencyNameTable(); public static CurrencyNameTable Instance = new();
public CurrencyNameTable() public CurrencyNameTable()
{ {
_Currencies = LoadCurrency().ToDictionary(k => k.Code); _Currencies = LoadCurrency().ToDictionary(k => k.Code, StringComparer.InvariantCultureIgnoreCase);
} }
static readonly Dictionary<string, IFormatProvider> _CurrencyProviders = new Dictionary<string, IFormatProvider>(); static readonly Dictionary<string, IFormatProvider> _CurrencyProviders = new();
public NumberFormatInfo GetNumberFormatInfo(string currency, bool useFallback) public NumberFormatInfo GetNumberFormatInfo(string currency, bool useFallback)
{ {

View File

@@ -19,7 +19,7 @@ namespace BTCPayServer.Rating
public static CurrencyPair Parse(string str) public static CurrencyPair Parse(string str)
{ {
if (!TryParse(str, out var result)) if (!TryParse(str, out var result))
throw new FormatException("Invalid currency pair"); throw new FormatException($"Invalid currency pair ({str})");
return result; return result;
} }
public static bool TryParse(string str, out CurrencyPair value) public static bool TryParse(string str, out CurrencyPair value)

View File

@@ -15,7 +15,7 @@ namespace BTCPayServer.Rating
while (true) while (true)
{ {
var rounded = decimal.Round(value, divisibility, MidpointRounding.AwayFromZero); var rounded = decimal.Round(value, divisibility, MidpointRounding.AwayFromZero);
if ((Math.Abs(rounded - value) / value) < 0.001m) if ((Math.Abs(rounded - value) / value) < 0.01m)
{ {
value = rounded; value = rounded;
break; break;

View File

@@ -1,29 +0,0 @@
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Rating;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.Rates
{
public class ArgoneumRateProvider : IRateProvider
{
private readonly HttpClient _httpClient;
public ArgoneumRateProvider(HttpClient httpClient)
{
_httpClient = httpClient ?? new HttpClient();
}
public RateSourceInfo RateSourceInfo => new("argoneum", "Argoneum", "https://rates.argoneum.net/rates");
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{
// Example result: AGM to BTC rate: {"agm":5000000.000000}
var response = await _httpClient.GetAsync("https://rates.argoneum.net/rates/btc", cancellationToken);
var jobj = await response.Content.ReadAsAsync<JObject>(cancellationToken);
var value = jobj["agm"].Value<decimal>();
return new[] { new PairRate(new CurrencyPair("BTC", "AGM"), new BidAsk(value)) };
}
}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,36 @@
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Rating;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.Rates;
public class FreeCurrencyRatesRateProvider : IRateProvider
{
public RateSourceInfo RateSourceInfo => new("free-currency-rates", "Free Currency Rates", "https://cdn.jsdelivr.net/gh/fawazahmed0/currency-api@1/latest/currencies/btc.min.json");
private readonly HttpClient _httpClient;
public FreeCurrencyRatesRateProvider(HttpClient httpClient)
{
_httpClient = httpClient ?? new HttpClient();
}
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{
var response = await _httpClient.GetAsync(RateSourceInfo.Url, cancellationToken);
response.EnsureSuccessStatusCode();
var jobj = await response.Content.ReadAsAsync<JObject>(cancellationToken);
var results = (JObject) jobj["btc"] ;
//key value is currency code to rate value
var list = new List<PairRate>();
foreach (var item in results)
{
string name = item.Key;
var value = item.Value.Value<decimal>();
list.Add(new PairRate(new CurrencyPair("BTC", name), new BidAsk(value)));
}
return list.ToArray();
}
}

View File

@@ -13,7 +13,7 @@ namespace BTCPayServer.Services.Rates
{ {
public class RipioExchangeProvider : IRateProvider public class RipioExchangeProvider : IRateProvider
{ {
public RateSourceInfo RateSourceInfo => new("ripio", "Ripio", "https://api.exchange.ripio.com/api/v1/rate/all/"); public RateSourceInfo RateSourceInfo => new("ripio", "Ripio", "https://api.ripiotrade.co/v4/public/tickers");
private readonly HttpClient _httpClient; private readonly HttpClient _httpClient;
public RipioExchangeProvider(HttpClient httpClient) public RipioExchangeProvider(HttpClient httpClient)
{ {
@@ -21,9 +21,9 @@ namespace BTCPayServer.Services.Rates
} }
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken) public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{ {
var response = await _httpClient.GetAsync("https://api.exchange.ripio.com/api/v1/rate/all/", cancellationToken); var response = await _httpClient.GetAsync("https://api.ripiotrade.co/v4/public/tickers", cancellationToken);
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
var jarray = (JArray)(await response.Content.ReadAsAsync<JArray>(cancellationToken)); var jarray = (JArray)(await response.Content.ReadAsAsync<JObject>(cancellationToken))["data"];
return jarray return jarray
.Children<JObject>() .Children<JObject>()
.Select(jobj => ParsePair(jobj)) .Select(jobj => ParsePair(jobj))

View File

@@ -1,21 +1,8 @@
using System; #nullable enable
using System.Collections.Generic; namespace BTCPayServer.Rating;
using System.Linq; public enum RateSource
using System.Text;
using System.Threading.Tasks;
namespace BTCPayServer.Rating
{ {
public class RateSourceInfo Coingecko,
{ Direct
public RateSourceInfo(string id, string displayName, string url)
{
Id = id;
DisplayName = displayName;
Url = url;
}
public string Id { get; set; }
public string DisplayName { get; set; }
public string Url { get; set; }
}
} }
public record RateSourceInfo(string Id, string DisplayName, string Url, RateSource Source = RateSource.Direct);

View File

@@ -85,14 +85,13 @@ namespace BTCPayServer.Services.Rates
bgFetcher.RefreshRate = TimeSpan.FromMinutes(1.0); bgFetcher.RefreshRate = TimeSpan.FromMinutes(1.0);
bgFetcher.ValidatyTime = TimeSpan.FromMinutes(5.0); bgFetcher.ValidatyTime = TimeSpan.FromMinutes(5.0);
Providers.Add(supportedExchange.Id, bgFetcher); Providers.Add(supportedExchange.Id, bgFetcher);
var rsi = coingecko.RateSourceInfo; AvailableRateProviders.Add(coingecko.RateSourceInfo);
AvailableRateProviders.Add(new(rsi.Id, rsi.DisplayName, rsi.Url, RateSource.Coingecko));
} }
} }
AvailableRateProviders.Sort((a, b) => StringComparer.Ordinal.Compare(a.DisplayName, b.DisplayName)); AvailableRateProviders.Sort((a, b) => StringComparer.Ordinal.Compare(a.DisplayName, b.DisplayName));
} }
public List<AvailableRateProvider> AvailableRateProviders { get; } = new List<AvailableRateProvider>(); public List<RateSourceInfo> AvailableRateProviders { get; } = new List<RateSourceInfo>();
public async Task<QueryRateResult> QueryRates(string exchangeName, CancellationToken cancellationToken) public async Task<QueryRateResult> QueryRates(string exchangeName, CancellationToken cancellationToken)
{ {

View File

@@ -6,6 +6,7 @@ using System.Threading.Tasks;
using BTCPayServer.Controllers; using BTCPayServer.Controllers;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.HostedServices; using BTCPayServer.HostedServices;
using BTCPayServer.Hosting;
using BTCPayServer.Lightning; using BTCPayServer.Lightning;
using BTCPayServer.Models.AppViewModels; using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Models.StoreViewModels; using BTCPayServer.Models.StoreViewModels;
@@ -245,7 +246,7 @@ namespace BTCPayServer.Tests
await tester.EnsureChannelsSetup(); await tester.EnsureChannelsSetup();
var user = tester.NewAccount(); var user = tester.NewAccount();
user.GrantAccess(true); user.GrantAccess(true);
user.RegisterLightningNode("BTC", LightningConnectionType.Charge); user.RegisterLightningNode("BTC");
user.RegisterDerivationScheme("BTC"); user.RegisterDerivationScheme("BTC");
user.RegisterDerivationScheme("LTC"); user.RegisterDerivationScheme("LTC");
@@ -651,6 +652,7 @@ donation:
price: 1.02 price: 1.02
custom: true custom: true
"; ";
vmpos.Template = AppService.SerializeTemplate(MigrationStartupTask.ParsePOSYML(vmpos.Template));
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result); Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>(); vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
Assert.Equal("hello", vmpos.Title); Assert.Equal("hello", vmpos.Title);
@@ -661,14 +663,13 @@ donation:
Assert.Equal(3, vmview.Items.Length); Assert.Equal(3, vmview.Items.Length);
Assert.Equal("good apple", vmview.Items[0].Title); Assert.Equal("good apple", vmview.Items[0].Title);
Assert.Equal("orange", vmview.Items[1].Title); Assert.Equal("orange", vmview.Items[1].Title);
Assert.Equal(10.0m, vmview.Items[1].Price.Value); Assert.Equal(10.0m, vmview.Items[1].Price);
Assert.Equal("$5.00", vmview.Items[0].Price.Formatted);
Assert.Equal("{0} Purchase", vmview.ButtonText); Assert.Equal("{0} Purchase", vmview.ButtonText);
Assert.Equal("Nicolas Sexy Hair", vmview.CustomButtonText); Assert.Equal("Nicolas Sexy Hair", vmview.CustomButtonText);
Assert.Equal("Wanna tip?", vmview.CustomTipText); Assert.Equal("Wanna tip?", vmview.CustomTipText);
Assert.Equal("15,18,20", string.Join(',', vmview.CustomTipPercentages)); Assert.Equal("15,18,20", string.Join(',', vmview.CustomTipPercentages));
Assert.IsType<RedirectToActionResult>(publicApps Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(app.Id, PosViewType.Cart, 0, null, null, null, null, "orange").Result); .ViewPointOfSale(app.Id, PosViewType.Cart, 0, choiceKey: "orange").Result);
// //
var invoices = await user.BitPay.GetInvoicesAsync(); var invoices = await user.BitPay.GetInvoicesAsync();
@@ -677,18 +678,18 @@ donation:
Assert.Equal("CAD", orangeInvoice.Currency); Assert.Equal("CAD", orangeInvoice.Currency);
Assert.Equal("orange", orangeInvoice.ItemDesc); Assert.Equal("orange", orangeInvoice.ItemDesc);
Assert.IsType<RedirectToActionResult>(publicApps Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(app.Id, PosViewType.Cart, 0, null, null, null, null, "apple").Result); .ViewPointOfSale(app.Id, PosViewType.Cart, 0, choiceKey: "apple").Result);
invoices = user.BitPay.GetInvoices(); invoices = await user.BitPay.GetInvoicesAsync();
var appleInvoice = invoices.SingleOrDefault(invoice => invoice.ItemCode.Equals("apple")); var appleInvoice = invoices.SingleOrDefault(invoice => invoice.ItemCode.Equals("apple"));
Assert.NotNull(appleInvoice); Assert.NotNull(appleInvoice);
Assert.Equal("good apple", appleInvoice.ItemDesc); Assert.Equal("good apple", appleInvoice.ItemDesc);
// testing custom amount // testing custom amount
var action = Assert.IsType<RedirectToActionResult>(publicApps var action = Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(app.Id, PosViewType.Cart, 6.6m, null, null, null, null, "donation").Result); .ViewPointOfSale(app.Id, PosViewType.Cart, 6.6m, choiceKey: "donation").Result);
Assert.Equal(nameof(UIInvoiceController.Checkout), action.ActionName); Assert.Equal(nameof(UIInvoiceController.Checkout), action.ActionName);
invoices = user.BitPay.GetInvoices(); invoices = await user.BitPay.GetInvoicesAsync();
var donationInvoice = invoices.Single(i => i.Price == 6.6m); var donationInvoice = invoices.Single(i => i.Price == 6.6m);
Assert.NotNull(donationInvoice); Assert.NotNull(donationInvoice);
Assert.Equal("CAD", donationInvoice.Currency); Assert.Equal("CAD", donationInvoice.Currency);
@@ -723,6 +724,7 @@ donation:
price: 1.02 price: 1.02
custom: true custom: true
"; ";
vmpos.Template = AppService.SerializeTemplate(MigrationStartupTask.ParsePOSYML(vmpos.Template));
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result); Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
publicApps = user.GetController<UIPointOfSaleController>(); publicApps = user.GetController<UIPointOfSaleController>();
vmview = await publicApps.ViewPointOfSale(app.Id, PosViewType.Cart).AssertViewModelAsync<ViewPointOfSaleViewModel>(); vmview = await publicApps.ViewPointOfSale(app.Id, PosViewType.Cart).AssertViewModelAsync<ViewPointOfSaleViewModel>();
@@ -750,26 +752,28 @@ inventoryitem:
inventory: 1 inventory: 1
noninventoryitem: noninventoryitem:
price: 10.0"; price: 10.0";
vmpos.Template = AppService.SerializeTemplate(MigrationStartupTask.ParsePOSYML(vmpos.Template));
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result); Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
//inventoryitem has 1 item available //inventoryitem has 1 item available
await tester.WaitForEvent<AppInventoryUpdaterHostedService.UpdateAppInventory>(() => await tester.WaitForEvent<AppInventoryUpdaterHostedService.UpdateAppInventory>(() =>
{ {
Assert.IsType<RedirectToActionResult>(publicApps Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, null, null, null, null, "inventoryitem").Result); .ViewPointOfSale(app.Id, PosViewType.Cart, 1, choiceKey: "inventoryitem").Result);
return Task.CompletedTask; return Task.CompletedTask;
}); });
//we already bought all available stock so this should fail //we already bought all available stock so this should fail
await Task.Delay(100); await Task.Delay(100);
Assert.IsType<RedirectToActionResult>(publicApps Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, null, null, null, null, "inventoryitem").Result); .ViewPointOfSale(app.Id, PosViewType.Cart, 1, choiceKey: "inventoryitem").Result);
//inventoryitem has unlimited items available //inventoryitem has unlimited items available
Assert.IsType<RedirectToActionResult>(publicApps Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, null, null, null, null, "noninventoryitem").Result); .ViewPointOfSale(app.Id, PosViewType.Cart, 1, choiceKey: "noninventoryitem").Result);
Assert.IsType<RedirectToActionResult>(publicApps Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, null, null, null, null, "noninventoryitem").Result); .ViewPointOfSale(app.Id, PosViewType.Cart, 1, choiceKey: "noninventoryitem").Result);
//verify invoices where created //verify invoices where created
invoices = user.BitPay.GetInvoices(); invoices = user.BitPay.GetInvoices();
@@ -780,15 +784,13 @@ noninventoryitem:
//let's mark the inventoryitem invoice as invalid, this should return the item to back in stock //let's mark the inventoryitem invoice as invalid, this should return the item to back in stock
var controller = tester.PayTester.GetController<UIInvoiceController>(user.UserId, user.StoreId); var controller = tester.PayTester.GetController<UIInvoiceController>(user.UserId, user.StoreId);
var appService = tester.PayTester.GetService<AppService>();
var eventAggregator = tester.PayTester.GetService<EventAggregator>();
Assert.IsType<JsonResult>(await controller.ChangeInvoiceState(inventoryItemInvoice.Id, "invalid")); Assert.IsType<JsonResult>(await controller.ChangeInvoiceState(inventoryItemInvoice.Id, "invalid"));
//check that item is back in stock //check that item is back in stock
await TestUtils.EventuallyAsync(async () => await TestUtils.EventuallyAsync(async () =>
{ {
vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>(); vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
Assert.Equal(1, Assert.Equal(1,
appService.Parse(vmpos.Template, "BTC").Single(item => item.Id == "inventoryitem").Inventory); AppService.Parse(vmpos.Template).Single(item => item.Id == "inventoryitem").Inventory);
}, 10000); }, 10000);
//test payment methods option //test payment methods option
@@ -803,11 +805,13 @@ btconly:
- BTC - BTC
normal: normal:
price: 1.0"; price: 1.0";
vmpos.Template = AppService.SerializeTemplate(MigrationStartupTask.ParsePOSYML(vmpos.Template));
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result); Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
Assert.IsType<RedirectToActionResult>(publicApps Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, null, null, null, null, "btconly").Result); .ViewPointOfSale(app.Id, PosViewType.Cart, 1, choiceKey: "btconly").Result);
Assert.IsType<RedirectToActionResult>(publicApps Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, null, null, null, null, "normal").Result); .ViewPointOfSale(app.Id, PosViewType.Cart, 1, choiceKey: "normal").Result);
invoices = user.BitPay.GetInvoices(); invoices = user.BitPay.GetInvoices();
var normalInvoice = invoices.Single(invoice => invoice.ItemCode == "normal"); var normalInvoice = invoices.Single(invoice => invoice.ItemCode == "normal");
var btcOnlyInvoice = invoices.Single(invoice => invoice.ItemCode == "btconly"); var btcOnlyInvoice = invoices.Single(invoice => invoice.ItemCode == "btconly");
@@ -847,20 +851,21 @@ g:
custom: topup custom: topup
"; ";
vmpos.Template = AppService.SerializeTemplate(MigrationStartupTask.ParsePOSYML(vmpos.Template));
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result); Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>(); vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
Assert.DoesNotContain("custom", vmpos.Template); Assert.DoesNotContain("custom", vmpos.Template);
var items = appService.Parse(vmpos.Template, vmpos.Currency); var items = AppService.Parse(vmpos.Template);
Assert.Contains(items, item => item.Id == "a" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed); Assert.Contains(items, item => item.Id == "a" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Fixed);
Assert.Contains(items, item => item.Id == "b" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed); Assert.Contains(items, item => item.Id == "b" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Fixed);
Assert.Contains(items, item => item.Id == "c" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Minimum); Assert.Contains(items, item => item.Id == "c" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Minimum);
Assert.Contains(items, item => item.Id == "d" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed); Assert.Contains(items, item => item.Id == "d" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Fixed);
Assert.Contains(items, item => item.Id == "e" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Minimum); Assert.Contains(items, item => item.Id == "e" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Minimum);
Assert.Contains(items, item => item.Id == "f" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup); Assert.Contains(items, item => item.Id == "f" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup);
Assert.Contains(items, item => item.Id == "g" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup); Assert.Contains(items, item => item.Id == "g" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup);
Assert.IsType<RedirectToActionResult>(publicApps Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(app.Id, PosViewType.Static, null, null, null, null, null, "g").Result); .ViewPointOfSale(app.Id, PosViewType.Static, choiceKey: "g").Result);
invoices = user.BitPay.GetInvoices(); invoices = user.BitPay.GetInvoices();
var topupInvoice = invoices.Single(invoice => invoice.ItemCode == "g"); var topupInvoice = invoices.Single(invoice => invoice.ItemCode == "g");
Assert.Equal(0, topupInvoice.Price); Assert.Equal(0, topupInvoice.Price);

View File

@@ -52,11 +52,12 @@ namespace BTCPayServer.Tests
{ {
tester.ActivateLBTC(); tester.ActivateLBTC();
await tester.StartAsync(); await tester.StartAsync();
//https://github.com/ElementsProject/elements/issues/956
await tester.LBTCExplorerNode.SendCommandAsync("rescanblockchain");
var user = tester.NewAccount(); var user = tester.NewAccount();
user.GrantAccess(); await user.GrantAccessAsync();
user.RegisterDerivationScheme("LBTC");
user.RegisterDerivationScheme("USDT");
user.RegisterDerivationScheme("ETB");
await tester.LBTCExplorerNode.GenerateAsync(4); await tester.LBTCExplorerNode.GenerateAsync(4);
//no tether on our regtest, lets create it and set it //no tether on our regtest, lets create it and set it
var tether = tester.NetworkProvider.GetNetwork<ElementsBTCPayNetwork>("USDT"); var tether = tester.NetworkProvider.GetNetwork<ElementsBTCPayNetwork>("USDT");
@@ -75,6 +76,10 @@ namespace BTCPayServer.Tests
.AssetId = etb.AssetId; .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 //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")); var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(0.1m, "BTC"));
Assert.Equal(3, invoice.SupportedTransactionCurrencies.Count); Assert.Equal(3, invoice.SupportedTransactionCurrencies.Count);
@@ -82,7 +87,7 @@ namespace BTCPayServer.Tests
//1 lbtc = 1 btc //1 lbtc = 1 btc
Assert.Equal(1, ci.Rate); Assert.Equal(1, ci.Rate);
var star = await tester.LBTCExplorerNode.SendCommandAsync("sendtoaddress", ci.Address, ci.Due, "", "", false, true, var star = await tester.LBTCExplorerNode.SendCommandAsync("sendtoaddress", ci.Address, ci.Due, "", "", false, true,
1, "UNSET", lbtc.AssetId); 1, "UNSET",false, lbtc.AssetId.ToString());
TestUtils.Eventually(() => TestUtils.Eventually(() =>
{ {
@@ -95,8 +100,7 @@ namespace BTCPayServer.Tests
ci = invoice.CryptoInfo.Single(info => info.CryptoCode.Equals("USDT")); ci = invoice.CryptoInfo.Single(info => info.CryptoCode.Equals("USDT"));
Assert.Equal(3, invoice.SupportedTransactionCurrencies.Count); Assert.Equal(3, invoice.SupportedTransactionCurrencies.Count);
star = await tester.LBTCExplorerNode.SendCommandAsync("sendtoaddress", ci.Address, ci.Due, "", "", false, true, star = tester.LBTCExplorerNode.SendCommand("sendtoaddress", ci.Address, decimal.Parse(ci.Due), "x", "z", false, true, 1, "unset", false, tether.AssetId.ToString());
1, "UNSET", tether.AssetId);
TestUtils.Eventually(() => TestUtils.Eventually(() =>
{ {

View File

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

View File

@@ -1,13 +1,9 @@
using System; using System;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Client.Models;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Tests.Logging;
using BTCPayServer.Views.Stores; using BTCPayServer.Views.Stores;
using NBitcoin; using NBitcoin;
using OpenQA.Selenium; using OpenQA.Selenium;
using OpenQA.Selenium.Support.Extensions;
using OpenQA.Selenium.Support.UI; using OpenQA.Selenium.Support.UI;
using Xunit; using Xunit;
using Xunit.Abstractions; using Xunit.Abstractions;
@@ -40,8 +36,10 @@ namespace BTCPayServer.Tests
// Configure store url // Configure store url
var storeUrl = "https://satoshisteaks.com/"; var storeUrl = "https://satoshisteaks.com/";
var supportUrl = "https://support.satoshisteaks.com/{InvoiceId}/";
s.GoToStore(); s.GoToStore();
s.Driver.FindElement(By.Id("StoreWebsite")).SendKeys(storeUrl); s.Driver.FindElement(By.Id("StoreWebsite")).SendKeys(storeUrl);
s.Driver.FindElement(By.Id("StoreSupportUrl")).SendKeys(supportUrl);
s.Driver.FindElement(By.Id("Save")).Click(); s.Driver.FindElement(By.Id("Save")).Click();
Assert.Contains("Store successfully updated", s.FindAlertMessage().Text); Assert.Contains("Store successfully updated", s.FindAlertMessage().Text);
@@ -64,9 +62,9 @@ namespace BTCPayServer.Tests
var qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value"); var qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
var address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard"); var address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
var payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href"); var payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
var copyAddress = s.Driver.FindElement(By.Id("Address_BTC")).GetAttribute("value"); var copyAddress = s.Driver.FindElement(By.CssSelector("#Address_BTC .truncate-center-start")).Text;
Assert.Equal($"bitcoin:{address}", payUrl); Assert.Equal($"bitcoin:{address}", payUrl);
Assert.StartsWith("bcrt", s.Driver.FindElement(By.Id("Address_BTC")).GetAttribute("value")); Assert.StartsWith("bcrt", s.Driver.FindElement(By.CssSelector("#Address_BTC .truncate-center-start")).Text);
Assert.DoesNotContain("lightning=", payUrl); Assert.DoesNotContain("lightning=", payUrl);
Assert.Equal(address, copyAddress); Assert.Equal(address, copyAddress);
Assert.Equal($"bitcoin:{address.ToUpperInvariant()}", qrValue); Assert.Equal($"bitcoin:{address.ToUpperInvariant()}", qrValue);
@@ -86,7 +84,7 @@ namespace BTCPayServer.Tests
{ {
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href"); payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
Assert.StartsWith("lightning:lnurl", payUrl); Assert.StartsWith("lightning:lnurl", payUrl);
Assert.StartsWith("lnurl", s.Driver.WaitForElement(By.Id("Lightning_BTC")).GetAttribute("value")); Assert.StartsWith("lnurl", s.Driver.WaitForElement(By.CssSelector("#Lightning_BTC .truncate-center-start")).Text);
s.Driver.ElementDoesNotExist(By.Id("Address_BTC")); s.Driver.ElementDoesNotExist(By.Id("Address_BTC"));
}); });
@@ -101,7 +99,7 @@ namespace BTCPayServer.Tests
qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value"); qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard"); address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href"); payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
copyAddress = s.Driver.FindElement(By.Id("Lightning_BTC_LightningLike")).GetAttribute("value"); copyAddress = s.Driver.FindElement(By.CssSelector("#Lightning_BTC_LightningLike .truncate-center-start")).Text;
Assert.Equal($"lightning:{address}", payUrl); Assert.Equal($"lightning:{address}", payUrl);
Assert.Equal(address, copyAddress); Assert.Equal(address, copyAddress);
Assert.Equal($"lightning:{address.ToUpperInvariant()}", qrValue); Assert.Equal($"lightning:{address.ToUpperInvariant()}", qrValue);
@@ -140,8 +138,47 @@ namespace BTCPayServer.Tests
var expiredSection = s.Driver.FindElement(By.Id("unpaid")); var expiredSection = s.Driver.FindElement(By.Id("unpaid"));
Assert.True(expiredSection.Displayed); Assert.True(expiredSection.Displayed);
Assert.Contains("Invoice Expired", expiredSection.Text); Assert.Contains("Invoice Expired", expiredSection.Text);
Assert.Contains("resubmit a payment", expiredSection.Text);
Assert.DoesNotContain("This invoice expired with partial payment", expiredSection.Text);
}); });
Assert.True(s.Driver.ElementDoesNotExist(By.Id("receipt-btn"))); 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"));
// Expire paid partial
s.GoToHome();
invoiceId = s.CreateInvoice(2100, "EUR");
s.GoToInvoiceCheckout(invoiceId);
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
await Task.Delay(200);
address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
var amountFraction = "0.00001";
await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(address, Network.RegTest),
Money.Parse(amountFraction));
await s.Server.ExplorerNode.GenerateAsync(1);
expirySeconds = s.Driver.FindElement(By.Id("ExpirySeconds"));
expirySeconds.Clear();
expirySeconds.SendKeys("3");
s.Driver.FindElement(By.Id("Expire")).Click();
paymentInfo = s.Driver.WaitForElement(By.Id("PaymentInfo"));
Assert.Contains("The invoice hasn't been paid in full.", paymentInfo.Text);
Assert.Contains("Please send", paymentInfo.Text);
TestUtils.Eventually(() =>
{
var expiredSection = s.Driver.FindElement(By.Id("unpaid"));
Assert.True(expiredSection.Displayed);
Assert.Contains("Invoice Expired", expiredSection.Text);
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")); Assert.Equal(storeUrl, s.Driver.FindElement(By.Id("StoreLink")).GetAttribute("href"));
// Test payment // Test payment
@@ -166,7 +203,7 @@ namespace BTCPayServer.Tests
// Pay partial amount // Pay partial amount
await Task.Delay(200); await Task.Delay(200);
address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard"); address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
var amountFraction = "0.00001"; amountFraction = "0.00001";
await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(address, Network.RegTest), await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(address, Network.RegTest),
Money.Parse(amountFraction)); Money.Parse(amountFraction));
await s.Server.ExplorerNode.GenerateAsync(1); await s.Server.ExplorerNode.GenerateAsync(1);
@@ -210,7 +247,8 @@ namespace BTCPayServer.Tests
Assert.Contains("Invoice Paid", settledSection.Text); Assert.Contains("Invoice Paid", settledSection.Text);
}); });
s.Driver.FindElement(By.Id("confetti")); s.Driver.FindElement(By.Id("confetti"));
s.Driver.FindElement(By.Id("receipt-btn")); 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")); Assert.Equal(storeUrl, s.Driver.FindElement(By.Id("StoreLink")).GetAttribute("href"));
// BIP21 // BIP21
@@ -229,8 +267,8 @@ namespace BTCPayServer.Tests
qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value"); qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard"); address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href"); payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
var copyAddressOnchain = s.Driver.FindElement(By.Id("Address_BTC")).GetAttribute("value"); var copyAddressOnchain = s.Driver.FindElement(By.CssSelector("#Address_BTC .truncate-center-start")).Text;
var copyAddressLightning = s.Driver.FindElement(By.Id("Lightning_BTC")).GetAttribute("value"); var copyAddressLightning = s.Driver.FindElement(By.CssSelector("#Lightning_BTC .truncate-center-start")).Text;
Assert.StartsWith($"bitcoin:{address}?amount=", payUrl); Assert.StartsWith($"bitcoin:{address}?amount=", payUrl);
Assert.Contains("?amount=", payUrl); Assert.Contains("?amount=", payUrl);
Assert.Contains("&lightning=", payUrl); Assert.Contains("&lightning=", payUrl);
@@ -297,8 +335,8 @@ namespace BTCPayServer.Tests
qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value"); qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard"); address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href"); payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
copyAddressOnchain = s.Driver.FindElement(By.Id("Address_BTC")).GetAttribute("value"); copyAddressOnchain = s.Driver.FindElement(By.CssSelector("#Address_BTC .truncate-center-start")).Text;
copyAddressLightning = s.Driver.FindElement(By.Id("Lightning_BTC")).GetAttribute("value"); copyAddressLightning = s.Driver.FindElement(By.CssSelector("#Lightning_BTC .truncate-center-start")).Text;
Assert.StartsWith($"bitcoin:{address}", payUrl); Assert.StartsWith($"bitcoin:{address}", payUrl);
Assert.Contains("?lightning=lnurl", payUrl); Assert.Contains("?lightning=lnurl", payUrl);
Assert.DoesNotContain("amount=", payUrl); Assert.DoesNotContain("amount=", payUrl);
@@ -358,6 +396,7 @@ namespace BTCPayServer.Tests
s.GoToHome(); s.GoToHome();
s.GoToLightningSettings(); s.GoToLightningSettings();
s.Driver.SetCheckbox(By.Id("LNURLEnabled"), false); s.Driver.SetCheckbox(By.Id("LNURLEnabled"), false);
s.Driver.ScrollTo(By.Id("save"));
s.Driver.FindElement(By.Id("save")).Click(); s.Driver.FindElement(By.Id("save")).Click();
Assert.Contains("BTC Lightning settings successfully updated", s.FindAlertMessage().Text); Assert.Contains("BTC Lightning settings successfully updated", s.FindAlertMessage().Text);

View File

@@ -1,5 +1,6 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Client.Models; using BTCPayServer.Client.Models;
using BTCPayServer.Controllers; using BTCPayServer.Controllers;
using BTCPayServer.Data; using BTCPayServer.Data;
@@ -53,13 +54,33 @@ namespace BTCPayServer.Tests
Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps2.ListApps(user2.StoreId).Result).Model); Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps2.ListApps(user2.StoreId).Result).Model);
Assert.Single(appList.Apps); Assert.Single(appList.Apps);
Assert.Empty(appList2.Apps); Assert.Empty(appList2.Apps);
Assert.Equal("test", appList.Apps[0].AppName); Assert.Equal("test", app.AppName);
Assert.Equal(apps.CreatedAppId, appList.Apps[0].Id); Assert.Equal(apps.CreatedAppId, app.Id);
Assert.True(appList.Apps[0].IsOwner); Assert.True(app.Role.ToPermissionSet(app.StoreId).Contains(Policies.CanModifyStoreSettings, app.StoreId));
Assert.Equal(user.StoreId, appList.Apps[0].StoreId); Assert.Equal(user.StoreId, app.StoreId);
Assert.IsType<NotFoundResult>(apps2.DeleteApp(appList.Apps[0].Id)); // Archive
Assert.IsType<ViewResult>(apps.DeleteApp(appList.Apps[0].Id)); redirect = Assert.IsType<RedirectResult>(apps.ToggleArchive(app.Id).Result);
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(appList.Apps[0].Id).Result); Assert.EndsWith("/settings/crowdfund", redirect.Url);
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
Assert.Empty(appList.Apps);
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId, archived: true).Result).Model);
app = appList.Apps[0];
Assert.True(app.Archived);
Assert.IsType<NotFoundResult>(await crowdfund.ViewCrowdfund(app.Id));
// Unarchive
redirect = Assert.IsType<RedirectResult>(apps.ToggleArchive(app.Id).Result);
Assert.EndsWith("/settings/crowdfund", redirect.Url);
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
app = appList.Apps[0];
Assert.False(app.Archived);
var crowdfundViewModel = await crowdfund.UpdateCrowdfund(app.Id).AssertViewModelAsync<UpdateCrowdfundViewModel>();
crowdfundViewModel.Enabled = true;
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
Assert.IsType<ViewResult>(await crowdfund.ViewCrowdfund(app.Id));
// Delete
Assert.IsType<NotFoundResult>(apps2.DeleteApp(app.Id));
Assert.IsType<ViewResult>(apps.DeleteApp(app.Id));
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(app.Id).Result);
Assert.Equal(nameof(UIStoresController.Dashboard), redirectToAction.ActionName); Assert.Equal(nameof(UIStoresController.Dashboard), redirectToAction.ActionName);
appList = await apps.ListApps(user.StoreId).AssertViewModelAsync<ListAppsViewModel>(); appList = await apps.ListApps(user.StoreId).AssertViewModelAsync<ListAppsViewModel>();
Assert.Empty(appList.Apps); Assert.Empty(appList.Apps);

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.Diagnostics;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -122,6 +123,13 @@ retry:
driver.ExecuteJavaScript($"document.getElementById('{element}').{funcName}()"); driver.ExecuteJavaScript($"document.getElementById('{element}').{funcName}()");
} }
public static void WaitWalletTransactionsLoaded(this IWebDriver driver)
{
var wait = new WebDriverWait(driver, SeleniumTester.ImplicitWait);
wait.UntilJsIsReady();
wait.Until(d => d.WaitForElement(By.CssSelector("#WalletTransactions[data-loaded='true']")));
}
public static IWebElement WaitForElement(this IWebDriver driver, By selector) public static IWebElement WaitForElement(this IWebDriver driver, By selector)
{ {
var wait = new WebDriverWait(driver, SeleniumTester.ImplicitWait); var wait = new WebDriverWait(driver, SeleniumTester.ImplicitWait);
@@ -189,6 +197,7 @@ retry:
driver.FindElement(selector).Click(); driver.FindElement(selector).Click();
} }
[DebuggerHidden]
public static bool ElementDoesNotExist(this IWebDriver driver, By selector) public static bool ElementDoesNotExist(this IWebDriver driver, By selector)
{ {
Assert.Throws<NoSuchElementException>(() => Assert.Throws<NoSuchElementException>(() =>

View File

@@ -23,9 +23,11 @@ using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payments.Lightning; using BTCPayServer.Payments.Lightning;
using BTCPayServer.Rating; using BTCPayServer.Rating;
using BTCPayServer.Services; using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Labels; using BTCPayServer.Services.Labels;
using BTCPayServer.Services.Rates; using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Validation; using BTCPayServer.Validation;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
@@ -345,165 +347,272 @@ namespace BTCPayServer.Tests
Assert.True(Torrc.TryParse(input, out torrc)); Assert.True(Torrc.TryParse(input, out torrc));
Assert.Equal(expected, torrc.ToString()); Assert.Equal(expected, torrc.ToString());
} }
[Fact]
public void CanParseCartItems()
{
Assert.True(AppService.TryParsePosCartItems(new JObject()
{
{"cart", new JArray()
{
new JObject()
{
{ "id", "ddd"},
{"price", 4},
{"count", 1}
}
}}
}, out var items));
Assert.Equal("ddd", items[0].Id);
Assert.Equal(1, items[0].Count);
Assert.Equal(4, items[0].Price);
// Using legacy parsing
Assert.True(AppService.TryParsePosCartItems(new JObject()
{
{"cart", new JArray()
{
new JObject()
{
{ "id", "ddd"},
{"price", new JObject()
{
{ "value", 8.49m }
}
},
{"count", 1}
}
}}
}, out items));
Assert.Equal("ddd", items[0].Id);
Assert.Equal(1, items[0].Count);
Assert.Equal(8.49m, items[0].Price);
Assert.False(AppService.TryParsePosCartItems(new JObject()
{
{"cart", new JArray()
{
new JObject()
{
{ "id", "ddd"},
{"price", new JObject()
{
{ "value", "nocrahs" }
}
},
{"count", 1}
}
}}
}, out items));
}
[Fact]
public void CanCalculateDust()
{
var entity = new InvoiceEntity() { Currency = "USD" };
entity.Networks = new BTCPayNetworkProvider(ChainName.Regtest);
#pragma warning disable CS0618
entity.Payments = new System.Collections.Generic.List<PaymentEntity>();
entity.SetPaymentMethod(new PaymentMethod()
{
Currency = "BTC",
Rate = 34_000m
});
entity.Price = 4000;
entity.UpdateTotals();
var accounting = entity.GetPaymentMethods().First().Calculate();
// Exact price should be 0.117647059..., but the payment method round up to one sat
Assert.Equal(0.11764706m, accounting.Due);
entity.Payments.Add(new PaymentEntity()
{
Currency = "BTC",
Output = new TxOut(Money.Coins(0.11764706m), new Key()),
Accounted = true
});
entity.UpdateTotals();
Assert.Equal(0.0m, entity.NetDue);
// The dust's value is below 1 sat
Assert.True(entity.Dust > 0.0m);
Assert.True(Money.Satoshis(1.0m).ToDecimal(MoneyUnit.BTC) * entity.Rates["BTC"] > entity.Dust);
Assert.True(!entity.IsOverPaid);
Assert.True(!entity.IsUnderPaid);
// Now, imagine there is litecoin. It might seem from its
// perspecitve that there has been a slight over payment.
// However, Calculate() should just cap it to 0.0m
entity.SetPaymentMethod(new PaymentMethod()
{
Currency = "LTC",
Rate = 3400m
});
entity.UpdateTotals();
var method = entity.GetPaymentMethods().First(p => p.Currency == "LTC");
accounting = method.Calculate();
Assert.Equal(0.0m, accounting.DueUncapped);
#pragma warning restore CS0618
}
#if ALTCOINS #if ALTCOINS
[Fact] [Fact]
public void CanCalculateCryptoDue() public void CanCalculateCryptoDue()
{ {
var networkProvider = new BTCPayNetworkProvider(ChainName.Regtest); var networkProvider = new BTCPayNetworkProvider(ChainName.Regtest);
var paymentMethodHandlerDictionary = new PaymentMethodHandlerDictionary(new IPaymentMethodHandler[] var entity = new InvoiceEntity() { Currency = "USD" };
{
new BitcoinLikePaymentHandler(null, networkProvider, null, null, null, null),
new LightningLikePaymentHandler(null, null, networkProvider, null, null, null),
});
var entity = new InvoiceEntity();
entity.Networks = networkProvider; entity.Networks = networkProvider;
#pragma warning disable CS0618 #pragma warning disable CS0618
entity.Payments = new System.Collections.Generic.List<PaymentEntity>(); entity.Payments = new System.Collections.Generic.List<PaymentEntity>();
entity.SetPaymentMethod(new PaymentMethod() entity.SetPaymentMethod(new PaymentMethod()
{ {
CryptoCode = "BTC", Currency = "BTC",
Rate = 5000, Rate = 5000,
NextNetworkFee = Money.Coins(0.1m) NextNetworkFee = Money.Coins(0.1m)
}); });
entity.Price = 5000; entity.Price = 5000;
entity.UpdateTotals();
var paymentMethod = entity.GetPaymentMethods().TryGet("BTC", PaymentTypes.BTCLike); var paymentMethod = entity.GetPaymentMethods().TryGet("BTC", PaymentTypes.BTCLike);
var accounting = paymentMethod.Calculate(); var accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(1.1m), accounting.Due); Assert.Equal(1.0m, accounting.ToSmallestUnit(Money.Satoshis(1.0m).ToDecimal(MoneyUnit.BTC)));
Assert.Equal(Money.Coins(1.1m), accounting.TotalDue); Assert.Equal(1.1m, accounting.Due);
Assert.Equal(1.1m, accounting.TotalDue);
entity.Payments.Add(new PaymentEntity() entity.Payments.Add(new PaymentEntity()
{ {
Currency = "BTC",
Output = new TxOut(Money.Coins(0.5m), new Key()), Output = new TxOut(Money.Coins(0.5m), new Key()),
Rate = 5000,
Accounted = true, Accounted = true,
NetworkFee = 0.1m NetworkFee = 0.1m
}); });
entity.UpdateTotals();
accounting = paymentMethod.Calculate(); accounting = paymentMethod.Calculate();
//Since we need to spend one more txout, it should be 1.1 - 0,5 + 0.1 //Since we need to spend one more txout, it should be 1.1 - 0,5 + 0.1
Assert.Equal(Money.Coins(0.7m), accounting.Due); Assert.Equal(0.7m, accounting.Due);
Assert.Equal(Money.Coins(1.2m), accounting.TotalDue); Assert.Equal(1.2m, accounting.TotalDue);
entity.Payments.Add(new PaymentEntity() entity.Payments.Add(new PaymentEntity()
{ {
Currency = "BTC",
Output = new TxOut(Money.Coins(0.2m), new Key()), Output = new TxOut(Money.Coins(0.2m), new Key()),
Accounted = true, Accounted = true,
NetworkFee = 0.1m NetworkFee = 0.1m
}); });
entity.UpdateTotals();
accounting = paymentMethod.Calculate(); accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(0.6m), accounting.Due); Assert.Equal(0.6m, accounting.Due);
Assert.Equal(Money.Coins(1.3m), accounting.TotalDue); Assert.Equal(1.3m, accounting.TotalDue);
entity.Payments.Add(new PaymentEntity() entity.Payments.Add(new PaymentEntity()
{ {
Currency = "BTC",
Output = new TxOut(Money.Coins(0.6m), new Key()), Output = new TxOut(Money.Coins(0.6m), new Key()),
Accounted = true, Accounted = true,
NetworkFee = 0.1m NetworkFee = 0.1m
}); });
entity.UpdateTotals();
accounting = paymentMethod.Calculate(); accounting = paymentMethod.Calculate();
Assert.Equal(Money.Zero, accounting.Due); Assert.Equal(0.0m, accounting.Due);
Assert.Equal(Money.Coins(1.3m), accounting.TotalDue); Assert.Equal(1.3m, accounting.TotalDue);
entity.Payments.Add( entity.Payments.Add(
new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()), Accounted = true }); new PaymentEntity() { Currency = "BTC", Output = new TxOut(Money.Coins(0.2m), new Key()), Accounted = true });
entity.UpdateTotals();
accounting = paymentMethod.Calculate(); accounting = paymentMethod.Calculate();
Assert.Equal(Money.Zero, accounting.Due); Assert.Equal(0.0m, accounting.Due);
Assert.Equal(Money.Coins(1.3m), accounting.TotalDue); Assert.Equal(1.3m, accounting.TotalDue);
entity = new InvoiceEntity(); entity = new InvoiceEntity();
entity.Networks = networkProvider; entity.Networks = networkProvider;
entity.Price = 5000; entity.Price = 5000;
PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary(); PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary();
paymentMethods.Add( paymentMethods.Add(
new PaymentMethod() { CryptoCode = "BTC", Rate = 1000, NextNetworkFee = Money.Coins(0.1m) }); new PaymentMethod() { Currency = "BTC", Rate = 1000, NextNetworkFee = Money.Coins(0.1m) });
paymentMethods.Add( paymentMethods.Add(
new PaymentMethod() { CryptoCode = "LTC", Rate = 500, NextNetworkFee = Money.Coins(0.01m) }); new PaymentMethod() { Currency = "LTC", Rate = 500, NextNetworkFee = Money.Coins(0.01m) });
entity.SetPaymentMethods(paymentMethods); entity.SetPaymentMethods(paymentMethods);
entity.Payments = new List<PaymentEntity>(); entity.Payments = new List<PaymentEntity>();
entity.UpdateTotals();
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike)); paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike));
accounting = paymentMethod.Calculate(); accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(5.1m), accounting.Due); Assert.Equal(5.1m, accounting.Due);
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike)); paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
accounting = paymentMethod.Calculate(); accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(10.01m), accounting.TotalDue); Assert.Equal(10.01m, accounting.TotalDue);
entity.Payments.Add(new PaymentEntity() entity.Payments.Add(new PaymentEntity()
{ {
CryptoCode = "BTC", Currency = "BTC",
Output = new TxOut(Money.Coins(1.0m), new Key()), Output = new TxOut(Money.Coins(1.0m), new Key()),
Accounted = true, Accounted = true,
NetworkFee = 0.1m NetworkFee = 0.1m
}); });
entity.UpdateTotals();
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike)); paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike));
accounting = paymentMethod.Calculate(); accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(4.2m), accounting.Due); Assert.Equal(4.2m, accounting.Due);
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid); Assert.Equal(1.0m, accounting.CryptoPaid);
Assert.Equal(Money.Coins(1.0m), accounting.Paid); Assert.Equal(1.0m, accounting.Paid);
Assert.Equal(Money.Coins(5.2m), accounting.TotalDue); Assert.Equal(5.2m, accounting.TotalDue);
Assert.Equal(2, accounting.TxRequired); Assert.Equal(2, accounting.TxRequired);
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike)); paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
accounting = paymentMethod.Calculate(); accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(10.01m + 0.1m * 2 - 2.0m /* 8.21m */), accounting.Due); Assert.Equal(10.01m + 0.1m * 2 - 2.0m /* 8.21m */, accounting.Due);
Assert.Equal(Money.Coins(0.0m), accounting.CryptoPaid); Assert.Equal(0.0m, accounting.CryptoPaid);
Assert.Equal(Money.Coins(2.0m), accounting.Paid); Assert.Equal(2.0m, accounting.Paid);
Assert.Equal(Money.Coins(10.01m + 0.1m * 2), accounting.TotalDue); Assert.Equal(10.01m + 0.1m * 2, accounting.TotalDue);
entity.Payments.Add(new PaymentEntity() entity.Payments.Add(new PaymentEntity()
{ {
CryptoCode = "LTC", Currency = "LTC",
Output = new TxOut(Money.Coins(1.0m), new Key()), Output = new TxOut(Money.Coins(1.0m), new Key()),
Accounted = true, Accounted = true,
NetworkFee = 0.01m NetworkFee = 0.01m
}); });
entity.UpdateTotals();
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike)); paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike));
accounting = paymentMethod.Calculate(); accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(4.2m - 0.5m + 0.01m / 2), accounting.Due); Assert.Equal(4.2m - 0.5m + 0.01m / 2, accounting.Due);
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid); Assert.Equal(1.0m, accounting.CryptoPaid);
Assert.Equal(Money.Coins(1.5m), accounting.Paid); Assert.Equal(1.5m, accounting.Paid);
Assert.Equal(Money.Coins(5.2m + 0.01m / 2), accounting.TotalDue); // The fee for LTC added Assert.Equal(5.2m + 0.01m / 2, accounting.TotalDue); // The fee for LTC added
Assert.Equal(2, accounting.TxRequired); Assert.Equal(2, accounting.TxRequired);
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike)); paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
accounting = paymentMethod.Calculate(); accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(8.21m - 1.0m + 0.01m), accounting.Due); Assert.Equal(8.21m - 1.0m + 0.01m, accounting.Due);
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid); Assert.Equal(1.0m, accounting.CryptoPaid);
Assert.Equal(Money.Coins(3.0m), accounting.Paid); Assert.Equal(3.0m, accounting.Paid);
Assert.Equal(Money.Coins(10.01m + 0.1m * 2 + 0.01m), accounting.TotalDue); Assert.Equal(10.01m + 0.1m * 2 + 0.01m, accounting.TotalDue);
Assert.Equal(2, accounting.TxRequired); Assert.Equal(2, accounting.TxRequired);
var remaining = Money.Coins(4.2m - 0.5m + 0.01m / 2); var remaining = Money.Coins(4.2m - 0.5m + 0.01m / 2.0m).ToDecimal(MoneyUnit.BTC);
entity.Payments.Add(new PaymentEntity() entity.Payments.Add(new PaymentEntity()
{ {
CryptoCode = "BTC", Currency = "BTC",
Output = new TxOut(remaining, new Key()), Output = new TxOut(Money.Coins(remaining), new Key()),
Accounted = true, Accounted = true,
NetworkFee = 0.1m NetworkFee = 0.1m
}); });
entity.UpdateTotals();
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike)); paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike));
accounting = paymentMethod.Calculate(); accounting = paymentMethod.Calculate();
Assert.Equal(Money.Zero, accounting.Due); Assert.Equal(0.0m, accounting.Due);
Assert.Equal(Money.Coins(1.0m) + remaining, accounting.CryptoPaid); Assert.Equal(1.0m + remaining, accounting.CryptoPaid);
Assert.Equal(Money.Coins(1.5m) + remaining, accounting.Paid); Assert.Equal(1.5m + remaining, accounting.Paid);
Assert.Equal(Money.Coins(5.2m + 0.01m / 2), accounting.TotalDue); Assert.Equal(5.2m + 0.01m / 2, accounting.TotalDue);
Assert.Equal(accounting.Paid, accounting.TotalDue); Assert.Equal(accounting.Paid, accounting.TotalDue);
Assert.Equal(2, accounting.TxRequired); Assert.Equal(2, accounting.TxRequired);
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike)); paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
accounting = paymentMethod.Calculate(); accounting = paymentMethod.Calculate();
Assert.Equal(Money.Zero, accounting.Due); Assert.Equal(0.0m, accounting.Due);
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid); Assert.Equal(1.0m, accounting.CryptoPaid);
Assert.Equal(Money.Coins(3.0m) + remaining * 2, accounting.Paid); Assert.Equal(3.0m + remaining * 2, accounting.Paid);
// Paying 2 BTC fee, LTC fee removed because fully paid // Paying 2 BTC fee, LTC fee removed because fully paid
Assert.Equal(Money.Coins(10.01m + 0.1m * 2 + 0.1m * 2 /* + 0.01m no need to pay this fee anymore */), Assert.Equal(10.01m + 0.1m * 2 + 0.1m * 2 /* + 0.01m no need to pay this fee anymore */,
accounting.TotalDue); accounting.TotalDue);
Assert.Equal(1, accounting.TxRequired); Assert.Equal(1, accounting.TxRequired);
Assert.Equal(accounting.Paid, accounting.TotalDue); Assert.Equal(accounting.Paid, accounting.TotalDue);
@@ -547,27 +656,29 @@ namespace BTCPayServer.Tests
entity.Payments = new List<PaymentEntity>(); entity.Payments = new List<PaymentEntity>();
entity.SetPaymentMethod(new PaymentMethod() entity.SetPaymentMethod(new PaymentMethod()
{ {
CryptoCode = "BTC", Currency = "BTC",
Rate = 5000, Rate = 5000,
NextNetworkFee = Money.Coins(0.1m) NextNetworkFee = Money.Coins(0.1m)
}); });
entity.Price = 5000; entity.Price = 5000;
entity.PaymentTolerance = 0; entity.PaymentTolerance = 0;
entity.UpdateTotals();
var paymentMethod = entity.GetPaymentMethods().TryGet("BTC", PaymentTypes.BTCLike); var paymentMethod = entity.GetPaymentMethods().TryGet("BTC", PaymentTypes.BTCLike);
var accounting = paymentMethod.Calculate(); var accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(1.1m), accounting.Due); Assert.Equal(1.1m, accounting.Due);
Assert.Equal(Money.Coins(1.1m), accounting.TotalDue); Assert.Equal(1.1m, accounting.TotalDue);
Assert.Equal(Money.Coins(1.1m), accounting.MinimumTotalDue); Assert.Equal(1.1m, accounting.MinimumTotalDue);
entity.PaymentTolerance = 10; entity.PaymentTolerance = 10;
entity.UpdateTotals();
accounting = paymentMethod.Calculate(); accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(0.99m), accounting.MinimumTotalDue); Assert.Equal(0.99m, accounting.MinimumTotalDue);
entity.PaymentTolerance = 100; entity.PaymentTolerance = 100;
entity.UpdateTotals();
accounting = paymentMethod.Calculate(); accounting = paymentMethod.Calculate();
Assert.Equal(Money.Satoshis(1), accounting.MinimumTotalDue); Assert.Equal(0.0000_0001m, accounting.MinimumTotalDue);
} }
[Fact] [Fact]
@@ -608,7 +719,7 @@ namespace BTCPayServer.Tests
} }
[Fact] [Fact]
public void CanDetectImage() public void CanDetectFileType()
{ {
Assert.True(FileTypeDetector.IsPicture(new byte[] { 0x42, 0x4D }, "test.bmp")); Assert.True(FileTypeDetector.IsPicture(new byte[] { 0x42, 0x4D }, "test.bmp"));
Assert.False(FileTypeDetector.IsPicture(new byte[] { 0x42, 0x4D }, ".bmp")); Assert.False(FileTypeDetector.IsPicture(new byte[] { 0x42, 0x4D }, ".bmp"));
@@ -621,6 +732,15 @@ namespace BTCPayServer.Tests
Assert.False(FileTypeDetector.IsPicture(new byte[] { 0x3C, 0x73, 0x76, 0x67 }, "test.jpg")); Assert.False(FileTypeDetector.IsPicture(new byte[] { 0x3C, 0x73, 0x76, 0x67 }, "test.jpg"));
Assert.False(FileTypeDetector.IsPicture(new byte[] { 0xFF }, "e.jpg")); Assert.False(FileTypeDetector.IsPicture(new byte[] { 0xFF }, "e.jpg"));
Assert.False(FileTypeDetector.IsPicture(new byte[] { }, "empty.jpg")); Assert.False(FileTypeDetector.IsPicture(new byte[] { }, "empty.jpg"));
Assert.False(FileTypeDetector.IsPicture(new byte[] { 0x49, 0x44, 0x33, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23 }, "music.mp3"));
Assert.True(FileTypeDetector.IsAudio(new byte[] { 0x49, 0x44, 0x33, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23 }, "music.mp3"));
Assert.True(FileTypeDetector.IsAudio(new byte[] { 0x52, 0x49, 0x46, 0x46, 0x24, 0x9A, 0x08, 0x00, 0x57, 0x41 }, "music.wav"));
Assert.True(FileTypeDetector.IsAudio(new byte[] { 0xFF, 0xF1, 0x50, 0x80, 0x1C, 0x3F, 0xFC, 0xDA, 0x00, 0x4C }, "music.aac"));
Assert.True(FileTypeDetector.IsAudio(new byte[] { 0x66, 0x4C, 0x61, 0x43, 0x00, 0x00, 0x00, 0x22, 0x04, 0x80 }, "music.flac"));
Assert.True(FileTypeDetector.IsAudio(new byte[] { 0x4F, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00 }, "music.ogg"));
Assert.True(FileTypeDetector.IsAudio(new byte[] { 0x1A, 0x45, 0xDF, 0xA3, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00 }, "music.weba"));
Assert.True(FileTypeDetector.IsAudio(new byte[] { 0xFF, 0xF3, 0xE4, 0x64, 0x00, 0x20, 0xAD, 0xBD, 0x04, 0x00 }, "music.mp3"));
} }
[Fact] [Fact]
@@ -1042,14 +1162,13 @@ namespace BTCPayServer.Tests
[Fact] [Fact]
public void CanParseFilter() public void CanParseFilter()
{ {
var storeId = "6DehZnc9S7qC6TUTNWuzJ1pFsHTHvES6An21r3MjvLey";
var filter = "storeid:abc, status:abed, blabhbalh "; var filter = "storeid:abc, status:abed, blabhbalh ";
var search = new SearchString(filter); var search = new SearchString(filter);
Assert.Equal("storeid:abc, status:abed, blabhbalh", search.ToString()); Assert.Equal("storeid:abc, status:abed, blabhbalh", search.ToString());
Assert.Equal("blabhbalh", search.TextSearch); Assert.Equal("blabhbalh", search.TextSearch);
Assert.Single(search.Filters["storeid"]); Assert.Single(search.Filters["storeid"], "abc");
Assert.Single(search.Filters["status"]); Assert.Single(search.Filters["status"], "abed");
Assert.Equal("abc", search.Filters["storeid"].First());
Assert.Equal("abed", search.Filters["status"].First());
filter = "status:abed, status:abed2"; filter = "status:abed, status:abed2";
search = new SearchString(filter); search = new SearchString(filter);
@@ -1064,6 +1183,48 @@ namespace BTCPayServer.Tests
search = new SearchString(filter); search = new SearchString(filter);
Assert.Equal("2019-04-25 01:00 AM", search.Filters["startdate"].First()); Assert.Equal("2019-04-25 01:00 AM", search.Filters["startdate"].First());
Assert.Equal("hekki", search.TextSearch); Assert.Equal("hekki", search.TextSearch);
// modify search
filter = $"status:settled,exceptionstatus:paidLate,unusual:true, fulltext searchterm, storeid:{storeId},startdate:2019-04-25 01:00:00";
search = new SearchString(filter);
Assert.Equal(filter, search.ToString());
Assert.Equal("fulltext searchterm", search.TextSearch);
Assert.Single(search.Filters["storeid"], storeId);
Assert.Single(search.Filters["status"], "settled");
Assert.Single(search.Filters["exceptionstatus"], "paidLate");
Assert.Single(search.Filters["unusual"], "true");
// toggle off bool with same value
var modified = new SearchString(search.Toggle("unusual", "true"));
Assert.Null(modified.GetFilterBool("unusual"));
// add to array
modified = new SearchString(modified.Toggle("status", "processing"));
var statusArray = modified.GetFilterArray("status");
Assert.Equal(2, statusArray.Length);
Assert.Contains("processing", statusArray);
Assert.Contains("settled", statusArray);
// toggle off array with same value
modified = new SearchString(modified.Toggle("status", "settled"));
statusArray = modified.GetFilterArray("status");
Assert.Single(statusArray, "processing");
// toggle off array with null value
modified = new SearchString(modified.Toggle("status", null));
Assert.Null(modified.GetFilterArray("status"));
// toggle off date with null value
modified = new SearchString(modified.Toggle("startdate", "-7d"));
Assert.Single(modified.GetFilterArray("startdate"), "-7d");
modified = new SearchString(modified.Toggle("startdate", null));
Assert.Null(modified.GetFilterArray("startdate"));
// toggle off date with same value
modified = new SearchString(modified.Toggle("enddate", "-7d"));
Assert.Single(modified.GetFilterArray("enddate"), "-7d");
modified = new SearchString(modified.Toggle("enddate", "-7d"));
Assert.Null(modified.GetFilterArray("enddate"));
} }
[Fact] [Fact]
@@ -1103,6 +1264,45 @@ namespace BTCPayServer.Tests
Assert.Equal("000000161", m.OrderId); Assert.Equal("000000161", m.OrderId);
} }
[Fact]
public void CanParseOldPosAppData()
{
var data = new JObject()
{
["price"] = 1.64m
}.ToString();
Assert.Equal(1.64m, JsonConvert.DeserializeObject<PosAppCartItem>(data).Price);
data = new JObject()
{
["price"] = new JObject()
{
["value"] = 1.65m
}
}.ToString();
Assert.Equal(1.65m, JsonConvert.DeserializeObject<PosAppCartItem>(data).Price);
data = new JObject()
{
["price"] = new JObject()
{
["value"] = "1.6305"
}
}.ToString();
Assert.Equal(1.6305m, JsonConvert.DeserializeObject<PosAppCartItem>(data).Price);
data = new JObject()
{
["price"] = new JObject()
{
["value"] = null
}
}.ToString();
Assert.Equal(0.0m, JsonConvert.DeserializeObject<PosAppCartItem>(data).Price);
var o = JObject.Parse(JsonConvert.SerializeObject(new PosAppCartItem() { Price = 1.356m }));
Assert.Equal(1.356m, o["price"].Value<decimal>());
}
[Fact] [Fact]
public void CanParseCurrencyValue() public void CanParseCurrencyValue()
{ {
@@ -1341,6 +1541,24 @@ namespace BTCPayServer.Tests
Assert.Equal(cache.States[0].Rates[0].Pair, cache2.States[0].Rates[0].Pair); Assert.Equal(cache.States[0].Rates[0].Pair, cache2.States[0].Rates[0].Pair);
} }
[Fact]
public void CanParseStoreRoleId()
{
var id = StoreRoleId.Parse("test::lol");
Assert.Equal("test", id.StoreId);
Assert.Equal("lol", id.Role);
Assert.Equal("test::lol", id.ToString());
Assert.Equal("test::lol", id.Id);
Assert.False(id.IsServerRole);
id = StoreRoleId.Parse("lol");
Assert.Null(id.StoreId);
Assert.Equal("lol", id.Role);
Assert.Equal("lol", id.ToString());
Assert.Equal("lol", id.Id);
Assert.True(id.IsServerRole);
}
[Fact] [Fact]
public void KitchenSinkTest() public void KitchenSinkTest()
{ {
@@ -1785,11 +2003,6 @@ namespace BTCPayServer.Tests
#pragma warning disable CS0618 #pragma warning disable CS0618
var dummy = new Key().PubKey.GetAddress(ScriptPubKeyType.Legacy, Network.RegTest).ToString(); var dummy = new Key().PubKey.GetAddress(ScriptPubKeyType.Legacy, Network.RegTest).ToString();
var networkProvider = new BTCPayNetworkProvider(ChainName.Regtest); var networkProvider = new BTCPayNetworkProvider(ChainName.Regtest);
var paymentMethodHandlerDictionary = new PaymentMethodHandlerDictionary(new IPaymentMethodHandler[]
{
new BitcoinLikePaymentHandler(null, networkProvider, null, null, null, null),
new LightningLikePaymentHandler(null, null, networkProvider, null, null, null),
});
var networkBTC = networkProvider.GetNetwork("BTC"); var networkBTC = networkProvider.GetNetwork("BTC");
var networkLTC = networkProvider.GetNetwork("LTC"); var networkLTC = networkProvider.GetNetwork("LTC");
InvoiceEntity invoiceEntity = new InvoiceEntity(); InvoiceEntity invoiceEntity = new InvoiceEntity();
@@ -1797,14 +2010,14 @@ namespace BTCPayServer.Tests
invoiceEntity.Payments = new System.Collections.Generic.List<PaymentEntity>(); invoiceEntity.Payments = new System.Collections.Generic.List<PaymentEntity>();
invoiceEntity.Price = 100; invoiceEntity.Price = 100;
PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary(); PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary();
paymentMethods.Add(new PaymentMethod() { Network = networkBTC, CryptoCode = "BTC", Rate = 10513.44m, } paymentMethods.Add(new PaymentMethod() { Network = networkBTC, Currency = "BTC", Rate = 10513.44m, }
.SetPaymentMethodDetails( .SetPaymentMethodDetails(
new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod() new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod()
{ {
NextNetworkFee = Money.Coins(0.00000100m), NextNetworkFee = Money.Coins(0.00000100m),
DepositAddress = dummy DepositAddress = dummy
})); }));
paymentMethods.Add(new PaymentMethod() { Network = networkLTC, CryptoCode = "LTC", Rate = 216.79m } paymentMethods.Add(new PaymentMethod() { Network = networkLTC, Currency = "LTC", Rate = 216.79m }
.SetPaymentMethodDetails( .SetPaymentMethodDetails(
new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod() new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod()
{ {
@@ -1820,7 +2033,7 @@ namespace BTCPayServer.Tests
new PaymentEntity() new PaymentEntity()
{ {
Accounted = true, Accounted = true,
CryptoCode = "BTC", Currency = "BTC",
NetworkFee = 0.00000100m, NetworkFee = 0.00000100m,
Network = networkProvider.GetNetwork("BTC"), Network = networkProvider.GetNetwork("BTC"),
} }
@@ -1829,34 +2042,33 @@ namespace BTCPayServer.Tests
Network = networkProvider.GetNetwork("BTC"), Network = networkProvider.GetNetwork("BTC"),
Output = new TxOut() { Value = Money.Coins(0.00151263m) } Output = new TxOut() { Value = Money.Coins(0.00151263m) }
})); }));
invoiceEntity.UpdateTotals();
accounting = btc.Calculate(); accounting = btc.Calculate();
invoiceEntity.Payments.Add( invoiceEntity.Payments.Add(
new PaymentEntity() new PaymentEntity()
{ {
Accounted = true, Accounted = true,
CryptoCode = "BTC", Currency = "BTC",
NetworkFee = 0.00000100m, NetworkFee = 0.00000100m,
Network = networkProvider.GetNetwork("BTC") Network = networkProvider.GetNetwork("BTC")
} }
.SetCryptoPaymentData(new BitcoinLikePaymentData() .SetCryptoPaymentData(new BitcoinLikePaymentData()
{ {
Network = networkProvider.GetNetwork("BTC"), Network = networkProvider.GetNetwork("BTC"),
Output = new TxOut() { Value = accounting.Due } Output = new TxOut() { Value = Money.Coins(accounting.Due) }
})); }));
invoiceEntity.UpdateTotals();
accounting = btc.Calculate(); accounting = btc.Calculate();
Assert.Equal(Money.Zero, accounting.Due); Assert.Equal(0.0m, accounting.Due);
Assert.Equal(Money.Zero, accounting.DueUncapped); Assert.Equal(0.0m, accounting.DueUncapped);
var ltc = invoiceEntity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike)); var ltc = invoiceEntity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
accounting = ltc.Calculate(); accounting = ltc.Calculate();
Assert.Equal(Money.Zero, accounting.Due); Assert.Equal(0.0m, accounting.Due);
// LTC might have over paid due to BTC paying above what it should (round 1 satoshi up) // LTC might should be over paid due to BTC paying above what it should (round 1 satoshi up), but we handle this case
Assert.True(accounting.DueUncapped < Money.Zero); // and set DueUncapped to zero.
Assert.Equal(0.0m, accounting.DueUncapped);
var paymentMethod = InvoiceWatcher.GetNearestClearedPayment(paymentMethods, out var accounting2);
Assert.Equal(btc.CryptoCode, paymentMethod.CryptoCode);
#pragma warning restore CS0618
} }
[Fact] [Fact]

View File

@@ -1,5 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Form; using BTCPayServer.Abstractions.Form;
using BTCPayServer.Forms; using BTCPayServer.Forms;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@@ -10,16 +11,25 @@ using Xunit.Abstractions;
namespace BTCPayServer.Tests; namespace BTCPayServer.Tests;
[Trait("Fast", "Fast")] [Collection(nameof(NonParallelizableCollectionDefinition))]
[Trait("Integration", "Integration")]
public class FormTests : UnitTestBase public class FormTests : UnitTestBase
{ {
public FormTests(ITestOutputHelper helper) : base(helper) public FormTests(ITestOutputHelper helper) : base(helper)
{ {
} }
[Fact]
public void CanParseForm() [Fact(Timeout = TestUtils.TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanParseForm()
{ {
using var tester = CreateServerTester();
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess();
var service = tester.PayTester.GetService<FormDataService>();
var form = new Form() var form = new Form()
{ {
Fields = new List<Field> Fields = new List<Field>
@@ -40,8 +50,6 @@ public class FormTests : UnitTestBase
} }
} }
}; };
var providers = new FormComponentProviders(new List<IFormComponentProvider>());
var service = new FormDataService(null, providers);
Assert.False(service.IsFormSchemaValid(form.ToString(), out _, out _)); Assert.False(service.IsFormSchemaValid(form.ToString(), out _, out _));
form = new Form form = new Form
{ {
@@ -164,7 +172,7 @@ public class FormTests : UnitTestBase
Assert.Equal("original", obj["invoice"]["test"].Value<string>()); Assert.Equal("original", obj["invoice"]["test"].Value<string>());
Assert.Equal("updated", obj["invoice_item3"].Value<string>()); Assert.Equal("updated", obj["invoice_item3"].Value<string>());
Clear(form); Clear(form);
form.SetValues(obj); service.SetValues(form, obj);
obj = service.GetValues(form); obj = service.GetValues(form);
Assert.Equal("original", obj["invoice"]["test"].Value<string>()); Assert.Equal("original", obj["invoice"]["test"].Value<string>());
Assert.Equal("updated", obj["invoice_item3"].Value<string>()); Assert.Equal("updated", obj["invoice_item3"].Value<string>());
@@ -182,10 +190,12 @@ public class FormTests : UnitTestBase
} }
} }
}; };
form.SetValues(obj);
service.SetValues(form, obj);
obj = service.GetValues(form); obj = service.GetValues(form);
Assert.Null(obj["test"].Value<string>()); Assert.Null(obj["test"].Value<string>());
form.SetValues(new JObject { ["test"] = "hello" });
service.SetValues(form, new JObject { ["test"] = "hello" });
obj = service.GetValues(form); obj = service.GetValues(form);
Assert.Equal("hello", obj["test"].Value<string>()); Assert.Equal("hello", obj["test"].Value<string>());
} }

View File

@@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Threading; using System.Threading;
@@ -16,14 +15,13 @@ using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning; using BTCPayServer.Payments.Lightning;
using BTCPayServer.PayoutProcessors; using BTCPayServer.PayoutProcessors;
using BTCPayServer.PayoutProcessors.OnChain;
using BTCPayServer.Services; using BTCPayServer.Services;
using BTCPayServer.Services.Custodian.Client.MockCustodian; using BTCPayServer.Services.Custodian.Client.MockCustodian;
using BTCPayServer.Services.Notifications; using BTCPayServer.Services.Notifications;
using BTCPayServer.Services.Notifications.Blobs; using BTCPayServer.Services.Notifications.Blobs;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Hosting;
using NBitcoin; using NBitcoin;
using NBitpayClient; using NBitpayClient;
using Newtonsoft.Json; using Newtonsoft.Json;
@@ -228,7 +226,7 @@ namespace BTCPayServer.Tests
await Assert.ThrowsAsync<GreenfieldAPIException>(() => newUserClient.GetInvoices(store.Id)); await Assert.ThrowsAsync<GreenfieldAPIException>(() => newUserClient.GetInvoices(store.Id));
// if user is a guest or owner, then it should be ok // if user is a guest or owner, then it should be ok
await unrestricted.AddStoreUser(store.Id, new StoreUserData() { UserId = newUser.Id, Role = "Guest" }); await unrestricted.AddStoreUser(store.Id, new StoreUserData() { UserId = newUser.Id});
await newUserClient.GetInvoices(store.Id); await newUserClient.GetInvoices(store.Id);
} }
@@ -299,6 +297,7 @@ namespace BTCPayServer.Tests
Assert.Equal(user.StoreId, app.StoreId); Assert.Equal(user.StoreId, app.StoreId);
Assert.Equal("PointOfSale", app.AppType); Assert.Equal("PointOfSale", app.AppType);
Assert.Equal("test app title", app.Title); Assert.Equal("test app title", app.Title);
Assert.False(app.Archived);
// Make sure we return a 404 if we try to get an app that doesn't exist // Make sure we return a 404 if we try to get an app that doesn't exist
await AssertHttpError(404, async () => await AssertHttpError(404, async () =>
@@ -322,17 +321,20 @@ namespace BTCPayServer.Tests
new CreatePointOfSaleAppRequest() new CreatePointOfSaleAppRequest()
{ {
AppName = "new app name", AppName = "new app name",
Title = "new app title" Title = "new app title",
Archived = true
} }
); );
// Test generic GET app endpoint first // Test generic GET app endpoint first
retrievedApp = await client.GetApp(app.Id); retrievedApp = await client.GetApp(app.Id);
Assert.Equal("new app name", retrievedApp.Name); Assert.Equal("new app name", retrievedApp.Name);
Assert.True(retrievedApp.Archived);
// Test the POS-specific endpoint also // Test the POS-specific endpoint also
var retrievedPosApp = await client.GetPosApp(app.Id); var retrievedPosApp = await client.GetPosApp(app.Id);
Assert.Equal("new app name", retrievedPosApp.Name); Assert.Equal("new app name", retrievedPosApp.Name);
Assert.Equal("new app title", retrievedPosApp.Title); Assert.Equal("new app title", retrievedPosApp.Title);
Assert.True(retrievedPosApp.Archived);
// Make sure we return a 404 if we try to delete an app that doesn't exist // Make sure we return a 404 if we try to delete an app that doesn't exist
await AssertHttpError(404, async () => await AssertHttpError(404, async () =>
@@ -464,6 +466,7 @@ namespace BTCPayServer.Tests
Assert.Equal("test app from API", app.Name); Assert.Equal("test app from API", app.Name);
Assert.Equal(user.StoreId, app.StoreId); Assert.Equal(user.StoreId, app.StoreId);
Assert.Equal("Crowdfund", app.AppType); Assert.Equal("Crowdfund", app.AppType);
Assert.False(app.Archived);
// Make sure we return a 404 if we try to get an app that doesn't exist // Make sure we return a 404 if we try to get an app that doesn't exist
await AssertHttpError(404, async () => await AssertHttpError(404, async () =>
@@ -480,11 +483,13 @@ namespace BTCPayServer.Tests
Assert.Equal(app.Name, retrievedApp.Name); Assert.Equal(app.Name, retrievedApp.Name);
Assert.Equal(app.StoreId, retrievedApp.StoreId); Assert.Equal(app.StoreId, retrievedApp.StoreId);
Assert.Equal(app.AppType, retrievedApp.AppType); Assert.Equal(app.AppType, retrievedApp.AppType);
Assert.False(retrievedApp.Archived);
// Test the crowdfund-specific endpoint also // Test the crowdfund-specific endpoint also
var retrievedPosApp = await client.GetCrowdfundApp(app.Id); var retrievedCfApp = await client.GetCrowdfundApp(app.Id);
Assert.Equal(app.Name, retrievedPosApp.Name); Assert.Equal(app.Name, retrievedCfApp.Name);
Assert.Equal(app.Title, retrievedPosApp.Title); Assert.Equal(app.Title, retrievedCfApp.Title);
Assert.False(retrievedCfApp.Archived);
// Make sure we return a 404 if we try to delete an app that doesn't exist // Make sure we return a 404 if we try to delete an app that doesn't exist
await AssertHttpError(404, async () => await AssertHttpError(404, async () =>
@@ -534,10 +539,12 @@ namespace BTCPayServer.Tests
Assert.Equal(posApp.Name, apps[0].Name); Assert.Equal(posApp.Name, apps[0].Name);
Assert.Equal(posApp.StoreId, apps[0].StoreId); Assert.Equal(posApp.StoreId, apps[0].StoreId);
Assert.Equal(posApp.AppType, apps[0].AppType); Assert.Equal(posApp.AppType, apps[0].AppType);
Assert.False(apps[0].Archived);
Assert.Equal(crowdfundApp.Name, apps[1].Name); Assert.Equal(crowdfundApp.Name, apps[1].Name);
Assert.Equal(crowdfundApp.StoreId, apps[1].StoreId); Assert.Equal(crowdfundApp.StoreId, apps[1].StoreId);
Assert.Equal(crowdfundApp.AppType, apps[1].AppType); Assert.Equal(crowdfundApp.AppType, apps[1].AppType);
Assert.False(apps[1].Archived);
// Get all apps for all store now // Get all apps for all store now
apps = await client.GetAllApps(); apps = await client.GetAllApps();
@@ -547,15 +554,17 @@ namespace BTCPayServer.Tests
Assert.Equal(posApp.Name, apps[0].Name); Assert.Equal(posApp.Name, apps[0].Name);
Assert.Equal(posApp.StoreId, apps[0].StoreId); Assert.Equal(posApp.StoreId, apps[0].StoreId);
Assert.Equal(posApp.AppType, apps[0].AppType); Assert.Equal(posApp.AppType, apps[0].AppType);
Assert.False(apps[0].Archived);
Assert.Equal(crowdfundApp.Name, apps[1].Name); Assert.Equal(crowdfundApp.Name, apps[1].Name);
Assert.Equal(crowdfundApp.StoreId, apps[1].StoreId); Assert.Equal(crowdfundApp.StoreId, apps[1].StoreId);
Assert.Equal(crowdfundApp.AppType, apps[1].AppType); Assert.Equal(crowdfundApp.AppType, apps[1].AppType);
Assert.False(apps[1].Archived);
Assert.Equal(newApp.Name, apps[2].Name); Assert.Equal(newApp.Name, apps[2].Name);
Assert.Equal(newApp.StoreId, apps[2].StoreId); Assert.Equal(newApp.StoreId, apps[2].StoreId);
Assert.Equal(newApp.AppType, apps[2].AppType); Assert.Equal(newApp.AppType, apps[2].AppType);
Assert.False(apps[2].Archived);
} }
[Fact(Timeout = TestTimeout)] [Fact(Timeout = TestTimeout)]
@@ -876,7 +885,7 @@ namespace BTCPayServer.Tests
TestLogs.LogInformation("Can't archive without knowing the walletId"); TestLogs.LogInformation("Can't archive without knowing the walletId");
var ex = await AssertAPIError("missing-permission", async () => await client.ArchivePullPayment("lol", result.Id)); var ex = await AssertAPIError("missing-permission", async () => await client.ArchivePullPayment("lol", result.Id));
Assert.Equal("btcpay.store.canmanagepullpayments", ((GreenfieldPermissionAPIError)ex.APIError).MissingPermission); Assert.Equal("btcpay.store.canarchivepullpayments", ((GreenfieldPermissionAPIError)ex.APIError).MissingPermission);
TestLogs.LogInformation("Can't archive without permission"); TestLogs.LogInformation("Can't archive without permission");
await AssertAPIError("unauthenticated", async () => await unauthenticated.ArchivePullPayment(storeId, result.Id)); await AssertAPIError("unauthenticated", async () => await unauthenticated.ArchivePullPayment(storeId, result.Id));
await client.ArchivePullPayment(storeId, result.Id); await client.ArchivePullPayment(storeId, result.Id);
@@ -1073,6 +1082,22 @@ namespace BTCPayServer.Tests
var lnrURLs = await unauthenticated.GetPullPaymentLNURL(test4.Id); var lnrURLs = await unauthenticated.GetPullPaymentLNURL(test4.Id);
Assert.IsType<string>(lnrURLs.LNURLBech32); Assert.IsType<string>(lnrURLs.LNURLBech32);
Assert.IsType<string>(lnrURLs.LNURLUri); Assert.IsType<string>(lnrURLs.LNURLUri);
Assert.Equal(12.303228134m, test4.Amount);
Assert.Equal("BTC", test4.Currency);
// Test with SATS denomination values
var testSats = await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest()
{
Name = "Test SATS",
Amount = 21000,
Currency = "SATS",
PaymentMethods = new[] { "BTC", "BTC-LightningNetwork", "BTC_LightningLike" }
});
lnrURLs = await unauthenticated.GetPullPaymentLNURL(testSats.Id);
Assert.IsType<string>(lnrURLs.LNURLBech32);
Assert.IsType<string>(lnrURLs.LNURLUri);
Assert.Equal(21000, testSats.Amount);
Assert.Equal("SATS", testSats.Currency);
//permission test around auto approved pps and payouts //permission test around auto approved pps and payouts
var nonApproved = await acc.CreateClient(Policies.CanCreateNonApprovedPullPayments); var nonApproved = await acc.CreateClient(Policies.CanCreateNonApprovedPullPayments);
@@ -1134,7 +1159,8 @@ namespace BTCPayServer.Tests
Approved = false, Approved = false,
PaymentMethod = "BTC", PaymentMethod = "BTC",
Amount = 0.0001m, Amount = 0.0001m,
Destination = address.ToString() Destination = address.ToString(),
}); });
await AssertAPIError("invalid-state", async () => await AssertAPIError("invalid-state", async () =>
{ {
@@ -1253,7 +1279,7 @@ namespace BTCPayServer.Tests
using var tester = CreateServerTester(); using var tester = CreateServerTester();
await tester.StartAsync(); await tester.StartAsync();
var user = tester.NewAccount(); var user = tester.NewAccount();
user.GrantAccess(); await user.GrantAccessAsync();
await user.MakeAdmin(); await user.MakeAdmin();
var client = await user.CreateClient(Policies.Unrestricted); var client = await user.CreateClient(Policies.Unrestricted);
@@ -1319,7 +1345,8 @@ namespace BTCPayServer.Tests
// We strip the user's Owner right, so the key should not work // We strip the user's Owner right, so the key should not work
using var ctx = tester.PayTester.GetService<Data.ApplicationDbContextFactory>().CreateContext(); using var ctx = tester.PayTester.GetService<Data.ApplicationDbContextFactory>().CreateContext();
var storeEntity = await ctx.UserStore.SingleAsync(u => u.ApplicationUserId == user.UserId && u.StoreDataId == newStore.Id); var storeEntity = await ctx.UserStore.SingleAsync(u => u.ApplicationUserId == user.UserId && u.StoreDataId == newStore.Id);
storeEntity.Role = "Guest"; var roleId = (await tester.PayTester.GetService<StoreRepository>().GetStoreRoles(null)).Single(r => r.Role == "Guest").Id;
storeEntity.StoreRoleId = roleId;
await ctx.SaveChangesAsync(); await ctx.SaveChangesAsync();
await AssertHttpError(403, async () => await client.UpdateStore(newStore.Id, new UpdateStoreRequest() { Name = "B" })); await AssertHttpError(403, async () => await client.UpdateStore(newStore.Id, new UpdateStoreRequest() { Name = "B" }));
@@ -1331,6 +1358,13 @@ namespace BTCPayServer.Tests
} }
tester.DeleteStore = false; tester.DeleteStore = false;
Assert.Empty(await client.GetStores()); Assert.Empty(await client.GetStores());
// Archive
var archivableStore = await client.CreateStore(new CreateStoreRequest { Name = "Archivable" });
Assert.False(archivableStore.Archived);
archivableStore = await client.UpdateStore(archivableStore.Id, new UpdateStoreRequest { Name = "Archived", Archived = true });
Assert.Equal("Archived", archivableStore.Name);
Assert.True(archivableStore.Archived);
} }
private async Task<GreenfieldValidationException> AssertValidationError(string[] fields, Func<Task> act) private async Task<GreenfieldValidationException> AssertValidationError(string[] fields, Func<Task> act)
@@ -1430,7 +1464,7 @@ namespace BTCPayServer.Tests
Assert.False(hook.AutomaticRedelivery); Assert.False(hook.AutomaticRedelivery);
Assert.Equal(fakeServer.ServerUri.AbsoluteUri, hook.Url); Assert.Equal(fakeServer.ServerUri.AbsoluteUri, hook.Url);
} }
using var tester = CreateServerTester(); using var tester = CreateServerTester(newDb: true);
using var fakeServer = new FakeServer(); using var fakeServer = new FakeServer();
await fakeServer.Start(); await fakeServer.Start();
await tester.StartAsync(); await tester.StartAsync();
@@ -1507,6 +1541,14 @@ namespace BTCPayServer.Tests
clientProfile = await user.CreateClient(Policies.CanModifyStoreSettings, Policies.CanCreateInvoice); clientProfile = await user.CreateClient(Policies.CanModifyStoreSettings, Policies.CanCreateInvoice);
await clientProfile.GetWebhookDeliveryRequest(user.StoreId, hook.Id, newDeliveryId); await clientProfile.GetWebhookDeliveryRequest(user.StoreId, hook.Id, newDeliveryId);
TestLogs.LogInformation("Can prune deliveries");
var cleanup = tester.PayTester.GetService<HostedServices.CleanupWebhookDeliveriesTask>();
cleanup.BatchSize = 1;
cleanup.PruneAfter = TimeSpan.Zero;
await cleanup.Do(default);
await AssertHttpError(409, () => clientProfile.RedeliverWebhook(user.StoreId, hook.Id, delivery.Id));
TestLogs.LogInformation("Testing corner cases"); TestLogs.LogInformation("Testing corner cases");
Assert.Null(await clientProfile.GetWebhookDeliveryRequest(user.StoreId, "lol", newDeliveryId)); Assert.Null(await clientProfile.GetWebhookDeliveryRequest(user.StoreId, "lol", newDeliveryId));
Assert.Null(await clientProfile.GetWebhookDeliveryRequest(user.StoreId, hook.Id, "lol")); Assert.Null(await clientProfile.GetWebhookDeliveryRequest(user.StoreId, hook.Id, "lol"));
@@ -1564,7 +1606,7 @@ namespace BTCPayServer.Tests
using var tester = CreateServerTester(); using var tester = CreateServerTester();
await tester.StartAsync(); await tester.StartAsync();
var user = tester.NewAccount(); var user = tester.NewAccount();
user.GrantAccess(); await user.GrantAccessAsync();
await user.MakeAdmin(); await user.MakeAdmin();
var client = await user.CreateClient(Policies.Unrestricted); var client = await user.CreateClient(Policies.Unrestricted);
var viewOnly = await user.CreateClient(Policies.CanViewPaymentRequests); var viewOnly = await user.CreateClient(Policies.CanViewPaymentRequests);
@@ -1647,7 +1689,14 @@ namespace BTCPayServer.Tests
}); });
await TestUtils.EventuallyAsync(async () => await TestUtils.EventuallyAsync(async () =>
{ {
Assert.Equal(Invoice.STATUS_PAID, user.BitPay.GetInvoice(invoiceId).Status); Assert.Equal(Invoice.STATUS_PAID, (await user.BitPay.GetInvoiceAsync(invoiceId)).Status);
if (!partialPayment)
Assert.Equal(PaymentRequestData.PaymentRequestStatus.Processing, (await client.GetPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id)).Status);
});
await tester.ExplorerNode.GenerateAsync(1);
await TestUtils.EventuallyAsync(async () =>
{
Assert.Equal(Invoice.STATUS_CONFIRMED, (await user.BitPay.GetInvoiceAsync(invoiceId)).Status);
if (!partialPayment) if (!partialPayment)
Assert.Equal(PaymentRequestData.PaymentRequestStatus.Completed, (await client.GetPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id)).Status); Assert.Equal(PaymentRequestData.PaymentRequestStatus.Completed, (await client.GetPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id)).Status);
}); });
@@ -1950,6 +1999,82 @@ namespace BTCPayServer.Tests
CustomCurrency = "BTC" CustomCurrency = "BTC"
}); });
Assert.True(pp.AutoApproveClaims); Assert.True(pp.AutoApproveClaims);
// test subtract percentage
validationError = await AssertValidationError(new[] { "SubtractPercentage" }, async () =>
{
await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest
{
PaymentMethod = method.PaymentMethod,
RefundVariant = RefundVariant.RateThen,
SubtractPercentage = 101
});
});
Assert.Contains("SubtractPercentage: Percentage must be a numeric value between 0 and 100", validationError.Message);
// should auto-approve
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest
{
PaymentMethod = method.PaymentMethod,
RefundVariant = RefundVariant.RateThen,
SubtractPercentage = 6.15m
});
Assert.Equal("BTC", pp.Currency);
Assert.True(pp.AutoApproveClaims);
Assert.Equal(0.9385m, pp.Amount);
// test RefundVariant.OverpaidAmount
validationError = await AssertValidationError(new[] { "RefundVariant" }, async () =>
{
await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest
{
PaymentMethod = method.PaymentMethod,
RefundVariant = RefundVariant.OverpaidAmount
});
});
Assert.Contains("Invoice is not overpaid", validationError.Message);
// should auto-approve
invoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest { Amount = 5000.0m, Currency = "USD" });
methods = await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id);
method = methods.First();
await tester.WaitForEvent<NewOnChainTransactionEvent>(async () =>
{
await tester.ExplorerNode.SendToAddressAsync(
BitcoinAddress.Create(method.Destination, tester.NetworkProvider.BTC.NBitcoinNetwork),
Money.Coins(method.Due * 2)
);
});
await tester.ExplorerNode.GenerateAsync(5);
await TestUtils.EventuallyAsync(async () =>
{
invoice = await client.GetInvoice(user.StoreId, invoice.Id);
Assert.True(invoice.Status == InvoiceStatus.Settled);
Assert.True(invoice.AdditionalStatus == InvoiceExceptionStatus.PaidOver);
});
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest
{
PaymentMethod = method.PaymentMethod,
RefundVariant = RefundVariant.OverpaidAmount
});
Assert.Equal("BTC", pp.Currency);
Assert.True(pp.AutoApproveClaims);
Assert.Equal(method.Due, pp.Amount);
// once more with subtract percentage
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest
{
PaymentMethod = method.PaymentMethod,
RefundVariant = RefundVariant.OverpaidAmount,
SubtractPercentage = 21m
});
Assert.Equal("BTC", pp.Currency);
Assert.True(pp.AutoApproveClaims);
Assert.Equal(0.79m, pp.Amount);
} }
[Fact(Timeout = TestTimeout)] [Fact(Timeout = TestTimeout)]
@@ -2257,7 +2382,7 @@ namespace BTCPayServer.Tests
Assert.Single(paymentMethods); Assert.Single(paymentMethods);
Assert.True(paymentMethods.First().Activated); Assert.True(paymentMethods.First().Activated);
var invoiceWithdefaultPaymentMethodLN = await client.CreateInvoice(user.StoreId, var invoiceWithDefaultPaymentMethodLN = await client.CreateInvoice(user.StoreId,
new CreateInvoiceRequest() new CreateInvoiceRequest()
{ {
Currency = "USD", Currency = "USD",
@@ -2268,9 +2393,9 @@ namespace BTCPayServer.Tests
DefaultPaymentMethod = "BTC_LightningLike" DefaultPaymentMethod = "BTC_LightningLike"
} }
}); });
Assert.Equal("BTC_LightningLike", invoiceWithdefaultPaymentMethodLN.Checkout.DefaultPaymentMethod); Assert.Equal("BTC_LightningLike", invoiceWithDefaultPaymentMethodLN.Checkout.DefaultPaymentMethod);
var invoiceWithdefaultPaymentMethodOnChain = await client.CreateInvoice(user.StoreId, var invoiceWithDefaultPaymentMethodOnChain = await client.CreateInvoice(user.StoreId,
new CreateInvoiceRequest() new CreateInvoiceRequest()
{ {
Currency = "USD", Currency = "USD",
@@ -2281,13 +2406,35 @@ namespace BTCPayServer.Tests
DefaultPaymentMethod = "BTC" DefaultPaymentMethod = "BTC"
} }
}); });
Assert.Equal("BTC", invoiceWithdefaultPaymentMethodOnChain.Checkout.DefaultPaymentMethod); Assert.Equal("BTC", invoiceWithDefaultPaymentMethodOnChain.Checkout.DefaultPaymentMethod);
// reset lazy payment methods
store = await client.GetStore(user.StoreId); store = await client.GetStore(user.StoreId);
store.LazyPaymentMethods = false; store.LazyPaymentMethods = false;
store = await client.UpdateStore(store.Id, store = await client.UpdateStore(store.Id,
JObject.FromObject(store).ToObject<UpdateStoreRequest>()); JObject.FromObject(store).ToObject<UpdateStoreRequest>());
Assert.False(store.LazyPaymentMethods);
// use store default payment method
store = await client.GetStore(user.StoreId);
Assert.Null(store.DefaultPaymentMethod);
var storeDefaultPaymentMethod = "BTC-LightningNetwork";
store.DefaultPaymentMethod = storeDefaultPaymentMethod;
store = await client.UpdateStore(store.Id,
JObject.FromObject(store).ToObject<UpdateStoreRequest>());
Assert.Equal(storeDefaultPaymentMethod, store.DefaultPaymentMethod);
var invoiceWithStoreDefaultPaymentMethod = await client.CreateInvoice(user.StoreId,
new CreateInvoiceRequest()
{
Currency = "USD",
Amount = 100,
Checkout = new CreateInvoiceRequest.CheckoutOptions()
{
PaymentMethods = new[] { "BTC", "BTC-LightningNetwork", "BTC_LightningLike" }
}
});
Assert.Equal(storeDefaultPaymentMethod, invoiceWithStoreDefaultPaymentMethod.Checkout.DefaultPaymentMethod);
//let's see the overdue amount //let's see the overdue amount
invoice = await client.CreateInvoice(user.StoreId, invoice = await client.CreateInvoice(user.StoreId,
@@ -2344,27 +2491,10 @@ namespace BTCPayServer.Tests
Assert.NotNull(merchantInvoice.PaymentHash); Assert.NotNull(merchantInvoice.PaymentHash);
Assert.Equal(merchantInvoice.Id, merchantInvoice.PaymentHash); Assert.Equal(merchantInvoice.Id, merchantInvoice.PaymentHash);
// The default client is using charge, so we should not be able to query channels var client = await user.CreateClient(Policies.CanUseInternalLightningNode);
var chargeClient = await user.CreateClient(Policies.CanUseInternalLightningNode);
var info = await chargeClient.GetLightningNodeInfo("BTC");
Assert.Single(info.NodeURIs);
Assert.NotEqual(0, info.BlockHeight);
Assert.NotNull(info.Alias);
Assert.NotNull(info.Color);
Assert.NotNull(info.Version);
Assert.NotNull(info.PeersCount);
Assert.NotNull(info.ActiveChannelsCount);
Assert.NotNull(info.InactiveChannelsCount);
Assert.NotNull(info.PendingChannelsCount);
var gex = await AssertAPIError("lightning-node-unavailable", () => chargeClient.ConnectToLightningNode("BTC", new ConnectToNodeRequest(NodeInfo.Parse($"{new Key().PubKey.ToHex()}@localhost:3827"))));
Assert.Contains("NotSupported", gex.Message);
await AssertAPIError("lightning-node-unavailable", () => chargeClient.GetLightningNodeChannels("BTC"));
// Not permission for the store! // Not permission for the store!
await AssertAPIError("missing-permission", () => chargeClient.GetLightningNodeChannels(user.StoreId, "BTC")); await AssertAPIError("missing-permission", () => client.GetLightningNodeChannels(user.StoreId, "BTC"));
var invoiceData = await chargeClient.CreateLightningInvoice("BTC", new CreateLightningInvoiceRequest() var invoiceData = await client.CreateLightningInvoice("BTC", new CreateLightningInvoiceRequest()
{ {
Amount = LightMoney.Satoshis(1000), Amount = LightMoney.Satoshis(1000),
Description = "lol", Description = "lol",
@@ -2372,17 +2502,17 @@ namespace BTCPayServer.Tests
PrivateRouteHints = false PrivateRouteHints = false
}); });
var chargeInvoice = invoiceData; var chargeInvoice = invoiceData;
Assert.NotNull(await chargeClient.GetLightningInvoice("BTC", invoiceData.Id)); Assert.NotNull(await client.GetLightningInvoice("BTC", invoiceData.Id));
// check list for internal node // check list for internal node
var invoices = await chargeClient.GetLightningInvoices("BTC"); var invoices = await client.GetLightningInvoices("BTC");
var pendingInvoices = await chargeClient.GetLightningInvoices("BTC", true); var pendingInvoices = await client.GetLightningInvoices("BTC", true);
Assert.NotEmpty(invoices); Assert.NotEmpty(invoices);
Assert.Contains(invoices, i => i.Id == invoiceData.Id); Assert.Contains(invoices, i => i.Id == invoiceData.Id);
Assert.NotEmpty(pendingInvoices); Assert.NotEmpty(pendingInvoices);
Assert.Contains(pendingInvoices, i => i.Id == invoiceData.Id); Assert.Contains(pendingInvoices, i => i.Id == invoiceData.Id);
var client = await user.CreateClient($"{Policies.CanUseLightningNodeInStore}:{user.StoreId}"); client = await user.CreateClient($"{Policies.CanUseLightningNodeInStore}:{user.StoreId}");
// Not permission for the server // Not permission for the server
await AssertAPIError("missing-permission", () => client.GetLightningNodeChannels("BTC")); await AssertAPIError("missing-permission", () => client.GetLightningNodeChannels("BTC"));
@@ -2461,7 +2591,7 @@ namespace BTCPayServer.Tests
Assert.Contains(payments, i => i.BOLT11 == merchantInvoice.BOLT11); Assert.Contains(payments, i => i.BOLT11 == merchantInvoice.BOLT11);
// Node info // Node info
info = await client.GetLightningNodeInfo(user.StoreId, "BTC"); var info = await client.GetLightningNodeInfo(user.StoreId, "BTC");
Assert.Single(info.NodeURIs); Assert.Single(info.NodeURIs);
Assert.NotEqual(0, info.BlockHeight); Assert.NotEqual(0, info.BlockHeight);
@@ -2502,7 +2632,12 @@ namespace BTCPayServer.Tests
user.RegisterLightningNode("BTC", LightningConnectionType.CLightning); user.RegisterLightningNode("BTC", LightningConnectionType.CLightning);
var client = await user.CreateClient(Policies.Unrestricted); var client = await user.CreateClient(Policies.Unrestricted);
var invoice = await client.CreateInvoice(user.StoreId, var invoices = new Task<Client.Models.InvoiceData>[5];
// Create invoices
for (int i = 0; i < invoices.Length; i++)
{
invoices[i] = client.CreateInvoice(user.StoreId,
new CreateInvoiceRequest new CreateInvoiceRequest
{ {
Currency = "USD", Currency = "USD",
@@ -2513,18 +2648,35 @@ namespace BTCPayServer.Tests
DefaultPaymentMethod = "BTC_LightningLike" DefaultPaymentMethod = "BTC_LightningLike"
} }
}); });
var pm = Assert.Single(await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id)); }
Assert.False(pm.AdditionalData.HasValues);
var resp = await tester.CustomerLightningD.Pay(pm.Destination); var pm = new InvoicePaymentMethodDataModel[invoices.Length];
for (int i = 0; i < invoices.Length; i++)
{
pm[i] = Assert.Single(await client.GetInvoicePaymentMethods(user.StoreId, (await invoices[i]).Id));
Assert.True(pm[i].AdditionalData.HasValues);
}
// Pay them all at once
Task<PayResponse>[] payResponses = new Task<PayResponse>[invoices.Length];
for (int i = 0; i < invoices.Length; i++)
{
payResponses[i] = tester.CustomerLightningD.Pay(pm[i].Destination);
}
// Checking the results
for (int i = 0; i < invoices.Length; i++)
{
var resp = await payResponses[i];
Assert.Equal(PayResult.Ok, resp.Result); Assert.Equal(PayResult.Ok, resp.Result);
Assert.NotNull(resp.Details.PaymentHash); Assert.NotNull(resp.Details.PaymentHash);
Assert.NotNull(resp.Details.Preimage); Assert.NotNull(resp.Details.Preimage);
pm = Assert.Single(await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id)); pm[i] = Assert.Single(await client.GetInvoicePaymentMethods(user.StoreId, (await invoices[i]).Id));
Assert.True(pm.AdditionalData.HasValues); Assert.True(pm[i].AdditionalData.HasValues);
Assert.Equal(resp.Details.PaymentHash.ToString(), pm.AdditionalData.GetValue("paymentHash")); Assert.Equal(resp.Details.PaymentHash.ToString(), pm[i].AdditionalData.GetValue("paymentHash"));
Assert.Equal(resp.Details.Preimage.ToString(), pm.AdditionalData.GetValue("preimage")); Assert.Equal(resp.Details.Preimage.ToString(), pm[i].AdditionalData.GetValue("preimage"));
}
} }
[Fact(Timeout = 60 * 20 * 1000)] [Fact(Timeout = 60 * 20 * 1000)]
@@ -3081,6 +3233,9 @@ namespace BTCPayServer.Tests
}); });
var transaction = await client.GetOnChainWalletTransaction(walletId.StoreId, walletId.CryptoCode, txdata.TransactionHash.ToString()); var transaction = await client.GetOnChainWalletTransaction(walletId.StoreId, walletId.CryptoCode, txdata.TransactionHash.ToString());
// Check skip doesn't crash
await client.ShowOnChainWalletTransactions(walletId.StoreId, walletId.CryptoCode, skip: 1);
Assert.Equal(transaction.TransactionHash, txdata.TransactionHash); Assert.Equal(transaction.TransactionHash, txdata.TransactionHash);
Assert.Equal(String.Empty, transaction.Comment); Assert.Equal(String.Empty, transaction.Comment);
#pragma warning disable CS0612 // Type or member is obsolete #pragma warning disable CS0612 // Type or member is obsolete
@@ -3262,11 +3417,16 @@ namespace BTCPayServer.Tests
var client = await user.CreateClient(Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings); var client = await user.CreateClient(Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings);
var roles = await client.GetServerRoles();
Assert.Equal(2,roles.Count);
#pragma warning disable CS0618
var ownerRole = roles.Single(data => data.Role == StoreRoles.Owner);
var guestRole = roles.Single(data => data.Role == StoreRoles.Guest);
#pragma warning restore CS0618
var users = await client.GetStoreUsers(user.StoreId); var users = await client.GetStoreUsers(user.StoreId);
var storeuser = Assert.Single(users); var storeuser = Assert.Single(users);
Assert.Equal(user.UserId, storeuser.UserId); Assert.Equal(user.UserId, storeuser.UserId);
Assert.Equal(StoreRoles.Owner, storeuser.Role); Assert.Equal(ownerRole.Id, storeuser.Role);
var user2 = tester.NewAccount(); var user2 = tester.NewAccount();
await user2.GrantAccessAsync(false); await user2.GrantAccessAsync(false);
@@ -3277,7 +3437,7 @@ namespace BTCPayServer.Tests
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await user2Client.AddStoreUser(user.StoreId, new StoreUserData())); await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await user2Client.AddStoreUser(user.StoreId, new StoreUserData()));
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await user2Client.RemoveStoreUser(user.StoreId, user.UserId)); await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await user2Client.RemoveStoreUser(user.StoreId, user.UserId));
await client.AddStoreUser(user.StoreId, new StoreUserData() { Role = StoreRoles.Guest, UserId = user2.UserId }); await client.AddStoreUser(user.StoreId, new StoreUserData() { Role = guestRole.Id, UserId = user2.UserId });
//test no access to api when only a guest //test no access to api when only a guest
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await user2Client.GetStoreUsers(user.StoreId)); await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await user2Client.GetStoreUsers(user.StoreId));
@@ -3291,10 +3451,10 @@ namespace BTCPayServer.Tests
await user2Client.GetStore(user.StoreId)); await user2Client.GetStore(user.StoreId));
await client.AddStoreUser(user.StoreId, new StoreUserData() { Role = StoreRoles.Owner, UserId = user2.UserId }); await client.AddStoreUser(user.StoreId, new StoreUserData() { Role = ownerRole.Id, UserId = user2.UserId });
await AssertAPIError("duplicate-store-user-role", async () => await AssertAPIError("duplicate-store-user-role", async () =>
await client.AddStoreUser(user.StoreId, await client.AddStoreUser(user.StoreId,
new StoreUserData() { Role = StoreRoles.Owner, UserId = user2.UserId })); new StoreUserData() { Role = ownerRole.Id, UserId = user2.UserId }));
await user2Client.RemoveStoreUser(user.StoreId, user.UserId); await user2Client.RemoveStoreUser(user.StoreId, user.UserId);
@@ -3410,6 +3570,7 @@ namespace BTCPayServer.Tests
PaymentMethod = "BTC_LightningNetwork", PaymentMethod = "BTC_LightningNetwork",
Destination = customerInvoice.BOLT11 Destination = customerInvoice.BOLT11
}); });
Assert.Equal(payout.Metadata.ToString(), new JObject().ToString()); //empty
Assert.Empty(await adminClient.GetStoreLightningAutomatedPayoutProcessors(admin.StoreId, "BTC_LightningNetwork")); Assert.Empty(await adminClient.GetStoreLightningAutomatedPayoutProcessors(admin.StoreId, "BTC_LightningNetwork"));
await adminClient.UpdateStoreLightningAutomatedPayoutProcessors(admin.StoreId, "BTC_LightningNetwork", await adminClient.UpdateStoreLightningAutomatedPayoutProcessors(admin.StoreId, "BTC_LightningNetwork",
new LightningAutomatedPayoutSettings() { IntervalSeconds = TimeSpan.FromSeconds(600) }); new LightningAutomatedPayoutSettings() { IntervalSeconds = TimeSpan.FromSeconds(600) });
@@ -3420,6 +3581,46 @@ namespace BTCPayServer.Tests
(await adminClient.GetStorePayouts(admin.StoreId, false)).Single(data => data.Id == payout.Id); (await adminClient.GetStorePayouts(admin.StoreId, false)).Single(data => data.Id == payout.Id);
Assert.Equal(PayoutState.Completed, payoutC.State); Assert.Equal(PayoutState.Completed, payoutC.State);
}); });
payout = await adminClient.CreatePayout(admin.StoreId,
new CreatePayoutThroughStoreRequest()
{
Approved = true,
PaymentMethod = "BTC",
Destination = (await tester.ExplorerNode.GetNewAddressAsync()).ToString(),
Amount = 0.0001m,
Metadata = JObject.FromObject(new
{
source ="apitest",
sourceLink = "https://chocolate.com"
})
});
Assert.Equal(payout.Metadata.ToString(), JObject.FromObject(new
{
source = "apitest",
sourceLink = "https://chocolate.com"
}).ToString());
payout =
(await adminClient.GetStorePayouts(admin.StoreId, false)).Single(data => data.Id == payout.Id);
Assert.Equal(payout.Metadata.ToString(), JObject.FromObject(new
{
source = "apitest",
sourceLink = "https://chocolate.com"
}).ToString());
customerInvoice = await tester.CustomerLightningD.CreateInvoice(LightMoney.FromUnit(10, LightMoneyUnit.Satoshi),
Guid.NewGuid().ToString(), TimeSpan.FromDays(40));
var payout2 = await adminClient.CreatePayout(admin.StoreId,
new CreatePayoutThroughStoreRequest()
{
Approved = true,
Amount = new Money(100, MoneyUnit.Satoshi).ToDecimal(MoneyUnit.BTC),
PaymentMethod = "BTC_LightningNetwork",
Destination = customerInvoice.BOLT11
});
Assert.Equal(payout2.Amount, new Money(100, MoneyUnit.Satoshi).ToDecimal(MoneyUnit.BTC));
} }
[Fact(Timeout = 60 * 2 * 1000)] [Fact(Timeout = 60 * 2 * 1000)]
@@ -3535,9 +3736,12 @@ namespace BTCPayServer.Tests
Assert.Single(payouts.Where(data => data.State == PayoutState.InProgress)); Assert.Single(payouts.Where(data => data.State == PayoutState.InProgress));
}); });
var txid = await tester.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create((await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address, uint256 txid = null;
await tester.WaitForEvent<NewOnChainTransactionEvent>(async () =>
{
txid = await tester.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create((await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
tester.ExplorerClient.Network.NBitcoinNetwork), Money.Coins(0.01m) + fee); tester.ExplorerClient.Network.NBitcoinNetwork), Money.Coins(0.01m) + fee);
await tester.WaitForEvent<NewOnChainTransactionEvent>(null, correctEvent: ev => ev.NewTransactionEvent.TransactionData.TransactionHash == txid); }, correctEvent: ev => ev.NewTransactionEvent.TransactionData.TransactionHash == txid);
await tester.PayTester.GetService<PayoutProcessorService>().Restart(new PayoutProcessorService.PayoutProcessorQuery(admin.StoreId, "BTC")); await tester.PayTester.GetService<PayoutProcessorService>().Restart(new PayoutProcessorService.PayoutProcessorQuery(admin.StoreId, "BTC"));
await TestUtils.EventuallyAsync(async () => await TestUtils.EventuallyAsync(async () =>
{ {
@@ -3545,6 +3749,122 @@ namespace BTCPayServer.Tests
payouts = await adminClient.GetStorePayouts(admin.StoreId); payouts = await adminClient.GetStorePayouts(admin.StoreId);
Assert.Empty(payouts.Where(data => data.State != PayoutState.InProgress)); Assert.Empty(payouts.Where(data => data.State != PayoutState.InProgress));
}); });
// settings that were added later
var settings =
Assert.Single(await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC"));
Assert.False( settings.ProcessNewPayoutsInstantly);
Assert.Equal(0m, settings.Threshold);
//let's use the ProcessNewPayoutsInstantly so that it will trigger instantly
settings.IntervalSeconds = TimeSpan.FromDays(1);
settings.ProcessNewPayoutsInstantly = true;
await tester.WaitForEvent<NewOnChainTransactionEvent>(async () =>
{
txid = await tester.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create((await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
tester.ExplorerClient.Network.NBitcoinNetwork), Money.Coins(1m) + fee);
}, correctEvent: ev => ev.NewTransactionEvent.TransactionData.TransactionHash == txid);
await adminClient.UpdateStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC", settings);
settings =
Assert.Single(await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC"));
Assert.True( settings.ProcessNewPayoutsInstantly);
var pluginHookService = tester.PayTester.GetService<IPluginHookService>();
var beforeHookTcs = new TaskCompletionSource();
var afterHookTcs = new TaskCompletionSource();
pluginHookService.ActionInvoked += (sender, tuple) =>
{
switch (tuple.hook)
{
case "before-automated-payout-processing":
beforeHookTcs.TrySetResult();
break;
case "after-automated-payout-processing":
afterHookTcs.TrySetResult();
break;
}
};
var payoutThatShouldBeProcessedStraightAway = await adminClient.CreatePayout(admin.StoreId, new CreatePayoutThroughStoreRequest()
{
PullPaymentId = pullPayment.Id,
Amount = 0.5m,
Approved = true,
PaymentMethod = "BTC",
Destination = (await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
});
await beforeHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
await afterHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
payouts = await adminClient.GetStorePayouts(admin.StoreId);
Assert.Single(payouts.Where(data => data.State == PayoutState.InProgress && data.Id == payoutThatShouldBeProcessedStraightAway.Id));
beforeHookTcs = new TaskCompletionSource();
afterHookTcs = new TaskCompletionSource();
//let's test the threshold limiter
settings.Threshold = 0.5m;
await adminClient.UpdateStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC", settings);
//quick test: when updating processor, it processes instantly
await beforeHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
await afterHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
settings =
Assert.Single(await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC"));
Assert.Equal(0.5m, settings.Threshold);
//create a payout that should not be processed straight away due to threshold
beforeHookTcs = new TaskCompletionSource();
afterHookTcs = new TaskCompletionSource();
var payoutThatShouldNotBeProcessedStraightAway = await adminClient.CreatePayout(admin.StoreId, new CreatePayoutThroughStoreRequest()
{
Amount = 0.1m,
Approved = true,
PaymentMethod = "BTC",
Destination = (await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
});
await beforeHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
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));
beforeHookTcs = new TaskCompletionSource();
afterHookTcs = new TaskCompletionSource();
var payoutThatShouldNotBeProcessedStraightAway2 = await adminClient.CreatePayout(admin.StoreId, new CreatePayoutThroughStoreRequest()
{
Amount = 0.3m,
Approved = true,
PaymentMethod = "BTC",
Destination = (await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
});
await beforeHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
await afterHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
payouts = await adminClient.GetStorePayouts(admin.StoreId);
Assert.Equal(2, payouts.Count(data => data.State == PayoutState.AwaitingPayment &&
(data.Id == payoutThatShouldNotBeProcessedStraightAway.Id || data.Id == payoutThatShouldNotBeProcessedStraightAway2.Id)));
beforeHookTcs = new TaskCompletionSource();
afterHookTcs = new TaskCompletionSource();
var payoutThatShouldNotBeProcessedStraightAway3 = await adminClient.CreatePayout(admin.StoreId, new CreatePayoutThroughStoreRequest()
{
Amount = 0.3m,
Approved = true,
PaymentMethod = "BTC",
Destination = (await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
});
await beforeHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
await afterHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
payouts = await adminClient.GetStorePayouts(admin.StoreId);
Assert.Empty(payouts.Where(data => data.State != PayoutState.InProgress));
} }
[Fact(Timeout = 60 * 2 * 1000)] [Fact(Timeout = 60 * 2 * 1000)]

View File

@@ -1,10 +1,14 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Controllers; using BTCPayServer.Controllers;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Hosting;
using BTCPayServer.Models.AppViewModels; using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Plugins.Crowdfund.Models;
using BTCPayServer.Plugins.PointOfSale; using BTCPayServer.Plugins.PointOfSale;
using BTCPayServer.Plugins.PointOfSale.Controllers; using BTCPayServer.Plugins.PointOfSale.Controllers;
using BTCPayServer.Plugins.PointOfSale.Models; using BTCPayServer.Plugins.PointOfSale.Models;
using BTCPayServer.Services.Apps;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Xunit; using Xunit;
using Xunit.Abstractions; using Xunit.Abstractions;
@@ -19,6 +23,74 @@ namespace BTCPayServer.Tests
{ {
} }
[Fact]
[Trait("Fast", "Fast")]
public void CanParseOldYmlCorrectly()
{
var testOriginalDefaultYmlTemplate = @"
green tea:
price: 1
title: Green Tea
description: Lovely, fresh and tender, Meng Ding Gan Lu ('sweet dew') is grown in the lush Meng Ding Mountains of the southwestern province of Sichuan where it has been cultivated for over a thousand years.
image: ~/img/pos-sample/green-tea.jpg
black tea:
price: 1
title: Black Tea
description: Tian Jian Tian Jian means 'heavenly tippy tea' in Chinese, and it describes the finest grade of dark tea. Our Tian Jian dark tea is from Hunan province which is famous for making some of the best dark teas available.
image: ~/img/pos-sample/black-tea.jpg
rooibos:
price: 1.2
title: Rooibos
description: Rooibos is a dramatic red tea made from a South African herb that contains polyphenols and flavonoids. Often called 'African redbush tea', Rooibos herbal tea delights the senses and delivers potential health benefits with each caffeine-free sip.
image: ~/img/pos-sample/rooibos.jpg
pu erh:
price: 2
title: Pu Erh
description: This loose pur-erh tea is produced in Yunnan Province, China. The process in a relatively high humidity environment has mellowed the elemental character of the tea when compared to young Pu-erh.
image: ~/img/pos-sample/pu-erh.jpg
herbal tea:
price: 1.8
title: Herbal Tea
description: Chamomile tea is made from the flower heads of the chamomile plant. The medicinal use of chamomile dates back to the ancient Egyptians, Romans and Greeks. Pay us what you want!
image: ~/img/pos-sample/herbal-tea.jpg
custom: true
fruit tea:
price: 1.5
title: Fruit Tea
description: The Tibetan Himalayas, the land is majestic and beautiful—a spiritual place where, despite the perilous environment, many journey seeking enlightenment. Pay us what you want!
image: ~/img/pos-sample/fruit-tea.jpg
inventory: 5
custom: true
";
var parsedDefault = MigrationStartupTask.ParsePOSYML(testOriginalDefaultYmlTemplate);
Assert.Equal(6, parsedDefault.Length);
Assert.Equal( "Green Tea" ,parsedDefault[0].Title);
Assert.Equal( "green tea" ,parsedDefault[0].Id);
Assert.Equal( "Lovely, fresh and tender, Meng Ding Gan Lu ('sweet dew') is grown in the lush Meng Ding Mountains of the southwestern province of Sichuan where it has been cultivated for over a thousand years." ,parsedDefault[0].Description);
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.Null( parsedDefault[0].AdditionalData);
Assert.Null( parsedDefault[0].PaymentMethods);
Assert.Equal( "Herbal Tea" ,parsedDefault[4].Title);
Assert.Equal( "herbal tea" ,parsedDefault[4].Id);
Assert.Equal( "Chamomile tea is made from the flower heads of the chamomile plant. The medicinal use of chamomile dates back to the ancient Egyptians, Romans and Greeks. Pay us what you want!" ,parsedDefault[4].Description);
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.Null( parsedDefault[4].AdditionalData);
Assert.Null( parsedDefault[4].PaymentMethods);
}
[Fact(Timeout = LongRunningTestTimeout)] [Fact(Timeout = LongRunningTestTimeout)]
[Trait("Integration", "Integration")] [Trait("Integration", "Integration")]
public async Task CanUsePoSApp1() public async Task CanUsePoSApp1()
@@ -53,21 +125,59 @@ donation:
price: 1.02 price: 1.02
custom: true custom: true
"; ";
vmpos.Currency = "EUR";
vmpos.Template = AppService.SerializeTemplate(MigrationStartupTask.ParsePOSYML(vmpos.Template));
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result); Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>(); await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
var publicApps = user.GetController<UIPointOfSaleController>(); var publicApps = user.GetController<UIPointOfSaleController>();
var vmview = await publicApps.ViewPointOfSale(app.Id, PosViewType.Cart).AssertViewModelAsync<ViewPointOfSaleViewModel>(); var vmview = await publicApps.ViewPointOfSale(app.Id, PosViewType.Cart).AssertViewModelAsync<ViewPointOfSaleViewModel>();
Assert.Equal("EUR", vmview.CurrencyCode);
// apple shouldn't be available since we it's set to "disabled: true" above // apple shouldn't be available since we it's set to "disabled: true" above
Assert.Equal(2, vmview.Items.Length); Assert.Equal(2, vmview.Items.Length);
Assert.Equal("orange", vmview.Items[0].Title); Assert.Equal("orange", vmview.Items[0].Title);
Assert.Equal("donation", vmview.Items[1].Title); Assert.Equal("donation", vmview.Items[1].Title);
// orange is available // orange is available
Assert.IsType<RedirectToActionResult>(publicApps Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(app.Id, PosViewType.Cart, 0, null, null, null, null, "orange").Result); .ViewPointOfSale(app.Id, PosViewType.Cart, 0, choiceKey: "orange").Result);
// apple is not found // apple is not found
Assert.IsType<NotFoundResult>(publicApps Assert.IsType<NotFoundResult>(publicApps
.ViewPointOfSale(app.Id, PosViewType.Cart, 0, null, null, null, null, "apple").Result); .ViewPointOfSale(app.Id, PosViewType.Cart, 0, choiceKey: "apple").Result);
// List
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
app = appList.Apps[0];
apps = user.GetController<UIAppsController>();
appData = new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType, Settings = "{\"currency\":\"EUR\"}" };
apps.HttpContext.SetAppData(appData);
pos.HttpContext.SetAppData(appData);
Assert.Single(appList.Apps);
Assert.Equal("test", app.AppName);
Assert.True(app.Role.ToPermissionSet(appList.Apps[0].StoreId).Contains(Policies.CanModifyStoreSettings, app.StoreId));
Assert.Equal(user.StoreId, app.StoreId);
Assert.False(app.Archived);
// Archive
redirect = Assert.IsType<RedirectResult>(apps.ToggleArchive(app.Id).Result);
Assert.EndsWith("/settings/pos", redirect.Url);
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
Assert.Empty(appList.Apps);
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId, archived: true).Result).Model);
app = appList.Apps[0];
Assert.True(app.Archived);
Assert.IsType<NotFoundResult>(await publicApps.ViewPointOfSale(app.Id, PosViewType.Static));
// Unarchive
redirect = Assert.IsType<RedirectResult>(apps.ToggleArchive(app.Id).Result);
Assert.EndsWith("/settings/pos", redirect.Url);
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
app = appList.Apps[0];
Assert.False(app.Archived);
Assert.IsType<ViewResult>(await publicApps.ViewPointOfSale(app.Id, PosViewType.Static));
// Delete
Assert.IsType<ViewResult>(apps.DeleteApp(app.Id));
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(app.Id).Result);
Assert.Equal(nameof(UIStoresController.Dashboard), redirectToAction.ActionName);
appList = await apps.ListApps(user.StoreId).AssertViewModelAsync<ListAppsViewModel>();
Assert.Empty(appList.Apps);
} }
} }
} }

View File

@@ -31,7 +31,6 @@ namespace BTCPayServer.Tests
user.RegisterDerivationScheme("BTC"); user.RegisterDerivationScheme("BTC");
var user2 = tester.NewAccount(); var user2 = tester.NewAccount();
await user2.GrantAccessAsync(); await user2.GrantAccessAsync();
var paymentRequestController = user.GetController<UIPaymentRequestController>(); var paymentRequestController = user.GetController<UIPaymentRequestController>();
@@ -162,7 +161,7 @@ namespace BTCPayServer.Tests
using var tester = CreateServerTester(); using var tester = CreateServerTester();
await tester.StartAsync(); await tester.StartAsync();
var user = tester.NewAccount(); var user = tester.NewAccount();
user.GrantAccess(); await user.GrantAccessAsync();
user.RegisterDerivationScheme("BTC"); user.RegisterDerivationScheme("BTC");
var paymentRequestController = user.GetController<UIPaymentRequestController>(); var paymentRequestController = user.GetController<UIPaymentRequestController>();
@@ -170,7 +169,7 @@ namespace BTCPayServer.Tests
Assert.IsType<NotFoundResult>(await Assert.IsType<NotFoundResult>(await
paymentRequestController.CancelUnpaidPendingInvoice(Guid.NewGuid().ToString(), false)); paymentRequestController.CancelUnpaidPendingInvoice(Guid.NewGuid().ToString(), false));
var request = new UpdatePaymentRequestViewModel() var request = new UpdatePaymentRequestViewModel
{ {
Title = "original juice", Title = "original juice",
Currency = "BTC", Currency = "BTC",

View File

@@ -180,7 +180,7 @@ namespace BTCPayServer.Tests
{ {
Driver.FindElement(By.Id("StoreSelectorToggle")).Click(); Driver.FindElement(By.Id("StoreSelectorToggle")).Click();
} }
Driver.WaitForElement(By.Id("StoreSelectorCreate")).Click(); GoToUrl("/stores/create");
var name = "Store" + RandomUtils.GetUInt64(); var name = "Store" + RandomUtils.GetUInt64();
TestLogs.LogInformation($"Created store {name}"); TestLogs.LogInformation($"Created store {name}");
Driver.WaitForElement(By.Id("Name")).SendKeys(name); Driver.WaitForElement(By.Id("Name")).SendKeys(name);
@@ -313,8 +313,6 @@ namespace BTCPayServer.Tests
var connectionString = connectionType switch var connectionString = connectionType switch
{ {
LightningConnectionType.Charge =>
$"type=charge;server={Server.MerchantCharge.Client.Uri.AbsoluteUri};allowinsecure=true",
LightningConnectionType.CLightning => LightningConnectionType.CLightning =>
$"type=clightning;server={((CLightningClient)Server.MerchantLightningD).Address.AbsoluteUri}", $"type=clightning;server={((CLightningClient)Server.MerchantLightningD).Address.AbsoluteUri}",
LightningConnectionType.LndREST => LightningConnectionType.LndREST =>
@@ -395,6 +393,10 @@ namespace BTCPayServer.Tests
public void GoToHome() public void GoToHome()
{ {
Driver.Navigate().GoToUrl(ServerUri); Driver.Navigate().GoToUrl(ServerUri);
if (Driver.PageSource.Contains("id=\"SkipWizard\""))
{
Driver.FindElement(By.Id("SkipWizard")).Click();
}
} }
public void Logout() public void Logout()
@@ -563,7 +565,7 @@ namespace BTCPayServer.Tests
walletId ??= WalletId; walletId ??= WalletId;
GoToWallet(walletId, WalletsNavPages.Receive); GoToWallet(walletId, WalletsNavPages.Receive);
Driver.FindElement(By.Id("generateButton")).Click(); Driver.FindElement(By.Id("generateButton")).Click();
var addressStr = Driver.FindElement(By.Id("Address")).GetAttribute("value"); var addressStr = Driver.FindElement(By.Id("Address")).GetAttribute("data-text");
var address = BitcoinAddress.Create(addressStr, ((BTCPayNetwork)Server.NetworkProvider.GetNetwork(walletId.CryptoCode)).NBitcoinNetwork); var address = BitcoinAddress.Create(addressStr, ((BTCPayNetwork)Server.NetworkProvider.GetNetwork(walletId.CryptoCode)).NBitcoinNetwork);
for (var i = 0; i < coins; i++) for (var i = 0; i < coins; i++)
{ {

File diff suppressed because it is too large Load Diff

View File

@@ -92,7 +92,7 @@ namespace BTCPayServer.Tests
#endif #endif
public void ActivateLightning() public void ActivateLightning()
{ {
ActivateLightning(LightningConnectionType.Charge); ActivateLightning(LightningConnectionType.CLightning);
} }
public void ActivateLightning(LightningConnectionType internalNode) public void ActivateLightning(LightningConnectionType internalNode)
{ {
@@ -109,14 +109,7 @@ namespace BTCPayServer.Tests
string connectionString = null; string connectionString = null;
if (connectionType is null) if (connectionType is null)
return LightningSupportedPaymentMethod.InternalNode; return LightningSupportedPaymentMethod.InternalNode;
if (connectionType == LightningConnectionType.Charge) if (connectionType == LightningConnectionType.CLightning)
{
if (isMerchant)
connectionString = $"type=charge;server={MerchantCharge.Client.Uri.AbsoluteUri};allowinsecure=true";
else
throw new NotSupportedException();
}
else if (connectionType == LightningConnectionType.CLightning)
{ {
if (isMerchant) if (isMerchant)
connectionString = "type=clightning;server=" + connectionString = "type=clightning;server=" +

View File

@@ -40,6 +40,7 @@ namespace BTCPayServer.Tests
public class TestAccount public class TestAccount
{ {
readonly ServerTester parent; readonly ServerTester parent;
public string LNAddress;
public TestAccount(ServerTester parent) public TestAccount(ServerTester parent)
{ {
@@ -242,7 +243,7 @@ namespace BTCPayServer.Tests
policies.LockSubscription = false; policies.LockSubscription = false;
await account.Register(RegisterDetails); await account.Register(RegisterDetails);
} }
TestLogs.LogInformation($"UserId: {account.RegisteredUserId} Password: {Password}");
UserId = account.RegisteredUserId; UserId = account.RegisteredUserId;
Email = RegisterDetails.Email; Email = RegisterDetails.Email;
IsAdmin = account.RegisteredAdmin; IsAdmin = account.RegisteredAdmin;
@@ -277,7 +278,7 @@ namespace BTCPayServer.Tests
public bool IsAdmin { get; internal set; } public bool IsAdmin { get; internal set; }
public void RegisterLightningNode(string cryptoCode, LightningConnectionType connectionType, bool isMerchant = true) public void RegisterLightningNode(string cryptoCode, LightningConnectionType? connectionType = null, bool isMerchant = true)
{ {
RegisterLightningNodeAsync(cryptoCode, connectionType, isMerchant).GetAwaiter().GetResult(); RegisterLightningNodeAsync(cryptoCode, connectionType, isMerchant).GetAwaiter().GetResult();
} }
@@ -309,8 +310,9 @@ namespace BTCPayServer.Tests
Assert.False(true, storeController.ModelState.FirstOrDefault().Value.Errors[0].ErrorMessage); Assert.False(true, storeController.ModelState.FirstOrDefault().Value.Errors[0].ErrorMessage);
} }
public async Task<Coin> ReceiveUTXO(Money value, BTCPayNetwork network) public async Task<Coin> ReceiveUTXO(Money value, BTCPayNetwork network = null)
{ {
network ??= SupportedNetwork;
var cashCow = parent.ExplorerNode; var cashCow = parent.ExplorerNode;
var btcPayWallet = parent.PayTester.GetService<BTCPayWalletProvider>().GetWallet(network); var btcPayWallet = parent.PayTester.GetService<BTCPayWalletProvider>().GetWallet(network);
var address = (await btcPayWallet.ReserveAddressAsync(this.DerivationScheme)).Address; var address = (await btcPayWallet.ReserveAddressAsync(this.DerivationScheme)).Address;
@@ -470,7 +472,10 @@ namespace BTCPayServer.Tests
var req = await _server.GetNextRequest(cancellation); var req = await _server.GetNextRequest(cancellation);
var bytes = await req.Request.Body.ReadBytesAsync((int)req.Request.Headers.ContentLength); var bytes = await req.Request.Body.ReadBytesAsync((int)req.Request.Headers.ContentLength);
var callback = Encoding.UTF8.GetString(bytes); var callback = Encoding.UTF8.GetString(bytes);
lock (_webhookEvents)
{
_webhookEvents.Add(JsonConvert.DeserializeObject<WebhookInvoiceEvent>(callback)); _webhookEvents.Add(JsonConvert.DeserializeObject<WebhookInvoiceEvent>(callback));
}
req.Response.StatusCode = 200; req.Response.StatusCode = 200;
_server.Done(); _server.Done();
} }
@@ -487,6 +492,8 @@ namespace BTCPayServer.Tests
{ {
int retry = 0; int retry = 0;
retry: retry:
lock (WebhookEvents)
{
foreach (var evt in WebhookEvents) foreach (var evt in WebhookEvents)
{ {
if (evt.Type == eventType) if (evt.Type == eventType)
@@ -502,6 +509,7 @@ retry:
} }
} }
} }
}
if (retry < 3) if (retry < 3)
{ {
Thread.Sleep(1000); Thread.Sleep(1000);
@@ -540,12 +548,101 @@ retry:
public async Task AddGuest(string userId) public async Task AddGuest(string userId)
{ {
var repo = this.parent.PayTester.GetService<StoreRepository>(); var repo = this.parent.PayTester.GetService<StoreRepository>();
await repo.AddStoreUser(StoreId, userId, "Guest"); await repo.AddStoreUser(StoreId, userId, StoreRoleId.Guest);
} }
public async Task AddOwner(string userId) public async Task AddOwner(string userId)
{ {
var repo = this.parent.PayTester.GetService<StoreRepository>(); var repo = this.parent.PayTester.GetService<StoreRepository>();
await repo.AddStoreUser(StoreId, userId, "Owner"); await repo.AddStoreUser(StoreId, userId, StoreRoleId.Owner);
}
public async Task<uint256> PayOnChain(string invoiceId)
{
var cryptoCode = "BTC";
var client = await CreateClient();
var methods = await client.GetInvoicePaymentMethods(StoreId, invoiceId);
var method = methods.First(m => m.PaymentMethod == cryptoCode);
var address = method.Destination;
var tx = await client.CreateOnChainTransaction(StoreId, cryptoCode, new CreateOnChainTransactionRequest()
{
Destinations = new List<CreateOnChainTransactionRequest.CreateOnChainTransactionRequestDestination>()
{
new ()
{
Destination = address,
Amount = method.Due
}
},
FeeRate = new FeeRate(1.0m)
});
await WaitInvoicePaid(invoiceId);
return tx.TransactionHash;
}
public async Task PayOnBOLT11(string invoiceId)
{
var cryptoCode = "BTC";
var client = await CreateClient();
var methods = await client.GetInvoicePaymentMethods(StoreId, invoiceId);
var method = methods.First(m => m.PaymentMethod == $"{cryptoCode}-LightningNetwork");
var bolt11 = method.Destination;
TestLogs.LogInformation("PAYING");
await parent.CustomerLightningD.Pay(bolt11);
TestLogs.LogInformation("PAID");
await WaitInvoicePaid(invoiceId);
}
public async Task PayOnLNUrl(string invoiceId)
{
var cryptoCode = "BTC";
var network = SupportedNetwork.NBitcoinNetwork;
var client = await CreateClient();
var methods = await client.GetInvoicePaymentMethods(StoreId, invoiceId);
var method = methods.First(m => m.PaymentMethod == $"{cryptoCode}-LNURLPAY");
var lnurL = LNURL.LNURL.Parse(method.PaymentLink, out var tag);
var http = new HttpClient();
var payreq = (LNURL.LNURLPayRequest)await LNURL.LNURL.FetchInformation(lnurL, tag, http);
var resp = await payreq.SendRequest(payreq.MinSendable, network, http);
var bolt11 = resp.Pr;
await parent.CustomerLightningD.Pay(bolt11);
await WaitInvoicePaid(invoiceId);
}
public Task WaitInvoicePaid(string invoiceId)
{
return TestUtils.EventuallyAsync(async () =>
{
var client = await CreateClient();
var invoice = await client.GetInvoice(StoreId, invoiceId);
if (invoice.Status == InvoiceStatus.Settled)
return;
Assert.Equal(InvoiceStatus.Processing, invoice.Status);
});
}
public async Task PayOnLNAddress(string lnAddrUser = null)
{
lnAddrUser ??= LNAddress;
var network = SupportedNetwork.NBitcoinNetwork;
var payReqStr = await (await parent.PayTester.HttpClient.GetAsync($".well-known/lnurlp/{lnAddrUser}")).Content.ReadAsStringAsync();
var payreq = JsonConvert.DeserializeObject<LNURL.LNURLPayRequest>(payReqStr);
var resp = await payreq.SendRequest(payreq.MinSendable, network, parent.PayTester.HttpClient);
var bolt11 = resp.Pr;
await parent.CustomerLightningD.Pay(bolt11);
}
public async Task<string> CreateLNAddress()
{
var lnAddrUser = Guid.NewGuid().ToString();
var ctx = parent.PayTester.GetService<ApplicationDbContextFactory>().CreateContext();
ctx.LightningAddresses.Add(new()
{
StoreDataId = StoreId,
Username = lnAddrUser
});
await ctx.SaveChangesAsync();
LNAddress = lnAddrUser;
return lnAddrUser;
} }
} }
} }

View File

@@ -16,12 +16,14 @@ using BTCPayServer.Storage.Models;
using BTCPayServer.Storage.Services.Providers.AzureBlobStorage.Configuration; using BTCPayServer.Storage.Services.Providers.AzureBlobStorage.Configuration;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.FileSystemGlobbing;
using NBitcoin; using NBitcoin;
using NBitpayClient; using NBitpayClient;
using Newtonsoft.Json; using Newtonsoft.Json;
using Xunit; using Xunit;
using Xunit.Abstractions; using Xunit.Abstractions;
using Xunit.Sdk; using Xunit.Sdk;
using static BTCPayServer.HostedServices.PullPaymentHostedService.PayoutApproval;
namespace BTCPayServer.Tests namespace BTCPayServer.Tests
{ {
@@ -177,7 +179,7 @@ namespace BTCPayServer.Tests
Assert.Contains(rates, e => e.CurrencyPair == new CurrencyPair("XMR", "BTC") && e.BidAsk.Bid < 1.0m); Assert.Contains(rates, e => e.CurrencyPair == new CurrencyPair("XMR", "BTC") && e.BidAsk.Bid < 1.0m);
// Check we didn't skip too many exchanges // Check we didn't skip too many exchanges
Assert.InRange(skipped, 0, 3); Assert.InRange(skipped, 0, 5);
} }
[Fact] [Fact]
@@ -290,9 +292,43 @@ retry:
} }
[Fact] [Fact]
public void CanGetRateCryptoCurrenciesByDefault() public async Task CanGetRateFromRecommendedExchanges()
{ {
string[] brokenShitcoins = { "BTX_USD", "CHC_USD" }; var factory = FastTests.CreateBTCPayRateFactory();
var fetcher = new RateFetcher(factory);
var provider = new BTCPayNetworkProvider(ChainName.Mainnet);
var b = new StoreBlob();
string[] temporarilyBroken = { "COP", "UGX" };
foreach (var k in StoreBlob.RecommendedExchanges)
{
b.DefaultCurrency = k.Key;
var rules = b.GetDefaultRateRules(provider);
var pairs = new[] { CurrencyPair.Parse($"BTC_{k.Key}") }.ToHashSet();
var result = fetcher.FetchRates(pairs, rules, default);
foreach ((CurrencyPair key, Task<RateResult> value) in result)
{
TestLogs.LogInformation($"Testing {key} when default currency is {k.Key}");
var rateResult = await value;
var hasRate = rateResult.BidAsk != null;
if (temporarilyBroken.Contains(k.Key))
{
if (!hasRate)
{
TestLogs.LogInformation($"Skipping {key} because it is marked as temporarily broken");
continue;
}
TestLogs.LogInformation($"Note: {key} is marked as temporarily broken, but the rate is available");
}
Assert.True(hasRate, $"Impossible to get the rate {rateResult.EvaluatedRule}");
}
}
}
[Fact]
public async Task CanGetRateCryptoCurrenciesByDefault()
{
using var cts = new CancellationTokenSource(60_000);
var provider = new BTCPayNetworkProvider(ChainName.Mainnet); var provider = new BTCPayNetworkProvider(ChainName.Mainnet);
var factory = FastTests.CreateBTCPayRateFactory(); var factory = FastTests.CreateBTCPayRateFactory();
var fetcher = new RateFetcher(factory); var fetcher = new RateFetcher(factory);
@@ -301,19 +337,29 @@ retry:
.Select(c => new CurrencyPair(c.CryptoCode, "USD")) .Select(c => new CurrencyPair(c.CryptoCode, "USD"))
.ToHashSet(); .ToHashSet();
string[] brokenShitcoins = { "BTG", "BTX" };
bool IsBrokenShitcoin(CurrencyPair p) => brokenShitcoins.Contains(p.Left) || brokenShitcoins.Contains(p.Right);
foreach (var _ in brokenShitcoins)
{
foreach (var p in pairs.Where(IsBrokenShitcoin).ToArray())
{
TestLogs.LogInformation($"Skipping {p} because it is marked as broken");
pairs.Remove(p);
}
}
var rules = new StoreBlob().GetDefaultRateRules(provider); var rules = new StoreBlob().GetDefaultRateRules(provider);
var result = fetcher.FetchRates(pairs, rules, default); var result = fetcher.FetchRates(pairs, rules, cts.Token);
foreach ((CurrencyPair key, Task<RateResult> value) in result) foreach ((CurrencyPair key, Task<RateResult> value) in result)
{ {
var rateResult = value.GetAwaiter().GetResult(); var rateResult = await value;
TestLogs.LogInformation($"Testing {key}"); TestLogs.LogInformation($"Testing {key}");
if (brokenShitcoins.Contains(key.ToString()))
continue;
Assert.True(rateResult.BidAsk != null, $"Impossible to get the rate {rateResult.EvaluatedRule}"); Assert.True(rateResult.BidAsk != null, $"Impossible to get the rate {rateResult.EvaluatedRule}");
} }
} }
[Fact] [Fact]
[Trait("Fast", "Fast")]
public async Task CheckJsContent() public async Task CheckJsContent()
{ {
// This test verify that no malicious js is added in the minified files. // This test verify that no malicious js is added in the minified files.
@@ -322,42 +368,77 @@ retry:
var actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "bootstrap", "bootstrap.bundle.min.js").Trim(); var actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "bootstrap", "bootstrap.bundle.min.js").Trim();
var version = Regex.Match(actual, "Bootstrap v([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value; var version = Regex.Match(actual, "Bootstrap v([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value;
var expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/bootstrap@{version}/dist/js/bootstrap.bundle.min.js")).Content.ReadAsStringAsync()).Trim(); var expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/bootstrap@{version}/dist/js/bootstrap.bundle.min.js")).Content.ReadAsStringAsync()).Trim();
Assert.Equal(expected, actual); EqualJsContent(expected, actual);
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "clipboard.js", "clipboard.js"); actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "clipboard.js", "clipboard.js");
expected = (await (await client.GetAsync("https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.8/clipboard.js")).Content.ReadAsStringAsync()).Trim(); expected = (await (await client.GetAsync("https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.8/clipboard.js")).Content.ReadAsStringAsync()).Trim();
Assert.Equal(expected, actual); EqualJsContent(expected, actual);
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "vuejs", "vue.min.js").Trim(); actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "vuejs", "vue.min.js").Trim();
version = Regex.Match(actual, "Vue\\.js v([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value; version = Regex.Match(actual, "Vue\\.js v([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value;
expected = (await (await client.GetAsync($"https://cdnjs.cloudflare.com/ajax/libs/vue/{version}/vue.min.js")).Content.ReadAsStringAsync()).Trim(); expected = (await (await client.GetAsync($"https://cdnjs.cloudflare.com/ajax/libs/vue/{version}/vue.min.js")).Content.ReadAsStringAsync()).Trim();
Assert.Equal(expected, actual); EqualJsContent(expected, actual);
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "i18next", "i18next.min.js").Trim(); actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "i18next", "i18next.min.js").Trim();
expected = (await (await client.GetAsync("https://cdnjs.cloudflare.com/ajax/libs/i18next/22.0.6/i18next.min.js")).Content.ReadAsStringAsync()).Trim(); expected = (await (await client.GetAsync("https://cdnjs.cloudflare.com/ajax/libs/i18next/22.0.6/i18next.min.js")).Content.ReadAsStringAsync()).Trim();
Assert.Equal(expected, actual); EqualJsContent(expected, actual);
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "i18next", "i18nextHttpBackend.min.js").Trim(); actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "i18next", "i18nextHttpBackend.min.js").Trim();
expected = (await (await client.GetAsync("https://cdnjs.cloudflare.com/ajax/libs/i18next-http-backend/2.0.1/i18nextHttpBackend.min.js")).Content.ReadAsStringAsync()).Trim(); expected = (await (await client.GetAsync("https://cdnjs.cloudflare.com/ajax/libs/i18next-http-backend/2.0.1/i18nextHttpBackend.min.js")).Content.ReadAsStringAsync()).Trim();
Assert.Equal(expected, actual); EqualJsContent(expected, actual);
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "i18next", "vue-i18next.js").Trim(); actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "i18next", "vue-i18next.js").Trim();
expected = (await (await client.GetAsync("https://unpkg.com/@panter/vue-i18next@0.15.2/dist/vue-i18next.js")).Content.ReadAsStringAsync()).Trim(); expected = (await (await client.GetAsync("https://unpkg.com/@panter/vue-i18next@0.15.2/dist/vue-i18next.js")).Content.ReadAsStringAsync()).Trim();
Assert.Equal(expected, actual); EqualJsContent(expected, actual);
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "vue-qrcode", "vue-qrcode.min.js").Trim(); actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "vue-qrcode", "vue-qrcode.min.js").Trim();
version = Regex.Match(actual, "vue-qrcode v([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value; version = Regex.Match(actual, "vue-qrcode v([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value;
expected = (await (await client.GetAsync($"https://unpkg.com/@chenfengyuan/vue-qrcode@{version}/dist/vue-qrcode.min.js")).Content.ReadAsStringAsync()).Trim(); expected = (await (await client.GetAsync($"https://unpkg.com/@chenfengyuan/vue-qrcode@{version}/dist/vue-qrcode.min.js")).Content.ReadAsStringAsync()).Trim();
Assert.Equal(expected, actual); EqualJsContent(expected, actual);
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "tom-select", "tom-select.complete.min.js").Trim(); actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "tom-select", "tom-select.complete.min.js").Trim();
expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/js/tom-select.complete.min.js")).Content.ReadAsStringAsync()).Trim(); version = Regex.Match(actual, "Tom Select v([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value;
Assert.Equal(expected, actual); expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/tom-select@{version}/dist/js/tom-select.complete.min.js")).Content.ReadAsStringAsync()).Trim();
EqualJsContent(expected, actual);
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "dom-confetti", "dom-confetti.min.js").Trim(); actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "dom-confetti", "dom-confetti.min.js").Trim();
version = Regex.Match(actual, "Original file: /npm/dom-confetti@([0-9]+.[0-9]+.[0-9]+)/lib/main.js").Groups[1].Value; version = Regex.Match(actual, "Original file: /npm/dom-confetti@([0-9]+.[0-9]+.[0-9]+)/lib/main.js").Groups[1].Value;
expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/dom-confetti@{version}/lib/main.min.js")).Content.ReadAsStringAsync()).Trim(); expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/dom-confetti@{version}/lib/main.min.js")).Content.ReadAsStringAsync()).Trim();
Assert.Equal(expected, actual); EqualJsContent(expected, actual);
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "vue-sortable", "sortable.min.js").Trim();
version = Regex.Match(actual, "Sortable ([0-9]+.[0-9]+.[0-9]+) ").Groups[1].Value;
expected = (await (await client.GetAsync($"https://unpkg.com/sortablejs@{version}/Sortable.min.js")).Content.ReadAsStringAsync()).Trim();
EqualJsContent(expected, actual);
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "bootstrap-vue", "bootstrap-vue.min.js").Trim();
version = Regex.Match(actual, "BootstrapVue ([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value;
expected = (await (await client.GetAsync($"https://cdnjs.cloudflare.com/ajax/libs/bootstrap-vue/{version}/bootstrap-vue.min.js")).Content.ReadAsStringAsync()).Trim();
EqualJsContent(expected, actual);
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "FileSaver", "FileSaver.min.js").Trim();
expected = (await (await client.GetAsync($"https://raw.githubusercontent.com/eligrey/FileSaver.js/43bbd2f0ae6794f8d452cd360e9d33aef6071234/dist/FileSaver.min.js")).Content.ReadAsStringAsync()).Trim();
EqualJsContent(expected, actual);
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "papaparse", "papaparse.min.js").Trim();
expected = (await (await client.GetAsync($"https://raw.githubusercontent.com/mholt/PapaParse/5.4.1/papaparse.min.js")).Content.ReadAsStringAsync()).Trim();
EqualJsContent(expected, actual);
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "vue-sanitize-directive", "vue-sanitize-directive.umd.min.js").Trim();
version = Regex.Match(actual, "Original file: /npm/vue-sanitize-directive@([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value;
expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/vue-sanitize-directive@{version}/dist/vue-sanitize-directive.umd.min.js")).Content.ReadAsStringAsync()).Trim();
EqualJsContent(expected, actual);
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "decimal.js", "decimal.min.js").Trim();
version = Regex.Match(actual, "Original file: /npm/decimal\\.js@([0-9]+.[0-9]+.[0-9]+)/decimal\\.js").Groups[1].Value;
expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/decimal.js@{version}/decimal.min.js")).Content.ReadAsStringAsync()).Trim();
EqualJsContent(expected, actual);
}
private void EqualJsContent(string expected, string actual)
{
if (expected != actual)
Assert.Equal(expected, actual.ReplaceLineEndings("\n"));
} }
string GetFileContent(params string[] path) string GetFileContent(params string[] path)

View File

@@ -39,6 +39,7 @@ using BTCPayServer.Plugins.PayButton;
using BTCPayServer.Plugins.PointOfSale; using BTCPayServer.Plugins.PointOfSale;
using BTCPayServer.Plugins.PointOfSale.Controllers; using BTCPayServer.Plugins.PointOfSale.Controllers;
using BTCPayServer.Security.Bitpay; using BTCPayServer.Security.Bitpay;
using BTCPayServer.Security.Greenfield;
using BTCPayServer.Services; using BTCPayServer.Services;
using BTCPayServer.Services.Apps; using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Invoices;
@@ -385,11 +386,11 @@ namespace BTCPayServer.Tests
var newBolt11 = newInvoice.CryptoInfo.First(o => o.PaymentUrls.BOLT11 != null).PaymentUrls.BOLT11; var newBolt11 = newInvoice.CryptoInfo.First(o => o.PaymentUrls.BOLT11 != null).PaymentUrls.BOLT11;
var oldBolt11 = invoice.CryptoInfo.First(o => o.PaymentUrls.BOLT11 != null).PaymentUrls.BOLT11; var oldBolt11 = invoice.CryptoInfo.First(o => o.PaymentUrls.BOLT11 != null).PaymentUrls.BOLT11;
Assert.NotEqual(newBolt11, oldBolt11); Assert.NotEqual(newBolt11, oldBolt11);
Assert.Equal(newInvoice.BtcDue.GetValue(), Assert.Equal(newInvoice.BtcDue.ToDecimal(MoneyUnit.BTC),
BOLT11PaymentRequest.Parse(newBolt11, Network.RegTest).MinimumAmount.ToDecimal(LightMoneyUnit.BTC)); BOLT11PaymentRequest.Parse(newBolt11, Network.RegTest).MinimumAmount.ToDecimal(LightMoneyUnit.BTC));
}, 40000); }, 40000);
TestLogs.LogInformation($"Paying invoice {newInvoice.Id} remaining due amount {newInvoice.BtcDue.GetValue()} via lightning"); TestLogs.LogInformation($"Paying invoice {newInvoice.Id} remaining due amount {newInvoice.BtcDue.GetValue((BTCPayNetwork) tester.DefaultNetwork)} via lightning");
var evt = await tester.WaitForEvent<InvoiceDataChangedEvent>(async () => var evt = await tester.WaitForEvent<InvoiceDataChangedEvent>(async () =>
{ {
await tester.SendLightningPaymentAsync(newInvoice); await tester.SendLightningPaymentAsync(newInvoice);
@@ -466,14 +467,6 @@ namespace BTCPayServer.Tests
await ProcessLightningPayment(LightningConnectionType.CLightning); await ProcessLightningPayment(LightningConnectionType.CLightning);
} }
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")]
[Trait("Lightning", "Lightning")]
public async Task CanSendLightningPaymentCharge()
{
await ProcessLightningPayment(LightningConnectionType.Charge);
}
[Fact(Timeout = 60 * 2 * 1000)] [Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")] [Trait("Integration", "Integration")]
[Trait("Lightning", "Lightning")] [Trait("Lightning", "Lightning")]
@@ -726,7 +719,7 @@ namespace BTCPayServer.Tests
btcDerivationScheme.GetDerivation(new KeyPath("0/90")).ScriptPubKey, Money.Coins(1.0m)); btcDerivationScheme.GetDerivation(new KeyPath("0/90")).ScriptPubKey, Money.Coins(1.0m));
tester.ExplorerNode.Generate(1); tester.ExplorerNode.Generate(1);
var transactions = Assert.IsType<ListTransactionsViewModel>(Assert var transactions = Assert.IsType<ListTransactionsViewModel>(Assert
.IsType<ViewResult>(walletController.WalletTransactions(walletId).Result).Model); .IsType<ViewResult>(walletController.WalletTransactions(walletId, loadTransactions: true).Result).Model);
Assert.Empty(transactions.Transactions); Assert.Empty(transactions.Transactions);
Assert.IsType<RedirectToActionResult>(walletController.WalletRescan(walletId, rescan).Result); Assert.IsType<RedirectToActionResult>(walletController.WalletRescan(walletId, rescan).Result);
@@ -755,7 +748,7 @@ namespace BTCPayServer.Tests
Assert.NotNull(rescan.TimeOfScan); Assert.NotNull(rescan.TimeOfScan);
Assert.Equal(1, rescan.LastSuccess.Found); Assert.Equal(1, rescan.LastSuccess.Found);
transactions = Assert.IsType<ListTransactionsViewModel>(Assert transactions = Assert.IsType<ListTransactionsViewModel>(Assert
.IsType<ViewResult>(walletController.WalletTransactions(walletId).Result).Model); .IsType<ViewResult>(walletController.WalletTransactions(walletId, loadTransactions: true).Result).Model);
var tx = Assert.Single(transactions.Transactions); var tx = Assert.Single(transactions.Transactions);
Assert.Equal(tx.Id, txId.ToString()); Assert.Equal(tx.Id, txId.ToString());
@@ -770,7 +763,7 @@ namespace BTCPayServer.Tests
await walletController.ModifyTransaction(walletId, tx.Id, addcomment: "hello")); await walletController.ModifyTransaction(walletId, tx.Id, addcomment: "hello"));
transactions = Assert.IsType<ListTransactionsViewModel>(Assert transactions = Assert.IsType<ListTransactionsViewModel>(Assert
.IsType<ViewResult>(walletController.WalletTransactions(walletId).Result).Model); .IsType<ViewResult>(walletController.WalletTransactions(walletId, loadTransactions: true).Result).Model);
tx = Assert.Single(transactions.Transactions); tx = Assert.Single(transactions.Transactions);
Assert.Equal("hello", tx.Comment); Assert.Equal("hello", tx.Comment);
@@ -782,7 +775,7 @@ namespace BTCPayServer.Tests
await walletController.ModifyTransaction(walletId, tx.Id, removelabel: "test2")); await walletController.ModifyTransaction(walletId, tx.Id, removelabel: "test2"));
transactions = Assert.IsType<ListTransactionsViewModel>(Assert transactions = Assert.IsType<ListTransactionsViewModel>(Assert
.IsType<ViewResult>(walletController.WalletTransactions(walletId).Result).Model); .IsType<ViewResult>(walletController.WalletTransactions(walletId, loadTransactions: true).Result).Model);
tx = Assert.Single(transactions.Transactions); tx = Assert.Single(transactions.Transactions);
Assert.Equal("hello", tx.Comment); Assert.Equal("hello", tx.Comment);
@@ -1634,7 +1627,7 @@ namespace BTCPayServer.Tests
var user = tester.NewAccount(); var user = tester.NewAccount();
var cryptoCode = "BTC"; var cryptoCode = "BTC";
user.GrantAccess(true); user.GrantAccess(true);
user.RegisterLightningNode(cryptoCode, LightningConnectionType.Charge); user.RegisterLightningNode(cryptoCode);
user.SetLNUrl(cryptoCode, false); user.SetLNUrl(cryptoCode, false);
var vm = user.GetController<UIStoresController>().CheckoutAppearance().AssertViewModel<CheckoutAppearanceViewModel>(); var vm = user.GetController<UIStoresController>().CheckoutAppearance().AssertViewModel<CheckoutAppearanceViewModel>();
var criteria = Assert.Single(vm.PaymentMethodCriteria); var criteria = Assert.Single(vm.PaymentMethodCriteria);
@@ -1654,7 +1647,7 @@ namespace BTCPayServer.Tests
Assert.Equal(PaymentTypes.LightningLike.ToString(), invoice.CryptoInfo[0].PaymentType); Assert.Equal(PaymentTypes.LightningLike.ToString(), invoice.CryptoInfo[0].PaymentType);
// Activating LNUrl, we should still have only 1 payment criteria that can be set. // Activating LNUrl, we should still have only 1 payment criteria that can be set.
user.RegisterLightningNode(cryptoCode, LightningConnectionType.Charge); user.RegisterLightningNode(cryptoCode);
user.SetLNUrl(cryptoCode, true); user.SetLNUrl(cryptoCode, true);
vm = user.GetController<UIStoresController>().CheckoutAppearance().AssertViewModel<CheckoutAppearanceViewModel>(); vm = user.GetController<UIStoresController>().CheckoutAppearance().AssertViewModel<CheckoutAppearanceViewModel>();
criteria = Assert.Single(vm.PaymentMethodCriteria); criteria = Assert.Single(vm.PaymentMethodCriteria);
@@ -1707,109 +1700,6 @@ namespace BTCPayServer.Tests
} }
} }
[Fact(Timeout = LongRunningTestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanExportInvoicesJson()
{
decimal GetFieldValue(string input, string fieldName)
{
var match = Regex.Match(input, $"\"{fieldName}\":([^,]*)");
Assert.True(match.Success);
return decimal.Parse(match.Groups[1].Value.Trim(), CultureInfo.InvariantCulture);
}
async Task<object[]> GetExport(TestAccount account, string storeId = null)
{
var content = await account.GetController<UIInvoiceController>(false)
.Export("json", storeId);
var result = Assert.IsType<ContentResult>(content);
Assert.Equal("application/json", result.ContentType);
return JsonConvert.DeserializeObject<object[]>(result.Content ?? "[]");
}
using var tester = CreateServerTester();
await tester.StartAsync();
var user = tester.NewAccount();
await user.GrantAccessAsync();
user.RegisterDerivationScheme("BTC");
await user.SetNetworkFeeMode(NetworkFeeMode.Always);
var invoice = await user.BitPay.CreateInvoiceAsync(
new Invoice
{
Price = 10,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some \", description",
FullNotifications = true
}, Facade.Merchant);
var networkFee = new FeeRate(invoice.MinerFees["BTC"].SatoshiPerBytes).GetFee(100);
var result = await GetExport(user);
Assert.Single(result);
var cashCow = tester.ExplorerNode;
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
var firstPayment = invoice.CryptoInfo[0].TotalDue - 3 * networkFee;
cashCow.SendToAddress(invoiceAddress, firstPayment);
Thread.Sleep(1000); // prevent race conditions, ordering payments
// look if you can reduce thread sleep, this was min value for me
// should reduce invoice due by 0 USD because payment = network fee
cashCow.SendToAddress(invoiceAddress, networkFee);
Thread.Sleep(1000);
// pay remaining amount
cashCow.SendToAddress(invoiceAddress, 4 * networkFee);
Thread.Sleep(1000);
await TestUtils.EventuallyAsync(async () =>
{
var parsedJson = await GetExport(user);
Assert.Equal(3, parsedJson.Length);
var invoiceDueAfterFirstPayment = (3 * networkFee).ToDecimal(MoneyUnit.BTC) * invoice.Rate;
var pay1str = parsedJson[0].ToString();
Assert.Contains("\"InvoiceItemDesc\": \"Some \\\", description\"", pay1str);
Assert.Equal(invoiceDueAfterFirstPayment, GetFieldValue(pay1str, "InvoiceDue"));
Assert.Contains("\"InvoicePrice\": 10.0", pay1str);
Assert.Contains("\"ConversionRate\": 5000.0", pay1str);
Assert.Contains($"\"InvoiceId\": \"{invoice.Id}\",", pay1str);
var pay2str = parsedJson[1].ToString();
Assert.Equal(invoiceDueAfterFirstPayment, GetFieldValue(pay2str, "InvoiceDue"));
var pay3str = parsedJson[2].ToString();
Assert.Contains("\"InvoiceDue\": 0", pay3str);
});
// create an invoice for a new store and check responses with and without store id
var otherUser = tester.NewAccount();
await otherUser.GrantAccessAsync();
otherUser.RegisterDerivationScheme("BTC");
await otherUser.SetNetworkFeeMode(NetworkFeeMode.Always);
var newInvoice = await otherUser.BitPay.CreateInvoiceAsync(
new Invoice
{
Price = 21,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some \", description",
FullNotifications = true
}, Facade.Merchant);
await otherUser.PayInvoice(newInvoice.Id);
Assert.Single(await GetExport(otherUser));
Assert.Single(await GetExport(otherUser, otherUser.StoreId));
Assert.Equal(3, (await GetExport(user, user.StoreId)).Length);
Assert.Equal(3, (await GetExport(user)).Length);
await otherUser.AddOwner(user.UserId);
Assert.Equal(4, (await GetExport(user)).Length);
Assert.Single(await GetExport(user, otherUser.StoreId));
}
[Fact(Timeout = LongRunningTestTimeout)] [Fact(Timeout = LongRunningTestTimeout)]
[Trait("Integration", "Integration")] [Trait("Integration", "Integration")]
public async Task CanChangeNetworkFeeMode() public async Task CanChangeNetworkFeeMode()
@@ -1899,45 +1789,6 @@ namespace BTCPayServer.Tests
} }
} }
[Fact(Timeout = LongRunningTestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanExportInvoicesCsv()
{
using var tester = CreateServerTester();
await tester.StartAsync();
var user = tester.NewAccount();
await user.GrantAccessAsync();
user.RegisterDerivationScheme("BTC");
await user.SetNetworkFeeMode(NetworkFeeMode.Always);
var invoice = user.BitPay.CreateInvoice(
new Invoice
{
Price = 500,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some \", description",
FullNotifications = true
}, Facade.Merchant);
var cashCow = tester.ExplorerNode;
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
var firstPayment = invoice.CryptoInfo[0].TotalDue - Money.Coins(0.001m);
cashCow.SendToAddress(invoiceAddress, firstPayment);
TestUtils.Eventually(() =>
{
var exportResultPaid =
user.GetController<UIInvoiceController>().Export("csv").GetAwaiter().GetResult();
var paidresult = Assert.IsType<ContentResult>(exportResultPaid);
Assert.Equal("application/csv", paidresult.ContentType);
Assert.Contains($",orderId,{invoice.Id},", paidresult.Content);
Assert.Contains($",On-Chain,BTC,0.0991,0.0001,5000.0", paidresult.Content);
Assert.Contains($",USD,5.00", paidresult.Content); // Seems hacky but some plateform does not render this decimal the same
Assert.Contains("0,,\"Some \"\", description\",New (paidPartial),new,paidPartial",
paidresult.Content);
});
}
[Fact(Timeout = LongRunningTestTimeout)] [Fact(Timeout = LongRunningTestTimeout)]
[Trait("Integration", "Integration")] [Trait("Integration", "Integration")]
public async Task CanCreateAndDeleteApps() public async Task CanCreateAndDeleteApps()
@@ -1970,7 +1821,8 @@ namespace BTCPayServer.Tests
Assert.Empty(appList2.Apps); Assert.Empty(appList2.Apps);
Assert.Equal("test", appList.Apps[0].AppName); Assert.Equal("test", appList.Apps[0].AppName);
Assert.Equal(apps.CreatedAppId, appList.Apps[0].Id); Assert.Equal(apps.CreatedAppId, appList.Apps[0].Id);
Assert.True(app.IsOwner);
Assert.True(app.Role.ToPermissionSet(app.StoreId).Contains(Policies.CanModifyStoreSettings, app.StoreId));
Assert.Equal(user.StoreId, appList.Apps[0].StoreId); Assert.Equal(user.StoreId, appList.Apps[0].StoreId);
Assert.IsType<NotFoundResult>(apps2.DeleteApp(appList.Apps[0].Id)); Assert.IsType<NotFoundResult>(apps2.DeleteApp(appList.Apps[0].Id));
Assert.IsType<ViewResult>(apps.DeleteApp(appList.Apps[0].Id)); Assert.IsType<ViewResult>(apps.DeleteApp(appList.Apps[0].Id));
@@ -1991,6 +1843,7 @@ namespace BTCPayServer.Tests
var user = tester.NewAccount(); var user = tester.NewAccount();
user.GrantAccess(true); user.GrantAccess(true);
user.RegisterDerivationScheme("BTC"); user.RegisterDerivationScheme("BTC");
var btcpayClient = await user.CreateClient();
DateTimeOffset expiration = DateTimeOffset.UtcNow + TimeSpan.FromMinutes(21); DateTimeOffset expiration = DateTimeOffset.UtcNow + TimeSpan.FromMinutes(21);
@@ -2071,6 +1924,20 @@ namespace BTCPayServer.Tests
var zeroInvoicePM = await greenfield.GetInvoicePaymentMethods(user.StoreId, zeroInvoice.Id); var zeroInvoicePM = await greenfield.GetInvoicePaymentMethods(user.StoreId, zeroInvoice.Id);
Assert.Empty(zeroInvoicePM); Assert.Empty(zeroInvoicePM);
var invoice6 = await btcpayClient.CreateInvoice(user.StoreId,
new CreateInvoiceRequest()
{
Amount = GreenfieldConstants.MaxAmount,
Currency = "USD"
});
var repo = tester.PayTester.GetService<InvoiceRepository>();
var entity = (await repo.GetInvoice(invoice6.Id));
Assert.Equal((decimal)ulong.MaxValue, entity.Price);
entity.GetPaymentMethods().First().Calculate();
// Shouldn't be possible as we clamp the value, but existing invoice may have that
entity.Price = decimal.MaxValue;
entity.GetPaymentMethods().First().Calculate();
} }
[Fact(Timeout = LongRunningTestTimeout)] [Fact(Timeout = LongRunningTestTimeout)]
@@ -2154,7 +2021,7 @@ namespace BTCPayServer.Tests
txFee = localInvoice.BtcDue - invoice.BtcDue; txFee = localInvoice.BtcDue - invoice.BtcDue;
Assert.Equal("paidPartial", localInvoice.ExceptionStatus.ToString()); Assert.Equal("paidPartial", localInvoice.ExceptionStatus.ToString());
Assert.Equal(1, localInvoice.CryptoInfo[0].TxCount); Assert.Equal(1, localInvoice.CryptoInfo[0].TxCount);
Assert.NotEqual(localInvoice.BitcoinAddress, invoice.BitcoinAddress); //New address Assert.Equal(localInvoice.BitcoinAddress, invoice.BitcoinAddress); //Same address
Assert.True(IsMapped(invoice, ctx)); Assert.True(IsMapped(invoice, ctx));
Assert.True(IsMapped(localInvoice, ctx)); Assert.True(IsMapped(localInvoice, ctx));
@@ -2850,7 +2717,7 @@ namespace BTCPayServer.Tests
using var tester = CreateServerTester(); using var tester = CreateServerTester();
await tester.StartAsync(); await tester.StartAsync();
var user = tester.NewAccount(); var user = tester.NewAccount();
user.GrantAccess(); await user.GrantAccessAsync();
var controller = tester.PayTester.GetController<UIServerController>(user.UserId, user.StoreId); var controller = tester.PayTester.GetController<UIServerController>(user.UserId, user.StoreId);
var fileSystemStorageConfiguration = Assert.IsType<FileSystemStorageConfiguration>(Assert var fileSystemStorageConfiguration = Assert.IsType<FileSystemStorageConfiguration>(Assert
@@ -2865,7 +2732,6 @@ namespace BTCPayServer.Tests
Assert.Equal(StorageProvider.FileSystem, Assert.Equal(StorageProvider.FileSystem,
shouldBeRedirectingToLocalStorageConfigPage.RouteValues["provider"]); shouldBeRedirectingToLocalStorageConfigPage.RouteValues["provider"]);
await CanUploadRemoveFiles(controller); await CanUploadRemoveFiles(controller);
} }
@@ -2897,7 +2763,7 @@ namespace BTCPayServer.Tests
//create a temporary link to file //create a temporary link to file
var tmpLinkGenerate = Assert.IsType<RedirectToActionResult>(await controller.CreateTemporaryFileUrl(fileId, var tmpLinkGenerate = Assert.IsType<RedirectToActionResult>(await controller.CreateTemporaryFileUrl(fileId,
new UIServerController.CreateTemporaryFileUrlViewModel() new UIServerController.CreateTemporaryFileUrlViewModel
{ {
IsDownload = true, IsDownload = true,
TimeAmount = 1, TimeAmount = 1,
@@ -2927,5 +2793,124 @@ namespace BTCPayServer.Tests
Assert.IsType<ViewFilesViewModel>(Assert.IsType<ViewResult>(await controller.Files(new string[] { fileId })).Model); Assert.IsType<ViewFilesViewModel>(Assert.IsType<ViewResult>(await controller.Files(new string[] { fileId })).Model);
Assert.Null(viewFilesViewModel.DirectUrlByFiles); Assert.Null(viewFilesViewModel.DirectUrlByFiles);
} }
[Fact]
[Trait("Selenium", "Selenium")]
public async Task CanCreateReports()
{
using var tester = CreateServerTester();
tester.ActivateLightning();
tester.DeleteStore = false;
await tester.StartAsync();
await tester.EnsureChannelsSetup();
var acc = tester.NewAccount();
await acc.GrantAccessAsync();
await acc.MakeAdmin();
acc.RegisterDerivationScheme("BTC", importKeysToNBX: true);
acc.RegisterLightningNode("BTC");
await acc.ReceiveUTXO(Money.Coins(1.0m));
var client = await acc.CreateClient();
var posController = acc.GetController<UIPointOfSaleController>();
var app = await client.CreatePointOfSaleApp(acc.StoreId, new CreatePointOfSaleAppRequest()
{
AppName = "Static",
DefaultView = Client.Models.PosViewType.Static,
Template = new PointOfSaleSettings().Template
});
var resp = await posController.ViewPointOfSale(app.Id, choiceKey: "green-tea");
var invoiceId = GetInvoiceId(resp);
await acc.PayOnChain(invoiceId);
app = await client.CreatePointOfSaleApp(acc.StoreId, new CreatePointOfSaleAppRequest()
{
AppName = "Cart",
DefaultView = Client.Models.PosViewType.Cart,
Template = new PointOfSaleSettings().Template
});
resp = await posController.ViewPointOfSale(app.Id, posData: new JObject()
{
["cart"] = new JArray()
{
new JObject()
{
["id"] = "green-tea",
["count"] = 2
},
new JObject()
{
["id"] = "black-tea",
["count"] = 1
},
}
}.ToString());
invoiceId = GetInvoiceId(resp);
await acc.PayOnBOLT11(invoiceId);
resp = await posController.ViewPointOfSale(app.Id, posData: new JObject()
{
["cart"] = new JArray()
{
new JObject()
{
["id"] = "green-tea",
["count"] = 5
}
}
}.ToString());
invoiceId = GetInvoiceId(resp);
await acc.PayOnLNUrl(invoiceId);
await acc.CreateLNAddress();
await acc.PayOnLNAddress();
var report = await GetReport(acc, new() { ViewName = "Payments" });
// 1 payment on LN Address
// 1 payment on LNURL
// 1 payment on BOLT11
// 1 payment on chain
Assert.Equal(4, report.Data.Count);
var lnAddressIndex = report.GetIndex("LightningAddress");
var paymentTypeIndex = report.GetIndex("PaymentType");
Assert.Contains(report.Data, d => d[lnAddressIndex]?.Value<string>()?.Contains(acc.LNAddress) is true);
var paymentTypes = report.Data
.GroupBy(d => d[paymentTypeIndex].Value<string>())
.ToDictionary(d => d.Key);
Assert.Equal(3, paymentTypes["Lightning"].Count());
Assert.Single(paymentTypes["On-Chain"]);
// 2 on-chain transactions: It received from the cashcow, then paid its own invoice
report = await GetReport(acc, new() { ViewName = "On-Chain Wallets" });
var txIdIndex = report.GetIndex("TransactionId");
var balanceIndex = report.GetIndex("BalanceChange");
Assert.Equal(2, report.Data.Count);
Assert.Equal(64, report.Data[0][txIdIndex].Value<string>().Length);
Assert.Contains(report.Data, d => d[balanceIndex]["v"].Value<decimal>() == 1.0m);
// Items sold
report = await GetReport(acc, new() { ViewName = "Products sold" });
var itemIndex = report.GetIndex("Product");
var countIndex = report.GetIndex("Quantity");
var itemsCount = report.Data.GroupBy(d => d[itemIndex].Value<string>())
.ToDictionary(d => d.Key, r => r.Sum(d => d[countIndex].Value<int>()));
Assert.Equal(8, itemsCount["green-tea"]);
Assert.Equal(1, itemsCount["black-tea"]);
}
private async Task<StoreReportResponse> GetReport(TestAccount acc, StoreReportRequest req)
{
var controller = acc.GetController<UIReportsController>();
return (await controller.StoreReportsJson(acc.StoreId, req)).AssertType<JsonResult>()
.Value
.AssertType<StoreReportResponse>();
}
private static string GetInvoiceId(IActionResult resp)
{
var redirect = resp.AssertType<RedirectToActionResult>();
Assert.Equal("Checkout", redirect.ActionName);
return (string)redirect.RouteValues["invoiceId"];
}
} }
} }

View File

@@ -24,7 +24,6 @@ services:
TESTS_AzureBlobStorageConnectionString: ${TESTS_AzureBlobStorageConnectionString:-none} TESTS_AzureBlobStorageConnectionString: ${TESTS_AzureBlobStorageConnectionString:-none}
TEST_MERCHANTLIGHTNINGD: "type=clightning;server=unix://etc/merchant_lightningd_datadir/lightning-rpc" TEST_MERCHANTLIGHTNINGD: "type=clightning;server=unix://etc/merchant_lightningd_datadir/lightning-rpc"
TEST_CUSTOMERLIGHTNINGD: "type=clightning;server=unix://etc/customer_lightningd_datadir/lightning-rpc" TEST_CUSTOMERLIGHTNINGD: "type=clightning;server=unix://etc/customer_lightningd_datadir/lightning-rpc"
TEST_MERCHANTCHARGE: "type=charge;server=http://lightning-charged:9112/;api-token=foiewnccewuify;allowinsecure=true"
TEST_MERCHANTLND: "http://lnd:lnd@merchant_lnd:8080/" TEST_MERCHANTLND: "http://lnd:lnd@merchant_lnd:8080/"
TESTS_INCONTAINER: "true" TESTS_INCONTAINER: "true"
TESTS_SSHCONNECTION: "root@sshd:22" TESTS_SSHCONNECTION: "root@sshd:22"
@@ -56,7 +55,6 @@ services:
- postgres - postgres
- customer_lightningd - customer_lightningd
- merchant_lightningd - merchant_lightningd
- lightning-charged
- customer_lnd - customer_lnd
- merchant_lnd - merchant_lnd
- sshd - sshd
@@ -75,7 +73,7 @@ services:
- "sshd_datadir:/root/.ssh" - "sshd_datadir:/root/.ssh"
devlnd: devlnd:
image: btcpayserver/bitcoin:24.0 image: btcpayserver/bitcoin:25.0
environment: environment:
BITCOIN_NETWORK: regtest BITCOIN_NETWORK: regtest
BITCOIN_WALLETDIR: "/data/wallets" BITCOIN_WALLETDIR: "/data/wallets"
@@ -89,14 +87,19 @@ services:
- postgres - postgres
- customer_lnd - customer_lnd
- merchant_lnd - merchant_lnd
selenium: selenium:
image: selenium/standalone-chrome:101.0 image: selenium/standalone-chrome:101.0
extra_hosts:
- "tests:172.23.0.18"
expose: expose:
- "4444" - "4444"
extra_hosts: networks:
- "tests:172.18.0.18" default:
custom:
nbxplorer: nbxplorer:
image: nicolasdorier/nbxplorer:2.3.58 image: nicolasdorier/nbxplorer:2.3.66
restart: unless-stopped restart: unless-stopped
ports: ports:
- "32838:32838" - "32838:32838"
@@ -132,7 +135,7 @@ services:
bitcoind: bitcoind:
restart: unless-stopped restart: unless-stopped
image: btcpayserver/bitcoin:24.0 image: btcpayserver/bitcoin:25.0
environment: environment:
BITCOIN_NETWORK: regtest BITCOIN_NETWORK: regtest
BITCOIN_WALLETDIR: "/data/wallets" BITCOIN_WALLETDIR: "/data/wallets"
@@ -160,7 +163,7 @@ services:
- "bitcoin_datadir:/data" - "bitcoin_datadir:/data"
customer_lightningd: customer_lightningd:
image: btcpayserver/lightning:v23.02-1-dev image: btcpayserver/lightning:v23.08-dev
stop_signal: SIGKILL stop_signal: SIGKILL
restart: unless-stopped restart: unless-stopped
environment: environment:
@@ -186,30 +189,8 @@ services:
depends_on: depends_on:
- bitcoind - bitcoind
lightning-charged:
image: shesek/lightning-charge:0.4.23-1-standalone
restart: unless-stopped
environment:
NETWORK: regtest
API_TOKEN: foiewnccewuify
BITCOIND_RPCCONNECT: bitcoind
LN_NET_PATH: /etc/lightning
LN_NET: /etc/lightning
volumes:
- "bitcoin_datadir:/etc/bitcoin"
- "lightning_charge_datadir:/data"
- "merchant_lightningd_datadir:/etc/lightning"
expose:
- "9112" # Charge
- "9735" # Lightning
ports:
- "54938:9112" # Charge
depends_on:
- bitcoind
- merchant_lightningd
merchant_lightningd: merchant_lightningd:
image: btcpayserver/lightning:v23.02-1-dev image: btcpayserver/lightning:v23.08-dev
stop_signal: SIGKILL stop_signal: SIGKILL
environment: environment:
EXPOSE_TCP: "true" EXPOSE_TCP: "true"
@@ -243,7 +224,7 @@ services:
- "5432" - "5432"
merchant_lnd: merchant_lnd:
image: btcpayserver/lnd:v0.16.2-beta image: btcpayserver/lnd:v0.16.4-beta-1
restart: unless-stopped restart: unless-stopped
environment: environment:
LND_CHAIN: "btc" LND_CHAIN: "btc"
@@ -278,7 +259,7 @@ services:
- bitcoind - bitcoind
customer_lnd: customer_lnd:
image: btcpayserver/lnd:v0.16.2-beta image: btcpayserver/lnd:v0.16.4-beta-1
restart: unless-stopped restart: unless-stopped
environment: environment:
LND_CHAIN: "btc" LND_CHAIN: "btc"
@@ -326,7 +307,7 @@ services:
- "torrcdir:/usr/local/etc/tor" - "torrcdir:/usr/local/etc/tor"
- "tor_servicesdir:/var/lib/tor/hidden_services" - "tor_servicesdir:/var/lib/tor/hidden_services"
monerod: monerod:
image: btcpayserver/monero:0.17.0.0-amd64 image: btcpayserver/monero:0.18.2.2-5
restart: unless-stopped restart: unless-stopped
container_name: xmr_monerod container_name: xmr_monerod
entrypoint: sleep 999999 entrypoint: sleep 999999
@@ -336,7 +317,7 @@ services:
ports: ports:
- "18081:18081" - "18081:18081"
monero_wallet: monero_wallet:
image: btcpayserver/monero:0.17.0.0-amd64 image: btcpayserver/monero:0.18.2.2-5
restart: unless-stopped restart: unless-stopped
container_name: xmr_wallet_rpc container_name: xmr_wallet_rpc
entrypoint: monero-wallet-rpc --testnet --rpc-bind-ip=0.0.0.0 --disable-rpc-login --confirm-external-bind --rpc-bind-port=18082 --non-interactive --trusted-daemon --daemon-address=monerod:18081 --wallet-file=/wallet/wallet.keys --password-file=/wallet/password --tx-notify="/bin/sh ./scripts/notifier.sh -k -X GET https://host.docker.internal:14142/monerolikedaemoncallback/tx?cryptoCode=xmr&hash=%s" entrypoint: monero-wallet-rpc --testnet --rpc-bind-ip=0.0.0.0 --disable-rpc-login --confirm-external-bind --rpc-bind-port=18082 --non-interactive --trusted-daemon --daemon-address=monerod:18081 --wallet-file=/wallet/wallet.keys --password-file=/wallet/password --tx-notify="/bin/sh ./scripts/notifier.sh -k -X GET https://host.docker.internal:14142/monerolikedaemoncallback/tx?cryptoCode=xmr&hash=%s"
@@ -368,7 +349,7 @@ services:
elementsd-liquid: elementsd-liquid:
restart: always restart: always
container_name: btcpayserver_elementsd_liquid container_name: btcpayserver_elementsd_liquid
image: btcpayserver/elements:0.21.0.1 image: btcpayserver/elements:0.21.0.2-4
environment: environment:
ELEMENTS_CHAIN: elementsregtest ELEMENTS_CHAIN: elementsregtest
ELEMENTS_EXTRA_ARGS: | ELEMENTS_EXTRA_ARGS: |
@@ -383,11 +364,9 @@ services:
whitelist=0.0.0.0/0 whitelist=0.0.0.0/0
rpcallowip=0.0.0.0/0 rpcallowip=0.0.0.0/0
validatepegin=0 validatepegin=0
initialfreecoins=210000000000000 initialfreecoins=2100000000000000
con_dyna_deploy_signal=1 con_dyna_deploy_signal=1
con_dyna_deploy_start=0 con_dyna_deploy_start=10
con_nminerconfirmationwindow=1
con_nrulechangeactivationthreshold=1
expose: expose:
- "19332" - "19332"
- "19444" - "19444"

View File

@@ -22,7 +22,6 @@ services:
TESTS_AzureBlobStorageConnectionString: ${TESTS_AzureBlobStorageConnectionString:-none} TESTS_AzureBlobStorageConnectionString: ${TESTS_AzureBlobStorageConnectionString:-none}
TEST_MERCHANTLIGHTNINGD: "type=clightning;server=unix://etc/merchant_lightningd_datadir/lightning-rpc" TEST_MERCHANTLIGHTNINGD: "type=clightning;server=unix://etc/merchant_lightningd_datadir/lightning-rpc"
TEST_CUSTOMERLIGHTNINGD: "type=clightning;server=unix://etc/customer_lightningd_datadir/lightning-rpc" TEST_CUSTOMERLIGHTNINGD: "type=clightning;server=unix://etc/customer_lightningd_datadir/lightning-rpc"
TEST_MERCHANTCHARGE: "type=charge;server=http://lightning-charged:9112/;api-token=foiewnccewuify;allowinsecure=true"
TEST_MERCHANTLND: "http://lnd:lnd@merchant_lnd:8080/" TEST_MERCHANTLND: "http://lnd:lnd@merchant_lnd:8080/"
TESTS_INCONTAINER: "true" TESTS_INCONTAINER: "true"
TESTS_SSHCONNECTION: "root@sshd:22" TESTS_SSHCONNECTION: "root@sshd:22"
@@ -54,7 +53,6 @@ services:
- postgres - postgres
- customer_lightningd - customer_lightningd
- merchant_lightningd - merchant_lightningd
- lightning-charged
- customer_lnd - customer_lnd
- merchant_lnd - merchant_lnd
- sshd - sshd
@@ -72,28 +70,33 @@ services:
- "sshd_datadir:/root/.ssh" - "sshd_datadir:/root/.ssh"
devlnd: devlnd:
image: btcpayserver/bitcoin:24.0 image: btcpayserver/bitcoin:25.0
environment: environment:
BITCOIN_NETWORK: regtest BITCOIN_NETWORK: regtest
BITCOIN_WALLETDIR: "/data/wallets" BITCOIN_WALLETDIR: "/data/wallets"
BITCOIN_EXTRA_ARGS: | BITCOIN_EXTRA_ARGS: |
deprecatedrpc=signrawtransaction deprecatedrpc=signrawtransaction
connect=bitcoind:39388 connect=bitcoind:39388
rpcallowip=0.0.0.0/0
fallbackfee=0.0002 fallbackfee=0.0002
rpcallowip=0.0.0.0/0
depends_on: depends_on:
- nbxplorer - nbxplorer
- postgres - postgres
- customer_lnd - customer_lnd
- merchant_lnd - merchant_lnd
selenium: selenium:
image: selenium/standalone-chrome:101.0 image: selenium/standalone-chrome:101.0
extra_hosts: extra_hosts:
- "tests:172.18.0.18" - "tests:172.23.0.18"
expose: expose:
- "4444" - "4444"
networks:
default:
custom:
nbxplorer: nbxplorer:
image: nicolasdorier/nbxplorer:2.3.58 image: nicolasdorier/nbxplorer:2.3.66
restart: unless-stopped restart: unless-stopped
ports: ports:
- "32838:32838" - "32838:32838"
@@ -118,7 +121,7 @@ services:
bitcoind: bitcoind:
restart: unless-stopped restart: unless-stopped
image: btcpayserver/bitcoin:24.0 image: btcpayserver/bitcoin:25.0
environment: environment:
BITCOIN_NETWORK: regtest BITCOIN_NETWORK: regtest
BITCOIN_WALLETDIR: "/data/wallets" BITCOIN_WALLETDIR: "/data/wallets"
@@ -146,7 +149,7 @@ services:
- "bitcoin_datadir:/data" - "bitcoin_datadir:/data"
customer_lightningd: customer_lightningd:
image: btcpayserver/lightning:v23.02-1-dev image: btcpayserver/lightning:v23.08-dev
stop_signal: SIGKILL stop_signal: SIGKILL
restart: unless-stopped restart: unless-stopped
environment: environment:
@@ -172,30 +175,8 @@ services:
depends_on: depends_on:
- bitcoind - bitcoind
lightning-charged:
image: shesek/lightning-charge:0.4.23-1-standalone
restart: unless-stopped
environment:
NETWORK: regtest
API_TOKEN: foiewnccewuify
BITCOIND_RPCCONNECT: bitcoind
LN_NET_PATH: /etc/lightning
LN_NET: /etc/lightning
volumes:
- "bitcoin_datadir:/etc/bitcoin"
- "lightning_charge_datadir:/data"
- "merchant_lightningd_datadir:/etc/lightning"
expose:
- "9112" # Charge
- "9735" # Lightning
ports:
- "54938:9112" # Charge
depends_on:
- bitcoind
- merchant_lightningd
merchant_lightningd: merchant_lightningd:
image: btcpayserver/lightning:v23.02-1-dev image: btcpayserver/lightning:v23.08-dev
stop_signal: SIGKILL stop_signal: SIGKILL
environment: environment:
EXPOSE_TCP: "true" EXPOSE_TCP: "true"
@@ -230,7 +211,7 @@ services:
- "5432" - "5432"
merchant_lnd: merchant_lnd:
image: btcpayserver/lnd:v0.16.2-beta image: btcpayserver/lnd:v0.16.4-beta-1
restart: unless-stopped restart: unless-stopped
environment: environment:
LND_CHAIN: "btc" LND_CHAIN: "btc"
@@ -267,7 +248,7 @@ services:
- bitcoind - bitcoind
customer_lnd: customer_lnd:
image: btcpayserver/lnd:v0.16.2-beta image: btcpayserver/lnd:v0.16.4-beta-1
restart: unless-stopped restart: unless-stopped
environment: environment:
LND_CHAIN: "btc" LND_CHAIN: "btc"

View File

@@ -45,21 +45,22 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="YamlDotNet" Version="8.0.0" />
<PackageReference Include="BIP78.Sender" Version="0.2.2" /> <PackageReference Include="BIP78.Sender" Version="0.2.2" />
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.2" /> <PackageReference Include="BTCPayServer.Hwi" Version="2.0.2" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.4.23" /> <PackageReference Include="BTCPayServer.Lightning.All" Version="1.4.31" />
<PackageReference Include="CsvHelper" Version="15.0.5" /> <PackageReference Include="CsvHelper" Version="15.0.5" />
<PackageReference Include="Dapper" Version="2.0.123" /> <PackageReference Include="Dapper" Version="2.0.123" />
<PackageReference Include="Fido2" Version="2.0.2" /> <PackageReference Include="Fido2" Version="2.0.2" />
<PackageReference Include="Fido2.AspNet" Version="2.0.2" /> <PackageReference Include="Fido2.AspNet" Version="2.0.2" />
<PackageReference Include="HtmlSanitizer" Version="5.0.372" /> <PackageReference Include="HtmlSanitizer" Version="5.0.372" />
<PackageReference Include="LNURL" Version="0.0.29" /> <PackageReference Include="LNURL" Version="0.0.34" />
<PackageReference Include="MailKit" Version="3.3.0" /> <PackageReference Include="MailKit" Version="3.3.0" />
<PackageReference Include="BTCPayServer.NETCore.Plugins.Mvc" Version="1.4.4" /> <PackageReference Include="BTCPayServer.NETCore.Plugins.Mvc" Version="1.4.4" />
<PackageReference Include="QRCoder" Version="1.4.3" /> <PackageReference Include="QRCoder" Version="1.4.3" />
<PackageReference Include="System.IO.Pipelines" Version="6.0.3" /> <PackageReference Include="System.IO.Pipelines" Version="6.0.3" />
<PackageReference Include="NBitpayClient" Version="1.0.0.39" /> <PackageReference Include="NBitpayClient" Version="1.0.0.39" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NicolasDorier.CommandLine" Version="2.0.0" /> <PackageReference Include="NicolasDorier.CommandLine" Version="2.0.0" />
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="2.0.0" /> <PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="2.0.0" />
<PackageReference Include="NicolasDorier.RateLimits" Version="1.2.3" /> <PackageReference Include="NicolasDorier.RateLimits" Version="1.2.3" />
@@ -75,12 +76,12 @@
<PackageReference Include="TwentyTwenty.Storage.Azure" 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.Google" Version="2.12.1" />
<PackageReference Include="TwentyTwenty.Storage.Local" Version="2.12.1" /> <PackageReference Include="TwentyTwenty.Storage.Local" Version="2.12.1" />
<PackageReference Include="YamlDotNet" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="6.0.9" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="6.0.9" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.9" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.9" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="Views\UIReports\StoreReports.cshtml" />
<None Include="wwwroot\vendor\font-awesome\fonts\fontawesome-webfont.svg" /> <None Include="wwwroot\vendor\font-awesome\fonts\fontawesome-webfont.svg" />
<None Include="wwwroot\vendor\font-awesome\fonts\fontawesome-webfont.woff2" /> <None Include="wwwroot\vendor\font-awesome\fonts\fontawesome-webfont.woff2" />
<None Include="wwwroot\vendor\font-awesome\less\animated.less" /> <None Include="wwwroot\vendor\font-awesome\less\animated.less" />
@@ -111,9 +112,6 @@
<None Include="wwwroot\vendor\font-awesome\scss\_screen-reader.scss" /> <None Include="wwwroot\vendor\font-awesome\scss\_screen-reader.scss" />
<None Include="wwwroot\vendor\font-awesome\scss\_stacked.scss" /> <None Include="wwwroot\vendor\font-awesome\scss\_stacked.scss" />
<None Include="wwwroot\vendor\font-awesome\scss\_variables.scss" /> <None Include="wwwroot\vendor\font-awesome\scss\_variables.scss" />
<None Include="wwwroot\vendor\jquery-easing\jquery.easing.compatibility.js" />
<None Include="wwwroot\vendor\jquery-easing\jquery.easing.js" />
<None Include="wwwroot\vendor\jquery-easing\jquery.easing.min.js" />
<None Include="wwwroot\vendor\jquery\jquery.js" /> <None Include="wwwroot\vendor\jquery\jquery.js" />
<None Include="wwwroot\vendor\jquery\jquery.min.js" /> <None Include="wwwroot\vendor\jquery\jquery.min.js" />
</ItemGroup> </ItemGroup>
@@ -122,6 +120,7 @@
<Folder Include="wwwroot\vendor\bootstrap" /> <Folder Include="wwwroot\vendor\bootstrap" />
<Folder Include="wwwroot\vendor\clipboard.js\" /> <Folder Include="wwwroot\vendor\clipboard.js\" />
<Folder Include="wwwroot\vendor\highlightjs\" /> <Folder Include="wwwroot\vendor\highlightjs\" />
<Folder Include="wwwroot\vendor\pivottable\" />
<Folder Include="wwwroot\vendor\summernote" /> <Folder Include="wwwroot\vendor\summernote" />
<Folder Include="wwwroot\vendor\tom-select" /> <Folder Include="wwwroot\vendor\tom-select" />
<Folder Include="wwwroot\vendor\ur-registry" /> <Folder Include="wwwroot\vendor\ur-registry" />
@@ -139,6 +138,7 @@
<ItemGroup> <ItemGroup>
<Watch Include="Views\**\*.*"></Watch> <Watch Include="Views\**\*.*"></Watch>
<Watch Remove="Views\UIAccount\CheatPermissions.cshtml" /> <Watch Remove="Views\UIAccount\CheatPermissions.cshtml" />
<Watch Remove="Views\UIReports\StoreReports.cshtml" />
<Content Update="Views\UIApps\_ViewImports.cshtml"> <Content Update="Views\UIApps\_ViewImports.cshtml">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory> <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<Pack>$(IncludeRazorContentInPack)</Pack> <Pack>$(IncludeRazorContentInPack)</Pack>

View File

@@ -0,0 +1,14 @@
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.JSInterop;
namespace BTCPayServer.Blazor
{
public static class BlazorExtensions
{
public static bool IsPreRendering(this IJSRuntime runtime)
{
// The peculiar thing in prerender is that Blazor circuit isn't yet created, so we can't use JSInterop
return !(bool)runtime.GetType().GetProperty("IsInitialized").GetValue(runtime);
}
}
}

View File

@@ -0,0 +1,22 @@
@using BTCPayServer.Abstractions.Extensions;
@using BTCPayServer.Configuration;
@using Microsoft.AspNetCore.Hosting;
@using Microsoft.AspNetCore.Mvc.Routing;
@using Microsoft.AspNetCore.Mvc.ViewFeatures;
@using Microsoft.AspNetCore.Mvc;
@inject IFileVersionProvider FileVersionProvider
@inject BTCPayServerOptions BTCPayServerOptions
<svg role="img" class="icon icon-@Symbol">
<use href="@GetPathTo(Symbol)"></use>
</svg>
@code {
public string GetPathTo(string symbol)
{
var versioned = FileVersionProvider.AddFileVersionToPath(default, "img/icon-sprite.svg");
var rootPath = (BTCPayServerOptions.RootPath ?? "/").WithTrailingSlash();
return $"{rootPath}{versioned}#{Symbol}";
}
[Parameter]
public string Symbol { get; set; }
}

View File

@@ -0,0 +1,152 @@
@using System.Security.Claims
@using BTCPayServer.Abstractions.Contracts;
@using BTCPayServer.Configuration;
@using BTCPayServer.Data;
@using BTCPayServer.Services.Notifications;
@using Microsoft.AspNetCore.Identity;
@using Microsoft.AspNetCore.Routing;
@implements IDisposable
@inject AuthenticationStateProvider _AuthenticationStateProvider
@inject NotificationManager _NotificationManager
@inject UserManager<ApplicationUser> _UserManager
@inject IJSRuntime _JSRuntime
@inject LinkGenerator _LinkGenerator
@inject BTCPayServerOptions _BTCPayServerOptions
@inject EventAggregator _EventAggregator
<div id="Notifications">
@if (UnseenCount == "0")
{
<a href="@NotificationsUrl" id="NotificationsHandle" class="mainMenuButton" title="Notifications">
<Icon Symbol="notifications" />
</a>
}
else
{
<button id="NotificationsHandle" class="mainMenuButton" title="Notifications" type="button" data-bs-toggle="dropdown">
<Icon Symbol="notifications" />
<span class="badge rounded-pill bg-danger p-1 ms-1" id="NotificationsBadge">@UnseenCount</span>
</button>
}
@if (UnseenCount != "0" && Last5 is not null)
{
<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>
</div>
<div id="NotificationsList" v-pre>
@foreach (var n in Last5)
{
<a href="@NotificationUrl(n.Id)" class="notification d-flex align-items-center dropdown-item border-bottom border-light py-3 px-4">
<div class="me-3">
<Icon Symbol="@NotificationIcon(n.Identifier)" />
</div>
<div class="notification-item__content">
<div class="text-start text-wrap">
@n.Body
</div>
<div class="text-start d-flex">
<small class="text-muted" data-timeago-unixms="@n.Created.ToUnixTimeMilliseconds()">@n.Created.ToTimeAgo()</small>
</div>
</div>
</a>
}
</div>
<div class="p-3">
<a href="@NotificationsUrl">View all</a>
</div>
</div>
}
</div>
@code {
string NotificationsUrl => _LinkGenerator.GetPathByAction("Index", "UINotifications", pathBase: _BTCPayServerOptions.RootPath);
string NotificationUrl(string notificationId) => _LinkGenerator.GetPathByAction("NotificationPassThrough", "UINotifications", values: new { id = notificationId }, pathBase: _BTCPayServerOptions.RootPath);
string UnseenCount;
List<NotificationViewModel> Last5;
IDisposable _EventAggregatorListener;
protected override void OnInitialized()
{
if (_JSRuntime.IsPreRendering())
return;
_EventAggregatorListener = _EventAggregator.Subscribe<UserNotificationsUpdatedEvent>((s, evt) =>
{
_ = InvokeAsync(async () =>
{
if (await GetUserId() is string userId)
{
var res = await _NotificationManager.GetSummaryNotifications(userId, cachedOnly: false);
UpdateState(res);
StateHasChanged();
}
});
});
}
public void Dispose() => _EventAggregatorListener?.Dispose();
string SeenCount(int? count)
{
if (count is not int c)
return "0";
if (c >= NotificationManager.MaxUnseen)
return $"{NotificationManager.MaxUnseen - 1}+";
return c.ToString();
}
void UpdateState((List<NotificationViewModel> Items, int? Count) res)
{
UnseenCount = SeenCount(res.Count);
Last5 = res.Items;
}
protected async override Task OnParametersSetAsync()
{
if (await GetUserId() is string userId)
{
// For prerendering and first rendering, always use the cached value
var res = await _NotificationManager.GetSummaryNotifications(userId, cachedOnly: true);
// If we forget to update the state here, the UI will flicker.
// Because the first rendering will think there is 0 events, until the DB call ends and the second rendering happens.
// By updating the state here, the first rendering will show the cached value until the second rendering happens
UpdateState(res);
// We don't want to block the pre-rendering, so we will render again when the costly request is over
if (!_JSRuntime.IsPreRendering())
{
res = await _NotificationManager.GetSummaryNotifications(userId, cachedOnly: false);
UpdateState(res);
}
}
}
async Task<string>
GetUserId()
{
var state = await _AuthenticationStateProvider.GetAuthenticationStateAsync();
if (!state.User.Identity.IsAuthenticated)
return null;
return _UserManager.GetUserId(state.User);
}
public async Task MarkAllAsSeen()
{
if (await GetUserId() is string userId)
{
await _NotificationManager.ToggleSeen(new NotificationsQuery() { Seen = false, UserId = userId }, true);
UnseenCount = "0";
}
}
private static string NotificationIcon(string type)
{
return type switch
{
"invoice_expired" => "notifications-invoice-failure",
"invoice_expiredpaidpartial" => "notifications-invoice-failure",
"invoice_failedtoconfirm" => "notifications-invoice-failure",
"invoice_confirmed" => "notifications-invoice-settled",
"invoice_paidafterexpiration" => "notifications-invoice-settled",
"external-payout-transaction" => "notifications-payout",
"payout_awaitingapproval" => "notifications-payout",
"payout_awaitingpayment" => "notifications-payout-approved",
"newversion" => "notifications-new-version",
_ => "note"
};
}
}

View File

@@ -0,0 +1,9 @@
@using System.Net.Http
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.JSInterop
@using BTCPayServer.Blazor
@using BTCPayServer.Abstractions.Extensions

View File

@@ -1,27 +1,24 @@
if (!window.appSales) { if (!window.appSales) {
window.appSales = window.appSales = {
{ dataLoaded (model) {
dataLoaded: function (model) { const id = `AppSales-${model.id}`;
const id = "AppSales-" + model.id;
const appId = model.id; const appId = model.id;
const period = model.period; const period = model.period;
const baseUrl = model.url; const baseUrl = model.dataUrl;
const data = model; const data = model;
const render = (data, period) => { const render = (data, period) => {
const series = data.series.map(s => s.salesCount); const series = data.series.map(s => s.salesCount);
const labels = data.series.map((s, i) => period === model.period ? s.label : (i % 5 === 0 ? s.label : '')); const labels = data.series.map((s, i) => period === 'Month' ? (i % 5 === 0 ? s.label : '') : s.label);
const min = Math.min(...series); const min = Math.min(...series);
const max = Math.max(...series); const max = Math.max(...series);
const low = min === max ? 0 : Math.max(min - ((max - min) / 5), 0); const low = min === max ? 0 : Math.max(min - ((max - min) / 5), 0);
document.querySelectorAll(`#${id} .sales-count`).innerText = data.salesCount; document.querySelectorAll(`#${id} .sales-count`).innerText = data.salesCount;
new Chartist.Bar(`#${id} .ct-chart`, { new Chartist.Bar(`#${id} .ct-chart`, {
labels, labels,
series: [series] series: [series]
}, { }, {
low, low
}); });
}; };

View File

@@ -24,7 +24,7 @@ public class AppTopItems : ViewComponent
public async Task<IViewComponentResult> InvokeAsync(string appId, string appType) public async Task<IViewComponentResult> InvokeAsync(string appId, string appType)
{ {
var type = _appService.GetAppType(appType); var type = _appService.GetAppType(appType);
if (type is not IHasItemStatsAppType salesAppType || type is not AppBaseType appBaseType) if (type is not (IHasItemStatsAppType and AppBaseType appBaseType))
return new HtmlContentViewComponentResult(new StringHtmlContent(string.Empty)); return new HtmlContentViewComponentResult(new StringHtmlContent(string.Empty));
var vm = new AppTopItemsViewModel var vm = new AppTopItemsViewModel
@@ -40,7 +40,7 @@ public class AppTopItems : ViewComponent
var app = HttpContext.GetAppData(); var app = HttpContext.GetAppData();
var entries = await _appService.GetItemStats(app); var entries = await _appService.GetItemStats(app);
vm.SalesCount = entries.Select(e => e.SalesCount).ToList(); vm.SalesCount = entries.Select(e => e.SalesCount).ToList();
vm.Entries = entries.ToList(); vm.Entries = entries.Take(5).ToList();
vm.AppType = app.AppType; vm.AppType = app.AppType;
vm.AppUrl = await appBaseType.ConfigureLink(app); vm.AppUrl = await appBaseType.ConfigureLink(app);
vm.Name = app.Name; vm.Name = app.Name;

View File

@@ -27,8 +27,8 @@
const response = await fetch(url); const response = await fetch(url);
if (response.ok) { if (response.ok) {
document.getElementById(`AppTopItems-${appId}`).outerHTML = await response.text(); document.getElementById(`AppTopItems-${appId}`).outerHTML = await response.text();
const data = document.querySelector(`#AppSales-${appId} template`); const data = document.querySelector(`#AppTopItems-${appId} template`);
if (data) window.appSales.dataLoaded(JSON.parse(data.innerHTML)); if (data) window.appTopItems.dataLoaded(JSON.parse(data.innerHTML));
} }
})(); })();
</script> </script>
@@ -48,7 +48,7 @@
<span class="app-item-point ct-point"></span> <span class="app-item-point ct-point"></span>
@entry.Title @entry.Title
</span> </span>
<span class="app-item-value"> <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 @($"{label}{(entry.SalesCount == 1 ? "" : "s")}"),</span>
@entry.TotalFormatted @entry.TotalFormatted
</span> </span>

View File

@@ -1,8 +1,7 @@
if (!window.appTopItems) { if (!window.appTopItems) {
window.appTopItems = window.appTopItems = {
{ dataLoaded (model) {
dataLoaded: function (model) { const id = `AppTopItems-${model.id}`;
const id = "AppTopItems-" + model.id;
const series = model.salesCount; const series = model.salesCount;
new Chartist.Bar(`#${id} .ct-chart`, { series }, { new Chartist.Bar(`#${id} .ct-chart`, { series }, {
distributeSeries: true, distributeSeries: true,

View File

@@ -0,0 +1,67 @@
@using BTCPayServer.Services.Invoices
@using BTCPayServer.Abstractions.Extensions
@model BTCPayServer.Components.InvoiceStatus.InvoiceStatusViewModel
@inject PaymentMethodHandlerDictionary PaymentMethodHandlerDictionary
@{
var state = Model.State.ToString();
var badgeClass = Model.State.Status.ToModernStatus().ToString().ToLower();
var canMark = !string.IsNullOrEmpty(Model.InvoiceId) && (Model.State.CanMarkComplete() || Model.State.CanMarkInvalid());
}
<div class="d-inline-flex align-items-center gap-2">
@if (Model.IsArchived)
{
<span class="badge bg-warning">archived</span>
}
<div class="badge badge-@badgeClass" data-invoice-state-badge="@Model.InvoiceId">
@if (canMark)
{
<span class="dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
@state
</span>
<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">
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">
Mark as settled
</button>
}
</div>
}
else
{
@state
}
</div>
@if (Model.Payments != null)
{
foreach (var paymentMethodId in Model.Payments.Select(payment => payment.GetPaymentMethodId()).Distinct())
{
var image = PaymentMethodHandlerDictionary[paymentMethodId]?.GetCryptoImage(paymentMethodId);
var badge = paymentMethodId.PaymentType.GetBadge();
if (!string.IsNullOrEmpty(image) || !string.IsNullOrEmpty(badge))
{
<span class="d-inline-flex align-items-center gap-1">
@if (!string.IsNullOrEmpty(image))
{
<img src="@Context.Request.GetRelativePathOrAbsolute(image)" alt="@paymentMethodId.PaymentType.ToString()" style="height:1.5em" />
}
@if (!string.IsNullOrEmpty(badge))
{
@badge
}
</span>
}
}
}
@if (Model.HasRefund)
{
<span class="badge bg-warning">Refund</span>
}
</div>

View File

@@ -0,0 +1,22 @@
using System.Collections.Generic;
using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Components.InvoiceStatus
{
public class InvoiceStatus : ViewComponent
{
public IViewComponentResult Invoke(InvoiceState state, List<PaymentEntity> payments, string invoiceId, bool isArchived = false, bool hasRefund = false)
{
var vm = new InvoiceStatusViewModel
{
State = state,
Payments = payments,
InvoiceId = invoiceId,
IsArchived = isArchived,
HasRefund = hasRefund
};
return View(vm);
}
}
}

View File

@@ -0,0 +1,14 @@
using System.Collections.Generic;
using BTCPayServer.Services.Invoices;
namespace BTCPayServer.Components.InvoiceStatus
{
public class InvoiceStatusViewModel
{
public InvoiceState State { get; set; }
public List<PaymentEntity> Payments { get; set; }
public string InvoiceId { get; set; }
public bool IsArchived { get; set; }
public bool HasRefund { get; set; }
}
}

View File

@@ -3,7 +3,7 @@
@model BTCPayServer.Components.LabelManager.LabelViewModel @model BTCPayServer.Components.LabelManager.LabelViewModel
@{ @{
var elementId = "a" + Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)); var elementId = "a" + Encoders.Base58.EncodeData(RandomUtils.GetBytes(16));
var fetchUrl = Url.Action("GetLabels", "UIWallets", new { var fetchUrl = Url.Action("LabelsJson", "UIWallets", new {
walletId = Model.WalletObjectId.WalletId, walletId = Model.WalletObjectId.WalletId,
excludeTypes = Safe.Json(Model.ExcludeTypes) excludeTypes = Safe.Json(Model.ExcludeTypes)
}); });

View File

@@ -1,13 +1,15 @@
@using BTCPayServer.Views.Server @using BTCPayServer.Views.Server
@using BTCPayServer.Views.Stores @using BTCPayServer.Views.Stores
@using BTCPayServer.Views.Apps
@using BTCPayServer.Views.Invoice @using BTCPayServer.Views.Invoice
@using BTCPayServer.Views.Manage @using BTCPayServer.Views.Manage
@using BTCPayServer.Views.PaymentRequest @using BTCPayServer.Views.PaymentRequest
@using BTCPayServer.Views.Wallets @using BTCPayServer.Views.Wallets
@using BTCPayServer.Abstractions.Extensions @using BTCPayServer.Abstractions.Extensions
@using BTCPayServer.Client @using BTCPayServer.Client
@using BTCPayServer.Components.ThemeSwitch
@using BTCPayServer.Components.UIExtensionPoint
@using BTCPayServer.Services @using BTCPayServer.Services
@using BTCPayServer.Views.Apps
@using BTCPayServer.Views.CustodianAccounts @using BTCPayServer.Views.CustodianAccounts
@inject Microsoft.AspNetCore.Http.IHttpContextAccessor HttpContext; @inject Microsoft.AspNetCore.Http.IHttpContextAccessor HttpContext;
@inject BTCPayServerEnvironment Env @inject BTCPayServerEnvironment Env
@@ -132,6 +134,12 @@
<span>Invoices</span> <span>Invoices</span>
</a> </a>
</li> </li>
<li class="nav-item" permission="@Policies.CanViewInvoices">
<a asp-area="" asp-controller="UIReports" asp-action="StoreReports" asp-route-storeId="@Model.Store.Id" class="nav-link @ViewData.IsActivePage(StoreNavPages.Reporting)" id="SectionNav-Reporting">
<vc:icon symbol="invoice" />
<span>Reporting</span>
</a>
</li>
<li class="nav-item" permission="@Policies.CanModifyStoreSettings"> <li class="nav-item" permission="@Policies.CanModifyStoreSettings">
<a asp-area="" asp-controller="UIPaymentRequest" asp-action="GetPaymentRequests" asp-route-storeId="@Model.Store.Id" class="nav-link @ViewData.IsActiveCategory(typeof(PaymentRequestsNavPages))" id="StoreNav-PaymentRequests"> <a asp-area="" asp-controller="UIPaymentRequest" asp-action="GetPaymentRequests" asp-route-storeId="@Model.Store.Id" class="nav-link @ViewData.IsActiveCategory(typeof(PaymentRequestsNavPages))" id="StoreNav-PaymentRequests">
<vc:icon symbol="payment-requests"/> <vc:icon symbol="payment-requests"/>
@@ -178,10 +186,18 @@
<ul class="navbar-nav"> <ul class="navbar-nav">
<li class="nav-item" permission="@Policies.CanModifyServerSettings"> <li class="nav-item" permission="@Policies.CanModifyServerSettings">
<a asp-area="" asp-controller="UIServer" asp-action="ListPlugins" class="nav-link @ViewData.IsActivePage(ServerNavPages.Plugins)" id="Nav-ManagePlugins"> <a asp-area="" asp-controller="UIServer" asp-action="ListPlugins" class="nav-link @ViewData.IsActivePage(ServerNavPages.Plugins)" id="Nav-ManagePlugins">
<vc:icon symbol="plugin"/> <vc:icon symbol="manage-plugins"/>
<span>Manage Plugins</span> <span>Manage Plugins</span>
</a> </a>
</li> </li>
@if (Model.Store != null && Model.ArchivedAppsCount > 0)
{
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyStoreSettings">
<a asp-area="" asp-controller="UIApps" asp-action="ListApps" asp-route-storeId="@Model.Store.Id" asp-route-archived="true" class="nav-link @ViewData.IsActivePage(AppsNavPages.Index)" id="Nav-ArchivedApps">
@Model.ArchivedAppsCount Archived App@(Model.ArchivedAppsCount == 1 ? "" : "s")
</a>
</li>
}
</ul> </ul>
</div> </div>
</div> </div>
@@ -239,7 +255,7 @@
<span>Account</span> <span>Account</span>
</a> </a>
<ul class="dropdown-menu py-0 w-100" aria-labelledby="Nav-Account"> <ul class="dropdown-menu py-0 w-100" aria-labelledby="Nav-Account">
<li class="p-3"> <li class="p-3 border-bottom">
<strong class="d-block text-truncate" style="max-width:195px">@User.Identity.Name</strong> <strong class="d-block text-truncate" style="max-width:195px">@User.Identity.Name</strong>
@if (User.IsInRole(Roles.ServerAdmin)) @if (User.IsInRole(Roles.ServerAdmin))
{ {
@@ -248,10 +264,19 @@
</li> </li>
@if (!Theme.CustomTheme) @if (!Theme.CustomTheme)
{ {
<li class="border-top py-1 px-3"> <li class="py-1 px-3">
<vc:theme-switch css-class="nav-link"/> <vc:theme-switch css-class="nav-link pb-0"/>
</li> </li>
} }
<li class="py-1 px-3">
<label class="d-flex align-items-center justify-content-between gap-3 nav-link">
<span class="fw-semibold">Hide Sensitive Info</span>
<input id="HideSensitiveInfo" name="HideSensitiveInfo" type="checkbox" class="btcpay-toggle" />
</label>
<script>
document.getElementById('HideSensitiveInfo').checked = window.localStorage.getItem('btcpay-hide-sensitive-info') === 'true';
</script>
</li>
<li class="border-top py-1 px-3"> <li class="border-top py-1 px-3">
<a asp-area="" asp-controller="UIManage" asp-action="Index" class="nav-link @ViewData.IsActiveCategory(typeof(ManageNavPages))" id="Nav-ManageAccount"> <a asp-area="" asp-controller="UIManage" asp-action="Index" class="nav-link @ViewData.IsActiveCategory(typeof(ManageNavPages))" id="Nav-ManageAccount">
<span>Manage Account</span> <span>Manage Account</span>

View File

@@ -68,15 +68,18 @@ namespace BTCPayServer.Components.MainNav
vm.LightningNodes = lightningNodes; vm.LightningNodes = lightningNodes;
// Apps // Apps
var apps = await _appService.GetAllApps(UserId, false, store.Id); var apps = await _appService.GetAllApps(UserId, false, store.Id, true);
vm.Apps = apps.Select(a => new StoreApp vm.Apps = apps
.Where(a => !a.Archived)
.Select(a => new StoreApp
{ {
Id = a.Id, Id = a.Id,
IsOwner = a.IsOwner,
AppName = a.AppName, AppName = a.AppName,
AppType = a.AppType AppType = a.AppType
}).ToList(); }).ToList();
vm.ArchivedAppsCount = apps.Count(a => a.Archived);
if (PoliciesSettings.Experimental) if (PoliciesSettings.Experimental)
{ {
// Custodian Accounts // Custodian Accounts

View File

@@ -1,7 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Models.StoreViewModels; using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Services.Apps;
namespace BTCPayServer.Components.MainNav namespace BTCPayServer.Components.MainNav
{ {
@@ -13,6 +12,7 @@ namespace BTCPayServer.Components.MainNav
public List<StoreApp> Apps { get; set; } public List<StoreApp> Apps { get; set; }
public CustodianAccountData[] CustodianAccounts { get; set; } public CustodianAccountData[] CustodianAccounts { get; set; }
public bool AltcoinsBuild { get; set; } public bool AltcoinsBuild { get; set; }
public int ArchivedAppsCount { get; set; }
} }
public class StoreApp public class StoreApp
@@ -20,6 +20,5 @@ namespace BTCPayServer.Components.MainNav
public string Id { get; set; } public string Id { get; set; }
public string AppName { get; set; } public string AppName { get; set; }
public string AppType { get; set; } public string AppType { get; set; }
public bool IsOwner { get; set; }
} }
} }

View File

@@ -1,31 +0,0 @@
@using BTCPayServer.Views.Notifications
@using BTCPayServer.Abstractions.Extensions
@model BTCPayServer.Components.Notifications.NotificationsViewModel
<div id="Notifications">
@if (Model.UnseenCount > 0)
{
<button id="NotificationsHandle" class="mainMenuButton @ViewData.IsActiveCategory(typeof(NotificationsNavPages))" title="Notifications" type="button" data-bs-toggle="dropdown">
<vc:icon symbol="notifications" />
<span class="badge rounded-pill bg-danger p-1 ms-1" id="NotificationsBadge">@Model.UnseenCount</span>
</button>
<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>
<form id="notificationsForm" asp-controller="UINotifications" asp-action="MarkAllAsSeen" asp-route-returnUrl="@Model.ReturnUrl" method="post">
<button class="btn btn-link p-0" type="submit">Mark all as seen</button>
</form>
</div>
<partial name="Components/Notifications/List" model="Model"/>
<div class="p-3">
<a asp-controller="UINotifications" asp-action="Index">View all</a>
</div>
</div>
}
else
{
<a asp-controller="UINotifications" asp-action="Index" id="NotificationsHandle" class="mainMenuButton @ViewData.IsActiveCategory(typeof(NotificationsNavPages))" title="Notifications">
<vc:icon symbol="notifications" />
</a>
}
</div>

View File

@@ -1,38 +0,0 @@
@using BTCPayServer.Abstractions.Extensions
@model BTCPayServer.Components.Notifications.NotificationsViewModel
@functions {
private static string NotificationIcon(string type)
{
return type switch
{
"invoice_expired" => "notifications-invoice-failure",
"invoice_expiredpaidpartial" => "notifications-invoice-failure",
"invoice_failedtoconfirm" => "notifications-invoice-failure",
"invoice_confirmed" => "notifications-invoice-settled",
"invoice_paidafterexpiration" => "notifications-invoice-settled",
"external-payout-transaction" => "notifications-payout",
"payout_awaitingapproval" => "notifications-payout",
"payout_awaitingpayment" => "notifications-payout-approved",
"newversion" => "notifications-new-version",
_ => "note"
};
}
}
<div id="NotificationsList">
@foreach (var n in Model.Last5)
{
<a asp-action="NotificationPassThrough" asp-controller="UINotifications" asp-route-id="@n.Id" class="notification d-flex align-items-center dropdown-item border-bottom border-light py-3 px-4">
<div class="me-3">
<vc:icon symbol="@NotificationIcon(n.Identifier)" />
</div>
<div class="notification-item__content">
<div class="text-start text-wrap">
@n.Body
</div>
<div class="text-start d-flex">
<small class="text-muted" data-timeago-unixms="@n.Created.ToUnixTimeMilliseconds()">@n.Created.ToTimeAgo()</small>
</div>
</div>
</a>
}
</div>

View File

@@ -1,27 +0,0 @@
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Notifications;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Components.Notifications
{
public class Notifications : ViewComponent
{
private readonly NotificationManager _notificationManager;
private static readonly string[] _views = { "List", "Dropdown", "Recent" };
public Notifications(NotificationManager notificationManager)
{
_notificationManager = notificationManager;
}
public async Task<IViewComponentResult> InvokeAsync(string appearance, string returnUrl)
{
var vm = await _notificationManager.GetSummaryNotifications(UserClaimsPrincipal);
vm.ReturnUrl = returnUrl;
var viewName = _views.Contains(appearance) ? appearance : _views[0];
return View(viewName, vm);
}
}
}

View File

@@ -1,12 +0,0 @@
using System.Collections.Generic;
using BTCPayServer.Abstractions.Contracts;
namespace BTCPayServer.Components.Notifications
{
public class NotificationsViewModel
{
public string ReturnUrl { get; set; }
public int UnseenCount { get; set; }
public List<NotificationViewModel> Last5 { get; set; }
}
}

View File

@@ -1,19 +0,0 @@
@model BTCPayServer.Components.Notifications.NotificationsViewModel
<div id="NotificationsRecent">
@if (Model.Last5.Any())
{
<div class="d-flex align-items-center justify-content-between mb-3">
<h4 class="mb-0">Recent Notifications</h4>
<a asp-controller="UINotifications" asp-action="Index">View all</a>
</div>
<partial name="Components/Notifications/List" model="Model"/>
}
else
{
<h4 class="mb-3">Notifications</h4>
<p class="text-secondary mt-3">
There are no recent unseen notifications.
</p>
}
</div>

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