Compare commits

..

170 Commits

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

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

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

* Add test

* Remove 2FA inclusion

* udate the btcpay network

* 2FA clean up

---------

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

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

Ported over from the mobile-working-branch.

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

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

* More timeframes

* Refactoring: Remove redundant WalletHistogram types

* Remove store property from dashboard tile view models

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

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

* Remove unused app item payment methods property

* Do not ignore nullable values in JSON

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

* Update Changelog.md

---------

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

Closes #6195.

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

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

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

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

* Footer

* Notifications

* Checkout Appearance

* Users list

* Forms

* Emails

* Pay Button

* Edit Dictionary

* Remove newlines, fix typos

* Forms

* Pull payments and payouts

* Various pages

* Use local docs link

* Fix

* Even more translations

* Fixes #6325

* Account pages

* Notifications

* Placeholders

* Various pages and components

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

* Clean up Pager

* Use current URL when paging

* Refactor
2024-10-25 22:23:03 +09:00
2849426092 Checkout: Allow breaking long item description texts 2024-10-25 13:11:26 +02:00
c4a2b4e975 Merge pull request #6327 from btcpayserver/bugfix/vaulticons
Properly cleaning up old feedback in vault feedback items
2024-10-24 15:33:44 -05:00
d508f5dc09 Properly cleaning up old feedback in vault feedback items 2024-10-22 21:50:22 -05:00
81ce8b0469 Fix flaky test 2024-10-23 00:17:02 +09:00
5a3a661e91 Revert "Revert "Fix flaky test""
This reverts commit bb5c6bd68d15bab091e3835c2d51687da8a55def.
2024-10-22 23:44:16 +09:00
bb5c6bd68d Revert "Fix flaky test"
This reverts commit 9dfabeab52644ba863230be39eca7496f4377365.
2024-10-22 23:41:41 +09:00
9dfabeab52 Fix flaky test 2024-10-22 23:36:12 +09:00
ad07330bf1 Update CLN support to 24.08.2 (#6323) 2024-10-21 00:25:50 +09:00
74011e50e3 Do not translate checkout with the backend language 2024-10-20 11:49:36 +09:00
3dfdbf544a Automated processor get disabled after some repeated failures (#6320) 2024-10-20 00:08:28 +09:00
4bf0b79c2a Simple rename 2024-10-19 22:07:20 +09:00
cc0ea0b3f8 Refactor payouts processing (#6314) 2024-10-19 21:33:34 +09:00
62d765125d Toggle color fix 2024-10-18 15:14:30 +02:00
b5b45d9a27 Rename Transaction->Translation 2024-10-18 16:06:51 +09:00
8e098710c1 Require non interactivity for boltcard payments (#6289) 2024-10-18 14:09:41 +09:00
6dfb369b55 UI: Inactive toggle hover color fix (#6299) 2024-10-18 14:08:34 +09:00
817522ff97 refactor(checkout): displayed payment methods vue component (#6316)
The displayed payment methods can change with updates aswell, so it
should be rendered as a vue component instead
2024-10-18 14:05:00 +09:00
8b5b90d247 refactor: make BeforeFetchingRates function public (#6310)
This function is useful when a payment method wants to update its
payment prompt after receiving a partial payment, like ln does
2024-10-18 14:03:37 +09:00
b670097592 feat: provide store info to modify-lnurlp-request filter (#6312)
adds store data to the filter using a new `StoreLNURLPayRequest` class
which simply adds a `Store` member.

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

* Dashboard

* Add more translations

* Moar

* Remove &nbsp; from translated texts

* Dictionary controller translations

* Batch 1 of controller updates

* Batch 2 of controller updates

* Component translations

* Batch 3 of controller updates

* Fixes
2024-10-17 22:51:40 +09:00
7e1712c8cd Merge pull request #6309 from NicolasDorier/fixmonero
Fix monero payments
2024-10-17 20:40:05 +09:00
b7affb1d34 Fix monero payments 2024-10-17 18:55:00 +09:00
d7fd90c4c3 Bump client lib 2024-10-17 16:41:36 +09:00
1d94782463 Merge pull request #6305 from NicolasDorier/fixelements
Fix elements payments
2024-10-16 22:34:32 +09:00
c7a05c3f09 Fix elements payments 2024-10-16 22:34:17 +09:00
2dc58a82b7 Item editor: Do not use only known props for diff (#6307)
Brooke the file seller plugin which adds a property and this did not detect the item change
2024-10-16 14:25:06 +02:00
755dbbab00 fix server nav ui extensin (#6306) 2024-10-16 14:24:56 +02:00
b470fe22f1 Remove potential NRE 2024-10-16 17:04:27 +09:00
5b2560ddf7 Merge pull request #6304 from NicolasDorier/cleanupetw
Cleanup useless code
2024-10-16 16:38:15 +09:00
be429c527c Cleanup useless code 2024-10-16 16:25:16 +09:00
65fd537200 Merge pull request #6303 from btcpayserver/feat/lnd-0.18.3
Bumping LND to 0.18.3-beta
2024-10-15 18:49:41 -05:00
6e43c7f06f Bumping LND to 0.18.3-beta 2024-10-15 18:21:26 -05:00
5867b5c000 Merge pull request #6297 from NicolasDorier/fioqnt
Pretty names of payment methods isn't provided by CheckoutExtensions
2024-10-15 23:28:42 +09:00
05887cf8b0 Fix potential stack overflow 2024-10-15 23:11:28 +09:00
c43721d489 Pretty names of payment methods isn't provided by CheckoutExtensions 2024-10-14 21:53:14 +09:00
0bf75d52d7 Merge pull request #6292 from NicolasDorier/addtranslations
Add translations to the Dashboard and more
2024-10-14 19:45:06 +09:00
c35af2dc69 Add translations to the Dashboard 2024-10-14 19:19:56 +09:00
73a9835a27 Fix build warning 2024-10-13 00:17:49 +09:00
87ab15f754 Fix crash on Monero/ZCash on invoices list 2024-10-13 00:10:49 +09:00
6bc608c081 Merge pull request #6287 from btcpayserver/feat/plugin-search
Support for searching plugins by name
2024-10-11 08:05:27 -05:00
cbea1d8691 Merge pull request #6291 from NicolasDorier/markedfordeletion
Improve UX for uninstalling disabled plugins
2024-10-11 21:41:01 +09:00
58f21a69aa Improve UX for uninstalling disabled plugins 2024-10-11 19:35:37 +09:00
426c5b9a24 Merge pull request #6290 from NicolasDorier/plugincrashdetect
Disable plugins crashing at startup
2024-10-11 10:56:46 +09:00
511e90efd1 Disable plugins crashing at startup 2024-10-11 10:50:49 +09:00
bc7b856654 Start sending BTCPay version string to help with filtering on plugin-builder side 2024-10-10 05:46:07 -05:00
d50d2f9ca0 Merge pull request #6288 from NicolasDorier/bettershowwarnings
Show warnings if NFC payment isn't complete
2024-10-10 19:23:42 +09:00
1b53defab3 Show warnings if NFC payment isn't complete 2024-10-10 19:16:09 +09:00
3e612921f3 Remove flaky test 2024-10-10 17:50:18 +09:00
ec51d43490 Improve error message if LNWithdraw fails 2024-10-10 17:24:19 +09:00
80dc5028f7 Fix migration crashes for instance having monero, zcash 2024-10-10 11:13:23 +09:00
2329c4a75f Support for searching plugins by name 2024-10-09 06:47:11 -05:00
ae76cc1ca2 Small UI improvements in the payout processors 2024-10-09 17:44:19 +09:00
e4f79f046a Remove unused field from automated payout settings 2024-10-09 13:13:10 +09:00
622d837ea1 Merge pull request #6286 from NicolasDorier/efwoiiqnf
Prevent double BOLT11 payment with LNUrlWithdraw
2024-10-09 13:10:17 +09:00
c0aa9a8bd4 Prevent double BOLT11 payment with LNUrlWithdraw 2024-10-09 13:10:04 +09:00
9b1052f023 Remove useless code 2024-10-08 21:25:37 +09:00
c77c2f8bd6 Merge pull request #6284 from btcpayserver/addfontawesome
Add fontawesome back
2024-10-08 21:19:43 +09:00
402eaa8f12 Add fontawesome back 2024-10-08 21:17:02 +09:00
212e8c3654 Fix potential crash in migration 2024-10-08 16:48:56 +09:00
7c77b16517 Fix potential crash on migration 2024-10-08 16:30:21 +09:00
e5bb0bcba3 Fix forgotten save 2024-10-08 16:25:11 +09:00
ca4a7d8771 Migrate excludedPaymentMethods from stores 2024-10-08 16:21:44 +09:00
663f97265a Merge pull request #6283 from NicolasDorier/activationbug
Fix: An unactivated payment method failing to activate would crash the checkout
2024-10-08 15:50:28 +09:00
b91f3048ef Fix: An unactivated payment method failing to activate would crash the checkout 2024-10-08 15:07:32 +09:00
4fe0bf1236 Merge pull request #6282 from NicolasDorier/refactorcheckout
Allow payment methods to modify all the payment model
2024-10-07 21:57:00 +09:00
dd35af3c55 Use AddUIExtension 2024-10-07 21:43:06 +09:00
68f24e47cd Rename more legacy fields 2024-10-07 21:22:03 +09:00
968223a953 Rename PaymentModel to CheckoutModel 2024-10-07 19:58:08 +09:00
2f287874e3 Rename legacy fields 2024-10-07 19:51:50 +09:00
c35e7406cd Cleanup AvailableCrypto from the model 2024-10-07 19:15:40 +09:00
34b2cca492 Simplify extension of payments extensions 2024-10-07 18:37:38 +09:00
e1bfc04451 Move checkout registration to the UI Extension 2024-10-07 17:38:02 +09:00
ef0ba7b0c4 Remove useless properties 2024-10-07 16:18:09 +09:00
0a2d8880ba Remove CheckoutBodyVueComponentName 2024-10-07 15:20:26 +09:00
8dcd7e6966 Remove no javascript for checkout 2024-10-07 15:18:41 +09:00
b744fd6167 Allow payment methods to modify all the payment model 2024-10-07 14:53:21 +09:00
5bcc5c919a Improve logging of rates in invoices (#6281) 2024-10-07 09:38:09 +09:00
01e12329e9 Remove additional cryptoCode from events (#6277) 2024-10-07 09:37:56 +09:00
471bf57835 Merge branch 'master' of github.com:btcpayserver/btcpayserver 2024-10-04 23:34:52 +09:00
b246beab3e Add the concept of RateDivisibility (#6278) 2024-10-04 23:34:31 +09:00
abc8161a08 Add the concept of RateDivisibility 2024-10-04 23:14:38 +09:00
64ba8248d2 Can inject currency data in CurrencyNameTable (#6276) 2024-10-04 22:24:44 +09:00
206d222455 Fix missing interpolation marker 2024-10-04 15:18:06 +02:00
2e114d7c29 Remove references to cryptoCode in SyncStatus (#6275) 2024-10-04 16:58:31 +09:00
5190c25be0 OnlyIfSupportAttribute should use PaymentMethodId (#6274) 2024-10-04 16:58:24 +09:00
5704919b3a BlockExplorer links should be using payment method ids (#6273) 2024-10-04 16:58:13 +09:00
c3e51f51b6 Fix warnings 2024-10-03 21:51:07 +09:00
8c35edb6e8 UI: Additional improvements to the User Invitation flow (#6233)
* UI: Additional improvements to the User Invitation flow

Closes #6224.

* Clear invitation token only after the user can sign in

Fixes "404 Error on Follow-Up Visits" of #6236.

* Minor spacing fix

* Update accordion button
2024-10-03 21:35:01 +09:00
2f2b4094f6 [UI] Do not show unabled payment methods in invoice creation (#6272) 2024-10-03 21:34:09 +09:00
413a9b4269 Add translation for store rate and wallet setup (#6271) 2024-10-03 19:21:19 +09:00
a698aa8a5b Do not crash if payment method disabled when store supports it 2024-10-03 19:21:01 +09:00
0f79526566 Do not make the test framework depends on CurrentDirectory 2024-10-03 16:04:16 +09:00
1ffbab7338 Small improvements to make development of plugins easier (#6270) 2024-10-03 15:16:21 +09:00
3a71c45a89 Add updated image upload support on Crowdfund plugin (#6254)
* Add updated image upload support on Crowdfund plugin

* Refactor crowdfund image upload fix

* update crowdfund url for greenfield api

* Resolve integration test assertion

* Remove superfluous and unused command argument

* Fix missing validation error

* Minor API controller update

* Property and usage fixes

* Fix test after merge

---------

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2024-10-03 10:39:41 +09:00
8f062f918b Add comments 2024-10-03 10:35:47 +09:00
2f05d00219 V2 compatibility: Re-add deprecated navigation methods (#6267)
Gives the new methods a new name and re-adds the old ones in order to not break plugins. Simple enough backwartds compatible change, makred the old methods as obsolete to make plugin developers aware that new methods are available.
2024-10-02 08:25:43 +09:00
b48ca92675 Fix app stats sorting (#6265) 2024-10-02 08:23:25 +09:00
4a31cf0a09 Migrate payment requests (#6260) 2024-10-01 16:07:51 +09:00
82620ee327 Move wallet payment settings back to store settings (#6251)
Intermediate solution, until we implement these settings on the payment method level. Closes #6237.
2024-09-30 19:13:51 +09:00
585 changed files with 17013 additions and 6034 deletions

1
.gitignore vendored
View File

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

View File

@ -2,6 +2,7 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using BTCPayServer.Abstractions.Models;
namespace BTCPayServer.Abstractions.Contracts;
@ -14,4 +15,5 @@ public interface IFileService
Task<string?> GetTemporaryFileUrl(Uri baseUri, string fileId, DateTimeOffset expiry,
bool isDownload);
Task RemoveFile(string fileId, string userId);
Task<UploadImageResultModel> UploadImage(IFormFile file, string userId, long maxFileSizeInBytes = 1_000_000);
}

View File

@ -12,7 +12,7 @@ namespace BTCPayServer.Abstractions.Contracts
public interface ISyncStatus
{
public string CryptoCode { get; set; }
public string PaymentMethodId { get; set; }
public bool Available { get; }
}
}

View File

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

View File

@ -55,7 +55,7 @@ namespace BTCPayServer.Abstractions.Extensions
viewData[ACTIVE_CATEGORY_KEY] = activeCategory;
}
public static bool IsActiveCategory(this ViewDataDictionary viewData, string category, object id = null)
public static bool IsCategoryActive(this ViewDataDictionary viewData, string category, object id = null)
{
if (!viewData.ContainsKey(ACTIVE_CATEGORY_KEY)) return false;
var activeId = viewData[ACTIVE_ID_KEY];
@ -65,12 +65,12 @@ namespace BTCPayServer.Abstractions.Extensions
return categoryMatch && idMatch;
}
public static bool IsActiveCategory<T>(this ViewDataDictionary viewData, T category, object id = null)
public static bool IsCategoryActive<T>(this ViewDataDictionary viewData, T category, object id = null)
{
return IsActiveCategory(viewData, category.ToString(), id);
return IsCategoryActive(viewData, category.ToString(), id);
}
public static bool IsActivePage(this ViewDataDictionary viewData, string page, string category, object id = null)
public static bool IsPageActive(this ViewDataDictionary viewData, string page, string category, object id = null)
{
if (!viewData.ContainsKey(ACTIVE_PAGE_KEY)) return false;
var activeId = viewData[ACTIVE_ID_KEY];
@ -82,7 +82,7 @@ namespace BTCPayServer.Abstractions.Extensions
return categoryAndPageMatch && idMatch;
}
public static bool IsActivePage<T>(this ViewDataDictionary viewData, IEnumerable<T> pages, object id = null)
public static bool IsPageActive<T>(this ViewDataDictionary viewData, IEnumerable<T> pages, object id = null)
where T : IConvertible
{
return pages.Any(page => ActivePageClass(viewData, page.ToString(), page.GetType().ToString(), id) == ACTIVE_CLASS);
@ -95,7 +95,7 @@ namespace BTCPayServer.Abstractions.Extensions
public static string ActiveCategoryClass(this ViewDataDictionary viewData, string category, object id = null)
{
return IsActiveCategory(viewData, category, id) ? ACTIVE_CLASS : null;
return IsCategoryActive(viewData, category, id) ? ACTIVE_CLASS : null;
}
public static string ActivePageClass<T>(this ViewDataDictionary viewData, T page, object id = null)
@ -106,12 +106,42 @@ namespace BTCPayServer.Abstractions.Extensions
public static string ActivePageClass(this ViewDataDictionary viewData, string page, string category, object id = null)
{
return IsActivePage(viewData, page, category, id) ? ACTIVE_CLASS : null;
return IsPageActive(viewData, page, category, id) ? ACTIVE_CLASS : null;
}
public static string ActivePageClass<T>(this ViewDataDictionary viewData, IEnumerable<T> pages, object id = null) where T : IConvertible
{
return IsActivePage(viewData, pages, id) ? ACTIVE_CLASS : null;
return IsPageActive(viewData, pages, id) ? ACTIVE_CLASS : null;
}
[Obsolete("Use ActiveCategoryClass instead")]
public static string IsActiveCategory<T>(this ViewDataDictionary viewData, T category, object id = null)
{
return ActiveCategoryClass(viewData, category, id);
}
[Obsolete("Use ActiveCategoryClass instead")]
public static string IsActiveCategory(this ViewDataDictionary viewData, string category, object id = null)
{
return ActiveCategoryClass(viewData, category, id);
}
[Obsolete("Use ActivePageClass instead")]
public static string IsActivePage<T>(this ViewDataDictionary viewData, T page, object id = null) where T : IConvertible
{
return ActivePageClass(viewData, page, id);
}
[Obsolete("Use ActivePageClass instead")]
public static string IsActivePage<T>(this ViewDataDictionary viewData, IEnumerable<T> pages, object id = null) where T : IConvertible
{
return ActivePageClass(viewData, pages, id);
}
[Obsolete("Use ActivePageClass instead")]
public static string IsActivePage(this ViewDataDictionary viewData, string page, string category, object id = null)
{
return ActivePageClass(viewData, page, category, id);
}
public static HtmlString ToBrowserDate(this DateTimeOffset date, string netFormat, string jsDateFormat = "short", string jsTimeFormat = "short")

View File

@ -14,14 +14,6 @@ namespace BTCPayServer.Abstractions.Models
public string SeverityCSS => ToString(Severity);
private void ParseNonJsonStatus(string s)
{
Message = s;
Severity = s.StartsWith("Error", StringComparison.InvariantCultureIgnoreCase)
? StatusSeverity.Error
: StatusSeverity.Success;
}
public static string ToString(StatusSeverity severity)
{
switch (severity)

View File

@ -0,0 +1,16 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Contracts;
namespace BTCPayServer.Abstractions.Models;
public class UploadImageResultModel
{
public bool Success { get; set; }
public string Response { get; set; } = string.Empty;
public IStoredFile? StoredFile { get; set; }
}

View File

@ -1,9 +1,11 @@
using System;
using BTCPayServer.Abstractions.Contracts;
namespace BTCPayServer.Abstractions.Services
{
public class UIExtension : IUIExtension
{
[Obsolete("Use extension method BTCPayServer.Extensions.AddUIExtension(this IServiceCollection services, string location, string partialViewName) instead")]
public UIExtension(string partial, string location)
{
Partial = partial;

View File

@ -16,7 +16,7 @@
<Platforms>AnyCPU</Platforms>
</PropertyGroup>
<PropertyGroup>
<Version Condition=" '$(Version)' == '' ">1.7.4</Version>
<Version Condition=" '$(Version)' == '' ">2.0.1</Version>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<PublishRepositoryUrl>true</PublishRepositoryUrl>
@ -31,7 +31,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.5.1" />
<PackageReference Include="NBitcoin" Version="7.0.37" />
<PackageReference Include="NBitcoin" Version="7.0.45" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>

View File

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

View File

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

View File

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

View File

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

View File

@ -19,14 +19,14 @@ public partial class BTCPayServerClient
await SendHttpRequest($"api/v1/stores/{storeId}/payout-processors/{processor}/{paymentMethod}", null, HttpMethod.Delete, token);
}
public virtual async Task<IEnumerable<LightningAutomatedPayoutSettings>> GetStoreLightningAutomatedPayoutProcessors(string storeId, string? paymentMethod = null, CancellationToken token = default)
public virtual async Task<IEnumerable<LightningAutomatedPayoutSettings>> GetStoreLightningAutomatedPayoutProcessors(string storeId, string? payoutMethodId = null, CancellationToken token = default)
{
return await SendHttpRequest<IEnumerable<LightningAutomatedPayoutSettings>>($"api/v1/stores/{storeId}/payout-processors/LightningAutomatedPayoutSenderFactory{(paymentMethod is null ? string.Empty : $"/{paymentMethod}")}", null, HttpMethod.Get, token);
return await SendHttpRequest<IEnumerable<LightningAutomatedPayoutSettings>>($"api/v1/stores/{storeId}/payout-processors/LightningAutomatedPayoutSenderFactory{(payoutMethodId is null ? string.Empty : $"/{payoutMethodId}")}", null, HttpMethod.Get, token);
}
public virtual async Task<LightningAutomatedPayoutSettings> UpdateStoreLightningAutomatedPayoutProcessors(string storeId, string paymentMethod, LightningAutomatedPayoutSettings request, CancellationToken token = default)
public virtual async Task<LightningAutomatedPayoutSettings> UpdateStoreLightningAutomatedPayoutProcessors(string storeId, string payoutMethodId, LightningAutomatedPayoutSettings request, CancellationToken token = default)
{
return await SendHttpRequest<LightningAutomatedPayoutSettings>($"api/v1/stores/{storeId}/payout-processors/LightningAutomatedPayoutSenderFactory/{paymentMethod}", request, HttpMethod.Put, token);
return await SendHttpRequest<LightningAutomatedPayoutSettings>($"api/v1/stores/{storeId}/payout-processors/LightningAutomatedPayoutSenderFactory/{payoutMethodId}", request, HttpMethod.Put, token);
}
public virtual async Task<OnChainAutomatedPayoutSettings> UpdateStoreOnChainAutomatedPayoutProcessors(string storeId, string paymentMethod, OnChainAutomatedPayoutSettings request, CancellationToken token = default)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,6 @@
#nullable enable
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client.Models;
@ -31,7 +29,7 @@ public abstract class PointOfSaleBaseData : AppBaseData
public class PointOfSaleAppData : PointOfSaleBaseData
{
public object? Items { get; set; }
public AppItem[]? Items { get; set; }
}
public class PointOfSaleAppRequest : PointOfSaleBaseData, IAppRequest

View File

@ -32,7 +32,7 @@ namespace BTCPayServer.Client.Models
public class SyncStatus
{
public string CryptoCode { get; set; }
public string PaymentMethodId { get; set; }
public virtual bool Available { get; set; }
}

View File

@ -4,10 +4,7 @@
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="NBXplorer.Client" Version="4.3.1" />
<PackageReference Include="NBXplorer.Client" Version="4.3.4" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="2.0.1" />
</ItemGroup>
<ItemGroup>
<Folder Include="Altcoins\" />
</ItemGroup>
</Project>

View File

@ -8,7 +8,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.7" />
<PackageReference Include="NBitcoin.Altcoins" Version="3.0.24" />
<PackageReference Include="NBitcoin.Altcoins" Version="3.0.31" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BTCPayServer.Abstractions\BTCPayServer.Abstractions.csproj" />

View File

@ -137,9 +137,10 @@ namespace BTCPayServer.Data
{
return paymentType switch
{
"BTCLike" => $"{cryptoCode}-CHAIN",
"BTCLike" or "MoneroLike" or "ZcashLike" => $"{cryptoCode}-CHAIN",
"LightningLike" or "LightningNetwork" => $"{cryptoCode}-LN",
"LNURLPAY" => $"{cryptoCode}-LNURL",
_ => throw new NotSupportedException("Unknown payment type " + paymentType)
};
}
@ -147,6 +148,23 @@ namespace BTCPayServer.Data
return $"{splitted[0]}-CHAIN";
throw new NotSupportedException("Unknown payment id " + paymentMethodId);
}
public static string TryMigratePaymentMethodId(string paymentMethodId)
{
var splitted = paymentMethodId.Split(new[] { '_', '-' });
if (splitted is [var cryptoCode, var paymentType])
{
return paymentType switch
{
"BTCLike" or "MoneroLike" or "ZcashLike" => $"{cryptoCode}-CHAIN",
"LightningLike" or "LightningNetwork" => $"{cryptoCode}-LN",
"LNURLPAY" => $"{cryptoCode}-LNURL",
_ => paymentMethodId
};
}
if (splitted.Length == 1)
return $"{splitted[0]}-CHAIN";
return paymentMethodId;
}
// Make postgres happy
public static string SanitizeJSON(string json) => json.Replace("\\u0000", string.Empty, StringComparison.OrdinalIgnoreCase);

View File

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

View File

@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Reflection.Metadata;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Data
{
public partial class PaymentRequestData : MigrationInterceptor.IHasMigration
{
[NotMapped]
public bool Migrated { get; set; }
public bool TryMigrate()
{
#pragma warning disable CS0618 // Type or member is obsolete
if (Blob is null && Blob2 is not null)
return false;
if (Blob2 is null)
{
Blob2 = Blob is not (null or { Length: 0 }) ? MigrationExtensions.Unzip(Blob) : "{}";
Blob2 = MigrationExtensions.SanitizeJSON(Blob2);
}
Blob = null;
#pragma warning restore CS0618 // Type or member is obsolete
var jobj = JObject.Parse(Blob2);
// Fixup some legacy payment requests
if (jobj["expiryDate"].Type == JTokenType.Date)
{
jobj["expiryDate"] = new JValue(NBitcoin.Utils.DateTimeToUnixTime(jobj["expiryDate"].Value<DateTime>()));
Blob2 = jobj.ToString(Newtonsoft.Json.Formatting.None);
}
return true;
}
}
}

View File

@ -4,7 +4,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
namespace BTCPayServer.Data
{
public class PaymentRequestData : IHasBlobUntyped
public partial class PaymentRequestData : IHasBlobUntyped
{
public string Id { get; set; }
public DateTimeOffset Created { get; set; }

View File

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

View File

@ -560,7 +560,7 @@ namespace BTCPayServer.Migrations
.HasColumnType("numeric");
b.Property<string>("Blob")
.HasColumnType("JSONB");
.HasColumnType("jsonb");
b.Property<string>("Currency")
.HasColumnType("text");

View File

@ -6,7 +6,7 @@
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.10.0" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="6.0.0" />
<PackageReference Include="NBitcoin" Version="7.0.37" />
<PackageReference Include="NBitcoin" Version="7.0.45" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="1.0.4" />
</ItemGroup>

View File

@ -1175,13 +1175,6 @@
"symbol":null,
"crypto":true
},
{
"name":"USDt",
"code":"USDT",
"divisibility":8,
"symbol":null,
"crypto":true
},
{
"name":"LCAD",
"code":"LCAD",
@ -1315,13 +1308,6 @@
"symbol": null,
"crypto": true
},
{
"name":"USDt",
"code":"USDT20",
"divisibility":6,
"symbol":null,
"crypto":true
},
{
"name":"FaucetToken",
"code":"FAU",

View File

@ -5,6 +5,9 @@ using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using NBitcoin;
using Newtonsoft.Json;
@ -18,14 +21,74 @@ namespace BTCPayServer.Services.Rates
public string Symbol { get; set; }
public bool Crypto { get; set; }
}
public class CurrencyNameTable
public interface CurrencyDataProvider
{
public static CurrencyNameTable Instance = new();
public CurrencyNameTable()
Task<CurrencyData[]> LoadCurrencyData(CancellationToken cancellationToken);
}
public class InMemoryCurrencyDataProvider : CurrencyDataProvider
{
private readonly CurrencyData[] _currencyData;
public InMemoryCurrencyDataProvider(CurrencyData[] currencyData)
{
_Currencies = LoadCurrency().ToDictionary(k => k.Code, StringComparer.InvariantCultureIgnoreCase);
_currencyData = currencyData;
}
public Task<CurrencyData[]> LoadCurrencyData(CancellationToken cancellationToken) => Task.FromResult(_currencyData);
}
public class AssemblyCurrencyDataProvider : CurrencyDataProvider
{
private readonly Assembly _assembly;
private readonly string _manifestResourceStream;
public AssemblyCurrencyDataProvider(Assembly assembly, string manifestResourceStream)
{
_assembly = assembly;
_manifestResourceStream = manifestResourceStream;
}
public Task<CurrencyData[]> LoadCurrencyData(CancellationToken cancellationToken)
{
var stream = _assembly.GetManifestResourceStream(_manifestResourceStream);
if (stream is null)
throw new InvalidOperationException("Unknown manifestResourceStream");
string content = null;
using (var reader = new StreamReader(stream, Encoding.UTF8))
{
content = reader.ReadToEnd();
}
var currencies = JsonConvert.DeserializeObject<CurrencyData[]>(content);
return Task.FromResult(currencies.ToArray());
}
}
public class CurrencyNameTable
{
public CurrencyNameTable(IEnumerable<CurrencyDataProvider> currencyDataProviders, ILogger<CurrencyNameTable> logger)
{
_currencyDataProviders = currencyDataProviders;
_logger = logger;
}
public async Task ReloadCurrencyData(CancellationToken cancellationToken)
{
var currencies = new Dictionary<string, CurrencyData>(StringComparer.InvariantCultureIgnoreCase);
var loadings = _currencyDataProviders.Select(c => (Task: c.LoadCurrencyData(cancellationToken), Prov: c)).ToList();
foreach (var loading in loadings)
{
try
{
foreach (var curr in await loading.Task)
{
currencies.TryAdd(curr.Code, curr);
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error loading currency data for " + loading.Prov.GetType().FullName);
}
}
_Currencies = currencies;
}
static readonly Dictionary<string, IFormatProvider> _CurrencyProviders = new();
public NumberFormatInfo GetNumberFormatInfo(string currency, bool useFallback)
@ -123,20 +186,9 @@ namespace BTCPayServer.Services.Rates
currencyProviders.TryAdd(code, number);
}
readonly Dictionary<string, CurrencyData> _Currencies;
static CurrencyData[] LoadCurrency()
{
var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("BTCPayServer.Rating.Currencies.json");
string content = null;
using (var reader = new StreamReader(stream, Encoding.UTF8))
{
content = reader.ReadToEnd();
}
var currencies = JsonConvert.DeserializeObject<CurrencyData[]>(content);
return currencies;
}
Dictionary<string, CurrencyData> _Currencies = new();
private readonly IEnumerable<CurrencyDataProvider> _currencyDataProviders;
private readonly ILogger<CurrencyNameTable> _logger;
public IEnumerable<CurrencyData> Currencies => _Currencies.Values;

View File

@ -1,11 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using BTCPayServer.Services.Rates;
namespace BTCPayServer.Rating
{
public class CurrencyPair
{
private static readonly HashSet<string> _knownCurrencies;
static CurrencyPair()
{
var prov = new AssemblyCurrencyDataProvider(typeof(BTCPayServer.Rating.BidAsk).Assembly, "BTCPayServer.Rating.Currencies.json");
// It's OK this is sync function
_knownCurrencies = prov.LoadCurrencyData(default).GetAwaiter().GetResult()
.Select(c => c.Code).ToHashSet(StringComparer.OrdinalIgnoreCase);
}
public CurrencyPair(string left, string right)
{
ArgumentNullException.ThrowIfNull(right);
@ -49,10 +60,9 @@ namespace BTCPayServer.Rating
for (int i = 3; i < 5; i++)
{
var potentialCryptoName = currencyPair.Substring(0, i);
var currency = CurrencyNameTable.Instance.GetCurrencyData(potentialCryptoName, false);
if (currency != null)
if (_knownCurrencies.Contains(potentialCryptoName))
{
value = new CurrencyPair(currency.Code, currencyPair.Substring(i));
value = new CurrencyPair(potentialCryptoName, currencyPair.Substring(i));
return true;
}
}

View File

@ -3,10 +3,9 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.HostedServices;
using BTCPayServer.Hosting;
using BTCPayServer.Lightning;
using BTCPayServer.Models.AppViewModels;
@ -25,7 +24,7 @@ using Newtonsoft.Json.Linq;
using OpenQA.Selenium;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;
using PosViewType = BTCPayServer.Plugins.PointOfSale.PosViewType;
using WalletSettingsViewModel = BTCPayServer.Models.StoreViewModels.WalletSettingsViewModel;
namespace BTCPayServer.Tests
@ -320,10 +319,10 @@ namespace BTCPayServer.Tests
var controller = tester.PayTester.GetController<UIInvoiceController>(null);
var checkout =
(Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id)
(Models.InvoicingModels.CheckoutModel)((JsonResult)controller.GetStatus(invoice.Id)
.GetAwaiter().GetResult()).Value;
Assert.Single(checkout.AvailableCryptos);
Assert.Equal("LTC", checkout.CryptoCode);
Assert.Single(checkout.AvailablePaymentMethods);
Assert.Equal("LTC", checkout.PaymentMethodCurrency);
//////////////////////
@ -337,7 +336,7 @@ namespace BTCPayServer.Tests
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal("paid", invoice.Status);
checkout = (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id)
checkout = (Models.InvoicingModels.CheckoutModel)((JsonResult)controller.GetStatus(invoice.Id)
.GetAwaiter().GetResult()).Value;
Assert.Equal("Processing", checkout.Status);
});
@ -475,10 +474,10 @@ namespace BTCPayServer.Tests
var controller = tester.PayTester.GetController<UIInvoiceController>(null);
var checkout =
(Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id, null)
(Models.InvoicingModels.CheckoutModel)((JsonResult)controller.GetStatus(invoice.Id, null)
.GetAwaiter().GetResult()).Value;
Assert.Single(checkout.AvailableCryptos);
Assert.Equal("BTC", checkout.CryptoCode);
Assert.Single(checkout.AvailablePaymentMethods);
Assert.Equal("BTC", checkout.PaymentMethodCurrency);
Assert.Single(invoice.PaymentCodes);
Assert.Single(invoice.SupportedTransactionCurrencies);
@ -536,10 +535,10 @@ namespace BTCPayServer.Tests
});
controller = tester.PayTester.GetController<UIInvoiceController>(null);
checkout = (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id, "LTC")
checkout = (Models.InvoicingModels.CheckoutModel)((JsonResult)controller.GetStatus(invoice.Id, "LTC")
.GetAwaiter().GetResult()).Value;
Assert.Equal(2, checkout.AvailableCryptos.Count);
Assert.Equal("LTC", checkout.CryptoCode);
Assert.Equal(2, checkout.AvailablePaymentMethods.Count);
Assert.Equal("LTC", checkout.PaymentMethodCurrency);
Assert.Equal(2, invoice.PaymentCodes.Count());
Assert.Equal(2, invoice.SupportedTransactionCurrencies.Count());
@ -759,39 +758,6 @@ noninventoryitem:
AppService.Parse(vmpos.Template).Single(item => item.Id == "inventoryitem").Inventory);
}, 10000);
//test payment methods option
vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
vmpos.Title = "hello";
vmpos.Currency = "BTC";
vmpos.Template = @"
btconly:
price: 1.0
title: good apple
payment_methods:
- BTC
normal:
price: 1.0";
vmpos.Template = AppService.SerializeTemplate(MigrationStartupTask.ParsePOSYML(vmpos.Template));
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, choiceKey: "btconly").Result);
Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, choiceKey: "normal").Result);
invoices = user.BitPay.GetInvoices();
var normalInvoice = invoices.Single(invoice => invoice.ItemCode == "normal");
var btcOnlyInvoice = invoices.Single(invoice => invoice.ItemCode == "btconly");
Assert.Single(btcOnlyInvoice.CryptoInfo);
Assert.Equal("BTC",
btcOnlyInvoice.CryptoInfo.First().CryptoCode);
Assert.Equal("BTC-CHAIN",
btcOnlyInvoice.CryptoInfo.First().PaymentType);
Assert.Equal(2, normalInvoice.CryptoInfo.Length);
Assert.Contains(
normalInvoice.CryptoInfo,
s => "BTC-CHAIN" == s.PaymentType && new[] { "BTC", "LTC" }.Contains(
s.CryptoCode));
//test topup option
vmpos.Template = @"
a:
@ -821,13 +787,13 @@ g:
vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
Assert.DoesNotContain("custom", vmpos.Template);
var items = AppService.Parse(vmpos.Template);
Assert.Contains(items, item => item.Id == "a" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Fixed);
Assert.Contains(items, item => item.Id == "b" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Fixed);
Assert.Contains(items, item => item.Id == "c" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Minimum);
Assert.Contains(items, item => item.Id == "d" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Fixed);
Assert.Contains(items, item => item.Id == "e" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Minimum);
Assert.Contains(items, item => item.Id == "f" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup);
Assert.Contains(items, item => item.Id == "g" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup);
Assert.Contains(items, item => item.Id == "a" && item.PriceType == AppItemPriceType.Fixed);
Assert.Contains(items, item => item.Id == "b" && item.PriceType == AppItemPriceType.Fixed);
Assert.Contains(items, item => item.Id == "c" && item.PriceType == AppItemPriceType.Minimum);
Assert.Contains(items, item => item.Id == "d" && item.PriceType == AppItemPriceType.Fixed);
Assert.Contains(items, item => item.Id == "e" && item.PriceType == AppItemPriceType.Minimum);
Assert.Contains(items, item => item.Id == "f" && item.PriceType == AppItemPriceType.Topup);
Assert.Contains(items, item => item.Id == "g" && item.PriceType == AppItemPriceType.Topup);
Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(app.Id, PosViewType.Static, choiceKey: "g").Result);

View File

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

View File

@ -7,6 +7,15 @@
<!--https://devblogs.microsoft.com/aspnet/testing-asp-net-core-mvc-web-apps-in-memory/-->
<PreserveCompilationContext>true</PreserveCompilationContext>
</PropertyGroup>
<ItemGroup>
<AssemblyAttribute Include="BTCPayServer.Tests.OutputPathAttribute">
<!-- _Parameter1, _Parameter2, etc. correspond to the
matching parameter of a constructor of that .NET attribute type -->
<_Parameter1>$([System.IO.Path]::Combine('$(MSBuildProjectDirectory)', '$(OutputPath)'))</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
<!--https://devblogs.microsoft.com/aspnet/testing-asp-net-core-mvc-web-apps-in-memory/-->
<Target Name="CopyAditionalFiles" AfterTargets="Build" Condition="'$(TargetFramework)'!=''">
<ItemGroup>
@ -61,4 +70,7 @@
<ProjectReference Include="..\BTCPayServer\BTCPayServer.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="obj\Debug\net8.0\" />
</ItemGroup>
</Project>

View File

@ -162,6 +162,8 @@ namespace BTCPayServer.Tests
HttpClient.BaseAddress = ServerUri;
Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development");
var confBuilder = new DefaultConfiguration() { Logger = LoggerProvider.CreateLogger("Console") }.CreateConfigurationBuilder(new[] { "--datadir", _Directory, "--conf", confPath, "--disable-registration", DisableRegistration ? "true" : "false" });
// This make sure that tests work outside of this assembly (ie, test project it a plugin)
confBuilder.SetBasePath(TestUtils.TestDirectory);
#if DEBUG
confBuilder.AddJsonFile("appsettings.dev.json", true, false);
#endif
@ -265,7 +267,7 @@ namespace BTCPayServer.Tests
private string FindBTCPayServerDirectory()
{
var solutionDirectory = TestUtils.TryGetSolutionDirectoryInfo(Directory.GetCurrentDirectory());
var solutionDirectory = TestUtils.TryGetSolutionDirectoryInfo();
return Path.Combine(solutionDirectory.FullName, "BTCPayServer");
}

View File

@ -81,7 +81,7 @@ namespace BTCPayServer.Tests
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<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel).Result);
Assert.IsType<ViewResult>(await crowdfund.ViewCrowdfund(app.Id));
// Delete
Assert.IsType<NotFoundResult>(apps2.DeleteApp(app.Id));
@ -121,7 +121,7 @@ namespace BTCPayServer.Tests
crowdfundViewModel.Enabled = false;
crowdfundViewModel.EndDate = null;
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel).Result);
var anonAppPubsController = tester.PayTester.GetController<UICrowdfundController>();
var crowdfundController = user.GetController<UICrowdfundController>();
@ -146,7 +146,7 @@ namespace BTCPayServer.Tests
crowdfundViewModel.StartDate = DateTime.Today.AddDays(2);
crowdfundViewModel.Enabled = true;
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel).Result);
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(app.Id, new ContributeToCrowdfund()
{
Amount = new decimal(0.01)
@ -157,7 +157,7 @@ namespace BTCPayServer.Tests
crowdfundViewModel.EndDate = DateTime.Today.AddDays(-1);
crowdfundViewModel.Enabled = true;
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel).Result);
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(app.Id, new ContributeToCrowdfund()
{
Amount = new decimal(0.01)
@ -170,7 +170,7 @@ namespace BTCPayServer.Tests
crowdfundViewModel.TargetAmount = 1;
crowdfundViewModel.TargetCurrency = "BTC";
crowdfundViewModel.EnforceTargetAmount = true;
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel).Result);
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(app.Id, new ContributeToCrowdfund()
{
Amount = new decimal(1.01)
@ -214,7 +214,7 @@ namespace BTCPayServer.Tests
crowdfundViewModel.TargetCurrency = "BTC";
crowdfundViewModel.UseAllStoreInvoices = true;
crowdfundViewModel.EnforceTargetAmount = true;
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel).Result);
var publicApps = user.GetController<UICrowdfundController>();
@ -268,7 +268,7 @@ namespace BTCPayServer.Tests
Assert.Contains(AppService.GetAppInternalTag(app.Id), invoiceEntity.InternalTags);
crowdfundViewModel.UseAllStoreInvoices = false;
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel).Result);
TestLogs.LogInformation("Because UseAllStoreInvoices is false, let's make sure the invoice is not tagged");
invoice = await user.BitPay.CreateInvoiceAsync(new Invoice
@ -287,7 +287,7 @@ namespace BTCPayServer.Tests
TestLogs.LogInformation("After turning setting a softcap, let's check that only actual payments are counted");
crowdfundViewModel.EnforceTargetAmount = false;
crowdfundViewModel.UseAllStoreInvoices = true;
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel).Result);
invoice = await user.BitPay.CreateInvoiceAsync(new Invoice
{
Buyer = new Buyer { email = "test@fwf.com" },
@ -356,7 +356,7 @@ namespace BTCPayServer.Tests
crowdfundViewModel.FormId = lstForms[0].Id;
crowdfundViewModel.TargetCurrency = "BTC";
crowdfundViewModel.Enabled = true;
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel).Result);
var vm2 = await crowdfund.CrowdfundForm(app.Id, (decimal?)0.01).AssertViewModelAsync<FormViewModel>();
var res = await crowdfund.CrowdfundFormSubmit(app.Id, (decimal)0.01, "", vm2);
@ -411,7 +411,7 @@ namespace BTCPayServer.Tests
crowdfundViewModel.TargetCurrency = "BTC";
crowdfundViewModel.Enabled = true;
crowdfundViewModel.PerksTemplate = "[{\"id\": \"xxx\",\"title\": \"Perk 1\",\"priceType\": \"Fixed\",\"price\": \"0.001\",\"image\": \"\",\"description\": \"\",\"categories\": [],\"disabled\": false}]";
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel).Result);
var vm2 = await crowdfund.CrowdfundForm(app.Id, (decimal?)0.01, "xxx").AssertViewModelAsync<FormViewModel>();
var res = await crowdfund.CrowdfundFormSubmit(app.Id, (decimal)0.01, "xxx", vm2);

View File

@ -40,6 +40,7 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NBitcoin;
using NBitcoin.DataEncoders;
@ -678,10 +679,29 @@ namespace BTCPayServer.Tests
Assert.Equal(utxo54, utxos[53]);
}
[Fact]
public void ResourceTrackerTest()
{
var tracker = new ResourceTracker<string>();
var t1 = tracker.StartTracking();
Assert.True(t1.TryTrack("1"));
Assert.False(t1.TryTrack("1"));
var t2 = tracker.StartTracking();
Assert.True(t2.TryTrack("2"));
Assert.False(t2.TryTrack("1"));
Assert.True(t1.Contains("1"));
Assert.True(t2.Contains("2"));
Assert.True(tracker.Contains("1"));
Assert.True(tracker.Contains("2"));
t1.Dispose();
Assert.False(tracker.Contains("1"));
Assert.True(tracker.Contains("2"));
Assert.True(t2.TryTrack("1"));
}
[Fact]
public void CanAcceptInvoiceWithTolerance()
{
var networkProvider = CreateNetworkProvider(ChainName.Regtest);
var entity = new InvoiceEntity() { Currency = "USD" };
#pragma warning disable CS0618
entity.Payments = new List<PaymentEntity>();
@ -738,10 +758,29 @@ namespace BTCPayServer.Tests
Assert.True(FileTypeDetector.IsAudio(new byte[] { 0xFF, 0xF3, 0xE4, 0x64, 0x00, 0x20, 0xAD, 0xBD, 0x04, 0x00 }, "music.mp3"));
}
CurrencyNameTable GetCurrencyNameTable()
{
ServiceCollection services = new ServiceCollection();
services.AddLogging(o => o.AddProvider(this.TestLogProvider));
BTCPayServerServices.RegisterCurrencyData(services);
// One test fail without.
services.AddCurrencyData(new CurrencyData()
{
Code = "USDt",
Name = "USDt",
Divisibility = 8,
Symbol = null,
Crypto = true
});
var table = services.BuildServiceProvider().GetRequiredService<CurrencyNameTable>();
table.ReloadCurrencyData(default).GetAwaiter().GetResult();
return table;
}
[Fact]
public void RoundupCurrenciesCorrectly()
{
DisplayFormatter displayFormatter = new(CurrencyNameTable.Instance);
DisplayFormatter displayFormatter = new(GetCurrencyNameTable());
foreach (var test in new[]
{
(0.0005m, "0.0005 USD", "USD"), (0.001m, "0.001 USD", "USD"), (0.01m, "0.01 USD", "USD"),
@ -754,8 +793,8 @@ namespace BTCPayServer.Tests
actual = actual.Replace("¥", "¥"); // Hack so JPY test pass on linux as well
Assert.Equal(test.Item2, actual);
}
Assert.Equal(0, CurrencyNameTable.Instance.GetNumberFormatInfo("ARS").CurrencyDecimalDigits);
Assert.Equal(0, CurrencyNameTable.Instance.GetNumberFormatInfo("COP").CurrencyDecimalDigits);
Assert.Equal(0, GetCurrencyNameTable().GetNumberFormatInfo("ARS").CurrencyDecimalDigits);
Assert.Equal(0, GetCurrencyNameTable().GetNumberFormatInfo("COP").CurrencyDecimalDigits);
}
[Fact]
@ -829,6 +868,13 @@ namespace BTCPayServer.Tests
Assert.IsType<MultisigDerivationStrategy>(((P2WSHDerivationStrategy)strategyBase).Inner);
Assert.Equal(expected, strategyBase.ToString());
foreach (var space in new[] { "\r\n", " ", "\t" })
{
var expectedWithNewLines = $"2-of-tpubDDXgATYzdQkHHhZZCMcNJj8BGDENvzMVou5v9NdxiP4rxDLj33nS233dGFW4htpVZSJ6zds9eVqAV9RyRHHiKtwQKX8eR4n4KN3Dwmj7A3h-{space}tpubDC8a54NFtQtMQAZ97VhoU9V6jVTvi9w4Y5SaAXJSBYETKg3AoX5CCKndznhPWxJUBToPCpT44s86QbKdGpKAnSjcMTGW4kE6UQ8vpBjcybW-tpubDChjnP9LXNrJp43biqjY7FH93wgRRNrNxB4Q8pH7PPRy8UPcH2S6V46WGVJ47zVGF7SyBJNCpnaogsFbsybVQckGtVhCkng3EtFn8qmxptS";
strategyBase = parser.Parse(expectedWithNewLines);
Assert.Equal(expected, strategyBase.ToString());
}
var inner = (MultisigDerivationStrategy)((P2WSHDerivationStrategy)strategyBase).Inner;
Assert.False(inner.IsLegacy);
Assert.Equal(3, inner.Keys.Count);
@ -845,7 +891,7 @@ namespace BTCPayServer.Tests
Assert.True(((DirectDerivationStrategy)strategyBase).Segwit);
// Failure cases
Assert.Throws<FormatException>(() => { parser.Parse("xpub 661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw"); }); // invalid format because of space
Assert.Throws<FormatException>(() => { parser.Parse("xpubZ661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw"); });
Assert.Throws<ParsingException>(() => { parser.ParseOutputDescriptor("invalid"); }); // invalid in general
Assert.Throws<ParsingException>(() => { parser.ParseOutputDescriptor("wpkh([8b60afd1/49h/0h/0h]xpub661MyMwAFXkMnyoBjyHndD3QwRbcGVBsTGeNZN6QGVHcfz4MPzBUxjSevweNFQx7SqmMHLdSA4FteGsRrEriu4pnVZMZWnruFFAYZATtcDw/0/*)#9x4vkw48"); }); // invalid checksum
}
@ -1377,7 +1423,7 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
var btcPayNetworkProvider = CreateNetworkProvider(ChainName.Regtest);
foreach (var network in btcPayNetworkProvider.GetAll())
{
var cd = CurrencyNameTable.Instance.GetCurrencyData(network.CryptoCode, false);
var cd = GetCurrencyNameTable().GetCurrencyData(network.CryptoCode, false);
Assert.NotNull(cd);
Assert.Equal(network.Divisibility, cd.Divisibility);
Assert.True(cd.Crypto);
@ -1445,8 +1491,8 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
Assert.True(CurrencyValue.TryParse("1usd", out result));
Assert.Equal("1 USD", result.ToString());
Assert.True(CurrencyValue.TryParse("1.501 usd", out result));
Assert.Equal("1.50 USD", result.ToString());
Assert.False(CurrencyValue.TryParse("1.501 WTFF", out result));
Assert.Equal("1.501 USD", result.ToString());
Assert.True(CurrencyValue.TryParse("1.501 WTFF", out result));
Assert.False(CurrencyValue.TryParse("1,501 usd", out result));
Assert.False(CurrencyValue.TryParse("1.501", out result));
}

View File

@ -17,6 +17,7 @@ using BTCPayServer.NTag424;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.PayoutProcessors;
using BTCPayServer.PayoutProcessors.Lightning;
using BTCPayServer.Plugins.PointOfSale.Controllers;
using BTCPayServer.Plugins.PointOfSale.Models;
using BTCPayServer.Services;
@ -317,6 +318,20 @@ namespace BTCPayServer.Tests
Assert.Empty(await client.GetFiles());
storeData = await client.GetStore(store.Id);
Assert.Null(storeData.LogoUrl);
// App Item Image
var app = await client.CreatePointOfSaleApp(store.Id, new PointOfSaleAppRequest { AppName = "Test App" });
await AssertValidationError(["file"],
async () => await client.UploadAppItemImage(app.Id, filePath, "text/csv")
);
var fileData = await client.UploadAppItemImage(app.Id, logoPath, "image/png");
Assert.Equal("logo.png", fileData.OriginalName);
files = await client.GetFiles();
Assert.Single(files);
await client.DeleteAppItemImage(app.Id, fileData.Id);
Assert.Empty(await client.GetFiles());
}
[Fact(Timeout = TestTimeout)]
@ -744,9 +759,9 @@ namespace BTCPayServer.Tests
await user.RegisterDerivationSchemeAsync("BTC");
var client = await user.CreateClient();
var item1 = new ViewPointOfSaleViewModel.Item { Id = "item1", Title = "Item 1", Price = 1, PriceType = ViewPointOfSaleViewModel.ItemPriceType.Fixed };
var item2 = new ViewPointOfSaleViewModel.Item { Id = "item2", Title = "Item 2", Price = 2, PriceType = ViewPointOfSaleViewModel.ItemPriceType.Fixed };
var item3 = new ViewPointOfSaleViewModel.Item { Id = "item3", Title = "Item 3", Price = 3, PriceType = ViewPointOfSaleViewModel.ItemPriceType.Fixed };
var item1 = new AppItem { Id = "item1", Title = "Item 1", Price = 1, PriceType = AppItemPriceType.Fixed };
var item2 = new AppItem { Id = "item2", Title = "Item 2", Price = 2, PriceType = AppItemPriceType.Fixed };
var item3 = new AppItem { Id = "item3", Title = "Item 3", Price = 3, PriceType = AppItemPriceType.Fixed };
var posItems = AppService.SerializeTemplate([item1, item2, item3]);
var posApp = await client.CreatePointOfSaleApp(user.StoreId, new PointOfSaleAppRequest { AppName = "test pos", Template = posItems, });
var crowdfundApp = await client.CreateCrowdfundApp(user.StoreId, new CrowdfundAppRequest { AppName = "test crowdfund" });
@ -1399,10 +1414,17 @@ namespace BTCPayServer.Tests
Assert.Equal(0, card.Version);
var card1keys = new[] { card.K0, card.K1, card.K2, card.K3, card.K4 };
Assert.DoesNotContain(null, card1keys);
var card2 = await client.RegisterBoltcard(test4.Id, new RegisterBoltcardRequest()
{
UID = uid
});
Assert.Equal(0, card2.Version);
card2 = await client.RegisterBoltcard(test4.Id, new RegisterBoltcardRequest()
{
UID = uid,
OnExisting = OnExistingBehavior.UpdateVersion
});
Assert.Equal(1, card2.Version);
Assert.StartsWith("lnurlw://", card2.LNURLW);
Assert.EndsWith("/boltcard", card2.LNURLW);
@ -1979,7 +2001,7 @@ namespace BTCPayServer.Tests
Assert.Contains("BTC-CHAIN", serverInfoData.SupportedPaymentMethods);
Assert.Contains("BTC-LN", serverInfoData.SupportedPaymentMethods);
Assert.NotNull(serverInfoData.SyncStatus);
Assert.Single(serverInfoData.SyncStatus.Select(s => s.CryptoCode == "BTC"));
Assert.Single(serverInfoData.SyncStatus.Select(s => s.PaymentMethodId == "BTC-CHAIN"));
}
[Fact(Timeout = TestTimeout)]
@ -2733,7 +2755,7 @@ namespace BTCPayServer.Tests
Assert.EndsWith($"/i/{newInvoice.Id}", newInvoice.CheckoutLink);
var controller = tester.PayTester.GetController<UIInvoiceController>(user.UserId, user.StoreId);
var model = (PaymentModel)((ViewResult)await controller.Checkout(newInvoice.Id)).Model;
var model = (CheckoutModel)((ViewResult)await controller.Checkout(newInvoice.Id)).Model;
Assert.Equal("it-IT", model.DefaultLang);
Assert.Equal("http://toto.com/lol", model.MerchantRefLink);
@ -2980,9 +3002,10 @@ namespace BTCPayServer.Tests
// check list for store with paid invoice
var merchantInvoices = await merchantClient.GetLightningInvoices(merchant.StoreId, "BTC");
merchantPendingInvoices = await merchantClient.GetLightningInvoices(merchant.StoreId, "BTC", true);
Assert.NotEmpty(merchantInvoices);
Assert.Empty(merchantPendingInvoices);
merchantPendingInvoices = await merchantClient.GetLightningInvoices(merchant.StoreId, "BTC", true);
Assert.True(merchantPendingInvoices.Length < merchantInvoices.Length);
Assert.All(merchantPendingInvoices, m => Assert.Equal(LightningInvoiceStatus.Unpaid, m.Status));
// if the test ran too many times the invoice might be on a later page
if (merchantInvoices.Length < 100)
Assert.Contains(merchantInvoices, i => i.Id == merchantInvoice.Id);
@ -2999,6 +3022,17 @@ namespace BTCPayServer.Tests
var info = await client.GetLightningNodeInfo(user.StoreId, "BTC");
Assert.Single(info.NodeURIs);
Assert.NotEqual(0, info.BlockHeight);
// balance
await TestUtils.EventuallyAsync(async () =>
{
var balance = await client.GetLightningNodeBalance(user.StoreId, "BTC");
var histogram = await client.GetLightningNodeHistogram(user.StoreId, "BTC");
var localBalance = balance.OffchainBalance.Local.ToDecimal(LightMoneyUnit.BTC);
Assert.Equal(histogram.Balance, histogram.Series.Last());
Assert.Equal(localBalance, histogram.Balance);
Assert.Equal(localBalance, histogram.Series.Last());
});
// As admin, can use the internal node through our store.
await user.MakeAdmin(true);
@ -3021,6 +3055,10 @@ namespace BTCPayServer.Tests
client = await guest.CreateClient(Policies.CanUseLightningNodeInStore);
// Can use lightning node is only granted to store's owner
await AssertPermissionError("btcpay.store.canuselightningnode", () => client.GetLightningNodeInfo(user.StoreId, "BTC"));
// balance and histogram should not be accessible with view only clients
await AssertPermissionError("btcpay.store.canuselightningnode", () => client.GetLightningNodeBalance(user.StoreId, "BTC"));
await AssertPermissionError("btcpay.store.canuselightningnode", () => client.GetLightningNodeHistogram(user.StoreId, "BTC"));
}
[Fact(Timeout = 60 * 20 * 1000)]
@ -3046,7 +3084,7 @@ namespace BTCPayServer.Tests
new CreateInvoiceRequest
{
Currency = "USD",
Amount = 100,
Amount = 0.1m,
Checkout = new CreateInvoiceRequest.CheckoutOptions
{
PaymentMethods = new[] { "BTC-LN" },
@ -3536,8 +3574,7 @@ namespace BTCPayServer.Tests
});
var overview = await client.ShowOnChainWalletOverview(walletId.StoreId, walletId.CryptoCode);
Assert.Equal(0m, overview.Balance);
var fee = await client.GetOnChainFeeRate(walletId.StoreId, walletId.CryptoCode);
Assert.NotNull(fee.FeeRate);
@ -3583,6 +3620,17 @@ namespace BTCPayServer.Tests
overview = await client.ShowOnChainWalletOverview(walletId.StoreId, walletId.CryptoCode);
Assert.Equal(0.01m, overview.Balance);
// histogram should not be accessible with view only clients
await AssertHttpError(403, async () =>
{
await viewOnlyClient.GetOnChainWalletHistogram(walletId.StoreId, walletId.CryptoCode);
});
var histogram = await client.GetOnChainWalletHistogram(walletId.StoreId, walletId.CryptoCode);
Assert.Equal(histogram.Balance, histogram.Series.Last());
Assert.Equal(0.01m, histogram.Balance);
Assert.Equal(0.01m, histogram.Series.Last());
Assert.Equal(0, histogram.Series.First());
//the simplest request:
var nodeAddress = await tester.ExplorerNode.GetNewAddressAsync();
var createTxRequest = new CreateOnChainTransactionRequest()
@ -3776,7 +3824,7 @@ namespace BTCPayServer.Tests
{
await tester.ExplorerNode.GenerateAsync(1);
}, bevent => bevent.CryptoCode.Equals("BTC", StringComparison.Ordinal));
}, bevent => bevent.PaymentMethodId == PaymentTypes.CHAIN.GetPaymentMethodId("BTC"));
Assert.Contains(
await client.ShowOnChainWalletTransactions(walletId.StoreId, walletId.CryptoCode,
@ -4171,13 +4219,17 @@ namespace BTCPayServer.Tests
await admin.GrantAccessAsync(true);
var adminClient = await admin.CreateClient(Policies.Unrestricted);
admin.RegisterLightningNode("BTC", LightningConnectionType.LndREST);
admin.RegisterLightningNode("BTC", LightningConnectionType.CLightning);
var payoutAmount = LightMoney.Satoshis(1000);
var inv = await tester.MerchantLnd.Client.CreateInvoice(payoutAmount, "Donation to merchant", TimeSpan.FromHours(1), default);
var resp = await tester.CustomerLightningD.Pay(inv.BOLT11);
Assert.Equal(PayResult.Ok, resp.Result);
var ppService = tester.PayTester.GetService<HostedServices.PullPaymentHostedService>();
var serializers = tester.PayTester.GetService<BTCPayNetworkJsonSerializerSettings>();
var store = tester.PayTester.GetService<StoreRepository>();
var dbContextFactory = tester.PayTester.GetService<Data.ApplicationDbContextFactory>();
Assert.True(await store.InternalNodePayoutAuthorized(admin.StoreId));
Assert.False(await store.InternalNodePayoutAuthorized("blah"));
await admin.MakeAdmin(false);
@ -4201,8 +4253,8 @@ namespace BTCPayServer.Tests
await TestUtils.EventuallyAsync(async () =>
{
var payoutC =
(await adminClient.GetStorePayouts(admin.StoreId, false)).Single(data => data.Id == payout.Id);
Assert.Equal(PayoutState.Completed, payoutC.State);
(await adminClient.GetStorePayouts(admin.StoreId, false)).SingleOrDefault(data => data.Id == payout.Id);
Assert.Equal(PayoutState.Completed, payoutC?.State);
});
payout = await adminClient.CreatePayout(admin.StoreId,
@ -4244,6 +4296,36 @@ namespace BTCPayServer.Tests
Destination = customerInvoice.BOLT11
});
Assert.Equal(payout2.OriginalAmount, new Money(100, MoneyUnit.Satoshi).ToDecimal(MoneyUnit.BTC));
// Checking if we can disable a payout...
var allLNPayouts = await ppService.GetPayouts(new ()
{
PayoutIds = new[] { payout2.Id },
Processor = LightningAutomatedPayoutSenderFactory.ProcessorName
});
Assert.NotEmpty(allLNPayouts);
var b = JsonConvert.DeserializeObject<Data.PayoutBlob>(allLNPayouts[0].Blob);
b.DisableProcessor(LightningAutomatedPayoutSenderFactory.ProcessorName);
Assert.Equal(1, b.IncrementErrorCount());
Assert.Equal(2, b.IncrementErrorCount());
allLNPayouts[0].Blob = JsonConvert.SerializeObject(b);
Assert.Equal(3, JsonConvert.DeserializeObject<Data.PayoutBlob>(allLNPayouts[0].Blob).IncrementErrorCount());
using var ctx = dbContextFactory.CreateContext();
var p = ctx.Payouts.Find(allLNPayouts[0].Id);
p.Blob = allLNPayouts[0].Blob;
await ctx.SaveChangesAsync();
var allLNPayouts2 = await ppService.GetPayouts(new()
{
PayoutIds = new[] { payout2.Id },
Processor = LightningAutomatedPayoutSenderFactory.ProcessorName
});
Assert.DoesNotContain(allLNPayouts[0].Id, allLNPayouts2.Select(a => a.Id));
allLNPayouts2 = await ppService.GetPayouts(new()
{
PayoutIds = new[] { payout2.Id },
Processor = "hello"
});
Assert.Contains(allLNPayouts[0].Id, allLNPayouts2.Select(a => a.Id));
}
[Fact(Timeout = 60 * 2 * 1000)]

View File

@ -0,0 +1,13 @@
using System;
namespace BTCPayServer.Tests
{
public class OutputPathAttribute : Attribute
{
public OutputPathAttribute(string builtPath)
{
BuiltPath = builtPath;
}
public string BuiltPath { get; }
}
}

View File

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

View File

@ -45,7 +45,7 @@ namespace BTCPayServer.Tests
var runInBrowser = config["RunSeleniumInBrowser"] == "true";
// Reset this using `dotnet user-secrets remove RunSeleniumInBrowser`
var chromeDriverPath = config["ChromeDriverDirectory"] ?? (Server.PayTester.InContainer ? "/usr/bin" : Directory.GetCurrentDirectory());
var chromeDriverPath = config["ChromeDriverDirectory"] ?? (Server.PayTester.InContainer ? "/usr/bin" : TestUtils.TestDirectory);
var options = new ChromeOptions();
if (!runInBrowser)
@ -132,11 +132,11 @@ retry:
/// Because for some reason, the selenium container can't resolve the tests container domain name
/// </summary>
public Uri ServerUri;
internal IWebElement FindAlertMessage(StatusMessageModel.StatusSeverity severity = StatusMessageModel.StatusSeverity.Success)
public IWebElement FindAlertMessage(StatusMessageModel.StatusSeverity severity = StatusMessageModel.StatusSeverity.Success)
{
return FindAlertMessage(new[] { severity });
}
internal IWebElement FindAlertMessage(params StatusMessageModel.StatusSeverity[] severity)
public IWebElement FindAlertMessage(params StatusMessageModel.StatusSeverity[] severity)
{
var className = string.Join(", ", severity.Select(statusSeverity => $".alert-{StatusMessageModel.ToString(statusSeverity)}"));
IWebElement el;
@ -182,13 +182,18 @@ retry:
Driver.FindElement(By.Id("RegisterButton")).Click();
Driver.AssertNoError();
CreatedUser = usr;
Password = "123456";
IsAdmin = isAdmin;
return usr;
}
string CreatedUser;
public string Password { get; private set; }
public bool IsAdmin { get; private set; }
public TestAccount AsTestAccount()
{
return new TestAccount(Server) { RegisterDetails = new Models.AccountViewModels.RegisterViewModel() { Password = "123456", Email = CreatedUser } };
return new TestAccount(Server) { StoreId = StoreId, Email = CreatedUser, Password = Password, RegisterDetails = new Models.AccountViewModels.RegisterViewModel() { Password = "123456", Email = CreatedUser }, IsAdmin = IsAdmin };
}
public (string storeName, string storeId) CreateNewStore(bool keepId = true)
@ -638,7 +643,15 @@ retry:
GoToUrl(url);
Assert.DoesNotMatch("404 - Page not found</h", Driver.PageSource);
if (shouldHaveAccess)
{
Assert.DoesNotMatch("- Denied</h", Driver.PageSource);
// check associated link is active if present
var sidebarLink = Driver.FindElements(By.CssSelector($"#mainNav a[href=\"{url}\"]")).FirstOrDefault();
if (sidebarLink != null)
{
Assert.Contains("active", sidebarLink.GetAttribute("class"));
}
}
else
Assert.Contains("- Denied</h", Driver.PageSource);
}

View File

@ -2487,7 +2487,16 @@ namespace BTCPayServer.Tests
$"LNurl w payout test {DateTime.UtcNow.Ticks}",
TimeSpan.FromHours(1), CancellationToken.None));
var response = await info.SendRequest(bolt2.BOLT11, s.Server.PayTester.HttpClient, null,null);
await TestUtils.EventuallyAsync(async () =>
// Oops!
Assert.Equal("The request has been approved. The sender needs to send the payment manually. (Or activate the lightning automated payment processor)", response.Reason);
var account = await s.AsTestAccount().CreateClient();
await account.UpdateStoreLightningAutomatedPayoutProcessors(s.StoreId, "BTC-LN", new()
{
ProcessNewPayoutsInstantly = true,
IntervalSeconds = TimeSpan.FromSeconds(60)
});
// Now it should process to complete
await TestUtils.EventuallyAsync(async () =>
{
s.Driver.Navigate().Refresh();
Assert.Contains(bolt2.BOLT11, s.Driver.PageSource);
@ -2577,7 +2586,9 @@ namespace BTCPayServer.Tests
$"LNurl w payout test {DateTime.UtcNow.Ticks}",
TimeSpan.FromHours(1), CancellationToken.None));
response = await info.SendRequest(bolt2.BOLT11, s.Server.PayTester.HttpClient, null,null);
TestUtils.Eventually(() =>
// Nope, you need to approve the claim automatically
Assert.Equal("The request has been recorded, but still need to be approved before execution.", response.Reason);
TestUtils.Eventually(() =>
{
s.Driver.Navigate().Refresh();
Assert.Contains(bolt2.BOLT11, s.Driver.PageSource);
@ -3264,6 +3275,7 @@ namespace BTCPayServer.Tests
public async Task CanUseLNAddress()
{
using var s = CreateSeleniumTester();
s.Server.DeleteStore = false;
s.Server.ActivateLightning();
await s.StartAsync();
await s.Server.EnsureChannelsSetup();
@ -3416,7 +3428,13 @@ namespace BTCPayServer.Tests
var succ = JsonConvert.DeserializeObject<LNURLPayRequest.LNURLPayRequestCallbackResponse>(str);
Assert.NotNull(succ.Pr);
Assert.Equal(new LightMoney(2001), BOLT11PaymentRequest.Parse(succ.Pr, Network.RegTest).MinimumAmount);
await s.Server.CustomerLightningD.Pay(succ.Pr);
}
// Can we find our comment and address in the payment list?
s.GoToInvoices();
var source = s.Driver.PageSource;
Assert.Contains(lnUsername, source);
}
[Fact]
@ -3765,7 +3783,7 @@ retry:
(_, string posId) = s.CreateApp("PointOfSale");
(_, string crowdfundId) = s.CreateApp("Crowdfund");
string GetStorePath(string subPath) => $"/stores/{storeId}/{subPath}";
string GetStorePath(string subPath) => $"/stores/{storeId}" + (string.IsNullOrEmpty(subPath) ? "" : $"/{subPath}");
// Owner access
s.AssertPageAccess(true, GetStorePath(""));

View File

@ -146,19 +146,27 @@ namespace BTCPayServer.Tests
public async Task ModifyPayment(Action<GeneralSettingsViewModel> modify)
{
var storeController = GetController<UIStoresController>();
var response = await storeController.GeneralSettings();
GeneralSettingsViewModel settings = (GeneralSettingsViewModel)((ViewResult)response).Model;
var response = await storeController.GeneralSettings(StoreId);
GeneralSettingsViewModel settings = (GeneralSettingsViewModel)((ViewResult)response).Model!;
modify(settings);
await storeController.GeneralSettings(settings);
}
public async Task ModifyGeneralSettings(Action<GeneralSettingsViewModel> modify)
{
var storeController = GetController<UIStoresController>();
var response = await storeController.GeneralSettings(StoreId);
GeneralSettingsViewModel settings = (GeneralSettingsViewModel)((ViewResult)response).Model!;
modify(settings);
storeController.GeneralSettings(settings).GetAwaiter().GetResult();
}
public async Task ModifyOnchainPaymentSettings(Action<WalletSettingsViewModel> modify)
{
var storeController = GetController<UIStoresController>();
var response = await storeController.WalletSettings(StoreId, "BTC");
WalletSettingsViewModel walletSettings = (WalletSettingsViewModel)((ViewResult)response).Model;
modify(walletSettings);
storeController.UpdatePaymentSettings(walletSettings).GetAwaiter().GetResult();
storeController.UpdateWalletSettings(walletSettings).GetAwaiter().GetResult();
}
@ -601,9 +609,7 @@ retry:
var methods = await client.GetInvoicePaymentMethods(StoreId, invoiceId);
var method = methods.First(m => m.PaymentMethodId == $"{cryptoCode}-LN");
var bolt11 = method.Destination;
TestLogs.LogInformation("PAYING");
await parent.CustomerLightningD.Pay(bolt11);
TestLogs.LogInformation("PAID");
await WaitInvoicePaid(invoiceId);
}

View File

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

View File

@ -1,3 +1,5 @@
Password => Cyphercode
Email address => Cypher ID
Welcome to {0} => Yo at {0}
{
"Password" : "Cyphercode",
"Email address" : "Cypher ID",
"Welcome to {0}" : "Yo at {0}"
}

View File

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

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

View File

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

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

View File

@ -20,10 +20,9 @@ namespace BTCPayServer.Tests
#else
public const int TestTimeout = 90_000;
#endif
public static DirectoryInfo TryGetSolutionDirectoryInfo(string currentPath = null)
public static DirectoryInfo TryGetSolutionDirectoryInfo()
{
var directory = new DirectoryInfo(
currentPath ?? Directory.GetCurrentDirectory());
var directory = new DirectoryInfo(TestDirectory);
while (directory != null && !directory.GetFiles("*.sln").Any())
{
directory = directory.Parent;
@ -31,10 +30,15 @@ namespace BTCPayServer.Tests
return directory;
}
static TestUtils()
{
TestDirectory = ((OutputPathAttribute)typeof(TestUtils).Assembly.GetCustomAttributes(typeof(OutputPathAttribute), true)[0]).BuiltPath;
}
public readonly static string TestDirectory;
public static string GetTestDataFullPath(string relativeFilePath)
{
var directory = new DirectoryInfo(Directory.GetCurrentDirectory());
var directory = new DirectoryInfo(TestDirectory);
while (directory != null && !directory.GetFiles("*.csproj").Any())
{
directory = directory.Parent;

View File

@ -345,7 +345,7 @@ retry:
var fetcher = new RateFetcher(factory);
Assert.True(RateRules.TryParse("X_X=kraken(X_BTC) * kraken(BTC_X)", out var rule));
foreach (var pair in new[] { "DOGE_USD", "DOGE_CAD", "DASH_CAD", "DASH_USD", "DASH_EUR" })
foreach (var pair in new[] { "DOGE_USD", "DOGE_CAD" })
{
var result = fetcher.FetchRate(CurrencyPair.Parse(pair), rule, null, default).GetAwaiter().GetResult();
Assert.NotNull(result.BidAsk);
@ -488,10 +488,14 @@ retry:
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();
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}")).Content.ReadAsStringAsync()).Trim();
EqualJsContent(expected, actual);
// This test is flaky probably because of the CDN sending the wrong file's version in some regions.
// https://app.circleci.com/pipelines/github/btcpayserver/btcpayserver/13750/workflows/44aaf31d-0057-4fd8-a5bb-1a2c47fc530f/jobs/42963
// It works locally depending on where you live.
//actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "dom-confetti", "dom-confetti.min.js").Trim();
//version = Regex.Match(actual, "Original file: /npm/dom-confetti@([0-9]+.[0-9]+.[0-9]+)/lib/main.js").Groups[1].Value;
//expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/dom-confetti@{version}")).Content.ReadAsStringAsync()).Trim();
//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;

View File

@ -303,7 +303,7 @@ namespace BTCPayServer.Tests
// Set tolerance to 50%
var stores = user.GetController<UIStoresController>();
var response = await stores.GeneralSettings();
var response = await stores.GeneralSettings(user.StoreId);
var vm = Assert.IsType<GeneralSettingsViewModel>(Assert.IsType<ViewResult>(response).Model);
Assert.Equal(0.0, vm.PaymentTolerance);
vm.PaymentTolerance = 50.0;
@ -385,7 +385,7 @@ namespace BTCPayServer.Tests
await user.RegisterDerivationSchemeAsync("BTC");
await user.RegisterLightningNodeAsync("BTC", LightningConnectionType.CLightning);
await user.SetNetworkFeeMode(NetworkFeeMode.Never);
await user.ModifyOnchainPaymentSettings(p => p.SpeedPolicy = SpeedPolicy.HighSpeed);
await user.ModifyGeneralSettings(p => p.SpeedPolicy = SpeedPolicy.HighSpeed);
var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(0.0001m, "BTC"));
await tester.WaitForEvent<InvoiceNewPaymentDetailsEvent>(async () =>
{
@ -445,7 +445,7 @@ namespace BTCPayServer.Tests
var user = tester.NewAccount();
await user.GrantAccessAsync(true);
var storeController = user.GetController<UIStoresController>();
var storeResponse = await storeController.GeneralSettings();
var storeResponse = await storeController.GeneralSettings(user.StoreId);
Assert.IsType<ViewResult>(storeResponse);
Assert.IsType<ViewResult>(storeController.SetupLightningNode(user.StoreId, "BTC"));
@ -568,10 +568,10 @@ namespace BTCPayServer.Tests
using var tester = CreateServerTester();
await tester.StartAsync();
var acc = tester.NewAccount();
acc.GrantAccess();
await acc.GrantAccessAsync();
acc.RegisterDerivationScheme("BTC");
await acc.ModifyOnchainPaymentSettings(p => p.SpeedPolicy = SpeedPolicy.LowSpeed);
var invoice = acc.BitPay.CreateInvoice(new Invoice
await acc.ModifyGeneralSettings(p => p.SpeedPolicy = SpeedPolicy.LowSpeed);
var invoice = await acc.BitPay.CreateInvoiceAsync(new Invoice
{
Price = 5.0m,
Currency = "USD",
@ -648,7 +648,7 @@ namespace BTCPayServer.Tests
var store2 = acc.GetController<UIStoresController>();
await store2.Pair(pairingCode.ToString(), store2.CurrentStore.Id);
Assert.Contains(nameof(PairingResult.ReusedKey),
(string)store2.TempData[WellKnownTempData.ErrorMessage], StringComparison.CurrentCultureIgnoreCase);
store2.TempData[WellKnownTempData.ErrorMessage].ToString(), StringComparison.CurrentCultureIgnoreCase);
}
[Fact(Timeout = LongRunningTestTimeout * 2)]
@ -1543,7 +1543,7 @@ namespace BTCPayServer.Tests
var vm = await user.GetController<UIStoresController>().CheckoutAppearance().AssertViewModelAsync<CheckoutAppearanceViewModel>();
Assert.Equal(2, vm.PaymentMethodCriteria.Count);
var criteria = Assert.Single(vm.PaymentMethodCriteria.Where(m => m.PaymentMethod == btcMethod.ToString()));
Assert.Equal(PaymentTypes.CHAIN.GetPaymentMethodId("BTC").ToString(), criteria.PaymentMethod);
Assert.Equal(btcMethod.ToString(), criteria.PaymentMethod);
criteria.Value = "5 USD";
criteria.Type = PaymentMethodCriteriaViewModel.CriteriaType.GreaterThan;
Assert.IsType<RedirectToActionResult>(user.GetController<UIStoresController>().CheckoutAppearance(vm)
@ -1587,14 +1587,14 @@ namespace BTCPayServer.Tests
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
var checkout = (await user.GetController<UIInvoiceController>().Checkout(invoice.Id)).AssertViewModel<PaymentModel>();
var checkout = (await user.GetController<UIInvoiceController>().Checkout(invoice.Id)).AssertViewModel<CheckoutModel>();
Assert.Equal(lnMethod, checkout.PaymentMethodId);
// If we change store's default, it should change the checkout's default
vm.DefaultPaymentMethod = btcMethod;
Assert.IsType<RedirectToActionResult>(user.GetController<UIStoresController>().CheckoutAppearance(vm)
.Result);
checkout = (await user.GetController<UIInvoiceController>().Checkout(invoice.Id)).AssertViewModel<PaymentModel>();
checkout = (await user.GetController<UIInvoiceController>().Checkout(invoice.Id)).AssertViewModel<CheckoutModel>();
Assert.Equal(btcMethod, checkout.PaymentMethodId);
}
@ -1625,7 +1625,7 @@ namespace BTCPayServer.Tests
// validate that invoice data model doesn't have lightning string initially
var res = await user.GetController<UIInvoiceController>().Checkout(invoice.Id);
var paymentMethodFirst = Assert.IsType<PaymentModel>(
var paymentMethodFirst = Assert.IsType<CheckoutModel>(
Assert.IsType<ViewResult>(res).Model
);
Assert.DoesNotContain("&lightning=", paymentMethodFirst.InvoiceBitcoinUrlQR);
@ -1641,7 +1641,7 @@ namespace BTCPayServer.Tests
// validate that QR code now has both onchain and offchain payment urls
res = await user.GetController<UIInvoiceController>().Checkout(invoice.Id);
var paymentMethodUnified = Assert.IsType<PaymentModel>(
var paymentMethodUnified = Assert.IsType<CheckoutModel>(
Assert.IsType<ViewResult>(res).Model
);
Assert.StartsWith("bitcoin:bcrt", paymentMethodUnified.InvoiceBitcoinUrl);
@ -1655,8 +1655,8 @@ namespace BTCPayServer.Tests
// Standard for all uppercase characters in QR codes is still not implemented in all wallets
// But we're proceeding with BECH32 being uppercase
Assert.Equal($"bitcoin:{paymentMethodUnified.BtcAddress}", paymentMethodUnified.InvoiceBitcoinUrl.Split('?')[0]);
Assert.Equal($"bitcoin:{paymentMethodUnified.BtcAddress.ToUpperInvariant()}", paymentMethodUnified.InvoiceBitcoinUrlQR.Split('?')[0]);
Assert.Equal($"bitcoin:{paymentMethodUnified.Address}", paymentMethodUnified.InvoiceBitcoinUrl.Split('?')[0]);
Assert.Equal($"bitcoin:{paymentMethodUnified.Address.ToUpperInvariant()}", paymentMethodUnified.InvoiceBitcoinUrlQR.Split('?')[0]);
// Fallback lightning invoice should be uppercase inside the QR code, lowercase in payment URI
var lightningFallback = paymentMethodUnified.InvoiceBitcoinUrl.Split(new[] { "&lightning=" }, StringSplitOptions.None)[1];
@ -2619,7 +2619,7 @@ namespace BTCPayServer.Tests
}
var controller = tester.PayTester.GetController<UIStoresController>(user.UserId, user.StoreId);
var vm = await controller.GeneralSettings().AssertViewModelAsync<GeneralSettingsViewModel>();
var vm = await controller.GeneralSettings(user.StoreId).AssertViewModelAsync<GeneralSettingsViewModel>();
Assert.Equal(tester.PayTester.ServerUriWithIP + "LocalStorage/8f890691-87f9-4c65-80e5-3b7ffaa3551f-store.png", vm.LogoUrl);
Assert.Equal(tester.PayTester.ServerUriWithIP + "LocalStorage/2a51c49a-9d54-4013-80a2-3f6e69d08523-store.css", vm.CssUrl);
@ -2909,6 +2909,11 @@ namespace BTCPayServer.Tests
}
Assert.True(await invoiceMigrator.IsComplete());
});
var invoiceRepo = tester.PayTester.GetService<InvoiceRepository>();
var invoice = await invoiceRepo.GetInvoice("Q7RqoHLngK9svM4MgRyi9y");
var p = invoice.Payments.First(p => p.Id == "26c879f3d27a894a62f8730c84205ac9dec38b7bbc0a11ccc0c196d1259b25aa-1");
var details = p.GetDetails<BitcoinLikePaymentData>(handlers.GetBitcoinHandler("BTC"));
Assert.Equal("6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d", details.AssetId.ToString());
}
private static async Task RestartMigration(ServerTester tester)
@ -3245,8 +3250,14 @@ namespace BTCPayServer.Tests
var date2018 = new DateTimeOffset(2018, 1, 1, 0, 0, 0, TimeSpan.Zero);
report = await GetReport(acc, new() { ViewName = "Payments", TimePeriod = new TimePeriod() { From = date2018, To = date2018 + TimeSpan.FromDays(365) } });
var invoiceIdIndex = report.GetIndex("InvoiceId");
var invoiceCurrencyAmountIndex = report.GetIndex("InvoiceCurrencyAmount");
var rateIndex = report.GetIndex("Rate");
var oldPaymentsCount = report.Data.Count(d => d[invoiceIdIndex].Value<string>() == "Q7RqoHLngK9svM4MgRyi9y");
Assert.Equal(8, oldPaymentsCount); // 10 payments, but 2 unaccounted
Assert.Equal(9, oldPaymentsCount); // 11 payments, but 2 unaccounted
Assert.Single(report.Data, d =>
d[invoiceIdIndex].Value<string>() == "Q7RqoHLngK9svM4MgRyi9y" &&
GetAmount(rateIndex, d) == 6596.35m &&
GetAmount(invoiceCurrencyAmountIndex, d) == 1.18m);
var addr = await tester.ExplorerNode.GetNewAddressAsync();
// Two invoices get refunded
@ -3268,9 +3279,9 @@ namespace BTCPayServer.Tests
var d = Assert.Single(report.Data.Where(d => d[report.GetIndex("InvoiceId")].Value<string>() == inv.Id));
Assert.Equal(fullyPaid, (bool)d[fullyPaidIndex]);
Assert.Equal(currency, d[currencyIndex].Value<string>());
Assert.Equal(completed, (((JObject)d[completedIndex])["v"]).Value<decimal>());
Assert.Equal(awaiting, (((JObject)d[awaitingIndex])["v"]).Value<decimal>());
Assert.Equal(limit, (((JObject)d[limitIndex])["v"]).Value<decimal>());
Assert.Equal(completed, GetAmount(completedIndex, d));
Assert.Equal(awaiting, GetAmount(awaitingIndex, d));
Assert.Equal(limit, GetAmount(limitIndex, d));
}
await AssertData("USD", awaiting: 0.0m, limit: 10.0m, completed: 0.0m, fullyPaid: false);
@ -3291,6 +3302,12 @@ namespace BTCPayServer.Tests
}
}
private static decimal GetAmount(int idx, JArray d)
{
var jobj = (JObject)d[idx];
return Math.Round(jobj["v"].Value<decimal>(), jobj["d"].Value<int>());
}
private async Task<StoreReportResponse> GetReport(TestAccount acc, StoreReportRequest req)
{
var controller = acc.GetController<UIReportsController>();

View File

@ -16,11 +16,13 @@ using BTCPayServer.Client.Models;
using BTCPayServer.Controllers;
using ExchangeSharp;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Localization;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.Extensions.FileSystemGlobbing;
using NBitcoin;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
@ -349,45 +351,66 @@ retry:
{
defaultTranslatedKeys.Add(k);
}
AddLocalizers(defaultTranslatedKeys, txt);
}
// Go through all cshtml file, search for text-translate or ViewLocalizer usage
using (var tester = CreateServerTester())
using (var tester = CreateServerTester(newDb: true))
{
await tester.StartAsync();
var engine = tester.PayTester.GetService<RazorProjectEngine>();
foreach (var file in soldir.EnumerateFiles("*.cshtml", SearchOption.AllDirectories))
var files = soldir.EnumerateFiles("*.cshtml", SearchOption.AllDirectories)
.Union(soldir.EnumerateFiles("*.razor", SearchOption.AllDirectories));
foreach (var file in files)
{
var filePath = file.FullName;
var txt = File.ReadAllText(file.FullName);
if (txt.Contains("ViewLocalizer"))
{
var matches = Regex.Matches(txt, "ViewLocalizer\\[\"(.*?)\"[\\],]");
foreach (Match match in matches)
{
defaultTranslatedKeys.Add(match.Groups[1].Value);
}
}
AddLocalizers(defaultTranslatedKeys, txt);
filePath = filePath.Replace(Path.Combine(soldir.FullName, "BTCPayServer"), "/");
var item = engine.FileSystem.GetItem(filePath);
var node = (DocumentIntermediateNode)engine.Process(item).Items[typeof(DocumentIntermediateNode)];
var w = new TranslatedKeyNodeWalker(defaultTranslatedKeys, txt);
w.Visit(node);
}
}
defaultTranslatedKeys = defaultTranslatedKeys.Select(d => d.Trim()).Distinct().OrderBy(o => o).ToList();
defaultTranslatedKeys = defaultTranslatedKeys.Select(d => d.Trim().Replace("\r\n", "\n")).Distinct().OrderBy(o => o).ToList();
JObject obj = new JObject();
foreach (var v in defaultTranslatedKeys)
{
obj.Add(v, "");
}
var path = Path.Combine(soldir.FullName, "BTCPayServer/Services/Translations.Default.cs");
var defaultTranslation = File.ReadAllText(path);
var startIdx = defaultTranslation.IndexOf("\"\"\"");
var endIdx = defaultTranslation.LastIndexOf("\"\"\"");
var content = defaultTranslation.Substring(0, startIdx + 3);
content += "\n" + String.Join('\n', defaultTranslatedKeys) + "\n";
content += "\n" + obj.ToString(Formatting.Indented) + "\n";
content += defaultTranslation.Substring(endIdx);
File.WriteAllText(path, content);
}
private static void AddLocalizers(List<string> defaultTranslatedKeys, string txt)
{
foreach (string localizer in new[] { "ViewLocalizer", "StringLocalizer" })
{
if (txt.Contains(localizer))
{
var matches = Regex.Matches(txt, localizer + "\\[\"(.*?)\"[\\],]");
foreach (Match match in matches)
{
var k = match.Groups[1].Value;
k = k.Replace("\\", "");
defaultTranslatedKeys.Add(k);
}
}
}
}
class DisplayNameWalker : CSharpSyntaxWalker
{
public List<string> Keys = new List<string>();

View File

@ -98,7 +98,7 @@ services:
custom:
nbxplorer:
image: nicolasdorier/nbxplorer:2.5.4
image: nicolasdorier/nbxplorer:2.5.12
restart: unless-stopped
ports:
- "32838:32838"
@ -162,7 +162,7 @@ services:
- "bitcoin_datadir:/data"
customer_lightningd:
image: btcpayserver/lightning:v24.05
image: btcpayserver/lightning:v24.08.2
stop_signal: SIGKILL
restart: unless-stopped
environment:
@ -190,7 +190,7 @@ services:
- bitcoind
merchant_lightningd:
image: btcpayserver/lightning:v24.05
image: btcpayserver/lightning:v24.08.2
stop_signal: SIGKILL
restart: unless-stopped
environment:
@ -227,7 +227,7 @@ services:
- "5432"
merchant_lnd:
image: btcpayserver/lnd:v0.18.1-beta
image: btcpayserver/lnd:v0.18.3-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -262,7 +262,7 @@ services:
- bitcoind
customer_lnd:
image: btcpayserver/lnd:v0.18.1-beta
image: btcpayserver/lnd:v0.18.3-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"

View File

@ -95,7 +95,7 @@ services:
custom:
nbxplorer:
image: nicolasdorier/nbxplorer:2.5.4
image: nicolasdorier/nbxplorer:2.5.12
restart: unless-stopped
ports:
- "32838:32838"
@ -148,7 +148,7 @@ services:
- "bitcoin_datadir:/data"
customer_lightningd:
image: btcpayserver/lightning:v24.05
image: btcpayserver/lightning:v24.08.2
stop_signal: SIGKILL
restart: unless-stopped
environment:
@ -176,7 +176,7 @@ services:
- bitcoind
merchant_lightningd:
image: btcpayserver/lightning:v24.05
image: btcpayserver/lightning:v24.08.2
stop_signal: SIGKILL
restart: unless-stopped
environment:
@ -213,7 +213,7 @@ services:
- "5432"
merchant_lnd:
image: btcpayserver/lnd:v0.18.1-beta
image: btcpayserver/lnd:v0.18.3-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -250,7 +250,7 @@ services:
- bitcoind
customer_lnd:
image: btcpayserver/lnd:v0.18.1-beta
image: btcpayserver/lnd:v0.18.3-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"

View File

@ -37,11 +37,11 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="BTCPayServer.NTag424" Version="1.0.23" />
<PackageReference Include="BTCPayServer.NTag424" Version="1.0.24" />
<PackageReference Include="YamlDotNet" Version="8.0.0" />
<PackageReference Include="BIP78.Sender" Version="0.2.2" />
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.2" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.6.0" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.6.7" />
<PackageReference Include="CsvHelper" Version="32.0.3" />
<PackageReference Include="Dapper" Version="2.1.35" />
<PackageReference Include="Fido2" Version="2.0.2" />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
@using BTCPayServer.Payments
@model BTCPayServer.Components.InvoiceStatus.InvoiceStatusViewModel
@inject Dictionary<PaymentMethodId, IPaymentModelExtension> Extensions
@inject Dictionary<PaymentMethodId, ICheckoutModelExtension> Extensions
@{
var state = Model.State.ToString();
@ -10,7 +10,7 @@
<div class="d-inline-flex align-items-center gap-2">
@if (Model.IsArchived)
{
<span class="badge bg-warning">archived</span>
<span class="badge bg-warning" text-translate="true">archived</span>
}
<div class="badge badge-@badgeClass" data-invoice-state-badge="@Model.InvoiceId">
@if (canMark)
@ -21,13 +21,13 @@
<div class="dropdown-menu">
@if (Model.State.CanMarkInvalid())
{
<button type="button" class="dropdown-item lh-base" data-invoice-id="@Model.InvoiceId" data-new-state="invalid">
<button type="button" class="dropdown-item lh-base" data-invoice-id="@Model.InvoiceId" data-new-state="invalid" text-translate="true">
Mark as invalid
</button>
}
@if (Model.State.CanMarkComplete())
{
<button type="button" class="dropdown-item lh-base" data-invoice-id="@Model.InvoiceId" data-new-state="settled">
<button type="button" class="dropdown-item lh-base" data-invoice-id="@Model.InvoiceId" data-new-state="settled" text-translate="true">
Mark as settled
</button>
}
@ -62,6 +62,6 @@
}
@if (Model.HasRefund)
{
<span class="badge bg-warning">Refund</span>
<span class="badge bg-warning" text-translate="true">Refund</span>
}
</div>

View File

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

View File

@ -43,33 +43,33 @@
<span text-translate="true">Settings</span>
</a>
</li>
@if (ViewData.IsActivePage([StoreNavPages.General, StoreNavPages.Rates, StoreNavPages.CheckoutAppearance, StoreNavPages.Tokens, StoreNavPages.Users, StoreNavPages.Roles, StoreNavPages.Webhooks, StoreNavPages.PayoutProcessors, StoreNavPages.Emails, StoreNavPages.Forms]))
@if (ViewData.IsPageActive([StoreNavPages.General, StoreNavPages.Rates, StoreNavPages.CheckoutAppearance, StoreNavPages.Tokens, StoreNavPages.Users, StoreNavPages.Roles, StoreNavPages.Webhooks, StoreNavPages.PayoutProcessors, StoreNavPages.Emails, StoreNavPages.Forms]))
{
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyStoreSettings">
<li class="nav-item nav-item-sub" permission="@Policies.CanViewStoreSettings">
<a id="StoreNav-@(nameof(StoreNavPages.Rates))" class="nav-link @ViewData.ActivePageClass(StoreNavPages.Rates)" asp-controller="UIStores" asp-action="Rates" asp-route-storeId="@Model.Store.Id" text-translate="true">Rates</a>
</li>
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyStoreSettings">
<li class="nav-item nav-item-sub" permission="@Policies.CanViewStoreSettings">
<a id="StoreNav-@(nameof(StoreNavPages.CheckoutAppearance))" class="nav-link @ViewData.ActivePageClass(StoreNavPages.CheckoutAppearance)" asp-controller="UIStores" asp-action="CheckoutAppearance" asp-route-storeId="@Model.Store.Id" text-translate="true">Checkout Appearance</a>
</li>
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyStoreSettings">
<li class="nav-item nav-item-sub" permission="@Policies.CanViewStoreSettings">
<a id="StoreNav-@(nameof(StoreNavPages.Tokens))" class="nav-link @ViewData.ActivePageClass(StoreNavPages.Tokens)" asp-controller="UIStores" asp-action="ListTokens" asp-route-storeId="@Model.Store.Id" text-translate="true">Access Tokens</a>
</li>
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyStoreSettings">
<li class="nav-item nav-item-sub" permission="@Policies.CanViewStoreSettings">
<a id="StoreNav-@(nameof(StoreNavPages.Users))" class="nav-link @ViewData.ActivePageClass(StoreNavPages.Users)" asp-controller="UIStores" asp-action="StoreUsers" asp-route-storeId="@Model.Store.Id" text-translate="true">Users</a>
</li>
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyStoreSettings">
<li class="nav-item nav-item-sub" permission="@Policies.CanViewStoreSettings">
<a id="StoreNav-@(nameof(StoreNavPages.Roles))" class="nav-link @ViewData.ActivePageClass(StoreNavPages.Roles)" asp-controller="UIStores" asp-action="ListRoles" asp-route-storeId="@Model.Store.Id" text-translate="true">Roles</a>
</li>
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyStoreSettings">
<li class="nav-item nav-item-sub" permission="@Policies.CanViewStoreSettings">
<a id="StoreNav-@(nameof(StoreNavPages.Webhooks))" class="nav-link @ViewData.ActivePageClass(StoreNavPages.Webhooks)" asp-controller="UIStores" asp-action="Webhooks" asp-route-storeId="@Model.Store.Id" text-translate="true">Webhooks</a>
</li>
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyStoreSettings">
<li class="nav-item nav-item-sub" permission="@Policies.CanViewStoreSettings">
<a id="StoreNav-@(nameof(StoreNavPages.PayoutProcessors))" class="nav-link @ViewData.ActivePageClass(StoreNavPages.PayoutProcessors)" asp-controller="UIPayoutProcessors" asp-action="ConfigureStorePayoutProcessors" asp-route-storeId="@Model.Store.Id" text-translate="true">Payout Processors</a>
</li>
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyStoreSettings">
<li class="nav-item nav-item-sub" permission="@Policies.CanViewStoreSettings">
<a id="StoreNav-@(nameof(StoreNavPages.Emails))" class="nav-link @ViewData.ActivePageClass(StoreNavPages.Emails)" asp-controller="UIStores" asp-action="StoreEmailSettings" asp-route-storeId="@Model.Store.Id" text-translate="true">Emails</a>
</li>
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyStoreSettings">
<li class="nav-item nav-item-sub" permission="@Policies.CanViewStoreSettings">
<a id="StoreNav-@(nameof(StoreNavPages.Forms))" class="nav-link @ViewData.ActivePageClass(StoreNavPages.Forms)" asp-controller="UIForms" asp-action="FormsList" asp-route-storeId="@Model.Store.Id" text-translate="true">Forms</a>
</li>
}
@ -104,16 +104,21 @@
</a>
}
</li>
@if (ViewData.IsActiveCategory(typeof(WalletsNavPages), scheme.WalletId.ToString()) || ViewData.IsActivePage([WalletsNavPages.Settings], scheme.WalletId.ToString()) || ViewData.IsActivePage([StoreNavPages.OnchainSettings], categoryId))
@if (ViewData.IsCategoryActive(typeof(WalletsNavPages), scheme.WalletId.ToString()) || ViewData.IsPageActive([WalletsNavPages.Settings], scheme.WalletId.ToString()) || ViewData.IsPageActive([StoreNavPages.OnchainSettings], categoryId))
{
@if (!scheme.ReadonlyWallet)
{
<li class="nav-item nav-item-sub">
<a id="WalletNav-Send" class="nav-link @ViewData.ActivePageClass([WalletsNavPages.Send, WalletsNavPages.PSBT], scheme.WalletId.ToString())" asp-area="" asp-controller="UIWallets" asp-action="WalletSend" asp-route-walletId="@scheme.WalletId" text-translate="true">Send</a>
</li>
}
<li class="nav-item nav-item-sub">
<a id="WalletNav-Send" class="nav-link @ViewData.ActivePageClass([WalletsNavPages.Send, WalletsNavPages.PSBT], scheme.WalletId.ToString())" asp-area="" asp-controller="UIWallets" asp-action="WalletSend" asp-route-walletId="@scheme.WalletId">Send</a>
<a id="WalletNav-Receive" class="nav-link @ViewData.ActivePageClass(WalletsNavPages.Receive, scheme.WalletId.ToString())" asp-area="" asp-controller="UIWallets" asp-action="WalletReceive" asp-route-walletId="@scheme.WalletId" text-translate="true">Receive</a>
</li>
<li class="nav-item nav-item-sub">
<a id="WalletNav-Receive" class="nav-link @ViewData.ActivePageClass(WalletsNavPages.Receive, scheme.WalletId.ToString())" asp-area="" asp-controller="UIWallets" asp-action="WalletReceive" asp-route-walletId="@scheme.WalletId">Receive</a>
</li>
<li class="nav-item nav-item-sub">
<a id="WalletNav-Settings" class="nav-link @ViewData.ActivePageClass(WalletsNavPages.Settings, scheme.WalletId.ToString()) @ViewData.ActivePageClass(StoreNavPages.OnchainSettings, categoryId)" asp-area="" asp-controller="UIStores" asp-action="WalletSettings" asp-route-cryptoCode="@scheme.WalletId.CryptoCode" asp-route-storeId="@scheme.WalletId.StoreId">Settings</a>
<a id="WalletNav-Settings" class="nav-link @ViewData.ActivePageClass(WalletsNavPages.Settings, scheme.WalletId.ToString()) @ViewData.ActivePageClass(StoreNavPages.OnchainSettings, categoryId)" asp-area="" asp-controller="UIStores" asp-action="WalletSettings" asp-route-cryptoCode="@scheme.WalletId.CryptoCode" asp-route-storeId="@scheme.WalletId.StoreId" text-translate="true">Settings</a>
</li>
<vc:ui-extension-point location="wallet-nav" model="@Model" />
}
@ -140,10 +145,10 @@
</a>
}
</li>
@if (ViewData.IsActivePage([StoreNavPages.Lightning, StoreNavPages.LightningSettings], $"{Model.Store.Id}-{scheme.CryptoCode}"))
@if (ViewData.IsPageActive([StoreNavPages.Lightning, StoreNavPages.LightningSettings], $"{Model.Store.Id}-{scheme.CryptoCode}"))
{
<li class="nav-item nav-item-sub">
<a id="StoreNav-@(nameof(StoreNavPages.LightningSettings))" class="nav-link @ViewData.ActivePageClass(StoreNavPages.LightningSettings)" asp-controller="UIStores" asp-action="LightningSettings" asp-route-storeId="@Model.Store.Id" asp-route-cryptoCode="@scheme.CryptoCode">Settings</a>
<a id="StoreNav-@(nameof(StoreNavPages.LightningSettings))" class="nav-link @ViewData.ActivePageClass(StoreNavPages.LightningSettings)" asp-controller="UIStores" asp-action="LightningSettings" asp-route-storeId="@Model.Store.Id" asp-route-cryptoCode="@scheme.CryptoCode" text-translate="true">Settings</a>
</li>
<vc:ui-extension-point location="lightning-nav" model="@Model"/>
}
@ -290,7 +295,7 @@
<span text-translate="true">Server Settings</span>
</a>
</li>
@if (ViewData.IsActiveCategory(typeof(ServerNavPages)) && !ViewData.IsActivePage([ServerNavPages.Plugins]))
@if (ViewData.IsCategoryActive(typeof(ServerNavPages)) && !ViewData.IsPageActive([ServerNavPages.Plugins]))
{
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyServerSettings">
<a asp-controller="UIServer" id="SectionNav-@ServerNavPages.Users" class="nav-link @ViewData.ActivePageClass(ServerNavPages.Users)" asp-action="ListUsers" text-translate="true">Users</a>
@ -322,8 +327,9 @@
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyServerSettings">
<a asp-controller="UIServer" id="SectionNav-@ServerNavPages.Files" class="nav-link @ViewData.ActivePageClass(ServerNavPages.Files)" asp-action="Files" text-translate="true">Files</a>
</li>
<vc:ui-extension-point location="server-nav" model="@Model"/>
}
<vc:ui-extension-point location="server-nav" model="@Model"/>
<li class="nav-item dropup">
<a class="nav-link @ViewData.ActivePageClass(ManageNavPages.Index)" role="button" data-bs-toggle="dropdown" data-bs-auto-close="outside" aria-expanded="false" id="Nav-Account">
<vc:icon symbol="nav-account"/>
@ -379,7 +385,7 @@
</li>
</ul>
</li>
@if (ViewData.IsActiveCategory(typeof(ManageNavPages)) || ViewData.IsActivePage([ManageNavPages.ChangePassword]))
@if (ViewData.IsCategoryActive(typeof(ManageNavPages)) || ViewData.IsPageActive([ManageNavPages.ChangePassword]))
{
<li class="nav-item nav-item-sub">
<a id="SectionNav-@ManageNavPages.ChangePassword.ToString()" class="nav-link @ViewData.ActivePageClass(ManageNavPages.ChangePassword)" asp-controller="UIManage" asp-action="ChangePassword" text-translate="true">Password</a>

View File

@ -1,3 +1,5 @@
@using System.Web
@using BTCPayServer.TagHelpers
@model BasePagingViewModel
@{
@ -13,7 +15,7 @@
@if (Model.Skip > 0)
{
<li class="page-item">
<a class="page-link" tabindex="-1" href="@NavigatePages(-1, Model.Count)">Prev</a>
<a class="page-link" tabindex="-1" href="@NavigatePages(-1, Model.Count)" text-translate="true">Prev</a>
</li>
}
<li class="page-item disabled">
@ -35,7 +37,7 @@
@if ((Model.Total is null && Model.CurrentPageCount >= Model.Count) || (Model.Total is not null && Model.Total.Value > Model.Skip + Model.Count))
{
<li class="page-item">
<a class="page-link" href="@NavigatePages(1, Model.Count)">Next</a>
<a class="page-link" href="@NavigatePages(1, Model.Count)" text-translate="true">Next</a>
</li>
}
</ul>
@ -45,7 +47,7 @@
{
<ul class="pagination ms-auto">
<li class="page-item disabled">
<span class="page-link">Page Size</span>
<span class="page-link" text-translate="true">Page Size</span>
</li>
@foreach (var pageSize in pageSizeOptions)
{
@ -85,10 +87,26 @@
{
// merge both, preferring the `query` properties in case of duplicate keys
query = query.Concat(Model.PaginationQuery)
.Where(e => e.Value != null)
.GroupBy(e => e.Key)
.ToDictionary(g => g.Key, g => g.First().Value);
}
return Url.Action(null, query);
return ReplaceQueryParameters(query);
}
string ReplaceQueryParameters(Dictionary<string, object> query)
{
var uri = new Uri(ViewContext.HttpContext.Request.GetCurrentUrlWithQueryString());
var queryParams = HttpUtility.ParseQueryString(uri.Query);
foreach (var (key, value) in query)
{
if (value != null) queryParams[key] = value?.ToString();
}
var uriBuilder = new UriBuilder(uri)
{
Query = queryParams.ToString()!
};
return uriBuilder.ToString();
}
}

View File

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

View File

@ -1,11 +1,15 @@
@using BTCPayServer.Abstractions.TagHelpers
@using BTCPayServer.Client.Models
@using BTCPayServer.TagHelpers
@using Microsoft.AspNetCore.Mvc.TagHelpers
@model BTCPayServer.Components.StoreLightningBalance.StoreLightningBalanceViewModel
@if(!Model.InitialRendering && Model.Balance == null)
@if (!Model.InitialRendering && Model.Balance == null)
{
return;
return;
}
<div id="StoreLightningBalance-@Model.Store.Id" class="widget store-lightning-balance">
<div id="StoreLightningBalance-@Model.StoreId" class="widget store-lightning-balance">
<div class="d-flex gap-3 align-items-center justify-content-between mb-2">
<h6>Lightning Balance</h6>
<h6 text-translate="true">Lightning Balance</h6>
@if (Model.CryptoCode != Model.DefaultCurrency && Model.Balance != null)
{
<div class="btn-group btn-group-sm gap-0 currency-toggle" role="group">
@ -29,7 +33,7 @@
<div class="d-flex align-items-baseline gap-1">
<h3 class="d-inline-block me-1" data-balance="@Model.TotalOffchain" data-sensitive>@Model.TotalOffchain</h3>
<span class="text-secondary fw-semibold text-nowrap">
<span class="currency">@Model.CryptoCode</span> in channels
@ViewLocalizer["<span class=\"currency\">{0}</span> in channels", Model.CryptoCode]
</span>
</div>
@ -41,7 +45,7 @@
@Model.Balance.OffchainBalance.Opening
</span>
<span class="text-secondary text-nowrap">
<span class="currency">@Model.CryptoCode</span> opening channels
@ViewLocalizer["<span class=\"currency\">{0}</span> opening channels", Model.CryptoCode]
</span>
</div>
}
@ -52,7 +56,7 @@
@Model.Balance.OffchainBalance.Local
</span>
<span class="text-secondary text-nowrap">
<span class="currency">@Model.CryptoCode</span> local balance
@ViewLocalizer["<span class=\"currency\">{0}</span> local balance", Model.CryptoCode]
</span>
</div>
}
@ -63,7 +67,7 @@
@Model.Balance.OffchainBalance.Remote
</span>
<span class="text-secondary text-nowrap">
<span class="currency">@Model.CryptoCode</span> remote balance
@ViewLocalizer["<span class=\"currency\">{0}</span> remote balance", Model.CryptoCode]
</span>
</div>
}
@ -74,7 +78,7 @@
@Model.Balance.OffchainBalance.Closing
</span>
<span class="text-secondary text-nowrap">
<span class="currency">@Model.CryptoCode</span> closing channels
@ViewLocalizer["<span class=\"currency\">{0}</span> closing channels", Model.CryptoCode]
</span>
</div>
}
@ -87,7 +91,7 @@
<div class="d-flex align-items-baseline gap-1">
<h3 class="d-inline-block me-1" data-balance="@Model.TotalOnchain" data-sensitive>@Model.TotalOnchain</h3>
<span class="text-secondary fw-semibold text-nowrap">
<span class="currency">@Model.CryptoCode</span> on-chain
@ViewLocalizer["<span class=\"currency\">{0}</span> on-chain", Model.CryptoCode]
</span>
</div>
<div class="balance-details collapse" id="balanceDetailsOnchain">
@ -98,7 +102,7 @@
@Model.Balance.OnchainBalance.Confirmed
</span>
<span class="text-secondary text-nowrap">
<span class="currency">@Model.CryptoCode</span> confirmed
@ViewLocalizer["<span class=\"currency\">{0}</span> confirmed", Model.CryptoCode]
</span>
</div>
}
@ -109,7 +113,7 @@
@Model.Balance.OnchainBalance.Unconfirmed
</span>
<span class="text-secondary text-nowrap">
<span class="currency">@Model.CryptoCode</span> unconfirmed
@ViewLocalizer["<span class=\"currency\">{0}</span> unconfirmed", Model.CryptoCode]
</span>
</div>
}
@ -120,7 +124,7 @@
@Model.Balance.OnchainBalance.Reserved
</span>
<span class="text-secondary text-nowrap">
<span class="currency">@Model.CryptoCode</span> reserved
@ViewLocalizer["<span class=\"currency\">{0}</span> reserved", Model.CryptoCode]
</span>
</div>
}
@ -128,62 +132,53 @@
</div>
}
</div>
@if (Model.Balance.OffchainBalance != null && Model.Balance.OnchainBalance != null)
<div class="d-flex flex-wrap align-items-center justify-content-between gap-3 @(Model.Series != null ? "my-3" : "mt-3")">
@if (Model.Balance.OffchainBalance != null && Model.Balance.OnchainBalance != null)
{
<button class="d-inline-flex align-items-center btn btn-link text-primary fw-semibold p-0 ms-n1" type="button" data-bs-toggle="collapse" data-bs-target=".balance-details" aria-expanded="false" aria-controls="balanceDetailsOffchain balanceDetailsOnchain">
<vc:icon symbol="caret-down" />
<span class="ms-1" text-translate="true">Details</span>
</button>
}
@if (Model.Series != null)
{
<div class="btn-group only-for-js mt-1" role="group" aria-label="Period">
<input type="radio" class="btn-check" name="StoreLightningBalancePeriod-@Model.StoreId" id="StoreLightningBalancePeriodWeek-@Model.StoreId" value="@HistogramType.Week" @(Model.Type == HistogramType.Week ? "checked" : "")>
<label class="btn btn-link" for="StoreLightningBalancePeriodWeek-@Model.StoreId">1W</label>
<input type="radio" class="btn-check" name="StoreLightningBalancePeriod-@Model.StoreId" id="StoreLightningBalancePeriodMonth-@Model.StoreId" value="@HistogramType.Month" @(Model.Type == HistogramType.Month ? "checked" : "")>
<label class="btn btn-link" for="StoreLightningBalancePeriodMonth-@Model.StoreId">1M</label>
<input type="radio" class="btn-check" name="StoreLightningBalancePeriod-@Model.StoreId" id="StoreLightningBalancePeriodYear-@Model.StoreId" value="@HistogramType.Year" @(Model.Type == HistogramType.Year ? "checked" : "")>
<label class="btn btn-link" for="StoreLightningBalancePeriodYear-@Model.StoreId">1Y</label>
</div>
}
</div>
@if (Model.Series != null)
{
<button class="d-inline-flex align-items-center btn btn-link text-primary fw-semibold p-0 mt-3 ms-n1" type="button" data-bs-toggle="collapse" data-bs-target=".balance-details" aria-expanded="false" aria-controls="balanceDetailsOffchain balanceDetailsOnchain">
<vc:icon symbol="caret-down"/>
<span class="ms-1">Details</span>
</button>
<div class="ct-chart"></div>
<template>
@Safe.Json(Model)
</template>
}
}
else
{
<div class="loading d-flex justify-content-center p-3">
<div class="spinner-border text-light" role="status">
<span class="visually-hidden">Loading...</span>
<span class="visually-hidden" text-translate="true">Loading...</span>
</div>
</div>
<script src="~/Components/StoreLightningBalance/Default.cshtml.js" asp-append-version="true"></script>
<script>
(async () => {
const url = @Safe.Json(Url.Action("LightningBalance", "UIStores", new { storeId = Model.Store.Id, cryptoCode = Model.CryptoCode }));
const storeId = @Safe.Json(Model.Store.Id);
const url = @Safe.Json(Model.DataUrl);
const storeId = @Safe.Json(Model.StoreId);
const response = await fetch(url);
if (response.ok) {
document.getElementById(`StoreLightningBalance-${storeId}`).outerHTML = await response.text();
const data = document.querySelector(`#StoreLightningBalance-${storeId} template`);
if (data) window.storeLightningBalance.dataLoaded(JSON.parse(data.innerHTML));
}
})();
</script>
}
</div>
<script>
(function () {
const storeId = @Safe.Json(Model.Store.Id);
const cryptoCode = @Safe.Json(Model.CryptoCode);
const defaultCurrency = @Safe.Json(Model.DefaultCurrency);
const divisibility = @Safe.Json(Model.CurrencyData.Divisibility);
const id = `StoreLightningBalance-${storeId}`;
const render = rate => {
const currency = rate ? defaultCurrency : cryptoCode;
document.querySelectorAll(`#${id} .currency`).forEach(c => c.innerText = currency)
document.querySelectorAll(`#${id} [data-balance]`).forEach(c => {
const value = Number.parseFloat(c.dataset.balance);
c.innerText = rate
? DashboardUtils.displayDefaultCurrency(value, rate, currency, divisibility)
: value
});
};
document.addEventListener('DOMContentLoaded', () => {
delegate('change', `#${id} .currency-toggle input`, async e => {
const { target } = e;
if (target.value === defaultCurrency) {
const rate = await DashboardUtils.fetchRate(`${cryptoCode}_${defaultCurrency}`);
if (rate) render(rate);
} else {
render(null);
}
});
});
})();
</script>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,28 +1,27 @@
@using BTCPayServer.Client.Models
@using BTCPayServer.Services
@using BTCPayServer.Services.Invoices
@inject DisplayFormatter DisplayFormatter
@model BTCPayServer.Components.StoreRecentInvoices.StoreRecentInvoicesViewModel
<div class="widget store-recent-invoices" id="StoreRecentInvoices-@Model.Store.Id">
<div class="widget store-recent-invoices" id="StoreRecentInvoices-@Model.StoreId">
<header>
<h3>Recent Invoices</h3>
<h3 text-translate="true">Recent Invoices</h3>
@if (Model.Invoices.Any())
{
<a asp-controller="UIInvoice" asp-action="ListInvoices" asp-route-storeId="@Model.Store.Id">View All</a>
<a asp-controller="UIInvoice" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" text-translate="true">View All</a>
}
</header>
@if (Model.InitialRendering)
{
<div class="loading d-flex justify-content-center p-3">
<div class="spinner-border text-light" role="status">
<span class="visually-hidden">Loading...</span>
<span class="visually-hidden" text-translate="true">Loading...</span>
</div>
</div>
<script>
(async () => {
const url = @Safe.Json(Url.Action("RecentInvoices", "UIStores", new { storeId = Model.Store.Id, cryptoCode = Model.CryptoCode }));
const storeId = @Safe.Json(Model.Store.Id);
const url = @Safe.Json(Url.Action("RecentInvoices", "UIStores", new { storeId = Model.StoreId, cryptoCode = Model.CryptoCode }));
const storeId = @Safe.Json(Model.StoreId);
const response = await fetch(url);
if (response.ok) {
document.getElementById(`StoreRecentInvoices-${storeId}`).outerHTML = await response.text();
@ -36,10 +35,10 @@
<table class="table table-hover mb-0">
<thead>
<tr>
<th class="w-125px">Date</th>
<th class="text-nowrap">Invoice Id</th>
<th>Status</th>
<th class="text-end">Amount</th>
<th class="w-125px" text-translate="true">Date</th>
<th class="text-nowrap" text-translate="true">Invoice Id</th>
<th text-translate="true">Status</th>
<th class="text-end" text-translate="true">Amount</th>
</tr>
</thead>
<tbody>
@ -65,10 +64,10 @@
}
else
{
<p class="text-secondary my-3">
<p class="text-secondary my-3" text-translate="true">
There are no recent invoices.
</p>
<a asp-controller="UIInvoice" asp-action="CreateInvoice" asp-route-storeId="@Model.Store.Id" class="fw-semibold">
<a asp-controller="UIInvoice" asp-action="CreateInvoice" asp-route-storeId="@Model.StoreId" class="fw-semibold" text-translate="true">
Create Invoice
</a>
}

View File

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

View File

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

View File

@ -2,25 +2,25 @@
@inject DisplayFormatter DisplayFormatter
@model BTCPayServer.Components.StoreRecentTransactions.StoreRecentTransactionsViewModel
<div class="widget store-recent-transactions" id="StoreRecentTransactions-@Model.Store.Id">
<div class="widget store-recent-transactions" id="StoreRecentTransactions-@Model.StoreId">
<header>
<h3>Recent Transactions</h3>
<h3 text-translate="true">Recent Transactions</h3>
@if (Model.Transactions.Any())
{
<a asp-controller="UIWallets" asp-action="WalletTransactions" asp-route-walletId="@Model.WalletId">View All</a>
<a asp-controller="UIWallets" asp-action="WalletTransactions" asp-route-walletId="@Model.WalletId" text-translate="true">View All</a>
}
</header>
@if (Model.InitialRendering)
{
<div class="loading d-flex justify-content-center p-3">
<div class="spinner-border text-light" role="status">
<span class="visually-hidden">Loading...</span>
<span class="visually-hidden" text-translate="true">Loading...</span>
</div>
</div>
<script>
(async () => {
const url = @Safe.Json(Url.Action("RecentTransactions", "UIStores", new { storeId = Model.Store.Id, cryptoCode = Model.CryptoCode }));
const storeId = @Safe.Json(Model.Store.Id);
const url = @Safe.Json(Url.Action("RecentTransactions", "UIStores", new { storeId = Model.StoreId, cryptoCode = Model.CryptoCode }));
const storeId = @Safe.Json(Model.StoreId);
const response = await fetch(url);
if (response.ok) {
document.getElementById(`StoreRecentTransactions-${storeId}`).outerHTML = await response.text();
@ -34,10 +34,10 @@
<table class="table table-hover mb-0">
<thead>
<tr>
<th class="w-125px">Date</th>
<th>Transaction</th>
<th>Labels</th>
<th class="text-end">Amount</th>
<th class="w-125px" text-translate="true">Date</th>
<th text-translate="true">Transaction</th>
<th text-translate="true">Labels</th>
<th class="text-end" text-translate="true">Amount</th>
</tr>
</thead>
<tbody>
@ -90,7 +90,7 @@
}
else
{
<p class="text-secondary mt-3 mb-0">
<p class="text-secondary mt-3 mb-0" text-translate="true">
There are no recent transactions.
</p>
}

View File

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

View File

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

View File

@ -17,9 +17,9 @@
<small class="badge bg-warning rounded-pill ms-1 ms-sm-0" title="@type">@displayType</small>
}
}
private static string StoreName(string title)
private string StoreName(string title)
{
return string.IsNullOrEmpty(title) ? "Unnamed Store" : title;
return string.IsNullOrEmpty(title) ? StringLocalizer["Unnamed Store"] : title;
}
#pragma warning restore 1998
}
@ -44,7 +44,7 @@ else
{
<vc:icon symbol="nav-store"/>
}
<span>@(Model.CurrentStoreId == null ? "Select Store" : Model.CurrentDisplayName)</span>
<span>@(Model.CurrentStoreId == null ? StringLocalizer["Select Store"] : Model.CurrentDisplayName)</span>
<vc:icon symbol="caret-down"/>
</button>
<ul id="StoreSelectorMenu" class="dropdown-menu" aria-labelledby="StoreSelectorToggle">
@ -58,15 +58,15 @@ else
{
<li><hr class="dropdown-divider"></li>
}
<li><a asp-controller="UIUserStores" asp-action="CreateStore" class="dropdown-item @ViewData.ActivePageClass(StoreNavPages.Create)" id="StoreSelectorCreate">Create Store</a></li>
<li><a asp-controller="UIUserStores" asp-action="CreateStore" class="dropdown-item @ViewData.ActivePageClass(StoreNavPages.Create)" id="StoreSelectorCreate" text-translate="true">Create Store</a></li>
@if (Model.ArchivedCount > 0)
{
<li><hr class="dropdown-divider"></li>
<li><a asp-controller="UIUserStores" asp-action="ListStores" asp-route-archived="true" class="dropdown-item @ViewData.ActivePageClass(StoreNavPages.Index)" id="StoreSelectorArchived">@Model.ArchivedCount Archived Store@(Model.ArchivedCount == 1 ? "" : "s")</a></li>
<li><a asp-controller="UIUserStores" asp-action="ListStores" asp-route-archived="true" class="dropdown-item @ViewData.ActivePageClass(StoreNavPages.Index)" id="StoreSelectorArchived">@(Model.ArchivedCount == 1 ? StringLocalizer["{0} Archived Store", Model.ArchivedCount] : StringLocalizer["{0} Archived Stores", Model.ArchivedCount])</a></li>
}
@*
<li permission="@Policies.CanModifyServerSettings"><hr class="dropdown-divider"></li>
<li permission="@Policies.CanModifyServerSettings"><a asp-controller="UIServer" asp-action="ListStores" class="dropdown-item @ViewData.ActivePageClass(ServerNavPages.Stores)" id="StoreSelectorAdminStores">Admin Store Overview</a></li>
<li permission="@Policies.CanModifyServerSettings"><a asp-controller="UIServer" asp-action="ListStores" class="dropdown-item @ViewData.ActivePageClass(ServerNavPages.Stores)" id="StoreSelectorAdminStores" text-translate="true">Admin Store Overview</a></li>
*@
</ul>
</div>

View File

@ -1,16 +1,11 @@
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client;
using BTCPayServer.Data;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace BTCPayServer.Components.StoreSelector
{
@ -38,13 +33,11 @@ namespace BTCPayServer.Components.StoreSelector
var archivedCount = stores.Count(s => s.Archived);
var options = stores
.Where(store => !store.Archived)
.Select(store =>
new StoreSelectorOption
.Select(store => new StoreSelectorOption
{
Text = store.StoreName,
Value = store.Id,
Selected = store.Id == currentStore?.Id,
Store = store
Selected = store.Id == currentStore?.Id
})
.OrderBy(s => s.Text)
.ToList();

View File

@ -1,5 +1,4 @@
using System.Collections.Generic;
using BTCPayServer.Data;
namespace BTCPayServer.Components.StoreSelector
{
@ -17,6 +16,5 @@ namespace BTCPayServer.Components.StoreSelector
public bool Selected { get; set; }
public string Text { get; set; }
public string Value { get; set; }
public StoreData Store { get; set; }
}
}

View File

@ -1,10 +1,10 @@
@using BTCPayServer.Services.Wallets
@using BTCPayServer.Payments
@using BTCPayServer.Abstractions.TagHelpers
@using BTCPayServer.Client.Models
@using BTCPayServer.TagHelpers
@model BTCPayServer.Components.StoreWalletBalance.StoreWalletBalanceViewModel
@inject BTCPayNetworkProvider NetworkProvider
<div id="StoreWalletBalance-@Model.Store.Id" class="widget store-wallet-balance">
<div id="StoreWalletBalance-@Model.StoreId" class="widget store-wallet-balance">
<div class="d-flex gap-3 align-items-center justify-content-between mb-2">
<h6>Wallet Balance</h6>
<h6 text-translate="true">Wallet Balance</h6>
@if (Model.CryptoCode != Model.DefaultCurrency)
{
<div class="btn-group btn-group-sm gap-0 currency-toggle" role="group">
@ -26,12 +26,12 @@
@if (Model.Series != null)
{
<div class="btn-group only-for-js mt-1" role="group" aria-label="Period">
<input type="radio" class="btn-check" name="StoreWalletBalancePeriod-@Model.Store.Id" id="StoreWalletBalancePeriodWeek-@Model.Store.Id" value="@WalletHistogramType.Week" @(Model.Type == WalletHistogramType.Week ? "checked" : "")>
<label class="btn btn-link" for="StoreWalletBalancePeriodWeek-@Model.Store.Id">1W</label>
<input type="radio" class="btn-check" name="StoreWalletBalancePeriod-@Model.Store.Id" id="StoreWalletBalancePeriodMonth-@Model.Store.Id" value="@WalletHistogramType.Month" @(Model.Type == WalletHistogramType.Month ? "checked" : "")>
<label class="btn btn-link" for="StoreWalletBalancePeriodMonth-@Model.Store.Id">1M</label>
<input type="radio" class="btn-check" name="StoreWalletBalancePeriod-@Model.Store.Id" id="StoreWalletBalancePeriodYear-@Model.Store.Id" value="@WalletHistogramType.Year" @(Model.Type == WalletHistogramType.Year ? "checked" : "")>
<label class="btn btn-link" for="StoreWalletBalancePeriodYear-@Model.Store.Id">1Y</label>
<input type="radio" class="btn-check" name="StoreWalletBalancePeriod-@Model.StoreId" id="StoreWalletBalancePeriodWeek-@Model.StoreId" value="@HistogramType.Week" @(Model.Type == HistogramType.Week ? "checked" : "")>
<label class="btn btn-link" for="StoreWalletBalancePeriodWeek-@Model.StoreId">1W</label>
<input type="radio" class="btn-check" name="StoreWalletBalancePeriod-@Model.StoreId" id="StoreWalletBalancePeriodMonth-@Model.StoreId" value="@HistogramType.Month" @(Model.Type == HistogramType.Month ? "checked" : "")>
<label class="btn btn-link" for="StoreWalletBalancePeriodMonth-@Model.StoreId">1M</label>
<input type="radio" class="btn-check" name="StoreWalletBalancePeriod-@Model.StoreId" id="StoreWalletBalancePeriodYear-@Model.StoreId" value="@HistogramType.Year" @(Model.Type == HistogramType.Year ? "checked" : "")>
<label class="btn btn-link" for="StoreWalletBalancePeriodYear-@Model.StoreId">1Y</label>
</div>
}
</header>
@ -39,10 +39,10 @@
{
<div class="ct-chart"></div>
}
else if (Model.Store.GetPaymentMethodConfig(PaymentTypes.CHAIN.GetPaymentMethodId(Model.CryptoCode)) is null)
else if (Model.MissingWalletConfig)
{
<p>
We would like to show you a chart of your balance but you have not yet <a href="@Url.Action("SetupWallet", "UIStores", new {storeId = Model.Store.Id, cryptoCode = Model.CryptoCode})">configured a wallet</a>.
We would like to show you a chart of your balance but you have not yet <a href="@Url.Action("SetupWallet", "UIStores", new { storeId = Model.StoreId, cryptoCode = Model.CryptoCode })">configured a wallet</a>.
</p>
}
else
@ -55,66 +55,65 @@
}
<script>
(function () {
const storeId = @Safe.Json(Model.Store.Id);
const storeId = @Safe.Json(Model.StoreId);
const cryptoCode = @Safe.Json(Model.CryptoCode);
const defaultCurrency = @Safe.Json(Model.DefaultCurrency);
const divisibility = @Safe.Json(Model.CurrencyData.Divisibility);
let data = { series: @Safe.Json(Model.Series), labels: @Safe.Json(Model.Labels), balance: @Safe.Json(Model.Balance) };
let rate = null;
const id = `StoreWalletBalance-${storeId}`;
const baseUrl = @Safe.Json(Url.Action("WalletHistogram", "UIWallets", new { walletId = Model.WalletId, type = WalletHistogramType.Week }));
const valueTransform = value => rate
? DashboardUtils.displayDefaultCurrency(value, rate, defaultCurrency, divisibility).toString()
: value
const baseUrl = @Safe.Json(Url.Action("WalletHistogram", "UIWallets", new { walletId = Model.WalletId, type = HistogramType.Week }));
const valueTransform = value => rate ? DashboardUtils.displayDefaultCurrency(value, rate, defaultCurrency, divisibility) : value
const labelCount = 6
const tooltip = Chartist.plugins.tooltip2({
template: '<div class="chartist-tooltip-value">{{value}}</div><div class="chartist-tooltip-line"></div>',
offset: {
x: 0,
y: -16
},
valueTransformFunction(value, label) {
return valueTransform(value) + ' ' + (rate ? defaultCurrency : cryptoCode)
}
})
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat
const dateFormatter = new Intl.DateTimeFormat('default', { month: 'short', day: 'numeric' })
const chartOpts = {
fullWidth: true,
showArea: true,
axisY: {
labelInterpolationFnc: valueTransform
}
showLabel: false,
offset: 0
},
plugins: [tooltip]
};
const render = data => {
let { series, labels } = data;
const currency = rate ? defaultCurrency : cryptoCode;
document.querySelectorAll(`#${id} .currency`).forEach(c => c.innerText = currency)
document.querySelectorAll(`#${id} .currency`).forEach(c => c.innerText = currency)
document.querySelectorAll(`#${id} [data-balance]`).forEach(c => {
const value = Number.parseFloat(c.dataset.balance);
c.innerText = valueTransform(value)
});
if (!series) return;
const min = Math.min(...series);
const max = Math.max(...series);
const low = Math.max(min - ((max - min) / 5), 0);
const tooltip = Chartist.plugins.tooltip2({
template: '<div class="chartist-tooltip-value">{{value}}</div><div class="chartist-tooltip-line"></div>',
offset: {
x: 0,
y: -16
},
valueTransformFunction: valueTransform
})
const renderOpts = Object.assign({}, chartOpts, { low, plugins: [tooltip] });
const chart = new Chartist.Line(`#${id} .ct-chart`, {
labels,
const renderOpts = Object.assign({}, chartOpts, { low, axisX: {
labelInterpolationFnc(date, i) {
return i % labelEvery === 0 ? dateFormatter.format(new Date(date)) : null
}
} });
const pointCount = series.length;
const labelEvery = pointCount / labelCount;
new Chartist.Line(`#${id} .ct-chart`, {
labels: labels,
series: [series]
}, renderOpts);
// prevent y-axis labels from getting cut off
window.setTimeout(() => {
const yLabels = [...document.querySelectorAll('.ct-label.ct-vertical.ct-start')];
if (yLabels) {
const width = Math.max(...(yLabels.map(l => l.innerText.length * 7.5)));
const opts = Object.assign({}, renderOpts, {
axisY: Object.assign({}, renderOpts.axisY, { offset: width })
});
chart.update(null, opts);
}
}, 0)
};
const update = async type => {
const url = baseUrl.replace(/\/week$/gi, `/${type}`);
const response = await fetch(url);
@ -123,10 +122,10 @@
render(data);
}
};
render(data);
document.addEventListener('DOMContentLoaded', () => {
function addEventListeners() {
delegate('change', `#${id} [name="StoreWalletBalancePeriod-${storeId}"]`, async e => {
const type = e.target.value;
await update(type);
@ -141,7 +140,13 @@
render(data);
}
});
});
}
if (document.readyState === "loading") {
window.addEventListener("DOMContentLoaded", addEventListeners);
} else {
addEventListeners();
}
})();
</script>
</div>

View File

@ -1,29 +1,21 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Data.Common;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
using Dapper;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBXplorer;
using NBXplorer.Client;
using StoreData = BTCPayServer.Data.StoreData;
namespace BTCPayServer.Components.StoreWalletBalance;
public class StoreWalletBalance : ViewComponent
{
private const WalletHistogramType DefaultType = WalletHistogramType.Week;
private const HistogramType DefaultType = HistogramType.Week;
private readonly StoreRepository _storeRepo;
private readonly CurrencyNameTable _currencies;
@ -57,7 +49,7 @@ public class StoreWalletBalance : ViewComponent
var vm = new StoreWalletBalanceViewModel
{
Store = store,
StoreId = store.Id,
CryptoCode = cryptoCode,
CurrencyData = _currencies.GetCurrencyData(defaultCurrency, true),
DefaultCurrency = defaultCurrency,
@ -82,6 +74,10 @@ public class StoreWalletBalance : ViewComponent
var balance = await wallet.GetBalance(derivation.AccountDerivation, cts.Token);
vm.Balance = balance.Available.GetValue(network);
}
else
{
vm.MissingWalletConfig = true;
}
}
return View(vm);

View File

@ -1,19 +1,20 @@
using System;
using System.Collections.Generic;
using BTCPayServer.Data;
using BTCPayServer.Client.Models;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Wallets;
namespace BTCPayServer.Components.StoreWalletBalance;
public class StoreWalletBalanceViewModel
{
public string StoreId { get; set; }
public decimal? Balance { get; set; }
public string CryptoCode { get; set; }
public string DefaultCurrency { get; set; }
public CurrencyData CurrencyData { get; set; }
public StoreData Store { get; set; }
public WalletId WalletId { get; set; }
public WalletHistogramType Type { get; set; }
public IList<string> Labels { get; set; }
public HistogramType Type { get; set; }
public IList<DateTimeOffset> Labels { get; set; }
public IList<decimal> Series { get; set; }
public bool MissingWalletConfig { get; set; }
}

View File

@ -3,8 +3,8 @@
<div class="btcpay-theme-switch @Model.CssClass">
<span class="btcpay-theme-switch-label" text-translate="true">Theme</span>
<div class="btcpay-theme-switch-themes">
<button type="button" title="System" data-theme="system"><vc:icon symbol="themes-system"/></button>
<button type="button" title="Light" data-theme="light"><vc:icon symbol="themes-light"/></button>
<button type="button" title="Dark" data-theme="dark"><vc:icon symbol="themes-dark"/></button>
<button type="button" title="@StringLocalizer["System"]" data-theme="system"><vc:icon symbol="themes-system"/></button>
<button type="button" title="@StringLocalizer["Light"]" data-theme="light"><vc:icon symbol="themes-light"/></button>
<button type="button" title="@StringLocalizer["Dark"]" data-theme="dark"><vc:icon symbol="themes-dark"/></button>
</div>
</div>

View File

@ -34,10 +34,8 @@ public class TruncateCenter : ViewComponent
};
if (!vm.IsVue)
{
vm.Start = vm.IsTruncated ? text[..padding] : text;
vm.Start = vm.IsTruncated && !vm.Elastic ? $"{text[..padding]}…" : text;
vm.End = vm.IsTruncated ? text[^padding..] : string.Empty;
if (!vm.Elastic && vm.IsTruncated)
vm.Start = $"{vm.Start}…";
}
return View(vm);
}

View File

@ -1,27 +1,15 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewComponents;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using NBitcoin;
using NBitcoin.Secp256k1;
using Microsoft.Extensions.Localization;
namespace BTCPayServer.Components.WalletNav
{
@ -33,6 +21,7 @@ namespace BTCPayServer.Components.WalletNav
private readonly CurrencyNameTable _currencies;
private readonly DefaultRulesCollection _defaultRules;
private readonly RateFetcher _rateFetcher;
private IStringLocalizer StringLocalizer { get; }
public WalletNav(
BTCPayWalletProvider walletProvider,
@ -40,6 +29,7 @@ namespace BTCPayServer.Components.WalletNav
UIWalletsController walletsController,
CurrencyNameTable currencies,
DefaultRulesCollection defaultRules,
IStringLocalizer stringLocalizer,
RateFetcher rateFetcher)
{
_walletProvider = walletProvider;
@ -48,6 +38,7 @@ namespace BTCPayServer.Components.WalletNav
_currencies = currencies;
_defaultRules = defaultRules;
_rateFetcher = rateFetcher;
StringLocalizer = stringLocalizer;
}
public async Task<IViewComponentResult> InvokeAsync(WalletId walletId)
@ -71,7 +62,7 @@ namespace BTCPayServer.Components.WalletNav
Network = network,
Balance = balance.ShowMoney(network),
DefaultCurrency = defaultCurrency,
Label = derivation?.Label ?? $"{store.StoreName} {walletId.CryptoCode} Wallet"
Label = derivation?.Label ?? $"{store.StoreName} {StringLocalizer["{0} Wallet", walletId.CryptoCode]}"
};
if (defaultCurrency != network.CryptoCode)

View File

@ -3,17 +3,21 @@ using System;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Plugins.Crowdfund;
using BTCPayServer.Plugins.PointOfSale;
using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
@ -28,21 +32,26 @@ namespace BTCPayServer.Controllers.Greenfield
public class GreenfieldAppsController : ControllerBase
{
private readonly AppService _appService;
private readonly UriResolver _uriResolver;
private readonly StoreRepository _storeRepository;
private readonly CurrencyNameTable _currencies;
private readonly UserManager<ApplicationUser> _userManager;
private readonly IFileService _fileService;
public GreenfieldAppsController(
AppService appService,
UriResolver uriResolver,
StoreRepository storeRepository,
BTCPayNetworkProvider btcPayNetworkProvider,
CurrencyNameTable currencies,
IFileService fileService,
UserManager<ApplicationUser> userManager
)
{
_appService = appService;
_uriResolver = uriResolver;
_storeRepository = storeRepository;
_currencies = currencies;
_fileService = fileService;
_userManager = userManager;
}
@ -72,12 +81,12 @@ namespace BTCPayServer.Controllers.Greenfield
Archived = request.Archived ?? false
};
var settings = ToCrowdfundSettings(request, new CrowdfundSettings { Title = request.Title ?? request.AppName });
var settings = ToCrowdfundSettings(request);
appData.SetSettings(settings);
await _appService.UpdateOrCreateApp(appData);
return Ok(ToCrowdfundModel(appData));
var model = await ToCrowdfundModel(appData);
return Ok(model);
}
[HttpPost("~/api/v1/stores/{storeId}/apps/pos")]
@ -208,7 +217,8 @@ namespace BTCPayServer.Controllers.Greenfield
return AppNotFound();
}
return Ok(ToCrowdfundModel(app));
var model = await ToCrowdfundModel(app);
return Ok(model);
}
[HttpDelete("~/api/v1/apps/{appId}")]
@ -216,10 +226,7 @@ namespace BTCPayServer.Controllers.Greenfield
public async Task<IActionResult> DeleteApp(string appId)
{
var app = await _appService.GetApp(appId, null, includeArchived: true);
if (app == null)
{
return AppNotFound();
}
if (app == null) return AppNotFound();
await _appService.DeleteApp(app);
@ -249,13 +256,64 @@ namespace BTCPayServer.Controllers.Greenfield
var items = stats.GetRange(offset, max);
return Ok(items);
}
[HttpPost("~/api/v1/apps/{appId}/image")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public async Task<IActionResult> UploadAppItemImage(string appId, IFormFile? file)
{
var app = await _appService.GetApp(appId, null, includeArchived: true);
var userId = _userManager.GetUserId(User);
if (app == null || userId == null) return AppNotFound();
UploadImageResultModel? upload = null;
if (file is null)
ModelState.AddModelError(nameof(file), "Invalid file");
else
{
upload = await _fileService.UploadImage(file, userId, 500_000);
if (!upload.Success)
ModelState.AddModelError(nameof(file), upload.Response);
}
if (!ModelState.IsValid)
return this.CreateValidationError(ModelState);
try
{
var storedFile = upload!.StoredFile!;
var fileData = new FileData
{
Id = storedFile.Id,
UserId = storedFile.ApplicationUserId,
Url = await _fileService.GetFileUrl(Request.GetAbsoluteRootUri(), storedFile.Id),
OriginalName = storedFile.FileName,
StorageName = storedFile.StorageFileName,
CreatedAt = storedFile.Timestamp
};
return Ok(fileData);
}
catch (Exception e)
{
return this.CreateAPIError(404, "file-upload-failed", e.Message);
}
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpDelete("~/api/v1/apps/{appId}/image/{fileId}")]
public async Task<IActionResult> DeleteAppItemImage(string appId, string fileId)
{
var app = await _appService.GetApp(appId, null, includeArchived: true);
var userId = _userManager.GetUserId(User);
if (app == null || userId == null) return AppNotFound();
if (!string.IsNullOrEmpty(fileId)) await _fileService.RemoveFile(fileId, userId);
return Ok();
}
private IActionResult AppNotFound()
{
return this.CreateAPIError(404, "app-not-found", "The app with specified ID was not found");
}
private CrowdfundSettings ToCrowdfundSettings(CrowdfundAppRequest request, CrowdfundSettings settings)
private CrowdfundSettings ToCrowdfundSettings(CrowdfundAppRequest request)
{
var parsedSounds = ValidateStringArray(request.Sounds);
var parsedColors = ValidateStringArray(request.AnimationColors);
@ -271,7 +329,7 @@ namespace BTCPayServer.Controllers.Greenfield
Description = request.Description?.Trim(),
EndDate = request.EndDate?.UtcDateTime,
TargetAmount = request.TargetAmount,
MainImageUrl = request.MainImageUrl?.Trim(),
MainImageUrl = request.MainImageUrl == null ? null : UnresolvedUri.Create(request.MainImageUrl),
NotificationUrl = request.NotificationUrl?.Trim(),
Tagline = request.Tagline?.Trim(),
PerksTemplate = request.PerksTemplate is not null ? AppService.SerializeTemplate(AppService.Parse(request.PerksTemplate.Trim())) : null,
@ -350,6 +408,7 @@ namespace BTCPayServer.Controllers.Greenfield
{
var settings = appData.GetSettings<PointOfSaleSettings>();
Enum.TryParse<PosViewType>(settings.DefaultView.ToString(), true, out var defaultView);
var items = AppService.Parse(settings.Template);
return new PointOfSaleAppData
{
@ -377,16 +436,7 @@ namespace BTCPayServer.Controllers.Greenfield
RedirectUrl = settings.RedirectUrl,
Description = settings.Description,
RedirectAutomatically = settings.RedirectAutomatically,
Items = JsonConvert.DeserializeObject(
JsonConvert.SerializeObject(
AppService.Parse(settings.Template),
new JsonSerializerSettings
{
ContractResolver =
new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver()
}
)
)
Items = items
};
}
@ -411,10 +461,11 @@ namespace BTCPayServer.Controllers.Greenfield
}
}
private CrowdfundAppData ToCrowdfundModel(AppData appData)
private async Task<CrowdfundAppData> ToCrowdfundModel(AppData appData)
{
var settings = appData.GetSettings<CrowdfundSettings>();
Enum.TryParse<CrowdfundResetEvery>(settings.ResetEvery.ToString(), true, out var resetEvery);
var perks = AppService.Parse(settings.PerksTemplate);
return new CrowdfundAppData
{
@ -432,7 +483,7 @@ namespace BTCPayServer.Controllers.Greenfield
Description = settings.Description,
EndDate = settings.EndDate,
TargetAmount = settings.TargetAmount,
MainImageUrl = settings.MainImageUrl,
MainImageUrl = settings.MainImageUrl == null ? null : await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), settings.MainImageUrl),
NotificationUrl = settings.NotificationUrl,
Tagline = settings.Tagline,
DisqusEnabled = settings.DisqusEnabled,
@ -446,15 +497,7 @@ namespace BTCPayServer.Controllers.Greenfield
SortPerksByPopularity = settings.SortPerksByPopularity,
Sounds = settings.Sounds,
AnimationColors = settings.AnimationColors,
Perks = JsonConvert.DeserializeObject(
JsonConvert.SerializeObject(
AppService.Parse(settings.PerksTemplate),
new JsonSerializerSettings
{
ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver()
}
)
)
Perks = perks
};
}

View File

@ -51,6 +51,7 @@ namespace BTCPayServer.Controllers.Greenfield
private readonly Dictionary<PaymentMethodId, IPaymentLinkExtension> _paymentLinkExtensions;
private readonly PayoutMethodHandlerDictionary _payoutHandlers;
private readonly PaymentMethodHandlerDictionary _handlers;
private readonly BTCPayNetworkProvider _networkProvider;
private readonly DefaultRulesCollection _defaultRules;
public LanguageService LanguageService { get; }
@ -65,6 +66,7 @@ namespace BTCPayServer.Controllers.Greenfield
Dictionary<PaymentMethodId, IPaymentLinkExtension> paymentLinkExtensions,
PayoutMethodHandlerDictionary payoutHandlers,
PaymentMethodHandlerDictionary handlers,
BTCPayNetworkProvider networkProvider,
DefaultRulesCollection defaultRules)
{
_invoiceController = invoiceController;
@ -79,6 +81,7 @@ namespace BTCPayServer.Controllers.Greenfield
_paymentLinkExtensions = paymentLinkExtensions;
_payoutHandlers = payoutHandlers;
_handlers = handlers;
_networkProvider = networkProvider;
_defaultRules = defaultRules;
LanguageService = languageService;
}
@ -338,6 +341,9 @@ namespace BTCPayServer.Controllers.Greenfield
}
PaymentPrompt? paymentPrompt = null;
PayoutMethodId? payoutMethodId = null;
if (request.PayoutMethodId is null)
request.PayoutMethodId = invoice.GetDefaultPaymentMethodId(store, _networkProvider)?.ToString();
if (request.PayoutMethodId is not null && PayoutMethodId.TryParse(request.PayoutMethodId, out payoutMethodId))
{
var supported = _payoutHandlers.GetSupportedPayoutMethods(store);

View File

@ -1,13 +1,10 @@
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Configuration;
using BTCPayServer.HostedServices;
using BTCPayServer.Lightning;
using BTCPayServer.Security;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Authorization;
@ -31,8 +28,9 @@ namespace BTCPayServer.Controllers.Greenfield
PoliciesSettings policiesSettings, LightningClientFactoryService lightningClientFactory,
IOptions<LightningNetworkOptions> lightningNetworkOptions,
IAuthorizationService authorizationService,
PaymentMethodHandlerDictionary handlers
) : base(policiesSettings, authorizationService, handlers)
PaymentMethodHandlerDictionary handlers,
LightningHistogramService lnHistogramService
) : base(policiesSettings, authorizationService, handlers, lnHistogramService)
{
_lightningClientFactory = lightningClientFactory;
_lightningNetworkOptions = lightningNetworkOptions;
@ -55,6 +53,14 @@ namespace BTCPayServer.Controllers.Greenfield
return base.GetBalance(cryptoCode, cancellationToken);
}
[Authorize(Policy = Policies.CanUseInternalLightningNode,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/server/lightning/{cryptoCode}/histogram")]
public override Task<IActionResult> GetHistogram(string cryptoCode, [FromQuery] HistogramType? type = null, CancellationToken cancellationToken = default)
{
return base.GetHistogram(cryptoCode, type, cancellationToken);
}
[Authorize(Policy = Policies.CanUseInternalLightningNode,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/server/lightning/{cryptoCode}/connect")]

View File

@ -1,8 +1,6 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
@ -34,7 +32,8 @@ namespace BTCPayServer.Controllers.Greenfield
IOptions<LightningNetworkOptions> lightningNetworkOptions,
LightningClientFactoryService lightningClientFactory, PaymentMethodHandlerDictionary handlers,
PoliciesSettings policiesSettings,
IAuthorizationService authorizationService) : base(policiesSettings, authorizationService, handlers)
IAuthorizationService authorizationService,
LightningHistogramService lnHistogramService) : base(policiesSettings, authorizationService, handlers, lnHistogramService)
{
_lightningNetworkOptions = lightningNetworkOptions;
_lightningClientFactory = lightningClientFactory;
@ -56,6 +55,13 @@ namespace BTCPayServer.Controllers.Greenfield
{
return base.GetBalance(cryptoCode, cancellationToken);
}
[Authorize(Policy = Policies.CanUseLightningNodeInStore, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/histogram")]
public override Task<IActionResult> GetHistogram(string cryptoCode, [FromQuery] HistogramType? type = null, CancellationToken cancellationToken = default)
{
return base.GetHistogram(cryptoCode, type, cancellationToken);
}
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
@ -64,6 +70,7 @@ namespace BTCPayServer.Controllers.Greenfield
{
return base.ConnectToNode(cryptoCode, request, cancellationToken);
}
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/channels")]
@ -71,6 +78,7 @@ namespace BTCPayServer.Controllers.Greenfield
{
return base.GetChannels(cryptoCode, cancellationToken);
}
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/channels")]

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