Compare commits

..

128 Commits

Author SHA1 Message Date
50efcefe8f Use the plugin builder website instead of docker to fetch plugins 2022-11-14 18:12:32 +09:00
c8a1024e24 Remove dead shitcoin MUE 2022-11-14 15:59:41 +09:00
9a2d2e2d89 Confirm modal: Prevent form submit without confirmation (#4262)
Fixes https://github.com/btcpayserver/btcpayserver/issues/4259
2022-11-13 12:38:13 +01:00
b7af234427 Payment Request: Fix invoice creation
Fix and test for a regression introduced with #4243: As the `PayPaymentRequest` action allows anonymous access, the `CookieAuthorizationHandler` isn;t run and hence the `GetCurrentStore` returns `null`.

This leads to an exception when creating the invoice. Store needs to be fetched seperately - like [before](4bbc7d9662 (diff-bdc264670a171e862d09fdf1a1c9f3ca14b41982a3c4c8e66d4f780cdde9f21dL241)).
2022-11-10 18:23:41 +01:00
a374e351e2 Use PluginLoader in the Plugin packer to prevent conflicts (#4277) 2022-11-09 15:28:16 +01:00
562f88555c Lightning: Better handling for non-public nodes (#4263)
Fixes #4246. 

`LightningLikePaymentHandler.GetNodeInfo` needed the `throws` argument to handle the cases as previously, otherwise the catch case in `ShowLightningNodeInfo` never occured.

State with this PR: A node can be available, but not have any public addresses. The latter will now be reported when testing the connection and on the public node info page.
2022-11-05 12:21:24 +01:00
167c5297fa Merge pull request #4268 from dennisreimann/supporters
Supporters: Remove Nomics; add SVGs for README
2022-11-05 12:17:11 +01:00
b281d09694 Bumping LND to 0.15.4-beta-1 (#4271)
* Bumping LND to 0.15.4-beta-1

* Bump LND in Altcoin docker-compose

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2022-11-05 12:15:04 +01:00
853a0ac5ea Remove old/unused images 2022-11-03 21:58:53 +01:00
ea948cfc3f Optimize SVGs 2022-11-03 21:55:11 +01:00
fdd13390fb Merge pull request #4267 from dennisreimann/fix-4266
Fix wording
2022-11-03 19:57:34 +01:00
b2f6f8b3c1 Supporters: Remove Nomics; add SVGs for README
The `img/readme` directory contains SVGs for the README, so that we can from now on use one markup for the supporters in all README files across our repositories. 

With these, we could finally get rid of the table layout for the supporters section in the README. This will make it much easier to maintain those.
2022-11-03 19:37:13 +01:00
cd12162b6f Fix wording
Closes #4266.
2022-11-03 13:13:03 +01:00
79717d1d64 Sync modal improvements (#4260) 2022-11-02 16:55:05 +01:00
e56cbf0baa Greenfield: Graceful return for in-flight HTLCs (#4252)
* Greenfield: Graceful return for in-flight HTLCs

Based on btcpayserver/BTCPayServer.Lightning#106 this closes #3781.

* Update descriptions
2022-11-02 21:03:34 +09:00
05232414ad Improve LN balance details toggle (#4253) 2022-11-02 18:48:23 +09:00
4bbc7d9662 [Greenfield] Can create an invoice for a payment request via Greenfield (#4243)
* [Greenfield] Can create an invoice for a payment request via Greenfield

* Add allowPendingInvoiceReuse so payment request invoices can be reused

* Add PayPaymentRequest to the LocalBTCPayServerClient

* Allow amount to be specified if same as PR amount
2022-11-02 18:41:19 +09:00
3805b7f287 Checkout v2 (#4157)
* Opt-in for new checkout

* Update wording

* Create invoice view update

* Remove jQuery from checkout testing code

* Checkout v2 basics

* WIP

* WIP 2

* Updates and fixes

* Updates

* Design updates

* More design updates

* Cheating and JS fixes

* Use checkout form id whenever invoices get created

* Improve email form handling

* Cleanups

* Payment method exclusion cases for Lightning and LNURL

TODO: Cases and implementation need to be discussed

* Introduce CheckoutType in API and replace UseNewCheckout in backend

Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2022-11-02 18:21:33 +09:00
63620409a9 Allow config to set default block explorer link (#4249)
This logic can potentially be inside BlockExplorerLinkStartupTask instead but it's not a big deal imo
2022-10-31 11:41:31 +09:00
ba423a79e3 Fix Public Node Info View
As the Node Info was used as an ID, this didn't work with IPv6 addresses, as those contain characters not suitable for HTML IDs.

Fixes #4245.

Also: Simplify the head section of that view by reusing the existing partial.
2022-10-30 11:23:09 +01:00
8806ba76eb Fix: On some circumstances, clicking on cancel invoice and pay button in PaymentRequest would throw an exception 2022-10-27 15:50:35 +09:00
9e73260230 Fix: Invoice's orderId equals to payreq id shouldn't appear part of the payreq 2022-10-27 13:17:18 +09:00
c0125b83d1 UI: Improve access token pairing (#4237)
Closes #4133.
2022-10-27 08:57:54 +09:00
1fa297fb73 Add donate link (#4239)
* Add donate link

Closes #4173.

* Simplify wording
2022-10-27 08:56:44 +09:00
57557748e2 Greenfield: Fix missing payment data (#4233)
* Greenfield: Fix missing payment data

Fixes #4229.

* Client: Return payment data from PayLightningInvoice

* Add test for PayLightningInvoice response
2022-10-27 08:56:24 +09:00
8b79212a6e [Greenfield] Fix: The route to connect to a peer lightning node was always crashing 2022-10-26 13:35:04 +09:00
f4af4ec4dc Fix OpenAPI 3.0 validation errors and warnings (#4235) 2022-10-25 20:37:36 +09:00
2e150f4bf4 Checkout: Fix Order ID text overflow (#4232)
Fixes #4230. Thanks for the pointer @handsomelatino!
2022-10-25 10:46:00 +09:00
4f4aa051c9 Add missing route parameter in /api/v1/stores/{storeId}/users swagger (See #4231) 2022-10-24 16:33:42 +09:00
da1dd7448e Add warnings in btcpay vault page for safari and brave (Fix https://github.com/btcpayserver/BTCPayServer.Vault/issues/54) (#4226)
* Add warnings in btcpay vault page for safari and brave (Fix https://github.com/btcpayserver/BTCPayServer.Vault/issues/54)

* Apply suggestions from code review

Co-authored-by: d11n <mail@dennisreimann.de>
2022-10-21 09:13:36 +02:00
0fd47eeee0 Asset-bundle cleanups (#4225)
Some cleanups in addition to #4222.
2022-10-21 09:17:06 +09:00
54c9d7283a Fix: PayjoinController could throw HTTP 500 of a few corner cases (#4215) 2022-10-20 11:19:48 +09:00
848db5f7de Remove the bundle minifier (#4222) 2022-10-20 11:17:42 +09:00
5fb32fe0e9 Remove some debug code 2022-10-19 13:06:15 +09:00
adf5b4ca0c Fix sync modal z-index 2022-10-18 21:00:54 +02:00
16bfb1dbfe Bump libraries (#4219) 2022-10-18 23:58:28 +09:00
e5421b8a9f Add script to create a regtest multisig wallet for testing (#4204)
* Add script to create a regtest multisig wallet for testing

* Unload wallets to prevent having to specify wallet in BTCPay, NBXplorer
2022-10-17 20:57:09 +09:00
f9f1a22e3b Store settings: Add branding options (#4131)
* Add logo upload

* Add brand color definition

* Cleanups

* Add logo to store selector

* Improve brand color handling

* Update color input

* Add logo dimensions hint

* Fixes

* Fix pattern and warning in js logs for color validation

* Fix condition, add test

Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2022-10-17 12:16:29 +02:00
9533809631 Bump NBitcoin (Fix #4212) 2022-10-17 18:47:49 +09:00
6d7c11f1b1 Greenfield: Get Lightning invoices (#4180)
* Greenfield: Get Lightning invoices

Matching the data added in btcpayserver/BTCPayServer.Lightning#98 and btcpayserver/BTCPayServer.Lightning#99.

* Small adjustments

Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2022-10-17 09:51:15 +02:00
0286c72256 Do not crash startup if ssh settings aren't correct 2022-10-14 14:51:05 +09:00
763aaa2926 UI: Fix checkbox flex-shrink (#4209)
Closes #4207.
2022-10-13 22:48:45 +09:00
ae4af7dd13 Bumping LND to 0.15.2-beta (#4211) 2022-10-13 09:40:07 +02:00
4ae2ea32e9 UI: Fix missing timezone in browser dates (#4210)
Fix for an issue brought up by @petzsch in todays dev meeting.
2022-10-13 09:29:30 +02:00
434298cba6 Greenfield: Store Rates Config (#3931)
* Greenfield: Store Rates Config

* FIX SWAGGER

* rebase fix

* Apply suggestions from code review

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

* Update BTCPayServer/wwwroot/swagger/v1/swagger.template.stores-rates-config.json

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

* Fix: Spread isn't converted from/to percentage, rename some fields, and move some routes

* Fix error handling

Co-authored-by: d11n <mail@dennisreimann.de>
Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2022-10-12 22:19:33 +09:00
a2fa688cde Refactor labels (#4179)
* Create new tables

* wip

* wip

* Refactor LegacyLabel

* Remove LabelFactory

* Add migration

* wip

* wip

* Add pull-payment attachment to tx

* Address kukks points
2022-10-11 17:34:29 +09:00
895462ac7f Import xpub: Surface error details (#4205)
Checks if the input is an output descriptor and explicitely handles that case instead of catching any errors. This allows us to display more detailed information about why an import might fail.
2022-10-11 12:19:10 +09:00
e883714446 Do not hide errors happening in tasks spawned by BaseAsyncService 2022-10-08 12:46:26 +09:00
e1a235b4e8 Changelog and bump (#4201) 2022-10-08 12:42:46 +09:00
ffa2c59df7 POS: Fix keypad view without custom amount (#4185)
The custom amount option was disabled by default in #4126. This requires some additional adaptations in the post action as otherwise the invoice won't be generated.

Fixes #4183.
2022-10-08 12:41:56 +09:00
3f19dc55fa Add "{Invoice.OrderId}" to list of supported email interpolation strings (#4202)
close #4112
2022-10-07 14:29:56 +02:00
66c2148a63 Increase tor test timeout 2022-10-07 16:04:22 +09:00
28850f534c Fix test warning 2022-10-07 15:05:11 +09:00
c40c11a822 bump ms aspnet packages (#4166)
* bump ms aspnet packages

* Bump .NET SDK in Dockerfile

* bump more packages

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2022-10-07 14:53:30 +09:00
b334e1aa00 Date display improvements (#4191)
Fixes styling issues introduced in #4074, because the `max-width` was to small for localized dates.

Also adds the ability to choose the prefered initial display format, which can be the localized or relative date.
2022-10-07 13:29:03 +09:00
b48986bfd6 Update default value for "showCustomAmount" in Swagger docs (#4200) 2022-10-07 13:27:10 +09:00
ced63baed6 fix custodian Swagger docs missing some path parameters (#4196) 2022-10-06 12:03:39 +02:00
880635d615 Make sure string is valid URL before rendering it as such in invoice details POS data section (#4197) 2022-10-06 10:43:18 +02:00
d9f8c8d3b1 Always show overpaid amount if invoice is overpaid (#4192)
close #4146
2022-10-06 12:59:05 +09:00
8155841a1d Fix receipts for Lightning Address invoices
`AdditionalData` needs to be null-checked, because it isn't set for invoices generated via Lightning Address. 

Fixes #4169.
2022-10-01 07:13:57 +02:00
30f83d8f3f Remove direct and temp link functionality from the File Storage (#4177) 2022-09-29 12:40:00 +02:00
96c86160df Fix warning error when rebooting the server caused by some shitcoin currency pair format 2022-09-29 15:45:27 +09:00
b7ea128132 Old Payout labels weren't displayed properly 2022-09-29 15:42:01 +09:00
4bee8e9bfe Greenfield: Extend LN GetInfo data (#4167)
Matching the data added in btcpayserver/BTCPayServer.Lightning#97.
2022-09-28 09:34:34 +09:00
bc195e771e Update WalletTransactions pagination default settings (#4074)
* Update WalletTransactions pagination default settings

Remove the numeric page selection and add displaying data of last 30 days by default.

* Update WalletTransactions to show txs based on Days

* Update text formatting on WalletTransactions view

Keeps the logic changes. Just undo the formatting of the file from previous commit

* Update WalletTransactions to show all after second load

Utilize Model.Days instead of new variables
Moved javascript code to PageFootContent section

* Update WalletTransactions to use ajax for infinite scroll

* Cleanups

* Apply skip and count only when not prefiltering

* Infinite scroll mode

* Improve datetime formatting and switching

* Upgrade NBXplorer to include get_wallets_recent bugfix

* Revert "Upgrade NBXplorer to include get_wallets_recent bugfix"

This reverts commit b390d942d74d88bb1da3ab8e3407184a527175ef.

* JS fixes

* Upgrade ChromeDriver and BundleMinifier

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2022-09-27 21:24:53 +09:00
0bc3e94052 bump 2022-09-26 22:51:49 +09:00
3eb3523b52 Changelog for v1.6.11 (#4130)
* Changelog for v1.6.11

* Update Changelog.md

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

* Update Changelog.md

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

* Update changelog

* Update changelog

Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com>
2022-09-26 22:50:59 +09:00
8e2f84a989 Upgrade Lightning libraries (#4164) 2022-09-26 22:48:47 +09:00
143ec7463f Fix crash on migration from old install (Fix #4162) 2022-09-26 22:00:49 +09:00
4a5fd08e51 Footer: Improve responsive display (#4163)
Enhancement in addition to #4160.
2022-09-26 10:29:35 +02:00
0306635a45 Merge pull request #4160 from daviogg/enhancement/4158-add-telegram-url
Add telegram icon and url on footer
2022-09-26 09:07:50 +02:00
0a4d32cdb5 Merge pull request #4161 from Bangalisch/SmallPageUpdate
Small update to Security.md
2022-09-26 09:07:33 +02:00
d590992d1d Fix crash on migration from old install (Fix #4162) 2022-09-26 10:31:04 +09:00
e8766946dd Improve currency selection (#4155)
Removes the current value on focus, so that the user gets to see the available options. If no selection or change is made, the value is reset to the previous value on blur.

Closes #4154.
2022-09-26 10:26:13 +09:00
8c35189b37 Small update to Security.md
Yo yo

Added title to page and ran it through Grammarly to slightly update the text.
2022-09-25 00:17:52 +02:00
e6390cde97 Add telegram icon and url on footer 2022-09-24 23:32:28 +02:00
db976a6408 Fix unit test and build warning 2022-09-23 16:41:51 +02:00
031c3ed055 Update README 2022-09-23 19:41:09 +09:00
cb391f08b9 Remove redundant exception status from invoice state label (#4151) 2022-09-23 15:17:50 +09:00
5387a6287e Fix pagination of wallet's transactions (#4150) 2022-09-22 10:39:48 +09:00
0e4544b2da POS - CustomAmount disabled by default also for App 2022-09-20 13:10:42 +02:00
e0cbb7bede POS - CustomAmount disabled by default 2022-09-20 13:10:42 +02:00
ed45b73274 Dashboard: Fix links in app tiles
Fixes #4144.
2022-09-20 10:10:54 +02:00
cadcb586a7 Fix settigs sidebar activation (#4138)
* Fix settigs sidebar activation

* remove active payout from settings

* Fix Store Settings nav highlight

Fixes #4134.

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2022-09-20 10:05:55 +02:00
9e31270459 Improve "Advanced Settings" button (#4140)
* Improve "Advanced Settings" button

Closes #4132.

* Use collapse toggle for multi-sig examples
2022-09-20 09:50:59 +02:00
dc07f046f2 Improve PayButton error page (#4129)
As this is a public page we should embed it in the non-navigation layout. Also improves the error display.
2022-09-19 21:56:42 +02:00
5032bbafb1 Consistent switch UI on Create Wallet views (#4135)
* ui+wallet: consistent switch ui update

* Cleanups

* Improve CTA wording

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2022-09-19 21:56:16 +02:00
1540bfb3a1 Merge pull request #4128 from bolatovumar/fix/4125 2022-09-17 09:38:54 +02:00
f3f5851118 Add missing store ID to invoice links
close #4125
2022-09-16 21:04:56 -07:00
9810edcd1a Fix Monero and Zcash nav extensions
They failed with an `System.NullReferenceException: Object reference not set to an instance of an object.` when navigating to a LNbank page, because LNbank uses Razor Pages and the controller part wan't defined. Brought up [on Mattermost](https://chat.btcpayserver.org/btcpayserver/pl/x3iohhct97nateyq4y1c4hp9mw).
2022-09-16 11:51:14 +02:00
e334b9162a Fix: Lnurl Max is set to min when item type is minmum
fixes #4108
2022-09-16 09:20:49 +02:00
836c676057 Upgrade Lightning lib 2022-09-15 16:57:57 +02:00
1abadd9c5d Update BTCPayServer/Storage/Services/FileService.cs 2022-09-15 16:26:22 +02:00
e8bd1d8237 FileService: Sanitize filename for downloaded files
Replaces invalid characters in filenames of files which are retrieved via URL.
2022-09-15 16:26:22 +02:00
8a7470500a Do not show set up wallet link in dashboard
fixes #4116
2022-09-13 16:14:46 +02:00
75689c665d Update source for urlib.min.js 2022-09-13 10:17:12 +02:00
d84f4f676b Document wider wallet import support 2022-09-13 10:17:12 +02:00
c97b859963 Refactor QR functionality
Based on the `ur-registry` upgrade I refactored the `CameraScanner` and `ShowQR` partials: Besides general code changes, the main change is that most of the configuration and result handling now happens on the outer view.
Those partials and functions are now generalized and don't know about their purpose (like handling PSBTs): They can be instantiated with simple data (e.g. for displaying a plain QR code) or different modes (like showing a static and the UR version of a QR code) and the result handling is done via callback.

The callbacks can now also distinguish between the different results (data as plain string vs. UR-type objects for wallet data or PSBT) and also handle the specific type of data. For instance: Before it wasn't possible to strip the leading derivation path from an xpub when scanning the QR code, because the scanner didn't know about the type of data it was handling. Now that the data is handled in the callback, we can implement that functionality for the scan view only.
2022-09-13 10:17:12 +02:00
6b8f4ee1d5 Remove bc-ur and upgrade ur-registry
Up to now we were supporting two versions of the UR standard: The legacy one implemented in `bc-ur` and the current version in `ur-registry`.
@Kukks forked a separate version of bc-ur for our web-bundle ([some more details](https://github.com/CoboVault/cobo-vault-blockchain-base/pull/8)), but it got hard to maintain the custom build, because the web-bundle needed manual assembly. We decided to get rid of the support for the legacy version and bc-ur, so that we can continue with the current version, which seems to be implemented across modern wallets (if they support UR at all). This way we can continue with only the `ur-registry` as a dependency, which handles encoding and decoding. 

I needed to make some modifications for the browser version of `ur-registry`. So I [forked their module to our org](https://github.com/btcpayserver/ur-registry) and I submitted the [modifications as PRs](https://github.com/KeystoneHQ/ur-registry/pulls) — hopefully we can eliminate our fork once those changes get merged. We are in contact with them and maintaining that fork wouldn't be as hard as the bc-ur one, because at least it has a working and automated build.
2022-09-13 10:17:12 +02:00
3532789c35 Improve Lightning Node setup examples (#4033)
* Catch connection string ToString errors

LNDhub connection string error fixed in btcpayserver/BTCPayServer.Lightning#92.

* Add Eclair bitcoin-host example

* Document LNDhub integration
2022-09-09 23:01:20 +09:00
267905b5e7 Reduce confusion 2022-09-09 13:49:24 +02:00
1626bd7a18 Show iframe when showing invoice in Shopify plugin
Close #4105
2022-09-09 13:49:24 +02:00
7106830be9 Make sure end date is after start date in Crowdfund app (#4084)
* Make sure end date is after start date in Crowdfund app

* Add null checks

* Add test case

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2022-09-09 19:26:11 +09:00
3b1946d65c bump lightning libs 2022-09-09 16:46:39 +09:00
6dedf4d44f Upgrade Lightning library to 1.4.1 (#4093)
This enables new feature of specifying `certfilepath`

Closes #2182
2022-09-09 14:33:17 +09:00
fe5e2584b1 Use mempool as default block explorer (#4100) 2022-09-09 14:32:40 +09:00
8fae38deca Set explicit cursor style property on pay button with custom text (#4107)
We need to set "cursor: pointer;" explicitly on pay buttons with custom text because the <button> HTML tag default pointer is not a cursor. This is not an issue for the default button because it uses the <input> tag which has "cursor: pointer;" by default in browsers.

Close #4104
2022-09-09 14:28:03 +09:00
7f8e322e9c Fixed text + simplification (#4109) 2022-09-09 14:27:49 +09:00
4d0f76f9e8 Update invoice amount description in Swagger template
Close #4067
2022-09-04 09:57:41 +03:00
5d2b42960b fix CanExportInvoicesJson 2022-08-30 09:16:17 +02:00
0098dacdff fix host in launchprofile 2022-08-30 09:16:17 +02:00
51666fbf0e fix CanUseInvoiceReceipts 2022-08-30 09:16:17 +02:00
defb9120fd Ensure apps can be deleted through UI
Bug was introduced by https://github.com/btcpayserver/btcpayserver/pull/3987
2022-08-26 11:23:00 +02:00
11ec72ce8c changelog and bump
(cherry picked from commit a661f08d7b97f44913221e3641f740274f8232b5)
2022-08-26 10:26:51 +02:00
f67bd69ecc do not try to mention payout ids/ pull payments if they were not saved for labels
fixes #4078
2022-08-26 09:41:48 +02:00
db2c29a6e1 PoS UI fix: scale-down item image (#4076) 2022-08-25 12:26:44 +02:00
e22e522245 LNURL: Fix missing route hints option
Fixes #4072.
2022-08-25 12:01:26 +02:00
7c8f4c0405 Allow specifing fee block target for onchain payout processor (#4065)
Co-authored-by: d11n <mail@dennisreimann.de>
2022-08-23 12:35:20 +02:00
01ab21e4c0 Dashboard: Fix app tiles
I broke this with #3987.
2022-08-23 12:07:18 +02:00
534a2912e1 Create dynamic manifest for pos apps (#4064)
Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2022-08-21 20:38:14 +02:00
1456f4e227 Enhance export function for invoices (#4060)
Co-authored-by: Andrew Camilleri <evilkukka@gmail.com>
2022-08-21 08:43:23 +02:00
63e11451ba Rename PutPointOfSaleApp to UpdatePointOfSaleApp 2022-08-21 08:41:04 +02:00
7f80674cf2 Update Swagger docs 2022-08-21 08:41:04 +02:00
16f4ca5fbf Add support for updating POS app through Greenfield API
Part of #3458
2022-08-21 08:41:04 +02:00
701ba59bd8 Convert public app parts 2022-08-21 08:38:25 +02:00
8c6705bccb Make POS and Crowdfund plugins 2022-08-21 08:38:25 +02:00
e4542c4ac4 Paybutton: Fix logo and URL
Fixes #4053.
2022-08-20 11:32:09 +02:00
45eea1d6de API docs: Fix server base path
Introduced in #4041. Fixes #4059.
2022-08-20 11:31:28 +02:00
345 changed files with 8641 additions and 3669 deletions

4
.gitignore vendored
View File

@ -288,10 +288,6 @@ __pycache__/
*.xsd.cs
/BTCPayServer/Build/dockerfiles
# Bundling JS/CSS
BTCPayServer/wwwroot/bundles/*
!BTCPayServer/wwwroot/bundles/.gitignore
.vscode/*
!.vscode/launch.json
!.vscode/tasks.json

View File

@ -32,9 +32,9 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="HtmlSanitizer" Version="5.0.372" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.7" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.9" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.7" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="6.0.1" />
</ItemGroup>
<ItemGroup>

View File

@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
@ -10,6 +12,13 @@ namespace BTCPayServer.Abstractions.Extensions
private const string ACTIVE_CATEGORY_KEY = "ActiveCategory";
private const string ACTIVE_PAGE_KEY = "ActivePage";
private const string ACTIVE_ID_KEY = "ActiveId";
private const string ActivePageClass = "active";
public enum DateDisplayFormat
{
Localized,
Relative
}
public static void SetActivePage<T>(this ViewDataDictionary viewData, T activePage, string title = null, string activeId = null)
where T : IConvertible
@ -52,7 +61,7 @@ namespace BTCPayServer.Abstractions.Extensions
var activeCategory = viewData[ACTIVE_CATEGORY_KEY]?.ToString();
var categoryMatch = category.Equals(activeCategory, StringComparison.InvariantCultureIgnoreCase);
var idMatch = id == null || activeId == null || id.Equals(activeId);
return categoryMatch && idMatch ? "active" : null;
return categoryMatch && idMatch ? ActivePageClass : null;
}
public static string IsActivePage<T>(this ViewDataDictionary viewData, T page, object id = null)
@ -60,6 +69,14 @@ namespace BTCPayServer.Abstractions.Extensions
{
return IsActivePage(viewData, page.ToString(), page.GetType().ToString(), id);
}
public static string IsActivePage<T>(this ViewDataDictionary viewData, IEnumerable<T> pages, object id = null)
where T : IConvertible
{
return pages.Any(page => IsActivePage(viewData, page.ToString(), page.GetType().ToString(), id) == ActivePageClass)
? ActivePageClass
: null;
}
public static string IsActivePage(this ViewDataDictionary viewData, string page, string category, object id = null)
{
@ -72,29 +89,32 @@ namespace BTCPayServer.Abstractions.Extensions
var activeCategory = viewData[ACTIVE_CATEGORY_KEY]?.ToString();
var categoryAndPageMatch = (category == null || activeCategory.Equals(category, StringComparison.InvariantCultureIgnoreCase)) && page.Equals(activePage, StringComparison.InvariantCultureIgnoreCase);
var idMatch = id == null || activeId == null || id.Equals(activeId);
return categoryAndPageMatch && idMatch ? "active" : null;
return categoryAndPageMatch && idMatch ? ActivePageClass : null;
}
public static HtmlString ToBrowserDate(this DateTimeOffset date)
public static HtmlString ToBrowserDate(this DateTimeOffset date, DateDisplayFormat format = DateDisplayFormat.Localized)
{
var displayDate = date.ToString("o", CultureInfo.InvariantCulture);
return new HtmlString($"<span class='localizeDate'>{displayDate}</span>");
var relative = date.ToTimeAgo();
var initial = format.ToString().ToLower();
var dateTime = date.ToString("o", CultureInfo.InvariantCulture);
var displayDate = format == DateDisplayFormat.Relative ? relative : date.ToString("g", CultureInfo.InvariantCulture);
return new HtmlString($"<time datetime=\"{dateTime}\" data-relative=\"{relative}\" data-initial=\"{initial}\">{displayDate}</time>");
}
public static HtmlString ToBrowserDate(this DateTime date)
public static HtmlString ToBrowserDate(this DateTime date, DateDisplayFormat format = DateDisplayFormat.Localized)
{
var displayDate = date.ToString("o", CultureInfo.InvariantCulture);
return new HtmlString($"<span class='localizeDate'>{displayDate}</span>");
var relative = date.ToTimeAgo();
var initial = format.ToString().ToLower();
var dateTime = date.ToString("o", CultureInfo.InvariantCulture);
var displayDate = format == DateDisplayFormat.Relative ? relative : date.ToString("g", CultureInfo.InvariantCulture);
return new HtmlString($"<time datetime=\"{dateTime}\" data-relative=\"{relative}\" data-initial=\"{initial}\">{displayDate}</time>");
}
public static string ToTimeAgo(this DateTimeOffset date)
{
var diff = DateTimeOffset.UtcNow - date;
var formatted = diff.TotalSeconds > 0
? $"{diff.TimeString()} ago"
: $"in {diff.Negate().TimeString()}";
return formatted;
}
public static string ToTimeAgo(this DateTimeOffset date) => (DateTimeOffset.UtcNow - date).ToTimeAgo();
public static string ToTimeAgo(this DateTime date) => (DateTimeOffset.UtcNow - date).ToTimeAgo();
public static string ToTimeAgo(this TimeSpan diff) => diff.TotalSeconds > 0 ? $"{diff.TimeString()} ago" : $"in {diff.Negate().TimeString()}";
public static string TimeString(this TimeSpan timeSpan)
{
@ -106,16 +126,14 @@ namespace BTCPayServer.Abstractions.Extensions
{
return $"{(int)timeSpan.TotalMinutes} minute{Plural((int)timeSpan.TotalMinutes)}";
}
if (timeSpan.Days < 1)
{
return $"{(int)timeSpan.TotalHours} hour{Plural((int)timeSpan.TotalHours)}";
}
return $"{(int)timeSpan.TotalDays} day{Plural((int)timeSpan.TotalDays)}";
return timeSpan.Days < 1
? $"{(int)timeSpan.TotalHours} hour{Plural((int)timeSpan.TotalHours)}"
: $"{(int)timeSpan.TotalDays} day{Plural((int)timeSpan.TotalDays)}";
}
private static string Plural(int value)
{
return value > 1 ? "s" : string.Empty;
return value == 1 ? string.Empty : "s";
}
}
}

View File

@ -28,8 +28,8 @@
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.3.8" />
<PackageReference Include="NBitcoin" Version="7.0.10" />
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.3.15" />
<PackageReference Include="NBitcoin" Version="7.0.14" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>
<ItemGroup>

View File

@ -20,6 +20,17 @@ namespace BTCPayServer.Client
return await HandleResponse<PointOfSaleAppData>(response);
}
public virtual async Task<PointOfSaleAppData> UpdatePointOfSaleApp(string appId,
CreatePointOfSaleAppRequest request, CancellationToken token = default)
{
if (request == null)
throw new ArgumentNullException(nameof(request));
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/apps/pos/{appId}", bodyPayload: request,
method: HttpMethod.Put), token);
return await HandleResponse<PointOfSaleAppData>(response);
}
public virtual async Task<AppDataBase> GetApp(string appId, CancellationToken token = default)
{
if (appId == null)

View File

@ -95,6 +95,24 @@ namespace BTCPayServer.Client
method: HttpMethod.Get), token);
return await HandleResponse<LightningInvoiceData>(response);
}
public virtual async Task<LightningInvoiceData[]> GetLightningInvoices(string cryptoCode,
bool? pendingOnly = null, long? offsetIndex = null, CancellationToken token = default)
{
var queryPayload = new Dictionary<string, object>();
if (pendingOnly is bool v)
{
queryPayload.Add("pendingOnly", v.ToString());
}
if (offsetIndex is > 0)
{
queryPayload.Add("offsetIndex", offsetIndex);
}
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/invoices", queryPayload), token);
return await HandleResponse<LightningInvoiceData[]>(response);
}
public virtual async Task<LightningInvoiceData> CreateLightningInvoice(string cryptoCode, CreateLightningInvoiceRequest request,
CancellationToken token = default)

View File

@ -65,7 +65,7 @@ namespace BTCPayServer.Client
return await HandleResponse<string>(response);
}
public virtual async Task PayLightningInvoice(string storeId, string cryptoCode, PayLightningInvoiceRequest request,
public virtual async Task<LightningPaymentData> PayLightningInvoice(string storeId, string cryptoCode, PayLightningInvoiceRequest request,
CancellationToken token = default)
{
if (request == null)
@ -73,7 +73,7 @@ namespace BTCPayServer.Client
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices/pay", bodyPayload: request,
method: HttpMethod.Post), token);
await HandleResponse(response);
return await HandleResponse<LightningPaymentData>(response);
}
public virtual async Task<LightningPaymentData> GetLightningPayment(string storeId, string cryptoCode,
@ -97,6 +97,24 @@ namespace BTCPayServer.Client
method: HttpMethod.Get), token);
return await HandleResponse<LightningInvoiceData>(response);
}
public virtual async Task<LightningInvoiceData[]> GetLightningInvoices(string storeId, string cryptoCode,
bool? pendingOnly = null, long? offsetIndex = null, CancellationToken token = default)
{
var queryPayload = new Dictionary<string, object>();
if (pendingOnly is bool v)
{
queryPayload.Add("pendingOnly", v.ToString());
}
if (offsetIndex is > 0)
{
queryPayload.Add("offsetIndex", offsetIndex);
}
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices", queryPayload), token);
return await HandleResponse<LightningInvoiceData[]>(response);
}
public virtual async Task<LightningInvoiceData> CreateLightningInvoice(string storeId, string cryptoCode,
CreateLightningInvoiceRequest request, CancellationToken token = default)

View File

@ -37,6 +37,20 @@ namespace BTCPayServer.Client
await HandleResponse(response);
}
public virtual async Task<Client.Models.InvoiceData> PayPaymentRequest(string storeId, string paymentRequestId, PayPaymentRequestRequest request, CancellationToken token = default)
{
if (request == null)
throw new ArgumentNullException(nameof(request));
if (storeId is null)
throw new ArgumentNullException(nameof(storeId));
if (paymentRequestId is null)
throw new ArgumentNullException(nameof(paymentRequestId));
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-requests/{paymentRequestId}/pay", bodyPayload: request,
method: HttpMethod.Post), token);
return await HandleResponse<Client.Models.InvoiceData>(response);
}
public virtual async Task<PaymentRequestData> CreatePaymentRequest(string storeId,
CreatePaymentRequestRequest request, CancellationToken token = default)
{

View File

@ -0,0 +1,53 @@
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
namespace BTCPayServer.Client
{
public partial class BTCPayServerClient
{
public virtual async Task<StoreRateConfiguration> GetStoreRateConfiguration(string storeId,
CancellationToken token = default)
{
using var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/rates/configuration", method: HttpMethod.Get),
token);
return await HandleResponse<StoreRateConfiguration>(response);
}
public virtual async Task<List<RateSource>> GetRateSources(
CancellationToken token = default)
{
using var response = await _httpClient.SendAsync(
CreateHttpRequest($"misc/rate-sources", method: HttpMethod.Get),
token);
return await HandleResponse<List<RateSource>>(response);
}
public virtual async Task<StoreRateConfiguration> UpdateStoreRateConfiguration(string storeId,
StoreRateConfiguration request,
CancellationToken token = default)
{
using var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/rates/configuration", bodyPayload: request,
method: HttpMethod.Put),
token);
return await HandleResponse<StoreRateConfiguration>(response);
}
public virtual async Task<List<StoreRatePreviewResult>> PreviewUpdateStoreRateConfiguration(string storeId,
StoreRateConfiguration request,
string[] currencyPair,
CancellationToken token = default)
{
using var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/rates/configuration/preview", bodyPayload: request,
queryPayload: new Dictionary<string, object>() {{"currencyPair", currencyPair}},
method: HttpMethod.Post),
token);
return await HandleResponse<List<StoreRatePreviewResult>>(response);
}
}
}

View File

@ -25,7 +25,7 @@ namespace BTCPayServer.Client.Models
public string Template { get; set; } = null;
[JsonConverter(typeof(StringEnumConverter))]
public PosViewType DefaultView { get; set; }
public bool ShowCustomAmount { get; set; } = true;
public bool ShowCustomAmount { get; set; } = false;
public bool ShowDiscount { get; set; } = true;
public bool EnableTips { get; set; } = true;
public string CustomAmountPayButtonText { get; set; } = null;
@ -36,6 +36,8 @@ namespace BTCPayServer.Client.Models
public string RedirectUrl { get; set; } = null;
public bool? RedirectAutomatically { get; set; } = null;
public bool? RequiresRefundEmail { get; set; } = null;
public string CheckoutFormId { get; set; } = null;
public string EmbeddedCSS { get; set; } = null;
public CheckoutType? CheckoutType { get; set; } = null;
}
}

View File

@ -85,6 +85,9 @@ namespace BTCPayServer.Client.Models
public bool? RedirectAutomatically { get; set; }
public bool? RequiresRefundEmail { get; set; } = null;
public string DefaultLanguage { get; set; }
[JsonProperty("checkoutFormId")]
public string CheckoutFormId { get; set; }
public CheckoutType? CheckoutType { get; set; }
}
}
public class InvoiceData : InvoiceDataBase

View File

@ -1,9 +1,11 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client.Models
{
[Obsolete]
public class LabelData
{
public string Type { get; set; }

View File

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using BTCPayServer.Client.JsonConverters;
using BTCPayServer.Lightning;
using Newtonsoft.Json;
@ -26,5 +27,8 @@ namespace BTCPayServer.Client.Models
[JsonConverter(typeof(LightMoneyJsonConverter))]
public LightMoney AmountReceived { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public Dictionary<ulong, string> CustomRecords { get; set; }
}
}

View File

@ -9,6 +9,13 @@ namespace BTCPayServer.Client.Models
[JsonProperty("nodeURIs", ItemConverterType = typeof(NodeUriJsonConverter))]
public NodeInfo[] NodeURIs { get; set; }
public int BlockHeight { get; set; }
public string Alias { get; set; }
public string Color { get; set; }
public string Version { get; set; }
public long? PeersCount { get; set; }
public long? ActiveChannelsCount { get; set; }
public long? InactiveChannelsCount { get; set; }
public long? PendingChannelsCount { get; set; }
}
public class LightningChannelData

View File

@ -10,4 +10,6 @@ public class OnChainAutomatedPayoutSettings
[JsonConverter(typeof(TimeSpanJsonConverter.Seconds))]
public TimeSpan IntervalSeconds { get; set; }
public int? FeeBlockTarget { get; set; }
}

View File

@ -13,7 +13,9 @@ namespace BTCPayServer.Client.Models
public uint256 TransactionHash { get; set; }
public string Comment { get; set; }
#pragma warning disable CS0612 // Type or member is obsolete
public Dictionary<string, LabelData> Labels { get; set; } = new Dictionary<string, LabelData>();
#pragma warning restore CS0612 // Type or member is obsolete
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal Amount { get; set; }

View File

@ -15,7 +15,9 @@ namespace BTCPayServer.Client.Models
[JsonConverter(typeof(OutpointJsonConverter))]
public OutPoint Outpoint { get; set; }
public string Link { get; set; }
#pragma warning disable CS0612 // Type or member is obsolete
public Dictionary<string, LabelData> Labels { get; set; }
#pragma warning restore CS0612 // Type or member is obsolete
[JsonConverter(typeof(DateTimeToUnixTimeConverter))]
public DateTimeOffset Timestamp { get; set; }
[JsonConverter(typeof(KeyPathJsonConverter))]

View File

@ -1,3 +1,4 @@
using System;
using BTCPayServer.Client.JsonConverters;
using BTCPayServer.JsonConverters;
using BTCPayServer.Lightning;
@ -19,5 +20,8 @@ namespace BTCPayServer.Client.Models
[JsonConverter(typeof(LightMoneyJsonConverter))]
public LightMoney Amount { get; set; }
[JsonConverter(typeof(TimeSpanJsonConverter.Seconds))]
public TimeSpan? SendTimeout { get; set; }
}
}

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Text;
using BTCPayServer.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models
{
public class PayPaymentRequestRequest
{
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal? Amount { get; set; }
public bool? AllowPendingInvoiceReuse { get; set; }
}
}

View File

@ -0,0 +1,7 @@
namespace BTCPayServer.Client.Models;
public class RateSource
{
public string Id { get; set; }
public string Name { get; set; }
}

View File

@ -31,6 +31,10 @@ namespace BTCPayServer.Client.Models
public bool AnyoneCanCreateInvoice { get; set; }
public string DefaultCurrency { get; set; }
public bool RequiresRefundEmail { get; set; }
public string CheckoutFormId { get; set; }
[JsonConverter(typeof(StringEnumConverter))]
public CheckoutType CheckoutType { get; set; }
public bool LightningAmountInSatoshi { get; set; }
public bool LightningPrivateRouteHints { get; set; }
public bool OnChainWithLnInvoiceFallback { get; set; }
@ -66,6 +70,12 @@ namespace BTCPayServer.Client.Models
public IDictionary<string, JToken> AdditionalData { get; set; }
}
public enum CheckoutType
{
V1,
V2
}
public enum NetworkFeeMode
{
MultiplePaymentsOnly,

View File

@ -0,0 +1,10 @@
namespace BTCPayServer.Client.Models
{
public class StoreRateConfiguration
{
public decimal Spread { get; set; }
public bool IsCustomScript { get; set; }
public string EffectiveScript { get; set; }
public string PreferredSource { get; set; }
}
}

View File

@ -0,0 +1,10 @@
using System.Collections.Generic;
namespace BTCPayServer.Client.Models;
public class StoreRatePreviewResult
{
public string CurrencyPair { get; set; }
public decimal? Rate { get; set; }
public List<string> Errors { get; set; }
}

View File

@ -0,0 +1,7 @@
namespace BTCPayServer.Client.Models;
public class StoreRateResult
{
public string CurrencyPair { get; set; }
public decimal Rate { get; set; }
}

View File

@ -1,28 +0,0 @@
using NBitcoin;
using NBXplorer;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
public void InitMonetaryUnit()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("MUE");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "MonetaryUnit",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://explorer.monetaryunit.org/#/MUE/mainnet/tx/{0}" : "https://explorer.monetaryunit.org/#/MUE/mainnet/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
DefaultRateRules = new[]
{
"MUE_X = MUE_BTC * BTC_X",
"MUE_BTC = bittrex(MUE_BTC)"
},
CryptoImagePath = "imlegacy/monetaryunit.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("31'") : new KeyPath("1'")
});
}
}
}

View File

@ -22,7 +22,7 @@ namespace BTCPayServer
"LBTC_X = LBTC_BTC * BTC_X",
"LBTC_BTC = 1",
},
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://blockstream.info/liquid/tx/{0}" : "https://blockstream.info/testnet/liquid/tx/{0}",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://liquid.network/tx/{0}" : "https://liquid.network/testnet/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
CryptoImagePath = "imlegacy/liquid.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),

View File

@ -21,7 +21,7 @@ namespace BTCPayServer
},
AssetId = new uint256("ce091c998b83c78bb71a632313ba3760f1763d9cfcffae02258ffa9865a37bd2"),
DisplayName = "Liquid Tether",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://blockstream.info/liquid/tx/{0}" : "https://blockstream.info/testnet/liquid/tx/{0}",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://liquid.network/tx/{0}" : "https://liquid.network/testnet/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
CryptoImagePath = "imlegacy/liquid-tether.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
@ -44,7 +44,7 @@ namespace BTCPayServer
Divisibility = 2,
AssetId = new uint256("aa775044c32a7df391902b3659f46dfe004ccb2644ce2ddc7dba31e889391caf"),
DisplayName = "Ethiopian Birr",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://blockstream.info/liquid/tx/{0}" : "https://blockstream.info/testnet/liquid/tx/{0}",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://liquid.network/tx/{0}" : "https://liquid.network/testnet/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
CryptoImagePath = "imlegacy/etb.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
@ -66,7 +66,7 @@ namespace BTCPayServer
},
AssetId = new uint256("0e99c1a6da379d1f4151fb9df90449d40d0608f6cb33a5bcbfc8c265f42bab0a"),
DisplayName = "Liquid CAD",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://blockstream.info/liquid/tx/{0}" : "https://blockstream.info/testnet/liquid/tx/{0}",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://liquid.network/tx/{0}" : "https://liquid.network/testnet/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
CryptoImagePath = "imlegacy/lcad.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),

View File

@ -13,9 +13,9 @@ namespace BTCPayServer
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Bitcoin",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://blockstream.info/tx/{0}" :
NetworkType == Bitcoin.Instance.Signet.ChainName ? "https://explorer.bc-2.jp/tx/{0}"
: "https://blockstream.info/testnet/tx/{0}",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://mempool.space/tx/{0}" :
NetworkType == Bitcoin.Instance.Signet.ChainName ? "https://mempool.space/signet/tx/{0}"
: "https://mempool.space/testnet/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
CryptoImagePath = "imlegacy/bitcoin.svg",
LightningImagePath = "imlegacy/bitcoin-lightning.svg",

View File

@ -58,7 +58,7 @@ namespace BTCPayServer
InitZcash();
InitChaincoin();
// InitArgoneum();//their rate source is down 9/15/20.
InitMonetaryUnit();
// InitMonetaryUnit(); Not supported from Bittrex from 11/23/2022, dead shitcoin
// Assume that electrum mappings are same as BTC if not specified
foreach (var network in _Networks.Values.OfType<BTCPayNetwork>())

View File

@ -4,7 +4,8 @@
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="NBXplorer.Client" Version="4.2.0" />
<PackageReference Include="NBXplorer.Client" Version="4.2.1" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.18" />
</ItemGroup>
<ItemGroup Condition="'$(Altcoins)' != 'true'">
<Compile Remove="Altcoins\**\*.cs"></Compile>

View File

@ -59,7 +59,11 @@ namespace BTCPayServer.Data
public DbSet<U2FDevice> U2FDevices { get; set; }
public DbSet<Fido2Credential> Fido2Credentials { get; set; }
public DbSet<UserStore> UserStore { get; set; }
[Obsolete]
public DbSet<WalletData> Wallets { get; set; }
public DbSet<WalletObjectData> WalletObjects { get; set; }
public DbSet<WalletObjectLinkData> WalletObjectLinks { get; set; }
[Obsolete]
public DbSet<WalletTransactionData> WalletTransactions { get; set; }
public DbSet<WebhookDeliveryData> WebhookDeliveries { get; set; }
public DbSet<WebhookData> Webhooks { get; set; }
@ -109,7 +113,11 @@ namespace BTCPayServer.Data
Fido2Credential.OnModelCreating(builder);
BTCPayServer.Data.UserStore.OnModelCreating(builder);
//WalletData.OnModelCreating(builder);
WalletObjectData.OnModelCreating(builder, Database);
WalletObjectLinkData.OnModelCreating(builder, Database);
#pragma warning disable CS0612 // Type or member is obsolete
WalletTransactionData.OnModelCreating(builder);
#pragma warning restore CS0612 // Type or member is obsolete
WebhookDeliveryData.OnModelCreating(builder);
LightningAddressData.OnModelCreating(builder);
PayoutProcessorData.OnModelCreating(builder);

View File

@ -3,11 +3,11 @@
<Import Project="../Build/Common.csproj" />
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.7">
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.9">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.9" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BTCPayServer.Abstractions\BTCPayServer.Abstractions.csproj" />

View File

@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
namespace BTCPayServer.Data
{
[Obsolete]
public class WalletData
{
[System.ComponentModel.DataAnnotations.Key]

View File

@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
namespace BTCPayServer.Data
{
public class WalletObjectData
{
public class Types
{
public const string Label = "label";
public const string Tx = "tx";
}
public string WalletId { get; set; }
public string Type { get; set; }
public string Id { get; set; }
public string Data { get; set; }
public List<WalletObjectLinkData> ChildLinks { get; set; }
public List<WalletObjectLinkData> ParentLinks { get; set; }
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
{
builder.Entity<WalletObjectData>().HasKey(o =>
new
{
o.WalletId,
o.Type,
o.Id,
});
if (databaseFacade.IsNpgsql())
{
builder.Entity<WalletObjectData>()
.Property(o => o.Data)
.HasColumnType("JSONB");
}
}
}
}

View File

@ -0,0 +1,61 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
namespace BTCPayServer.Data
{
public class WalletObjectLinkData
{
public string WalletId { get; set; }
public string ParentType { get; set; }
public string ParentId { get; set; }
public string ChildType { get; set; }
public string ChildId { get; set; }
public string Data { get; set; }
public WalletObjectData Parent { get; set; }
public WalletObjectData Child { get; set; }
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
{
builder.Entity<WalletObjectLinkData>().HasKey(o =>
new
{
o.WalletId,
o.ParentType,
o.ParentId,
o.ChildType,
o.ChildId,
});
builder.Entity<WalletObjectLinkData>().HasIndex(o => new
{
o.WalletId,
o.ChildType,
o.ChildId,
});
builder.Entity<WalletObjectLinkData>()
.HasOne(o => o.Parent)
.WithMany(o => o.ChildLinks)
.HasForeignKey(o => new { o.WalletId, o.ParentType, o.ParentId })
.OnDelete(DeleteBehavior.Cascade);
builder.Entity<WalletObjectLinkData>()
.HasOne(o => o.Child)
.WithMany(o => o.ParentLinks)
.HasForeignKey(o => new { o.WalletId, o.ChildType, o.ChildId })
.OnDelete(DeleteBehavior.Cascade);
if (databaseFacade.IsNpgsql())
{
builder.Entity<WalletObjectLinkData>()
.Property(o => o.Data)
.HasColumnType("JSONB");
}
}
}
}

View File

@ -1,7 +1,9 @@
using System;
using Microsoft.EntityFrameworkCore;
namespace BTCPayServer.Data
{
[Obsolete]
public class WalletTransactionData
{
public string WalletDataId { get; set; }

View File

@ -0,0 +1,77 @@
// <auto-generated />
using System;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20220929132704_label")]
public partial class label : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "WalletObjects",
columns: table => new
{
WalletId = table.Column<string>(type: "TEXT", nullable: false),
Type = table.Column<string>(type: "TEXT", nullable: false),
Id = table.Column<string>(type: "TEXT", nullable: false),
Data = table.Column<string>(type: migrationBuilder.IsNpgsql() ? "JSONB" : "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_WalletObjects", x => new { x.WalletId, x.Type, x.Id });
});
migrationBuilder.CreateTable(
name: "WalletObjectLinks",
columns: table => new
{
WalletId = table.Column<string>(type: "TEXT", nullable: false),
ParentType = table.Column<string>(type: "TEXT", nullable: false),
ParentId = table.Column<string>(type: "TEXT", nullable: false),
ChildType = table.Column<string>(type: "TEXT", nullable: false),
ChildId = table.Column<string>(type: "TEXT", nullable: false),
Data = table.Column<string>(type: migrationBuilder.IsNpgsql() ? "JSONB" : "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_WalletObjectLinks", x => new { x.WalletId, x.ParentType, x.ParentId, x.ChildType, x.ChildId });
table.ForeignKey(
name: "FK_WalletObjectLinks_WalletObjects_WalletId_ChildType_ChildId",
columns: x => new { x.WalletId, x.ChildType, x.ChildId },
principalTable: "WalletObjects",
principalColumns: new[] { "WalletId", "Type", "Id" },
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_WalletObjectLinks_WalletObjects_WalletId_ParentType_ParentId",
columns: x => new { x.WalletId, x.ParentType, x.ParentId },
principalTable: "WalletObjects",
principalColumns: new[] { "WalletId", "Type", "Id" },
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_WalletObjectLinks_WalletId_ChildType_ChildId",
table: "WalletObjectLinks",
columns: new[] { "WalletId", "ChildType", "ChildId" });
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "WalletObjectLinks");
migrationBuilder.DropTable(
name: "WalletObjects");
}
}
}

View File

@ -16,7 +16,7 @@ namespace BTCPayServer.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.1");
modelBuilder.HasAnnotation("ProductVersion", "6.0.7");
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
@ -189,6 +189,7 @@ namespace BTCPayServer.Migrations
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
@ -845,6 +846,52 @@ namespace BTCPayServer.Migrations
b.ToTable("Wallets");
});
modelBuilder.Entity("BTCPayServer.Data.WalletObjectData", b =>
{
b.Property<string>("WalletId")
.HasColumnType("TEXT");
b.Property<string>("Type")
.HasColumnType("TEXT");
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<string>("Data")
.HasColumnType("TEXT");
b.HasKey("WalletId", "Type", "Id");
b.ToTable("WalletObjects");
});
modelBuilder.Entity("BTCPayServer.Data.WalletObjectLinkData", b =>
{
b.Property<string>("WalletId")
.HasColumnType("TEXT");
b.Property<string>("ParentType")
.HasColumnType("TEXT");
b.Property<string>("ParentId")
.HasColumnType("TEXT");
b.Property<string>("ChildType")
.HasColumnType("TEXT");
b.Property<string>("ChildId")
.HasColumnType("TEXT");
b.Property<string>("Data")
.HasColumnType("TEXT");
b.HasKey("WalletId", "ParentType", "ParentId", "ChildType", "ChildId");
b.HasIndex("WalletId", "ChildType", "ChildId");
b.ToTable("WalletObjectLinks");
});
modelBuilder.Entity("BTCPayServer.Data.WalletTransactionData", b =>
{
b.Property<string>("WalletDataId")
@ -1333,6 +1380,25 @@ namespace BTCPayServer.Migrations
b.Navigation("StoreData");
});
modelBuilder.Entity("BTCPayServer.Data.WalletObjectLinkData", b =>
{
b.HasOne("BTCPayServer.Data.WalletObjectData", "Child")
.WithMany("ParentLinks")
.HasForeignKey("WalletId", "ChildType", "ChildId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("BTCPayServer.Data.WalletObjectData", "Parent")
.WithMany("ChildLinks")
.HasForeignKey("WalletId", "ParentType", "ParentId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Child");
b.Navigation("Parent");
});
modelBuilder.Entity("BTCPayServer.Data.WalletTransactionData", b =>
{
b.HasOne("BTCPayServer.Data.WalletData", "WalletData")
@ -1475,6 +1541,13 @@ namespace BTCPayServer.Migrations
b.Navigation("WalletTransactions");
});
modelBuilder.Entity("BTCPayServer.Data.WalletObjectData", b =>
{
b.Navigation("ChildLinks");
b.Navigation("ParentLinks");
});
modelBuilder.Entity("BTCPayServer.Data.WebhookData", b =>
{
b.Navigation("Deliveries");

View File

@ -26,6 +26,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="McMaster.NETCore.Plugins.Mvc" Version="1.4.0" />
<ProjectReference Include="..\BTCPayServer.Abstractions\BTCPayServer.Abstractions.csproj" />
<None Include="icon.png" Pack="true" PackagePath="\" />
</ItemGroup>

View File

@ -8,6 +8,7 @@ using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Contracts;
using McMaster.NETCore.Plugins;
using NBitcoin.Crypto;
using NBitcoin.DataEncoders;
using NBitcoin.Secp256k1;
@ -33,7 +34,8 @@ namespace BTCPayServer.PluginPacker
throw new Exception($"{rootDLLPath} could not be found");
}
var assembly = Assembly.LoadFrom(rootDLLPath);
var plugin = PluginLoader.CreateFromAssemblyFile(rootDLLPath, false, new[] { typeof(IBTCPayServerPlugin) });
var assembly = plugin.LoadAssembly(name);
var extension = GetAllExtensionTypesFromAssembly(assembly).FirstOrDefault();
if (extension is null)
{

View File

@ -13,7 +13,7 @@
<EmbeddedResource Include="Resources\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.7">
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.9">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@ -4,9 +4,9 @@
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.2.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.9" />
<PackageReference Include="NBitcoin" Version="7.0.10" />
<PackageReference Include="NBitcoin" Version="7.0.14" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="1.0.2" />
</ItemGroup>

View File

@ -1,4 +1,5 @@
using System;
using System.Linq;
using BTCPayServer.Services.Rates;
namespace BTCPayServer.Rating
@ -56,6 +57,13 @@ namespace BTCPayServer.Rating
}
}
}
else if (splitted.Length > 2)
{
// Some shitcoin have _ their own ticker name... Since we don't care about those, let's
// parse it anyway assuming the first part is one currency.
value = new CurrencyPair(splitted[0], string.Join("_", splitted.Skip(1).ToArray()));
return true;
}
return false;
}

View File

@ -44,9 +44,6 @@ namespace BTCPayServer.Services.Rates
{
if (notFoundSymbols.TryGetValue(ticker.Key, out _))
return null;
if (ticker.Key.Contains("XMR"))
{
}
try
{
CurrencyPair pair;

View File

@ -11,6 +11,8 @@ using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Models.WalletViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Plugins.PointOfSale.Controllers;
using BTCPayServer.Plugins.PointOfSale.Models;
using BTCPayServer.Services.Apps;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
@ -617,6 +619,7 @@ namespace BTCPayServer.Tests
user.RegisterDerivationScheme("BTC");
user.RegisterDerivationScheme("LTC");
var apps = user.GetController<UIAppsController>();
var pos = user.GetController<UIPointOfSaleController>();
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId)).Model);
var appType = AppType.PointOfSale.ToString();
vm.AppName = "test";
@ -624,8 +627,10 @@ namespace BTCPayServer.Tests
Assert.IsType<RedirectToActionResult>(apps.CreateApp(user.StoreId, vm).Result);
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
var app = appList.Apps[0];
apps.HttpContext.SetAppData(new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType });
var vmpos = await apps.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
var appData = new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType };
apps.HttpContext.SetAppData(appData);
pos.HttpContext.SetAppData(appData);
var vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
vmpos.Title = "hello";
vmpos.Currency = "CAD";
vmpos.ButtonText = "{0} Purchase";
@ -642,11 +647,11 @@ donation:
price: 1.02
custom: true
";
Assert.IsType<RedirectToActionResult>(apps.UpdatePointOfSale(app.Id, vmpos).Result);
vmpos = await apps.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
Assert.Equal("hello", vmpos.Title);
var publicApps = user.GetController<UIAppsPublicController>();
var publicApps = user.GetController<UIPointOfSaleController>();
var vmview = await publicApps.ViewPointOfSale(app.Id, PosViewType.Cart).AssertViewModelAsync<ViewPointOfSaleViewModel>();
Assert.Equal("hello", vmview.Title);
Assert.Equal(3, vmview.Items.Length);
@ -698,7 +703,7 @@ donation:
})
{
TestLogs.LogInformation($"Testing for {test.Code}");
vmpos = await apps.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
vmpos.Title = "hello";
vmpos.Currency = test.Item1;
vmpos.ButtonText = "{0} Purchase";
@ -714,8 +719,8 @@ donation:
price: 1.02
custom: true
";
Assert.IsType<RedirectToActionResult>(apps.UpdatePointOfSale(app.Id, vmpos).Result);
publicApps = user.GetController<UIAppsPublicController>();
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
publicApps = user.GetController<UIPointOfSaleController>();
vmview = await publicApps.ViewPointOfSale(app.Id, PosViewType.Cart).AssertViewModelAsync<ViewPointOfSaleViewModel>();
Assert.Equal(test.Code, vmview.CurrencyCode);
Assert.Equal(test.ExpectedSymbol,
@ -731,7 +736,7 @@ donation:
}
//test inventory related features
vmpos = await apps.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
vmpos.Title = "hello";
vmpos.Currency = "BTC";
vmpos.Template = @"
@ -741,7 +746,7 @@ inventoryitem:
inventory: 1
noninventoryitem:
price: 10.0";
Assert.IsType<RedirectToActionResult>(apps.UpdatePointOfSale(app.Id, vmpos).Result);
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
//inventoryitem has 1 item available
await tester.WaitForEvent<AppInventoryUpdaterHostedService.UpdateAppInventory>(() =>
@ -777,13 +782,13 @@ noninventoryitem:
//check that item is back in stock
await TestUtils.EventuallyAsync(async () =>
{
vmpos = await apps.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
Assert.Equal(1,
appService.Parse(vmpos.Template, "BTC").Single(item => item.Id == "inventoryitem").Inventory);
}, 10000);
//test payment methods option
vmpos = await apps.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
vmpos.Title = "hello";
vmpos.Currency = "BTC";
vmpos.Template = @"
@ -794,7 +799,7 @@ btconly:
- BTC
normal:
price: 1.0";
Assert.IsType<RedirectToActionResult>(apps.UpdatePointOfSale(app.Id, vmpos).Result);
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, null, null, null, null, "btconly").Result);
Assert.IsType<RedirectToActionResult>(publicApps
@ -838,8 +843,8 @@ g:
custom: topup
";
Assert.IsType<RedirectToActionResult>(apps.UpdatePointOfSale(app.Id, vmpos).Result);
vmpos = await apps.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
Assert.DoesNotContain("custom", vmpos.Template);
var items = appService.Parse(vmpos.Template, vmpos.Currency);
Assert.Contains(items, item => item.Id == "a" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed);

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<Import Project="../Build/Common.csproj" />
<PropertyGroup>
<IsPackable>false</IsPackable>
@ -19,12 +19,12 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.14" />
<PackageReference Include="Selenium.Support" Version="4.1.1" />
<PackageReference Include="Selenium.WebDriver" Version="4.1.1" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="104.0.5112.7900" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="105.0.5195.5200" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>

View File

@ -4,6 +4,8 @@ using BTCPayServer.Client.Models;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Plugins.Crowdfund.Controllers;
using BTCPayServer.Plugins.Crowdfund.Models;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Tests.Logging;
@ -35,6 +37,7 @@ namespace BTCPayServer.Tests
var stores = user.GetController<UIStoresController>();
var apps = user.GetController<UIAppsController>();
var apps2 = user2.GetController<UIAppsController>();
var crowdfund = user.GetController<UICrowdfundController>();
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId)).Model);
var appType = AppType.Crowdfund.ToString();
Assert.NotNull(vm.SelectedAppType);
@ -42,10 +45,12 @@ namespace BTCPayServer.Tests
vm.AppName = "test";
vm.SelectedAppType = appType;
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.CreateApp(user.StoreId, vm).Result);
Assert.Equal(nameof(apps.UpdateCrowdfund), redirectToAction.ActionName);
Assert.Equal(nameof(crowdfund.UpdateCrowdfund), redirectToAction.ActionName);
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
var app = appList.Apps[0];
apps.HttpContext.SetAppData(new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType});
var appData = new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType };
apps.HttpContext.SetAppData(appData);
crowdfund.HttpContext.SetAppData(appData);
var appList2 =
Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps2.ListApps(user2.StoreId).Result).Model);
Assert.Single(appList.Apps);
@ -72,6 +77,7 @@ namespace BTCPayServer.Tests
await user.GrantAccessAsync();
user.RegisterDerivationScheme("BTC");
var apps = user.GetController<UIAppsController>();
var crowdfund = user.GetController<UICrowdfundController>();
var vm = apps.CreateApp(user.StoreId).AssertViewModel<CreateAppViewModel>();
var appType = AppType.Crowdfund.ToString();
vm.AppName = "test";
@ -79,18 +85,20 @@ namespace BTCPayServer.Tests
Assert.IsType<RedirectToActionResult>(apps.CreateApp(user.StoreId, vm).Result);
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
var app = appList.Apps[0];
apps.HttpContext.SetAppData(new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType });
var appData = new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType };
apps.HttpContext.SetAppData(appData);
crowdfund.HttpContext.SetAppData(appData);
//Scenario 1: Not Enabled - Not Allowed
var crowdfundViewModel = await apps.UpdateCrowdfund(app.Id).AssertViewModelAsync<UpdateCrowdfundViewModel>();
var crowdfundViewModel = await crowdfund.UpdateCrowdfund(app.Id).AssertViewModelAsync<UpdateCrowdfundViewModel>();
crowdfundViewModel.TargetCurrency = "BTC";
crowdfundViewModel.Enabled = false;
crowdfundViewModel.EndDate = null;
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
var anonAppPubsController = tester.PayTester.GetController<UIAppsPublicController>();
var publicApps = user.GetController<UIAppsPublicController>();
var anonAppPubsController = tester.PayTester.GetController<UICrowdfundController>();
var crowdfundController = user.GetController<UICrowdfundController>();
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(app.Id, new ContributeToCrowdfund()
{
@ -100,19 +108,19 @@ namespace BTCPayServer.Tests
Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(app.Id, string.Empty));
//Scenario 2: Not Enabled But Admin - Allowed
Assert.IsType<OkObjectResult>(await publicApps.ContributeToCrowdfund(app.Id, new ContributeToCrowdfund()
Assert.IsType<OkObjectResult>(await crowdfundController.ContributeToCrowdfund(app.Id, new ContributeToCrowdfund()
{
RedirectToCheckout = false,
Amount = new decimal(0.01)
}, default));
Assert.IsType<ViewResult>(await publicApps.ViewCrowdfund(app.Id, string.Empty));
Assert.IsType<ViewResult>(await crowdfundController.ViewCrowdfund(app.Id, string.Empty));
Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(app.Id, string.Empty));
//Scenario 3: Enabled But Start Date > Now - Not Allowed
crowdfundViewModel.StartDate = DateTime.Today.AddDays(2);
crowdfundViewModel.Enabled = true;
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(app.Id, new ContributeToCrowdfund()
{
Amount = new decimal(0.01)
@ -123,7 +131,7 @@ namespace BTCPayServer.Tests
crowdfundViewModel.EndDate = DateTime.Today.AddDays(-1);
crowdfundViewModel.Enabled = true;
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(app.Id, new ContributeToCrowdfund()
{
Amount = new decimal(0.01)
@ -136,7 +144,7 @@ namespace BTCPayServer.Tests
crowdfundViewModel.TargetAmount = 1;
crowdfundViewModel.TargetCurrency = "BTC";
crowdfundViewModel.EnforceTargetAmount = true;
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(app.Id, new ContributeToCrowdfund()
{
Amount = new decimal(1.01)
@ -160,6 +168,7 @@ namespace BTCPayServer.Tests
user.RegisterDerivationScheme("BTC");
await user.SetNetworkFeeMode(NetworkFeeMode.Never);
var apps = user.GetController<UIAppsController>();
var crowdfund = user.GetController<UICrowdfundController>();
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId)).Model);
var appType = AppType.Crowdfund.ToString();
vm.AppName = "test";
@ -167,20 +176,21 @@ namespace BTCPayServer.Tests
Assert.IsType<RedirectToActionResult>(apps.CreateApp(user.StoreId, vm).Result);
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
var app = appList.Apps[0];
apps.HttpContext.SetAppData(new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType });
var appData = new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType };
apps.HttpContext.SetAppData(appData);
crowdfund.HttpContext.SetAppData(appData);
TestLogs.LogInformation("We create an invoice with a hardcap");
var crowdfundViewModel = await apps.UpdateCrowdfund(app.Id).AssertViewModelAsync<UpdateCrowdfundViewModel>();
var crowdfundViewModel = await crowdfund.UpdateCrowdfund(app.Id).AssertViewModelAsync<UpdateCrowdfundViewModel>();
crowdfundViewModel.Enabled = true;
crowdfundViewModel.EndDate = null;
crowdfundViewModel.TargetAmount = 100;
crowdfundViewModel.TargetCurrency = "BTC";
crowdfundViewModel.UseAllStoreInvoices = true;
crowdfundViewModel.EnforceTargetAmount = true;
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
var anonAppPubsController = tester.PayTester.GetController<UIAppsPublicController>();
var publicApps = user.GetController<UIAppsPublicController>();
var publicApps = user.GetController<UICrowdfundController>();
var model = Assert.IsType<ViewCrowdfundViewModel>(Assert
.IsType<ViewResult>(publicApps.ViewCrowdfund(app.Id, String.Empty).Result).Model);
@ -232,7 +242,7 @@ namespace BTCPayServer.Tests
Assert.Contains(AppService.GetAppInternalTag(app.Id), invoiceEntity.InternalTags);
crowdfundViewModel.UseAllStoreInvoices = false;
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
TestLogs.LogInformation("Because UseAllStoreInvoices is false, let's make sure the invoice is not tagged");
invoice = await user.BitPay.CreateInvoiceAsync(new Invoice
@ -251,7 +261,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>(apps.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
invoice = await user.BitPay.CreateInvoiceAsync(new Invoice
{
Buyer = new Buyer { email = "test@fwf.com" },

View File

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

View File

@ -483,93 +483,6 @@ namespace BTCPayServer.Tests
}
#endif
[Fact]
public void CanParseLegacyLabels()
{
static void AssertContainsRawLabel(WalletTransactionInfo info)
{
foreach (var item in new[] { "blah", "lol", "hello" })
{
Assert.True(info.Labels.ContainsKey(item));
var rawLabel = Assert.IsType<RawLabel>(info.Labels[item]);
Assert.Equal("raw", rawLabel.Type);
Assert.Equal(item, rawLabel.Text);
}
}
var data = new WalletTransactionData();
data.Labels = "blah,lol,hello,lol";
var info = data.GetBlobInfo();
Assert.Equal(3, info.Labels.Count);
AssertContainsRawLabel(info);
data.SetBlobInfo(info);
Assert.Contains("raw", data.Labels);
Assert.Contains("{", data.Labels);
Assert.Contains("[", data.Labels);
info = data.GetBlobInfo();
AssertContainsRawLabel(info);
data = new WalletTransactionData()
{
Labels = "pos",
Blob = Encoders.Hex.DecodeData("1f8b08000000000000037abf7b7fb592737e6e6e6a5e89929592522d000000ffff030036bc6ad911000000")
};
info = data.GetBlobInfo();
var label = Assert.Single(info.Labels);
Assert.Equal("raw", label.Value.Type);
Assert.Equal("pos", label.Value.Text);
Assert.Equal("pos", label.Key);
static void AssertContainsLabel(WalletTransactionInfo info)
{
Assert.Equal(2, info.Labels.Count);
var invoiceLabel = Assert.IsType<ReferenceLabel>(info.Labels["invoice"]);
Assert.Equal("BFm1MCJPBCDeRoWXvPcwnM", invoiceLabel.Reference);
Assert.Equal("invoice", invoiceLabel.Text);
Assert.Equal("invoice", invoiceLabel.Type);
var appLabel = Assert.IsType<ReferenceLabel>(info.Labels["app"]);
Assert.Equal("87kj5yKay8mB4UUZcJhZH5TqDKMD3CznjwLjiu1oYZXe", appLabel.Reference);
Assert.Equal("app", appLabel.Text);
Assert.Equal("app", appLabel.Type);
}
data = new WalletTransactionData()
{
Labels = "[\"{\\n \\\"value\\\": \\\"invoice\\\",\\n \\\"id\\\": \\\"BFm1MCJPBCDeRoWXvPcwnM\\\"\\n}\",\"{\\n \\\"value\\\": \\\"app\\\",\\n \\\"id\\\": \\\"87kj5yKay8mB4UUZcJhZH5TqDKMD3CznjwLjiu1oYZXe\\\"\\n}\"]",
};
info = data.GetBlobInfo();
AssertContainsLabel(info);
data.SetBlobInfo(info);
info = data.GetBlobInfo();
AssertContainsLabel(info);
static void AssertPayoutLabel(WalletTransactionInfo info)
{
Assert.Single(info.Labels);
var l = Assert.IsType<PayoutLabel>(info.Labels["payout"]);
Assert.Single(Assert.Single(l.PullPaymentPayouts, k => k.Key == "pullPaymentId").Value, "payoutId");
Assert.Equal("walletId", l.WalletId);
}
var payoutId = "payoutId";
var pullPaymentId = "pullPaymentId";
var walletId = "walletId";
// How it was serialized before
data = new WalletTransactionData()
{
Labels = new JArray(JObject.FromObject(new { value = "payout", id = payoutId, pullPaymentId, walletId })).ToString()
};
info = data.GetBlobInfo();
AssertPayoutLabel(info);
data.SetBlobInfo(info);
info = data.GetBlobInfo();
AssertPayoutLabel(info);
}
[Fact]
public void DeterministicUTXOSorter()
{
@ -1294,6 +1207,9 @@ namespace BTCPayServer.Tests
[Fact]
public void CanParseRateRules()
{
var pair = CurrencyPair.Parse("USD_EMAT_IC");
Assert.Equal("USD", pair.Left);
Assert.Equal("EMAT_IC", pair.Right);
// Check happy path
StringBuilder builder = new StringBuilder();
builder.AppendLine("// Some cool comments");
@ -1389,7 +1305,7 @@ namespace BTCPayServer.Tests
rule2.Reevaluate();
Assert.False(rule2.HasError);
Assert.Equal("5000 * 2000.4 * 1.1", rule2.ToString(true));
Assert.Equal(rule2.BidAsk.Bid, 5000m * 2000.4m * 1.1m);
Assert.Equal(5000m * 2000.4m * 1.1m, rule2.BidAsk.Bid);
////////
// Make sure parenthesis are correctly calculated

View File

@ -19,6 +19,7 @@ using BTCPayServer.Services.Custodian.Client.MockCustodian;
using BTCPayServer.Services;
using BTCPayServer.Services.Notifications;
using BTCPayServer.Services.Notifications.Blobs;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
@ -88,7 +89,7 @@ namespace BTCPayServer.Tests
Assert.Equal("missing-permission", e.APIError.Code);
Assert.NotNull(e.APIError.Message);
GreenfieldPermissionAPIError permissionError = Assert.IsType<GreenfieldPermissionAPIError>(e.APIError);
Assert.Equal(permissionError.MissingPermission, Policies.CanModifyStoreSettings);
Assert.Equal(Policies.CanModifyStoreSettings, permissionError.MissingPermission);
}
[Fact(Timeout = TestTimeout)]
@ -195,7 +196,7 @@ namespace BTCPayServer.Tests
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanCreateReadAndDeletePointOfSaleApp()
public async Task CanCreateReadUpdateAndDeletePointOfSaleApp()
{
using var tester = CreateServerTester();
await tester.StartAsync();
@ -203,8 +204,58 @@ namespace BTCPayServer.Tests
await user.RegisterDerivationSchemeAsync("BTC");
var client = await user.CreateClient();
// Test creating a POS app
var app = await client.CreatePointOfSaleApp(user.StoreId, new CreatePointOfSaleAppRequest() { AppName = "test app from API" });
// Test validation for creating the app
await AssertValidationError(new[] { "AppName" },
async () => await client.CreatePointOfSaleApp(user.StoreId, new CreatePointOfSaleAppRequest() {}));
await AssertValidationError(new[] { "AppName" },
async () => await client.CreatePointOfSaleApp(
user.StoreId,
new CreatePointOfSaleAppRequest()
{
AppName = "this is a really long app name this is a really long app name this is a really long app name",
}
)
);
await AssertValidationError(new[] { "Currency" },
async () => await client.CreatePointOfSaleApp(
user.StoreId,
new CreatePointOfSaleAppRequest()
{
AppName = "good name",
Currency = "fake currency"
}
)
);
await AssertValidationError(new[] { "Template" },
async () => await client.CreatePointOfSaleApp(
user.StoreId,
new CreatePointOfSaleAppRequest()
{
AppName = "good name",
Template = "lol invalid template"
}
)
);
await AssertValidationError(new[] { "AppName", "Currency", "Template" },
async () => await client.CreatePointOfSaleApp(
user.StoreId,
new CreatePointOfSaleAppRequest()
{
Currency = "fake currency",
Template = "lol invalid template"
}
)
);
// Test creating a POS app successfully
var app = await client.CreatePointOfSaleApp(
user.StoreId,
new CreatePointOfSaleAppRequest()
{
AppName = "test app from API",
Currency = "JPY"
}
);
Assert.Equal("test app from API", app.Name);
Assert.Equal(user.StoreId, app.StoreId);
Assert.Equal("PointOfSale", app.AppType);
@ -220,6 +271,11 @@ namespace BTCPayServer.Tests
Assert.Equal(app.StoreId, retrievedApp.StoreId);
Assert.Equal(app.AppType, retrievedApp.AppType);
// Test that we can update the app data
await client.UpdatePointOfSaleApp(app.Id, new CreatePointOfSaleAppRequest() { AppName = "new app name" });
retrievedApp = await client.GetApp(app.Id);
Assert.Equal("new app name", retrievedApp.Name);
// Make sure we return a 404 if we try to delete an app that doesn't exist
await AssertHttpError(404, async () =>
{
@ -1113,25 +1169,93 @@ namespace BTCPayServer.Tests
await client.ArchivePaymentRequest(user.StoreId, paymentRequest.Id);
Assert.DoesNotContain(paymentRequest.Id,
(await client.GetPaymentRequests(user.StoreId)).Select(data => data.Id));
//let's test some payment stuff
var archivedPrId = paymentRequest.Id;
//let's test some payment stuff with the UI
await user.RegisterDerivationSchemeAsync("BTC");
var paymentTestPaymentRequest = await client.CreatePaymentRequest(user.StoreId,
new CreatePaymentRequestRequest() { Amount = 0.1m, Currency = "BTC", Title = "Payment test title" });
var invoiceId = Assert.IsType<string>(Assert.IsType<OkObjectResult>(await user.GetController<UIPaymentRequestController>()
.PayPaymentRequest(paymentTestPaymentRequest.Id, false)).Value);
var invoice = user.BitPay.GetInvoice(invoiceId);
await tester.WaitForEvent<InvoiceDataChangedEvent>(async () =>
async Task Pay(string invoiceId, bool partialPayment = false)
{
await tester.ExplorerNode.SendToAddressAsync(
BitcoinAddress.Create(invoice.BitcoinAddress, tester.ExplorerNode.Network), invoice.BtcDue);
TestLogs.LogInformation($"Paying invoice {invoiceId}");
var invoice = user.BitPay.GetInvoice(invoiceId);
await tester.WaitForEvent<InvoiceDataChangedEvent>(async () =>
{
TestLogs.LogInformation($"Paying address {invoice.BitcoinAddress}");
await tester.ExplorerNode.SendToAddressAsync(
BitcoinAddress.Create(invoice.BitcoinAddress, tester.ExplorerNode.Network), invoice.BtcDue);
});
await TestUtils.EventuallyAsync(async () =>
{
Assert.Equal(Invoice.STATUS_PAID, user.BitPay.GetInvoice(invoiceId).Status);
if (!partialPayment)
Assert.Equal(PaymentRequestData.PaymentRequestStatus.Completed, (await client.GetPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id)).Status);
});
}
await Pay(invoiceId);
//Same thing, but with the API
paymentTestPaymentRequest = await client.CreatePaymentRequest(user.StoreId,
new CreatePaymentRequestRequest() { Amount = 0.1m, Currency = "BTC", Title = "Payment test title" });
var paidPrId = paymentTestPaymentRequest.Id;
var invoiceData = await client.PayPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new PayPaymentRequestRequest());
await Pay(invoiceData.Id);
// Let's tests some unhappy path
paymentTestPaymentRequest = await client.CreatePaymentRequest(user.StoreId,
new CreatePaymentRequestRequest() { Amount = 0.1m, AllowCustomPaymentAmounts = false, Currency = "BTC", Title = "Payment test title" });
await AssertValidationError(new[] { "Amount" }, () => client.PayPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new PayPaymentRequestRequest() { Amount = -0.04m }));
await AssertValidationError(new[] { "Amount" }, () => client.PayPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new PayPaymentRequestRequest() { Amount = 0.04m }));
await client.UpdatePaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new UpdatePaymentRequestRequest()
{
Amount = 0.1m,
AllowCustomPaymentAmounts = true,
Currency = "BTC",
Title = "Payment test title"
});
await TestUtils.EventuallyAsync(async () =>
{
Assert.Equal(Invoice.STATUS_PAID, user.BitPay.GetInvoice(invoiceId).Status);
Assert.Equal(PaymentRequestData.PaymentRequestStatus.Completed, (await client.GetPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id)).Status);
});
await AssertValidationError(new[] { "Amount" }, () => client.PayPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new PayPaymentRequestRequest() { Amount = -0.04m }));
invoiceData = await client.PayPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new PayPaymentRequestRequest() { Amount = 0.04m });
Assert.Equal(0.04m, invoiceData.Amount);
var firstPaymentId = invoiceData.Id;
await AssertAPIError("archived", () => client.PayPaymentRequest(user.StoreId, archivedPrId, new PayPaymentRequestRequest()));
await client.UpdatePaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new UpdatePaymentRequestRequest()
{
Amount = 0.1m,
AllowCustomPaymentAmounts = true,
Currency = "BTC",
Title = "Payment test title",
ExpiryDate = DateTimeOffset.UtcNow - TimeSpan.FromDays(1.0)
});
await AssertAPIError("expired", () => client.PayPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new PayPaymentRequestRequest()));
await AssertAPIError("already-paid", () => client.PayPaymentRequest(user.StoreId, paidPrId, new PayPaymentRequestRequest()));
await client.UpdatePaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new UpdatePaymentRequestRequest()
{
Amount = 0.1m,
AllowCustomPaymentAmounts = true,
Currency = "BTC",
Title = "Payment test title",
ExpiryDate = null
});
await Pay(firstPaymentId, true);
invoiceData = await client.PayPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new PayPaymentRequestRequest());
Assert.Equal(0.06m, invoiceData.Amount);
Assert.Equal("BTC", invoiceData.Currency);
var expectedInvoiceId = invoiceData.Id;
invoiceData = await client.PayPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new PayPaymentRequestRequest() { AllowPendingInvoiceReuse = true });
Assert.Equal(expectedInvoiceId, invoiceData.Id);
var notExpectedInvoiceId = invoiceData.Id;
invoiceData = await client.PayPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new PayPaymentRequestRequest() { AllowPendingInvoiceReuse = false });
Assert.NotEqual(notExpectedInvoiceId, invoiceData.Id);
}
[Fact(Timeout = TestTimeout)]
@ -1269,12 +1393,14 @@ namespace BTCPayServer.Tests
Checkout = new CreateInvoiceRequest.CheckoutOptions()
{
RedirectAutomatically = true,
RequiresRefundEmail = true
RequiresRefundEmail = true,
CheckoutFormId = GenericFormOption.Email.ToString()
},
AdditionalSearchTerms = new string[] { "Banana" }
});
Assert.True(newInvoice.Checkout.RedirectAutomatically);
Assert.True(newInvoice.Checkout.RequiresRefundEmail);
Assert.Equal(GenericFormOption.Email.ToString(), newInvoice.Checkout.CheckoutFormId);
Assert.Equal(user.StoreId, newInvoice.StoreId);
//list
var invoices = await viewOnly.GetInvoices(user.StoreId);
@ -1598,25 +1724,35 @@ namespace BTCPayServer.Tests
await tester.StartAsync();
await tester.EnsureChannelsSetup();
var user = tester.NewAccount();
user.GrantAccess(true);
await user.GrantAccessAsync(true);
user.RegisterLightningNode("BTC", LightningConnectionType.CLightning, false);
var merchant = tester.NewAccount();
merchant.GrantAccess(true);
await merchant.GrantAccessAsync(true);
merchant.RegisterLightningNode("BTC", LightningConnectionType.LndREST);
var merchantClient = await merchant.CreateClient($"{Policies.CanUseLightningNodeInStore}:{merchant.StoreId}");
var merchantInvoice = await merchantClient.CreateLightningInvoice(merchant.StoreId, "BTC", new CreateLightningInvoiceRequest(LightMoney.Satoshis(1_000), "hey", TimeSpan.FromSeconds(60)));
// The default client is using charge, so we should not be able to query channels
var client = await user.CreateClient(Policies.CanUseInternalLightningNode);
var chargeClient = await user.CreateClient(Policies.CanUseInternalLightningNode);
var info = await client.GetLightningNodeInfo("BTC");
var info = await chargeClient.GetLightningNodeInfo("BTC");
Assert.Single(info.NodeURIs);
Assert.NotEqual(0, info.BlockHeight);
Assert.NotNull(info.Alias);
Assert.NotNull(info.Color);
Assert.NotNull(info.Version);
Assert.NotNull(info.PeersCount);
Assert.NotNull(info.ActiveChannelsCount);
Assert.NotNull(info.InactiveChannelsCount);
Assert.NotNull(info.PendingChannelsCount);
await AssertAPIError("lightning-node-unavailable", () => client.GetLightningNodeChannels("BTC"));
var gex = await AssertAPIError("lightning-node-unavailable", () => chargeClient.ConnectToLightningNode("BTC", new ConnectToNodeRequest(NodeInfo.Parse($"{new Key().PubKey.ToHex()}@localhost:3827"))));
Assert.Contains("NotSupported", gex.Message);
await AssertAPIError("lightning-node-unavailable", () => chargeClient.GetLightningNodeChannels("BTC"));
// Not permission for the store!
await AssertAPIError("missing-permission", () => client.GetLightningNodeChannels(user.StoreId, "BTC"));
var invoiceData = await client.CreateLightningInvoice("BTC", new CreateLightningInvoiceRequest()
await AssertAPIError("missing-permission", () => chargeClient.GetLightningNodeChannels(user.StoreId, "BTC"));
var invoiceData = await chargeClient.CreateLightningInvoice("BTC", new CreateLightningInvoiceRequest()
{
Amount = LightMoney.Satoshis(1000),
Description = "lol",
@ -1624,9 +1760,17 @@ namespace BTCPayServer.Tests
PrivateRouteHints = false
});
var chargeInvoice = invoiceData;
Assert.NotNull(await client.GetLightningInvoice("BTC", invoiceData.Id));
Assert.NotNull(await chargeClient.GetLightningInvoice("BTC", invoiceData.Id));
client = await user.CreateClient($"{Policies.CanUseLightningNodeInStore}:{user.StoreId}");
// check list for internal node
var invoices = await chargeClient.GetLightningInvoices("BTC");
var pendingInvoices = await chargeClient.GetLightningInvoices("BTC", true);
Assert.NotEmpty(invoices);
Assert.Contains(invoices, i => i.Id == invoiceData.Id);
Assert.NotEmpty(pendingInvoices);
Assert.Contains(pendingInvoices, i => i.Id == invoiceData.Id);
var client = await user.CreateClient($"{Policies.CanUseLightningNodeInStore}:{user.StoreId}");
// Not permission for the server
await AssertAPIError("missing-permission", () => client.GetLightningNodeChannels("BTC"));
@ -1644,10 +1788,22 @@ namespace BTCPayServer.Tests
Assert.NotNull(await client.GetLightningInvoice(user.StoreId, "BTC", invoiceData.Id));
await client.PayLightningInvoice(user.StoreId, "BTC", new PayLightningInvoiceRequest()
// check pending list
var merchantPendingInvoices = await merchantClient.GetLightningInvoices(merchant.StoreId, "BTC", true);
Assert.NotEmpty(merchantPendingInvoices);
Assert.Contains(merchantPendingInvoices, i => i.Id == merchantInvoice.Id);
var payResponse = await client.PayLightningInvoice(user.StoreId, "BTC", new PayLightningInvoiceRequest
{
BOLT11 = merchantInvoice.BOLT11
});
Assert.Equal(merchantInvoice.BOLT11, payResponse.BOLT11);
Assert.Equal(LightningPaymentStatus.Complete, payResponse.Status);
Assert.NotNull(payResponse.Preimage);
Assert.NotNull(payResponse.FeeAmount);
Assert.NotNull(payResponse.TotalAmount);
Assert.NotNull(payResponse.PaymentHash);
await Assert.ThrowsAsync<GreenfieldValidationException>(async () => await client.PayLightningInvoice(user.StoreId, "BTC", new PayLightningInvoiceRequest()
{
BOLT11 = "lol"
@ -1664,6 +1820,15 @@ namespace BTCPayServer.Tests
var invoice = await merchantClient.GetLightningInvoice(merchant.StoreId, "BTC", merchantInvoice.Id);
Assert.NotNull(invoice.PaidAt);
Assert.Equal(LightMoney.Satoshis(1000), invoice.Amount);
// 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);
// 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);
// Amount received might be bigger because of internal implementation shit from lightning
Assert.True(LightMoney.Satoshis(1000) <= invoice.AmountReceived);
@ -1671,7 +1836,6 @@ namespace BTCPayServer.Tests
Assert.Single(info.NodeURIs);
Assert.NotEqual(0, info.BlockHeight);
// As admin, can use the internal node through our store.
await user.MakeAdmin(true);
await user.RegisterInternalLightningNodeAsync("BTC");
@ -1681,7 +1845,7 @@ namespace BTCPayServer.Tests
await AssertPermissionError("btcpay.server.canuseinternallightningnode", () => client.GetLightningNodeInfo(user.StoreId, "BTC"));
// However, even as a guest, you should be able to create an invoice
var guest = tester.NewAccount();
guest.GrantAccess(false);
await guest.GrantAccessAsync();
await user.AddGuest(guest.UserId);
client = await guest.CreateClient(Policies.CanCreateLightningInvoiceInStore);
await client.CreateLightningInvoice(user.StoreId, "BTC", new CreateLightningInvoiceRequest()
@ -2208,6 +2372,7 @@ namespace BTCPayServer.Tests
Assert.Equal(transaction.TransactionHash, txdata.TransactionHash);
Assert.Equal(String.Empty, transaction.Comment);
#pragma warning disable CS0612 // Type or member is obsolete
Assert.Equal(new Dictionary<string, LabelData>(), transaction.Labels);
// transaction patch tests
@ -2228,7 +2393,7 @@ namespace BTCPayServer.Tests
}.ToJson(),
patchedTransaction.Labels.ToJson()
);
#pragma warning restore CS0612 // Type or member is obsolete
await AssertHttpError(403, async () =>
{
await viewOnlyClient.ShowOnChainWalletTransactions(walletId.StoreId, walletId.CryptoCode);
@ -2638,8 +2803,67 @@ namespace BTCPayServer.Tests
Assert.NotNull(custodians);
Assert.NotEmpty(custodians);
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task StoreRateConfigTests()
{
using var tester = CreateServerTester();
await tester.StartAsync();
var unauthClient = new BTCPayServerClient(tester.PayTester.ServerUri);
await AssertHttpError(401, async () => await unauthClient.GetRateSources());
var user = tester.NewAccount();
await user.GrantAccessAsync();
var clientBasic = await user.CreateClient();
Assert.NotEmpty(await clientBasic.GetRateSources());
var config = await clientBasic.GetStoreRateConfiguration(user.StoreId);
Assert.NotNull(config);
Assert.False(config.IsCustomScript);
Assert.Equal("X_X = coingecko(X_X);", config.EffectiveScript);
Assert.Equal("coingecko", config.PreferredSource);
Assert.Equal(0.9m,
Assert.Single(await clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId,
new StoreRateConfiguration() {IsCustomScript = true, EffectiveScript = "BTC_XYZ = 1;", Spread = 10m,},
new[] {"BTC_XYZ"})).Rate);
Assert.True((await clientBasic.UpdateStoreRateConfiguration(user.StoreId,
new StoreRateConfiguration() { IsCustomScript = true, EffectiveScript = "BTC_XYZ = 1", Spread = 10m,}))
.IsCustomScript);
config = await clientBasic.GetStoreRateConfiguration(user.StoreId);
Assert.NotNull(config);
Assert.NotNull(config.EffectiveScript);
Assert.Equal("BTC_XYZ = 1;", config.EffectiveScript);
Assert.Equal(10m, config.Spread);
Assert.Null(config.PreferredSource);
Assert.NotNull((await clientBasic.GetStoreRateConfiguration(user.StoreId)).EffectiveScript);
Assert.NotNull((await clientBasic.UpdateStoreRateConfiguration(user.StoreId,
new StoreRateConfiguration() { IsCustomScript = false, PreferredSource = "coingecko"}))
.PreferredSource);
config = await clientBasic.GetStoreRateConfiguration(user.StoreId);
Assert.Equal("X_X = coingecko(X_X);", config.EffectiveScript);
await AssertValidationError(new[] { "EffectiveScript", "PreferredSource" }, () =>
clientBasic.UpdateStoreRateConfiguration(user.StoreId, new StoreRateConfiguration() { IsCustomScript = false, EffectiveScript = "BTC_XYZ = 1;" }));
await AssertValidationError(new[] { "EffectiveScript" }, () =>
clientBasic.UpdateStoreRateConfiguration(user.StoreId, new StoreRateConfiguration() { IsCustomScript = true, EffectiveScript = "BTC_XYZ rg8w*# 1;" }));
await AssertValidationError(new[] { "PreferredSource" }, () =>
clientBasic.UpdateStoreRateConfiguration(user.StoreId, new StoreRateConfiguration() { IsCustomScript = true, EffectiveScript = "", PreferredSource = "coingecko" }));
await AssertValidationError(new[] { "PreferredSource", "Spread" }, () =>
clientBasic.UpdateStoreRateConfiguration(user.StoreId, new StoreRateConfiguration() { IsCustomScript = false, PreferredSource = "coingeckoOOO", Spread = -1m }));
await AssertValidationError(new[] { "currencyPair" }, () =>
clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId, new StoreRateConfiguration() { IsCustomScript = false, PreferredSource = "coingecko" }, new[] { "BTC_USD_USD_BTC" }));
await AssertValidationError(new[] { "PreferredSource", "currencyPair" }, () =>
clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId, new StoreRateConfiguration() { IsCustomScript = false, PreferredSource = "coingeckoOOO" }, new[] { "BTC_USD_USD_BTC" }));
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CustodianAccountControllerTests()

View File

@ -2,6 +2,8 @@ using System.Threading.Tasks;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Plugins.PointOfSale.Controllers;
using BTCPayServer.Plugins.PointOfSale.Models;
using BTCPayServer.Services.Apps;
using BTCPayServer.Tests.Logging;
using Microsoft.AspNetCore.Mvc;
@ -28,6 +30,7 @@ namespace BTCPayServer.Tests
await user.GrantAccessAsync();
user.RegisterDerivationScheme("BTC");
var apps = user.GetController<UIAppsController>();
var pos = user.GetController<UIPointOfSaleController>();
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId)).Model);
var appType = AppType.PointOfSale.ToString();
vm.AppName = "test";
@ -35,8 +38,10 @@ namespace BTCPayServer.Tests
Assert.IsType<RedirectToActionResult>(apps.CreateApp(user.StoreId, vm).Result);
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
var app = appList.Apps[0];
apps.HttpContext.SetAppData(new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType });
var vmpos = await apps.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
var appData = new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType };
apps.HttpContext.SetAppData(appData);
pos.HttpContext.SetAppData(appData);
var vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
vmpos.Template = @"
apple:
price: 5.0
@ -48,9 +53,9 @@ donation:
price: 1.02
custom: true
";
Assert.IsType<RedirectToActionResult>(apps.UpdatePointOfSale(app.Id, vmpos).Result);
vmpos = await apps.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
var publicApps = user.GetController<UIAppsPublicController>();
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
var publicApps = user.GetController<UIPointOfSaleController>();
var vmview = await publicApps.ViewPointOfSale(app.Id, PosViewType.Cart).AssertViewModelAsync<ViewPointOfSaleViewModel>();
// apple shouldn't be available since we it's set to "disabled: true" above

View File

@ -447,9 +447,12 @@ namespace BTCPayServer.Tests
s.AddDerivationScheme();
s.GoToInvoices();
var i = s.CreateInvoice();
s.GoToInvoiceCheckout(i);
s.PayInvoice(true);
TestUtils.Eventually(() => s.Driver.FindElement(By.LinkText("View receipt")).Click());
await s.Server.PayTester.InvoiceRepository.MarkInvoiceStatus(i, InvoiceStatus.Settled);
TestUtils.Eventually(() =>
{
s.Driver.Navigate().Refresh();
s.Driver.FindElement(By.Id($"Receipt")).Click();
});
TestUtils.Eventually(() =>
{
s.Driver.Navigate().Refresh();
@ -472,7 +475,11 @@ namespace BTCPayServer.Tests
s.GoToInvoiceCheckout(i);
var checkouturi = s.Driver.Url;
s.PayInvoice();
TestUtils.Eventually(() => s.Driver.FindElement(By.LinkText("View receipt")).Click());
TestUtils.Eventually(() =>
{
s.Driver.Navigate().Refresh();
s.Driver.FindElement(By.Id("receipt-btn")).Click();
});
TestUtils.Eventually(() =>
{
s.Driver.Navigate().Refresh();
@ -480,9 +487,10 @@ namespace BTCPayServer.Tests
Assert.Contains("invoice-processing", s.Driver.PageSource);
});
s.GoToUrl(checkouturi);
s.MineBlockOnInvoiceCheckout();
await s.Server.PayTester.InvoiceRepository.MarkInvoiceStatus(i, InvoiceStatus.Settled);
TestUtils.Eventually(() => s.Driver.FindElement(By.LinkText("View receipt")).Click());
TestUtils.Eventually(() => s.Driver.FindElement(By.Id("receipt-btn")).Click());
TestUtils.Eventually(() =>
{
s.Driver.Navigate().Refresh();
@ -636,15 +644,26 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("enable-pay-button")).Click();
s.Driver.FindElement(By.Id("disable-pay-button")).Click();
s.FindAlertMessage();
s.GoToStore(StoreNavPages.General);
s.GoToStore();
Assert.False(s.Driver.FindElement(By.Id("AnyoneCanCreateInvoice")).Selected);
s.Driver.SetCheckbox(By.Id("AnyoneCanCreateInvoice"), true);
s.Driver.FindElement(By.Id("Save")).Click();
s.FindAlertMessage();
Assert.True(s.Driver.FindElement(By.Id("AnyoneCanCreateInvoice")).Selected);
// Store settings: Set and unset brand color
s.GoToStore();
s.Driver.FindElement(By.Id("BrandColor")).SendKeys("#f7931a");
s.Driver.FindElement(By.Id("Save")).Click();
Assert.Contains("Store successfully updated", s.FindAlertMessage().Text);
Assert.Equal("#f7931a", s.Driver.FindElement(By.Id("BrandColor")).GetAttribute("value"));
s.Driver.FindElement(By.Id("BrandColor")).Clear();
s.Driver.FindElement(By.Id("Save")).Click();
Assert.Contains("Store successfully updated", s.FindAlertMessage().Text);
Assert.Equal(string.Empty, s.Driver.FindElement(By.Id("BrandColor")).GetAttribute("value"));
// Alice should be able to delete the store
s.GoToStore(StoreNavPages.General);
s.GoToStore();
s.Driver.FindElement(By.Id("DeleteStore")).Click();
s.Driver.WaitForElement(By.Id("ConfirmInput")).SendKeys("DELETE");
s.Driver.FindElement(By.Id("ConfirmContinue")).Click();
@ -800,6 +819,16 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("TargetCurrency")).Clear();
s.Driver.FindElement(By.Id("TargetCurrency")).SendKeys("JPY");
s.Driver.FindElement(By.Id("TargetAmount")).SendKeys("700");
// test wrong dates
s.Driver.ExecuteJavaScript("const now = new Date();document.getElementById('StartDate').value = now.toISOString();" +
"const yst = new Date(now.setDate(now.getDate() -1));document.getElementById('EndDate').value = yst.toISOString()");
s.Driver.FindElement(By.Id("SaveSettings")).Click();
Assert.Contains("End date cannot be before start date", s.Driver.PageSource);
Assert.DoesNotContain("App updated", s.Driver.PageSource);
// unset end date
s.Driver.ExecuteJavaScript("document.getElementById('EndDate').value = ''");
s.Driver.FindElement(By.Id("SaveSettings")).Click();
Assert.Contains("App updated", s.FindAlertMessage().Text);
@ -842,9 +871,8 @@ namespace BTCPayServer.Tests
var viewUrl = s.Driver.Url;
Assert.Equal("Amount due", s.Driver.FindElement(By.CssSelector("[data-test='amount-due-title']")).Text);
Assert.Equal("Pay Invoice",
s.Driver.FindElement(By.CssSelector("[data-test='pay-button']")).Text.Trim());
Assert.Equal("Pay Invoice", s.Driver.FindElement(By.CssSelector("[data-test='pay-button']")).Text.Trim());
// expire
s.GoToUrl(editUrl);
s.Driver.ExecuteJavaScript("document.getElementById('ExpiryDate').value = '2021-01-21T21:00:00.000Z'");
@ -862,8 +890,13 @@ namespace BTCPayServer.Tests
s.GoToUrl(viewUrl);
s.Driver.AssertElementNotFound(By.CssSelector("[data-test='status']"));
Assert.Equal("Pay Invoice",
s.Driver.FindElement(By.CssSelector("[data-test='pay-button']")).Text.Trim());
Assert.Equal("Pay Invoice", s.Driver.FindElement(By.CssSelector("[data-test='pay-button']")).Text.Trim());
// test invoice creation, click with JS, because the button is inside a sticky header
s.Driver.ExecuteJavaScript("document.querySelector('[data-test=\"pay-button\"]').click()");
// checkout v1
s.Driver.WaitForElement(By.CssSelector("invoice"));
Assert.Contains("Awaiting Payment", s.Driver.PageSource);
// archive (from details page)
s.GoToUrl(editUrl);
@ -992,10 +1025,10 @@ namespace BTCPayServer.Tests
}
// This one should be checked
Assert.Contains($"value=\"InvoiceProcessing\" checked", s.Driver.PageSource);
Assert.Contains($"value=\"InvoiceCreated\" checked", s.Driver.PageSource);
Assert.Contains("value=\"InvoiceProcessing\" checked", s.Driver.PageSource);
Assert.Contains("value=\"InvoiceCreated\" checked", s.Driver.PageSource);
// This one never been checked
Assert.DoesNotContain($"value=\"InvoiceReceivedPayment\" checked", s.Driver.PageSource);
Assert.DoesNotContain("value=\"InvoiceReceivedPayment\" checked", s.Driver.PageSource);
s.Driver.FindElement(By.Name("update")).Click();
s.FindAlertMessage();
@ -1026,6 +1059,7 @@ namespace BTCPayServer.Tests
s.GoToStore(StoreNavPages.Webhooks);
s.Driver.FindElement(By.LinkText("Modify")).Click();
var elements = s.Driver.FindElements(By.ClassName("redeliver"));
// One worked, one failed
s.Driver.FindElement(By.ClassName("fa-times"));
s.Driver.FindElement(By.ClassName("fa-check"));

View File

@ -75,11 +75,11 @@ namespace BTCPayServer.Tests
public async Task CanQueryDirectProviders()
{
// TODO: Check once in a while whether or not they are working again
string[] brokenShitcoinCasinos = { };
string[] brokenShitcoinCasinos = {};
var skipped = 0;
var factory = FastTests.CreateBTCPayRateFactory();
var directlySupported = factory.GetSupportedExchanges().Where(s => s.Source == RateSource.Direct)
.Select(s => s.Id).ToHashSet();
var all = string.Join("\r\n", factory.GetSupportedExchanges().Select(e => e.Id).ToArray());
foreach (var result in factory
.Providers
.Where(p => p.Value is BackgroundFetcherRateProvider bf &&
@ -91,14 +91,26 @@ namespace BTCPayServer.Tests
var name = result.ExpectedName;
if (brokenShitcoinCasinos.Contains(name))
{
TestLogs.LogInformation($"Skipping {name}");
TestLogs.LogInformation($"Skipping {name}: Broken shitcoin casino");
skipped++;
continue;
}
TestLogs.LogInformation($"Testing {name}");
result.Fetcher.InvalidateCache();
var exchangeRates = new ExchangeRates(name, result.ResultAsync.Result);
ExchangeRates exchangeRates = null;
try
{
exchangeRates = new ExchangeRates(name, result.ResultAsync.Result);
}
catch (Exception exception)
{
TestLogs.LogInformation($"Skipping {name}: {exception.Message}");
skipped++;
continue;
}
result.Fetcher.InvalidateCache();
Assert.NotNull(exchangeRates);
Assert.NotEmpty(exchangeRates);
@ -160,11 +172,12 @@ namespace BTCPayServer.Tests
// Kraken emit one request only after first GetRates
factory.Providers["kraken"].GetRatesAsync(default).GetAwaiter().GetResult();
var p = new KrakenExchangeRateProvider();
var rates = await p.GetRatesAsync(default);
Assert.Contains(rates, e => e.CurrencyPair == new CurrencyPair("XMR", "BTC") && e.BidAsk.Bid < 1.0m);
// Check we didn't skip too many exchanges
Assert.InRange(skipped, 0, 3);
}
[Fact]

View File

@ -35,10 +35,12 @@ using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Payments.PayJoin.Sender;
using BTCPayServer.Plugins.PointOfSale.Controllers;
using BTCPayServer.Security.Bitpay;
using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Labels;
using BTCPayServer.Services.Mails;
using BTCPayServer.Services.Rates;
using BTCPayServer.Storage.Models;
@ -50,6 +52,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBitcoin.Payment;
@ -651,7 +654,7 @@ namespace BTCPayServer.Tests
(string)store2.TempData[WellKnownTempData.ErrorMessage], StringComparison.CurrentCultureIgnoreCase);
}
[Fact(Timeout = LongRunningTestTimeout)]
[Fact(Timeout = LongRunningTestTimeout * 2)]
[Trait("Integration", "Integration")]
public async Task CanUseTorClient()
{
@ -784,9 +787,9 @@ namespace BTCPayServer.Tests
tx = Assert.Single(transactions.Transactions);
Assert.Equal("hello", tx.Comment);
Assert.Contains("test", tx.Labels.Select(l => l.Text));
Assert.Contains("test2", tx.Labels.Select(l => l.Text));
Assert.Equal(2, tx.Labels.GroupBy(l => l.Color).Count());
Assert.Contains("test", tx.Tags.Select(l => l.Text));
Assert.Contains("test2", tx.Tags.Select(l => l.Text));
Assert.Equal(2, tx.Tags.GroupBy(l => l.Color).Count());
Assert.IsType<RedirectToActionResult>(
await walletController.ModifyTransaction(walletId, tx.Id, removelabel: "test2"));
@ -796,12 +799,9 @@ namespace BTCPayServer.Tests
tx = Assert.Single(transactions.Transactions);
Assert.Equal("hello", tx.Comment);
Assert.Contains("test", tx.Labels.Select(l => l.Text));
Assert.DoesNotContain("test2", tx.Labels.Select(l => l.Text));
Assert.Single(tx.Labels.GroupBy(l => l.Color));
var walletInfo = await tester.PayTester.GetService<WalletRepository>().GetWalletInfo(walletId);
Assert.Single(walletInfo.LabelColors); // the test2 color should have been removed
Assert.Contains("test", tx.Tags.Select(l => l.Text));
Assert.DoesNotContain("test2", tx.Tags.Select(l => l.Text));
Assert.Single(tx.Tags.GroupBy(l => l.Color));
}
[Fact(Timeout = LongRunningTestTimeout)]
@ -1752,7 +1752,7 @@ namespace BTCPayServer.Tests
await user.GrantAccessAsync();
user.RegisterDerivationScheme("BTC");
await user.SetNetworkFeeMode(NetworkFeeMode.Always);
var invoice = user.BitPay.CreateInvoice(
var invoice = await user.BitPay.CreateInvoiceAsync(
new Invoice
{
Price = 10,
@ -1764,11 +1764,10 @@ namespace BTCPayServer.Tests
}, Facade.Merchant);
var networkFee = new FeeRate(invoice.MinerFees["BTC"].SatoshiPerBytes).GetFee(100);
// ensure 0 invoices exported because there are no payments yet
var jsonResult = user.GetController<UIInvoiceController>().Export("json").GetAwaiter().GetResult();
var result = Assert.IsType<ContentResult>(jsonResult);
Assert.Equal("application/json", result.ContentType);
Assert.Equal("[]", result.Content);
Assert.Single(JArray.Parse(result.Content));
var cashCow = tester.ExplorerNode;
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
@ -1953,6 +1952,7 @@ namespace BTCPayServer.Tests
var stores = user.GetController<UIStoresController>();
var apps = user.GetController<UIAppsController>();
var apps2 = user2.GetController<UIAppsController>();
var pos = user.GetController<UIPointOfSaleController>();
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId)).Model);
var appType = AppType.PointOfSale.ToString();
Assert.NotNull(vm.SelectedAppType);
@ -1960,12 +1960,14 @@ namespace BTCPayServer.Tests
vm.AppName = "test";
vm.SelectedAppType = appType;
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.CreateApp(user.StoreId, vm).Result);
Assert.Equal(nameof(apps.UpdatePointOfSale), redirectToAction.ActionName);
Assert.Equal(nameof(pos.UpdatePointOfSale), redirectToAction.ActionName);
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
var appList2 =
Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps2.ListApps(user2.StoreId).Result).Model);
var app = appList.Apps[0];
apps.HttpContext.SetAppData(new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType });
var appData = new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType };
apps.HttpContext.SetAppData(appData);
pos.HttpContext.SetAppData(appData);
Assert.Single(appList.Apps);
Assert.Empty(appList2.Apps);
Assert.Equal("test", appList.Apps[0].AppName);
@ -2518,6 +2520,79 @@ namespace BTCPayServer.Tests
Assert.True(lnMethod.IsInternalNode);
}
[Fact(Timeout = LongRunningTestTimeout)]
[Trait("Integration", "Integration")]
[Obsolete]
public async Task CanDoLabelMigrations()
{
using var tester = CreateServerTester(newDb: true);
await tester.StartAsync();
var dbf = tester.PayTester.GetService<ApplicationDbContextFactory>();
int walletCount = 1000;
var wallet = "walletttttttttttttttttttttttttttt";
using (var db = dbf.CreateContext())
{
for (int i = 0; i < walletCount; i++)
{
var walletData = new WalletData() { Id = $"S-{wallet}{i}-BTC" };
walletData.Blob = ZipUtils.Zip("{\"LabelColors\": { \"label1\" : \"black\", \"payout\":\"green\" }}");
db.Wallets.Add(walletData);
}
await db.SaveChangesAsync();
}
uint256 firstTxId = null;
using (var db = dbf.CreateContext())
{
int transactionCount = 10_000;
for (int i = 0; i < transactionCount; i++)
{
var txId = RandomUtils.GetUInt256();
var wt = new WalletTransactionData()
{
WalletDataId = $"S-{wallet}{i % walletCount}-BTC",
TransactionId = txId.ToString(),
};
firstTxId ??= txId;
if (i != 10)
wt.Blob = ZipUtils.Zip("{\"Comment\":\"test\"}");
if (i % 1240 != 0)
{
wt.Labels = "[{\"type\":\"raw\", \"text\":\"label1\"}]";
}
else if (i == 0)
{
wt.Labels = "[{\"type\":\"raw\", \"text\":\"label1\"},{\"type\":\"raw\", \"text\":\"labelo" + i + "\"}, " +
"{\"type\":\"payout\", \"text\":\"payout\", \"pullPaymentPayouts\":{\"pp1\":[\"p1\",\"p2\"],\"pp2\":[\"p3\"]}}]";
}
else
{
wt.Labels = "[{\"type\":\"raw\", \"text\":\"label1\"},{\"type\":\"raw\", \"text\":\"labelo" + i + "\"}]";
}
db.WalletTransactions.Add(wt);
}
await db.SaveChangesAsync();
}
await RestartMigration(tester);
var migrator = tester.PayTester.GetService<IEnumerable<IHostedService>>().OfType<DbMigrationsHostedService>().First();
await migrator.MigratedTransactionLabels(0);
var walletRepo = tester.PayTester.GetService<WalletRepository>();
var wi1 = await walletRepo.GetWalletLabels(new WalletId($"{wallet}0", "BTC"));
Assert.Equal(3, wi1.Length);
Assert.Contains(wi1, o => o.Label == "label1" && o.Color == "black");
Assert.Contains(wi1, o => o.Label == "labelo0" && o.Color == "#000");
Assert.Contains(wi1, o => o.Label == "payout" && o.Color == "green");
var txInfo = await walletRepo.GetWalletTransactionsInfo(new WalletId($"{wallet}0", "BTC"), new[] { firstTxId.ToString() });
Assert.Equal("test", txInfo.Values.First().Comment);
// Should have the 2 raw labels, and one legacy label for payouts
Assert.Equal(3, txInfo.Values.First().LegacyLabels.Count);
var payoutLabel = txInfo.Values.First().LegacyLabels.Select(l => l.Value).OfType<PayoutLabel>().First();
Assert.Equal(2, payoutLabel.PullPaymentPayouts.Count);
Assert.Equal(2, payoutLabel.PullPaymentPayouts["pp1"].Count);
Assert.Single(payoutLabel.PullPaymentPayouts["pp2"]);
}
[Fact(Timeout = LongRunningTestTimeout)]
[Trait("Integration", "Integration")]

View File

@ -0,0 +1,67 @@
#!/bin/bash
# Creates a 2-of-3 multisig setup, following the procedure described here:
# https://github.com/bitcoin/bitcoin/blob/master/doc/multisig-tutorial.md
# https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md
#
# Usage:
# ./docker-bitcoin-multisig-setup.sh custom-name
#
# The custom name/prefix is optional and defaults to "multisig".
prefix="${1:-"multi_sig"}"
declare -A xpubs
printf "\n👛 Create descriptor wallets\n\n"
for ((n=1;n<=3;n++)); do
# Create descriptor wallets, surpress error output in case wallet already exists
./docker-bitcoin-cli.sh -named createwallet wallet_name="${prefix}_part_${n}" descriptors=true > /dev/null 2>&1
# Collect xpubs
./docker-bitcoin-cli.sh -rpcwallet="${prefix}_part_${n}" listdescriptors > /dev/null 2>&1
xpubs["internal_xpub_${n}"]=$(./docker-bitcoin-cli.sh -rpcwallet="${prefix}_part_${n}" listdescriptors | jq '.descriptors | [.[] | select(.desc | startswith("wpkh") and contains("/1/*"))][0] | .desc' | grep -Po '(?<=\().*(?=\))')
xpubs["external_xpub_${n}"]=$(./docker-bitcoin-cli.sh -rpcwallet="${prefix}_part_${n}" listdescriptors | jq '.descriptors | [.[] | select(.desc | startswith("wpkh") and contains("/0/*"))][0] | .desc' | grep -Po '(?<=\().*(?=\))')
done
for x in "${!xpubs[@]}"; do
printf "[%s]=%s\n" "$x" "${xpubs[$x]}";
done
external_desc="wsh(sortedmulti(2,${xpubs["external_xpub_1"]},${xpubs["external_xpub_2"]},${xpubs["external_xpub_3"]}))"
internal_desc="wsh(sortedmulti(2,${xpubs["internal_xpub_1"]},${xpubs["internal_xpub_2"]},${xpubs["internal_xpub_3"]}))"
external_desc_sum=$(./docker-bitcoin-cli.sh getdescriptorinfo $external_desc | jq '.descriptor')
internal_desc_sum=$(./docker-bitcoin-cli.sh getdescriptorinfo $internal_desc | jq '.descriptor')
multisig_ext_desc="{\"desc\": $external_desc_sum, \"active\": true, \"internal\": false, \"timestamp\": \"now\"}"
multisig_int_desc="{\"desc\": $internal_desc_sum, \"active\": true, \"internal\": true, \"timestamp\": \"now\"}"
multisig_desc="[$multisig_ext_desc, $multisig_int_desc]"
# Create multisig wallet, surpress error output in case wallet already exists
printf "\n🔐 Create multisig wallet\n"
printf "\nExternal descriptor: $external_desc\n"
printf "\nInternal descriptor: $internal_desc\n"
multisig_name="${prefix}_wallet"
./docker-bitcoin-cli.sh -named createwallet wallet_name="$multisig_name" disable_private_keys=true blank=true descriptors=true > /dev/null 2>&1
./docker-bitcoin-cli.sh -rpcwallet="$multisig_name" importdescriptors "$multisig_desc" > /dev/null 2>&1
# Fund the wallet from the default wallet
printf "\n💰 Fund multisig wallet\n"
newaddress=$(./docker-bitcoin-cli.sh -rpcwallet="$multisig_name" getnewaddress "MultiSig Funding" | tr -d "[:cntrl:]")
txid=$(./docker-bitcoin-cli.sh -rpcwallet="" sendtoaddress "$newaddress" 0.615)
printf "\nReceiving address: $newaddress\n"
printf "\nTransaction ID: $txid\n"
# Confirm everything worked
printf "\n Multisig wallet info\n\n"
./docker-bitcoin-cli.sh -rpcwallet="$multisig_name" getwalletinfo
# Unload wallets to prevent having to specify which wallet to use in BTCPay, NBXplorer etc.
for ((n=1;n<=3;n++)); do
./docker-bitcoin-cli.sh unloadwallet "${prefix}_part_${n}" > /dev/null 2>&1
done
./docker-bitcoin-cli.sh unloadwallet "$multisig_name" > /dev/null 2>&1

View File

@ -71,7 +71,7 @@ services:
- "sshd_datadir:/root/.ssh"
devlnd:
image: btcpayserver/bitcoin:22.0
image: btcpayserver/bitcoin:23.0-1
environment:
BITCOIN_NETWORK: regtest
BITCOIN_WALLETDIR: "/data/wallets"
@ -90,7 +90,7 @@ services:
expose:
- "4444"
nbxplorer:
image: nicolasdorier/nbxplorer:2.3.14
image: nicolasdorier/nbxplorer:2.3.40
restart: unless-stopped
ports:
- "32838:32838"
@ -126,7 +126,7 @@ services:
bitcoind:
restart: unless-stopped
image: btcpayserver/bitcoin:22.0
image: btcpayserver/bitcoin:23.0-1
environment:
BITCOIN_NETWORK: regtest
BITCOIN_WALLETDIR: "/data/wallets"
@ -237,7 +237,7 @@ services:
- "5432"
merchant_lnd:
image: btcpayserver/lnd:v0.15.0-beta
image: btcpayserver/lnd:v0.15.4-beta-1
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -272,7 +272,7 @@ services:
- bitcoind
customer_lnd:
image: btcpayserver/lnd:v0.15.0-beta
image: btcpayserver/lnd:v0.15.4-beta-1
restart: unless-stopped
environment:
LND_CHAIN: "btc"

View File

@ -68,7 +68,7 @@ services:
- "sshd_datadir:/root/.ssh"
devlnd:
image: btcpayserver/bitcoin:22.0
image: btcpayserver/bitcoin:23.0-1
environment:
BITCOIN_NETWORK: regtest
BITCOIN_WALLETDIR: "/data/wallets"
@ -87,7 +87,7 @@ services:
expose:
- "4444"
nbxplorer:
image: nicolasdorier/nbxplorer:2.3.14
image: nicolasdorier/nbxplorer:2.3.40
restart: unless-stopped
ports:
- "32838:32838"
@ -113,7 +113,7 @@ services:
bitcoind:
restart: unless-stopped
image: btcpayserver/bitcoin:22.0
image: btcpayserver/bitcoin:23.0-1
environment:
BITCOIN_NETWORK: regtest
BITCOIN_WALLETDIR: "/data/wallets"
@ -225,7 +225,7 @@ services:
- "5432"
merchant_lnd:
image: btcpayserver/lnd:v0.15.0-beta
image: btcpayserver/lnd:v0.15.4-beta-1
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -262,7 +262,7 @@ services:
- bitcoind
customer_lnd:
image: btcpayserver/lnd:v0.15.0-beta
image: btcpayserver/lnd:v0.15.4-beta-1
restart: unless-stopped
environment:
LND_CHAIN: "btc"

View File

@ -1,4 +1,4 @@
 <Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<Import Project="../Build/Version.csproj" Condition="Exists('../Build/Version.csproj')" />
<Import Project="../Build/Common.csproj" />
@ -9,16 +9,12 @@
</PropertyGroup>
<ItemGroup>
<Compile Remove="Build\**" />
<Compile Remove="wwwroot\bundles\jqueryvalidate\**" />
<Compile Remove="wwwroot\vendor\jquery-nice-select\**" />
<Content Remove="Build\**" />
<Content Remove="wwwroot\bundles\jqueryvalidate\**" />
<Content Remove="wwwroot\vendor\jquery-nice-select\**" />
<EmbeddedResource Remove="Build\**" />
<EmbeddedResource Remove="wwwroot\bundles\jqueryvalidate\**" />
<EmbeddedResource Remove="wwwroot\vendor\jquery-nice-select\**" />
<None Remove="Build\**" />
<None Remove="wwwroot\bundles\jqueryvalidate\**" />
<None Remove="wwwroot\vendor\jquery-nice-select\**" />
<Content Update="Views\UIStorePullPayments\NewPullPayment.cshtml">
<Pack>false</Pack>
@ -33,9 +29,6 @@
<ItemGroup>
<None Remove="Currencies.txt" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="bundleconfig.json" />
</ItemGroup>
<ItemGroup Condition="'$(Altcoins)' != 'true'">
<Content Remove="Services\Altcoins\**\*" />
@ -48,10 +41,7 @@
<ItemGroup>
<PackageReference Include="BIP78.Sender" Version="0.2.2" />
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.2" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.3.13" />
<PackageReference Include="BuildBundlerMinifier" Version="3.2.449" />
<PackageReference Include="BundlerMinifier.Core" Version="3.2.435" />
<PackageReference Include="BundlerMinifier.TagHelpers" Version="3.2.435" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.4.8" />
<PackageReference Include="CsvHelper" Version="15.0.5" />
<PackageReference Include="Dapper" Version="2.0.123" />
<PackageReference Include="Fido2" Version="2.0.2" />
@ -72,7 +62,6 @@
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.2" />
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.3" />
<PackageReference Include="NicolasDorier.RateLimits" Version="1.2.3" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.18" />
<PackageReference Include="Serilog" Version="2.9.0" />
<PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
@ -90,8 +79,8 @@
<PackageReference Include="TwentyTwenty.Storage.Google" Version="2.12.1" />
<PackageReference Include="TwentyTwenty.Storage.Local" Version="2.12.1" />
<PackageReference Include="YamlDotNet" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="6.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="6.0.9" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.9" />
</ItemGroup>
<ItemGroup>

View File

@ -0,0 +1,63 @@
using System;
using System.Drawing;
using System.Text;
using System.Text.RegularExpressions;
using NBitcoin.Crypto;
namespace BTCPayServer
{
public class ColorPalette
{
public const string Pattern = "^#[0-9a-fA-F]{6}$";
public static bool IsValid(string color)
{
return Regex.Match(color, Pattern).Success;
}
public string TextColor(string bgColor)
{
int nThreshold = 105;
var bg = ColorTranslator.FromHtml(bgColor);
int bgDelta = Convert.ToInt32((bg.R * 0.299) + (bg.G * 0.587) + (bg.B * 0.114));
Color color = (255 - bgDelta < nThreshold) ? Color.Black : Color.White;
return ColorTranslator.ToHtml(color);
}
// Borrowed from https://github.com/ManageIQ/guides/blob/master/labels.md
public static readonly ColorPalette Default = new ColorPalette(new string[] {
"#fbca04",
"#0e8a16",
"#ff7619",
"#84b6eb",
"#5319e7",
"#cdcdcd",
"#cc317c",
});
private ColorPalette(string[] labels)
{
Labels = labels;
}
public readonly string[] Labels;
public string DeterministicColor(string label)
{
switch (label)
{
case "payjoin":
return "#51b13e";
case "invoice":
return "#cedc21";
case "payment-request":
return "#489D77";
case "app":
return "#5093B6";
case "pj-exposed":
return "#51b13e";
case "payout":
return "#3F88AF";
default:
var num = NBitcoin.Utils.ToUInt32(Hashes.SHA256(Encoding.UTF8.GetBytes(label)), 0, true);
return Labels[num % Labels.Length];
}
}
}
}

View File

@ -3,6 +3,7 @@
@model BTCPayServer.Components.AppSales.AppSalesViewModel
@{
var controller = $"UI{Model.App.AppType}";
var action = $"Update{Model.App.AppType}";
var label = Model.App.AppType == nameof(AppType.Crowdfund) ? "Contributions" : "Sales";
}
@ -10,7 +11,7 @@
<div id="AppSales-@Model.App.Id" class="widget app-sales">
<header class="mb-3">
<h3>@Model.App.Name @label</h3>
<a asp-controller="UIApps" asp-action="@action" asp-route-appId="@Model.App.Id">Manage</a>
<a asp-controller="@controller" asp-action="@action" asp-route-appId="@Model.App.Id">Manage</a>
</header>
@if (Model.InitialRendering)
{

View File

@ -2,6 +2,7 @@
@model BTCPayServer.Components.AppTopItems.AppTopItemsViewModel
@{
var controller = $"UI{Model.App.AppType}";
var action = $"Update{Model.App.AppType}";
var label = Model.App.AppType == nameof(AppType.Crowdfund) ? "contribution" : "sale";
}
@ -9,7 +10,7 @@
<div id="AppTopItems-@Model.App.Id" class="widget app-top-items">
<header class="mb-3">
<h3>Top @(Model.App.AppType == nameof(AppType.Crowdfund) ? "Perks" : "Items")</h3>
<a asp-controller="UIApps" asp-action="@action" asp-route-appId="@Model.App.Id">View All</a>
<a asp-controller="@controller" asp-action="@action" asp-route-appId="@Model.App.Id">View All</a>
</header>
@if (Model.InitialRendering)
{

View File

@ -7,9 +7,6 @@
@using BTCPayServer.Views.Wallets
@using BTCPayServer.Abstractions.Extensions
@using BTCPayServer.Client
@using BTCPayServer.Components.Icon
@using BTCPayServer.Components.ThemeSwitch
@using BTCPayServer.Components.UIExtensionPoint
@using BTCPayServer.Services
@using BTCPayServer.Views.CustodianAccounts
@inject BTCPayServerEnvironment Env
@ -35,7 +32,7 @@
</a>
</li>
<li class="nav-item">
<a asp-area="" asp-controller="UIStores" asp-action="GeneralSettings" asp-route-storeId="@Model.Store.Id" class="nav-link js-scroll-trigger @ViewData.IsActivePage(StoreNavPages.Rates) @ViewData.IsActivePage(StoreNavPages.CheckoutAppearance) @ViewData.IsActivePage(StoreNavPages.General) @ViewData.IsActivePage(StoreNavPages.Tokens) @ViewData.IsActivePage(StoreNavPages.Users) @ViewData.IsActivePage(StoreNavPages.Plugins) @ViewData.IsActivePage(StoreNavPages.Webhooks)" id="StoreNav-StoreSettings">
<a asp-area="" asp-controller="UIStores" asp-action="GeneralSettings" asp-route-storeId="@Model.Store.Id" class="nav-link js-scroll-trigger @ViewData.IsActivePage(new [] {StoreNavPages.Rates, StoreNavPages.CheckoutAppearance, StoreNavPages.General, StoreNavPages.Tokens, StoreNavPages.Users, StoreNavPages.Plugins, StoreNavPages.Webhooks, StoreNavPages.PayoutProcessors, StoreNavPages.Emails})" id="StoreNav-StoreSettings">
<vc:icon symbol="settings"/>
<span>Settings</span>
</a>
@ -100,9 +97,7 @@
{
<li class="nav-item">
<a asp-area="" asp-controller="UICustodianAccounts" asp-action="ViewCustodianAccount" asp-route-storeId="@custodianAccount.StoreId" asp-route-accountId="@custodianAccount.Id" class="nav-link js-scroll-trigger @ViewData.IsActivePage(CustodianAccountsNavPages.View, custodianAccount.Id)" id="@($"StoreNav-CustodianAccount-{custodianAccount.Id}")">
<!--
TODO which icon should we use?
-->
@* TODO which icon should we use? *@
<span>@custodianAccount.Name</span>
<span class="badge bg-warning ms-1" style="font-size:10px;">Experimental</span>
</a>
@ -171,12 +166,7 @@
<ul class="navbar-nav">
@foreach (var app in Model.Apps)
{
<li class="nav-item">
<a asp-area="" asp-controller="UIApps" asp-action="@app.Action" asp-route-appId="@app.Id" class="nav-link js-scroll-trigger @ViewData.IsActivePage(AppsNavPages.Update, app.Id)" id="@($"StoreNav-App-{app.Id}")">
<vc:icon symbol="@app.AppType.ToLower()"/>
<span>@app.AppName</span>
</a>
</li>
<vc:ui-extension-point location="apps-nav" model="@app"/>
}
<li class="nav-item">
<a asp-area="" asp-controller="UIApps" asp-action="CreateApp" asp-route-storeId="@Model.Store.Id" class="nav-link js-scroll-trigger @ViewData.IsActivePage(AppsNavPages.Create)" id="StoreNav-CreateApp">

View File

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
@ -71,12 +72,11 @@ namespace BTCPayServer.Components.MainNav
vm.Apps = apps.Select(a => new StoreApp
{
Id = a.Id,
IsOwner = a.IsOwner,
AppName = a.AppName,
AppType = a.AppType,
IsOwner = a.IsOwner
AppType = Enum.Parse<AppType>(a.AppType)
}).ToList();
if (PoliciesSettings.Experimental)
{
// Custodian Accounts

View File

@ -1,6 +1,7 @@
using System.Collections.Generic;
using BTCPayServer.Data;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Services.Apps;
namespace BTCPayServer.Components.MainNav
{
@ -18,8 +19,7 @@ namespace BTCPayServer.Components.MainNav
{
public string Id { get; set; }
public string AppName { get; set; }
public string AppType { get; set; }
public string Action { get => $"Update{AppType}"; }
public AppType AppType { get; set; }
public bool IsOwner { get; set; }
}
}

View File

@ -123,7 +123,10 @@
</div>
@if (Model.Balance.OffchainBalance != null && Model.Balance.OnchainBalance != null)
{
<a class="d-inline-block mt-3" role="button" data-bs-toggle="collapse" data-bs-target=".balance-details" aria-expanded="false" aria-controls="balanceDetailsOffchain balanceDetailsOnchain">Show details</a>
<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>
}
}
else

View File

@ -1,5 +1,10 @@
@inject BTCPayServer.Services.BTCPayServerEnvironment _env
@inject SignInManager<ApplicationUser> _signInManager
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using BTCPayServer.Abstractions.Extensions
@using BTCPayServer.Abstractions.TagHelpers
@using BTCPayServer.Abstractions.Contracts
@inject BTCPayServer.Services.BTCPayServerEnvironment Env
@inject SignInManager<ApplicationUser> SignInManager
@inject IFileService FileService
@model BTCPayServer.Components.StoreSelector.StoreSelectorViewModel
@functions {
@* ReSharper disable once CSharpWarnings::CS1998 *@
@ -8,9 +13,9 @@
{
var logoSrc = $"{ViewContext.HttpContext.Request.PathBase}/img/logo.svg";
<svg xmlns="http://www.w3.org/2000/svg" role="img" alt="BTCPay Server" class="logo"><use href="@logoSrc#small" class="logo-small" /><use href="@logoSrc#large" class="logo-large" /></svg>
@if (_env.NetworkType != NBitcoin.ChainName.Mainnet)
@if (Env.NetworkType != NBitcoin.ChainName.Mainnet)
{
<span class="badge bg-warning ms-1 ms-sm-0" style="font-size:10px;">@_env.NetworkType.ToString()</span>
<span class="badge bg-warning ms-1 ms-sm-0" style="font-size:10px;">@Env.NetworkType.ToString()</span>
}
}
private string StoreName(string title)
@ -37,7 +42,14 @@ else
{
<div id="StoreSelectorDropdown" class="dropdown only-for-js">
<button id="StoreSelectorToggle" class="btn btn-secondary dropdown-toggle rounded-pill px-3 @(Model.CurrentStoreId == null ? "text-secondary" : "")" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<vc:icon symbol="store"/>
@if (!string.IsNullOrEmpty(Model.CurrentStoreLogoFileId))
{
<img class="logo" src="@(await FileService.GetFileUrl(Context.Request.GetAbsoluteRootUri(), Model.CurrentStoreLogoFileId))" alt="@Model.CurrentDisplayName" />
}
else
{
<vc:icon symbol="store"/>
}
<span>@(Model.CurrentStoreId == null ? "Select Store" : Model.CurrentDisplayName)</span>
<vc:icon symbol="caret-down"/>
</button>
@ -60,7 +72,7 @@ else
</ul>
</div>
}
else if (_signInManager.IsSignedIn(User))
else if (SignInManager.IsSignedIn(User))
{
<a asp-controller="UIUserStores" asp-action="CreateStore" class="btn btn-primary w-100 rounded-pill" id="StoreSelectorCreate">Create Store</a>
}

View File

@ -50,12 +50,15 @@ namespace BTCPayServer.Components.StoreSelector
.OrderBy(s => s.Text)
.ToList();
var blob = currentStore?.GetStoreBlob();
var vm = new StoreSelectorViewModel
{
Options = options,
CurrentStoreId = currentStore?.Id,
CurrentDisplayName = currentStore?.StoreName,
CurrentStoreIsOwner = currentStore?.Role == StoreRoles.Owner
CurrentStoreIsOwner = currentStore?.Role == StoreRoles.Owner,
CurrentStoreLogoFileId = blob?.LogoFileId
};
return View(vm);

View File

@ -6,6 +6,7 @@ namespace BTCPayServer.Components.StoreSelector
{
public List<StoreSelectorOption> Options { get; set; }
public string CurrentStoreId { get; set; }
public string CurrentStoreLogoFileId { get; set; }
public string CurrentDisplayName { get; set; }
public bool CurrentStoreIsOwner { get; set; }
}

View File

@ -65,11 +65,7 @@ namespace BTCPayServer.Configuration
if (conf.GetOrDefault<bool>("launchsettings", false) && NetworkType != ChainName.Regtest)
throw new ConfigException($"You need to run BTCPayServer with the run.sh or run.ps1 script");
BundleJsCss = conf.GetOrDefault<bool>("bundlejscss", true);
DockerDeployment = conf.GetOrDefault<bool>("dockerdeployment", true);
TorrcFile = conf.GetOrDefault<string>("torrcfile", null);
TorServices = conf.GetOrDefault<string>("torservices", null)
?.Split(new[] { ';', ',' }, StringSplitOptions.RemoveEmptyEntries);
@ -112,9 +108,9 @@ namespace BTCPayServer.Configuration
{
Logs.Configuration.LogWarning($"The SSH key is not supported ({ex.Message}), try to generate the key with ssh-keygen using \"-m PEM\". Skipping SSH configuration...");
}
catch
catch (Exception ex)
{
throw new ConfigException($"sshkeyfilepassword is invalid");
Logs.Configuration.LogWarning(ex, "Error while loading SSH settings");
}
}
@ -144,14 +140,15 @@ namespace BTCPayServer.Configuration
}
DisableRegistration = conf.GetOrDefault<bool>("disable-registration", true);
PluginRemote = conf.GetOrDefault("plugin-remote", "btcpayserver/btcpayserver-plugins");
var pluginRemote = conf.GetOrDefault<string>("plugin-remote", null);
if (pluginRemote != null)
Logs.Configuration.LogWarning("plugin-remote is an obsolete configuration setting, please remove it from configuration");
RecommendedPlugins = conf.GetOrDefault("recommended-plugins", "").ToLowerInvariant().Split('\r', '\n', '\t', ' ').Where(s => !string.IsNullOrEmpty(s)).Distinct().ToArray();
CheatMode = conf.GetOrDefault("cheatmode", false);
if (CheatMode && this.NetworkType == ChainName.Mainnet)
throw new ConfigException($"cheatmode can't be used on mainnet");
}
public string PluginRemote { get; set; }
public string[] RecommendedPlugins { get; set; }
public bool CheatMode { get; set; }
@ -192,16 +189,7 @@ namespace BTCPayServer.Configuration
public string RootPath { get; set; }
public bool DockerDeployment { get; set; }
public bool BundleJsCss
{
get;
set;
}
public SSHSettings SSHSettings
{
get;
set;
}
public SSHSettings SSHSettings { get; set; }
public string TorrcFile { get; set; }
public string[] TorServices { get; set; }
public Uri UpdateUrl { get; set; }

View File

@ -31,7 +31,6 @@ namespace BTCPayServer.Configuration
app.Option("--nocsp", $"Disable CSP (default false)", CommandOptionType.BoolValue);
app.Option("--sqlitefile", $"File name to an SQLite database file inside the data directory", CommandOptionType.SingleValue);
app.Option("--externalservices", $"Links added to external services inside Server Settings / Services under the format service1:path2;service2:path2.(default: empty)", CommandOptionType.SingleValue);
app.Option("--bundlejscss", $"Bundle JavaScript and CSS files for better performance (default: true)", CommandOptionType.SingleValue);
app.Option("--rootpath", "The root path in the URL to access BTCPay (default: /)", CommandOptionType.SingleValue);
app.Option("--sshconnection", "SSH server to manage BTCPay under the form user@server:port (default: root@externalhost or empty)", CommandOptionType.SingleValue);
app.Option("--sshpassword", "SSH password to manage BTCPay (default: empty)", CommandOptionType.SingleValue);
@ -46,7 +45,7 @@ namespace BTCPayServer.Configuration
app.Option("--debuglog", "A rolling log file for debug messages.", CommandOptionType.SingleValue);
app.Option("--debugloglevel", "The severity you log (default:information)", CommandOptionType.SingleValue);
app.Option("--disable-registration", "Disables new user registrations (default:true)", CommandOptionType.SingleValue);
app.Option("--plugin-remote", "Which github repository to fetch the available plugins list (default:btcpayserver/btcpayserver-plugins)", CommandOptionType.SingleValue);
app.Option("--plugin-remote", "Obsolete, do not use", CommandOptionType.SingleValue);
app.Option("--recommended-plugins", "Plugins which would be marked as recommended to be installed. Separated by newline or space", CommandOptionType.MultipleValue);
app.Option("--xforwardedproto", "If specified, set X-Forwarded-Proto to the specified value, this may be useful if your reverse proxy handle https but is not configured to add X-Forwarded-Proto (example: --xforwardedproto https)", CommandOptionType.SingleValue);
app.Option("--cheatmode", "Add some helper UI to facilitate dev-time testing (Default false)", CommandOptionType.BoolValue);
@ -139,6 +138,7 @@ namespace BTCPayServer.Configuration
{
builder.AppendLine(CultureInfo.InvariantCulture, $"#{n.CryptoCode}.explorer.url={n.NBXplorerNetwork.DefaultSettings.DefaultUrl}");
builder.AppendLine(CultureInfo.InvariantCulture, $"#{n.CryptoCode}.explorer.cookiefile={ n.NBXplorerNetwork.DefaultSettings.DefaultCookieFile}");
builder.AppendLine(CultureInfo.InvariantCulture, $"#{n.CryptoCode}.blockexplorerlink=https://mempool.space/tx/{{0}}");
if (n.SupportLightning)
{
builder.AppendLine(CultureInfo.InvariantCulture, $"#{n.CryptoCode}.lightning=/root/.lightning/lightning-rpc");

View File

@ -6,6 +6,7 @@ using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Abstractions.Extensions;
using Microsoft.AspNetCore.Authorization;
@ -22,33 +23,38 @@ namespace BTCPayServer.Controllers.Greenfield
{
private readonly AppService _appService;
private readonly StoreRepository _storeRepository;
private readonly CurrencyNameTable _currencies;
public GreenfieldAppsController(
AppService appService,
StoreRepository storeRepository,
UserManager<ApplicationUser> userManager,
BTCPayNetworkProvider btcPayNetworkProvider
BTCPayNetworkProvider btcPayNetworkProvider,
CurrencyNameTable currencies
)
{
_appService = appService;
_storeRepository = storeRepository;
_currencies = currencies;
}
[HttpPost("~/api/v1/stores/{storeId}/apps/pos")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public async Task<IActionResult> CreatePointOfSaleApp(string storeId, CreatePointOfSaleAppRequest request)
{
var validationResult = Validate(request);
var store = await _storeRepository.FindStore(storeId);
if (store == null)
return this.CreateAPIError(404, "store-not-found", "The store was not found");
// This is not obvious but we must have a non-null currency or else request validation may work incorrectly
request.Currency = request.Currency ?? store.GetStoreBlob().DefaultCurrency;
var validationResult = ValidatePOSAppRequest(request);
if (validationResult != null)
{
return validationResult;
}
var store = await _storeRepository.FindStore(storeId);
if (store == null)
return this.CreateAPIError(404, "store-not-found", "The store was not found");
var defaultCurrency = store.GetStoreBlob().DefaultCurrency;
var appData = new AppData
{
StoreDataId = storeId,
@ -56,36 +62,55 @@ namespace BTCPayServer.Controllers.Greenfield
AppType = AppType.PointOfSale.ToString()
};
appData.SetSettings(new PointOfSaleSettings
{
Title = request.Title,
DefaultView = (Services.Apps.PosViewType)request.DefaultView,
ShowCustomAmount = request.ShowCustomAmount,
ShowDiscount = request.ShowDiscount,
EnableTips = request.EnableTips,
Currency = request.Currency ?? defaultCurrency,
Template = request.Template,
ButtonText = request.FixedAmountPayButtonText ?? PointOfSaleSettings.BUTTON_TEXT_DEF,
CustomButtonText = request.CustomAmountPayButtonText ?? PointOfSaleSettings.CUSTOM_BUTTON_TEXT_DEF,
CustomTipText = request.TipText ?? PointOfSaleSettings.CUSTOM_TIP_TEXT_DEF,
CustomCSSLink = request.CustomCSSLink,
NotificationUrl = request.NotificationUrl,
RedirectUrl = request.RedirectUrl,
Description = request.Description,
EmbeddedCSS = request.EmbeddedCSS,
RedirectAutomatically = request.RedirectAutomatically,
RequiresRefundEmail = request.RequiresRefundEmail == true ?
RequiresRefundEmail.On :
request.RequiresRefundEmail == false ?
RequiresRefundEmail.Off :
RequiresRefundEmail.InheritFromStore,
});
appData.SetSettings(ToPointOfSaleSettings(request));
await _appService.UpdateOrCreateApp(appData);
return Ok(ToModel(appData));
}
[HttpPut("~/api/v1/apps/pos/{appId}")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public async Task<IActionResult> UpdatePointOfSaleApp(string appId, CreatePointOfSaleAppRequest request)
{
var app = await _appService.GetApp(appId, AppType.PointOfSale);
if (app == null)
{
return AppNotFound();
}
var settings = app.GetSettings<PointOfSaleSettings>();
// This is not obvious but we must have a non-null currency or else request validation may work incorrectly
request.Currency = request.Currency ?? settings.Currency;
var validationResult = ValidatePOSAppRequest(request);
if (validationResult != null)
{
return validationResult;
}
app.Name = request.AppName;
app.SetSettings(ToPointOfSaleSettings(request));
await _appService.UpdateOrCreateApp(app);
return Ok(ToModel(app));
}
private RequiresRefundEmail? BoolToRequiresRefundEmail(bool? requiresRefundEmail)
{
switch (requiresRefundEmail)
{
case true:
return RequiresRefundEmail.On;
case false:
return RequiresRefundEmail.Off;
default:
return null;
}
}
[HttpGet("~/api/v1/apps/{appId}")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public async Task<IActionResult> GetApp(string appId)
@ -118,19 +143,75 @@ namespace BTCPayServer.Controllers.Greenfield
return this.CreateAPIError(404, "app-not-found", "The app with specified ID was not found");
}
private PointOfSaleSettings ToPointOfSaleSettings(CreatePointOfSaleAppRequest request)
{
return new PointOfSaleSettings()
{
Title = request.Title,
DefaultView = (Services.Apps.PosViewType)request.DefaultView,
ShowCustomAmount = request.ShowCustomAmount,
ShowDiscount = request.ShowDiscount,
EnableTips = request.EnableTips,
Currency = request.Currency,
Template = request.Template != null ? _appService.SerializeTemplate(_appService.Parse(request.Template, request.Currency)) : null,
ButtonText = request.FixedAmountPayButtonText ?? PointOfSaleSettings.BUTTON_TEXT_DEF,
CustomButtonText = request.CustomAmountPayButtonText ?? PointOfSaleSettings.CUSTOM_BUTTON_TEXT_DEF,
CustomTipText = request.TipText ?? PointOfSaleSettings.CUSTOM_TIP_TEXT_DEF,
CustomCSSLink = request.CustomCSSLink,
NotificationUrl = request.NotificationUrl,
RedirectUrl = request.RedirectUrl,
Description = request.Description,
EmbeddedCSS = request.EmbeddedCSS,
RedirectAutomatically = request.RedirectAutomatically,
RequiresRefundEmail = BoolToRequiresRefundEmail(request.RequiresRefundEmail) ?? RequiresRefundEmail.InheritFromStore,
CheckoutFormId = request.CheckoutFormId,
CheckoutType = request.CheckoutType ?? CheckoutType.V1
};
}
private PointOfSaleAppData ToModel(AppData appData)
{
var settings = appData.GetSettings<PointOfSaleSettings>();
return new PointOfSaleAppData
{
Id = appData.Id,
AppType = appData.AppType,
Name = appData.Name,
StoreId = appData.StoreDataId,
Created = appData.Created
Created = appData.Created,
};
}
private IActionResult? Validate(CreateAppRequest request)
private IActionResult? ValidatePOSAppRequest(CreatePointOfSaleAppRequest request)
{
var validationResult = ValidateCreateAppRequest(request);
if (request.Currency != null && _currencies.GetCurrencyData(request.Currency, false) == null)
{
ModelState.AddModelError(nameof(request.Currency), "Invalid currency");
}
if (request.Template != null)
{
try
{
_appService.SerializeTemplate(_appService.Parse(request.Template, request.Currency));
}
catch
{
ModelState.AddModelError(nameof(request.Template), "Invalid template");
}
}
if (!ModelState.IsValid)
{
validationResult = this.CreateValidationError(ModelState);
}
return validationResult;
}
private IActionResult? ValidateCreateAppRequest(CreateAppRequest request)
{
if (request is null)
{

View File

@ -437,6 +437,8 @@ namespace BTCPayServer.Controllers.Greenfield
DefaultLanguage = entity.DefaultLanguage,
RedirectAutomatically = entity.RedirectAutomatically,
RequiresRefundEmail = entity.RequiresRefundEmail,
CheckoutFormId = entity.CheckoutFormId,
CheckoutType = entity.CheckoutType,
RedirectURL = entity.RedirectURLTemplate
},
Receipt = entity.ReceiptOptions

View File

@ -101,6 +101,14 @@ namespace BTCPayServer.Controllers.Greenfield
return base.GetInvoice(cryptoCode, id, cancellationToken);
}
[Authorize(Policy = Policies.CanUseInternalLightningNode,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/server/lightning/{cryptoCode}/invoices")]
public override Task<IActionResult> GetInvoices(string cryptoCode, [FromQuery] bool? pendingOnly, [FromQuery] long? offsetIndex, CancellationToken cancellationToken = default)
{
return base.GetInvoices(cryptoCode, pendingOnly, offsetIndex, cancellationToken);
}
[Authorize(Policy = Policies.CanUseInternalLightningNode,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/server/lightning/{cryptoCode}/invoices/pay")]

View File

@ -111,6 +111,14 @@ namespace BTCPayServer.Controllers.Greenfield
return base.GetInvoice(cryptoCode, id, cancellationToken);
}
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices")]
public override Task<IActionResult> GetInvoices(string cryptoCode, [FromQuery] bool? pendingOnly, [FromQuery] long? offsetIndex, CancellationToken cancellationToken = default)
{
return base.GetInvoices(cryptoCode, pendingOnly, offsetIndex, cancellationToken);
}
[Authorize(Policy = Policies.CanCreateLightningInvoiceInStore,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices")]

View File

@ -2,7 +2,6 @@ using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
@ -46,7 +45,14 @@ namespace BTCPayServer.Controllers.Greenfield
return Ok(new LightningNodeInformationData
{
BlockHeight = info.BlockHeight,
NodeURIs = info.NodeInfoList.Select(nodeInfo => nodeInfo).ToArray()
NodeURIs = info.NodeInfoList.Select(nodeInfo => nodeInfo).ToArray(),
Alias = info.Alias,
Color = info.Color,
Version = info.Version,
PeersCount = info.PeersCount,
ActiveChannelsCount = info.ActiveChannelsCount,
InactiveChannelsCount = info.InactiveChannelsCount,
PendingChannelsCount = info.PendingChannelsCount
});
}
@ -202,9 +208,10 @@ namespace BTCPayServer.Controllers.Greenfield
{
var lightningClient = await GetLightningClient(cryptoCode, true);
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
if (lightningInvoice?.BOLT11 is null ||
!BOLT11PaymentRequest.TryParse(lightningInvoice.BOLT11, out _, network.NBitcoinNetwork))
BOLT11PaymentRequest bolt11 = null;
if (string.IsNullOrEmpty(lightningInvoice.BOLT11) ||
!BOLT11PaymentRequest.TryParse(lightningInvoice.BOLT11, out bolt11, network.NBitcoinNetwork))
{
ModelState.AddModelError(nameof(lightningInvoice.BOLT11), "The BOLT11 invoice was invalid.");
}
@ -214,21 +221,54 @@ namespace BTCPayServer.Controllers.Greenfield
return this.CreateValidationError(ModelState);
}
var param = lightningInvoice?.MaxFeeFlat != null || lightningInvoice?.MaxFeePercent != null || lightningInvoice?.Amount != null
? new PayInvoiceParams { MaxFeePercent = lightningInvoice.MaxFeePercent, MaxFeeFlat = lightningInvoice.MaxFeeFlat, Amount = lightningInvoice.Amount }
var param = lightningInvoice.MaxFeeFlat != null || lightningInvoice.MaxFeePercent != null
|| lightningInvoice.Amount != null || lightningInvoice.SendTimeout != null
? new PayInvoiceParams
{
MaxFeePercent = lightningInvoice.MaxFeePercent,
MaxFeeFlat = lightningInvoice.MaxFeeFlat,
Amount = lightningInvoice.Amount,
SendTimeout = lightningInvoice.SendTimeout
}
: null;
var result = await lightningClient.Pay(lightningInvoice.BOLT11, param, cancellationToken);
if (result.Result is PayResult.Ok or PayResult.Unknown && bolt11?.PaymentHash is not null)
{
// get a new instance of the LN client, because the old one might have disposed its HTTPClient
lightningClient = await GetLightningClient(cryptoCode, true);
var paymentHash = bolt11.PaymentHash.ToString();
var payment = await lightningClient.GetPayment(paymentHash, cancellationToken);
var data = new LightningPaymentData
{
Id = payment.Id,
PaymentHash = paymentHash,
Status = payment.Status,
BOLT11 = payment.BOLT11,
Preimage = payment.Preimage,
CreatedAt = payment.CreatedAt,
TotalAmount = payment.AmountSent,
FeeAmount = payment.Fee,
};
return result.Result is PayResult.Ok ? Ok(data) : Accepted(data);
}
return result.Result switch
{
PayResult.CouldNotFindRoute => this.CreateAPIError("could-not-find-route", "Impossible to find a route to the peer"),
PayResult.Error => this.CreateAPIError("generic-error", result.ErrorDetail),
PayResult.Unknown => Accepted(new LightningPaymentData
{
Status = LightningPaymentStatus.Unknown
}),
PayResult.Ok => Ok(new LightningPaymentData
{
Status = LightningPaymentStatus.Complete,
TotalAmount = result.Details?.TotalAmount,
FeeAmount = result.Details?.FeeAmount
}),
_ => throw new NotSupportedException("Unsupported Payresult")
_ => throw new NotSupportedException("Unsupported PayResult")
};
}
@ -239,6 +279,14 @@ namespace BTCPayServer.Controllers.Greenfield
return inv == null ? this.CreateAPIError(404, "invoice-not-found", "Impossible to find a lightning invoice with this id") : Ok(ToModel(inv));
}
public virtual async Task<IActionResult> GetInvoices(string cryptoCode, [FromQuery] bool? pendingOnly, [FromQuery] long? offsetIndex, CancellationToken cancellationToken = default)
{
var lightningClient = await GetLightningClient(cryptoCode, false);
var param = new ListInvoicesParams { PendingOnly = pendingOnly, OffsetIndex = offsetIndex };
var invoices = await lightningClient.ListInvoices(param, cancellationToken);
return Ok(invoices.Select(ToModel));
}
public virtual async Task<IActionResult> CreateInvoice(string cryptoCode, CreateLightningInvoiceRequest request, CancellationToken cancellationToken = default)
{
var lightningClient = await GetLightningClient(cryptoCode, false);
@ -296,7 +344,7 @@ namespace BTCPayServer.Controllers.Greenfield
private LightningInvoiceData ToModel(LightningInvoice invoice)
{
return new LightningInvoiceData
var data = new LightningInvoiceData
{
Amount = invoice.Amount,
Id = invoice.Id,
@ -306,6 +354,12 @@ namespace BTCPayServer.Controllers.Greenfield
BOLT11 = invoice.BOLT11,
ExpiresAt = invoice.ExpiresAt
};
if (invoice.CustomRecords != null)
{
data.CustomRecords = invoice.CustomRecords;
}
return data;
}
private LightningPaymentData ToModel(LightningPayment payment)

View File

@ -1,18 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.PaymentRequest;
using BTCPayServer.Security;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.PaymentRequests;
using BTCPayServer.Services.Rates;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using PaymentRequestData = BTCPayServer.Data.PaymentRequestData;
namespace BTCPayServer.Controllers.Greenfield
@ -22,14 +26,26 @@ namespace BTCPayServer.Controllers.Greenfield
[EnableCors(CorsPolicies.All)]
public class GreenfieldPaymentRequestsController : ControllerBase
{
private readonly InvoiceRepository _InvoiceRepository;
private readonly UIInvoiceController _invoiceController;
private readonly PaymentRequestRepository _paymentRequestRepository;
private readonly CurrencyNameTable _currencyNameTable;
private readonly LinkGenerator _linkGenerator;
public GreenfieldPaymentRequestsController(PaymentRequestRepository paymentRequestRepository,
CurrencyNameTable currencyNameTable)
public GreenfieldPaymentRequestsController(
InvoiceRepository invoiceRepository,
UIInvoiceController invoiceController,
PaymentRequestRepository paymentRequestRepository,
PaymentRequestService paymentRequestService,
CurrencyNameTable currencyNameTable,
LinkGenerator linkGenerator)
{
_InvoiceRepository = invoiceRepository;
_invoiceController = invoiceController;
_paymentRequestRepository = paymentRequestRepository;
PaymentRequestService = paymentRequestService;
_currencyNameTable = currencyNameTable;
_linkGenerator = linkGenerator;
}
[Authorize(Policy = Policies.CanViewPaymentRequests, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
@ -56,6 +72,62 @@ namespace BTCPayServer.Controllers.Greenfield
return Ok(FromModel(pr.First()));
}
[Authorize(Policy = Policies.CanViewPaymentRequests, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/stores/{storeId}/payment-requests/{paymentRequestId}/pay")]
public async Task<IActionResult> PayPaymentRequest(string storeId, string paymentRequestId, [FromBody] PayPaymentRequestRequest pay, CancellationToken cancellationToken)
{
var pr = await this.PaymentRequestService.GetPaymentRequest(paymentRequestId);
if (pr is null || pr.StoreId != storeId)
return PaymentRequestNotFound();
var amount = pay?.Amount;
if (amount.HasValue && amount.Value <= 0)
{
ModelState.AddModelError(nameof(pay.Amount), "The amount should be more than 0");
}
if (amount.HasValue && !pr.AllowCustomPaymentAmounts && amount.Value != pr.AmountDue)
{
ModelState.AddModelError(nameof(pay.Amount), "This payment request doesn't allow custom payment amount");
}
if (!ModelState.IsValid)
return this.CreateValidationError(ModelState);
if (pr.Archived)
{
return this.CreateAPIError("archived", "You cannot pay an archived payment request");
}
if (pr.AmountDue <= 0)
{
return this.CreateAPIError("already-paid", "This payment request is already paid");
}
if (pr.ExpiryDate.HasValue && DateTime.UtcNow >= pr.ExpiryDate)
{
return this.CreateAPIError("expired", "This payment request is expired");
}
if (pay?.AllowPendingInvoiceReuse is true)
{
if (pr.Invoices.GetReusableInvoice(amount)?.Id is string invoiceId)
{
var inv = await _InvoiceRepository.GetInvoice(invoiceId);
return Ok(GreenfieldInvoiceController.ToModel(inv, _linkGenerator, Request));
}
}
try
{
var invoice = await _invoiceController.CreatePaymentRequestInvoice(pr, amount, this.StoreData, Request, cancellationToken);
return Ok(GreenfieldInvoiceController.ToModel(invoice, _linkGenerator, Request));
}
catch (BitpayHttpException e)
{
return this.CreateAPIError(null, e.Message);
}
}
[Authorize(Policy = Policies.CanModifyPaymentRequests,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpDelete("~/api/v1/stores/{storeId}/payment-requests/{paymentRequestId}")]
@ -97,6 +169,9 @@ namespace BTCPayServer.Controllers.Greenfield
return Ok(FromModel(pr));
}
public Data.StoreData StoreData => HttpContext.GetStoreData();
public PaymentRequestService PaymentRequestService { get; }
[HttpPut("~/api/v1/stores/{storeId}/payment-requests/{paymentRequestId}")]
[Authorize(Policy = Policies.CanModifyPaymentRequests,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]

View File

@ -52,16 +52,22 @@ namespace BTCPayServer.Controllers.Greenfield
private static OnChainAutomatedPayoutSettings ToModel(PayoutProcessorData data)
{
var blob = BaseAutomatedPayoutProcessor<OnChainAutomatedPayoutBlob>.GetBlob(data);
return new OnChainAutomatedPayoutSettings()
{
FeeBlockTarget = blob.FeeTargetBlock,
PaymentMethod = data.PaymentMethod,
IntervalSeconds = InvoiceRepository.FromBytes<AutomatedPayoutBlob>(data.Blob).Interval
IntervalSeconds = blob.Interval
};
}
private static AutomatedPayoutBlob FromModel(OnChainAutomatedPayoutSettings data)
private static OnChainAutomatedPayoutBlob FromModel(OnChainAutomatedPayoutSettings data)
{
return new AutomatedPayoutBlob() {Interval = data.IntervalSeconds};
return new OnChainAutomatedPayoutBlob()
{
FeeTargetBlock = data.FeeBlockTarget ?? 1,
Interval = data.IntervalSeconds
};
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]

View File

@ -35,7 +35,6 @@ namespace BTCPayServer.Controllers.Greenfield
private readonly StoreRepository _storeRepository;
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly IAuthorizationService _authorizationService;
private readonly ISettingsRepository _settingsRepository;
public GreenfieldStoreLightningNetworkPaymentMethodsController(
StoreRepository storeRepository,
@ -47,7 +46,6 @@ namespace BTCPayServer.Controllers.Greenfield
_storeRepository = storeRepository;
_btcPayNetworkProvider = btcPayNetworkProvider;
_authorizationService = authorizationService;
_settingsRepository = settingsRepository;
PoliciesSettings = policiesSettings;
}

View File

@ -52,7 +52,6 @@ namespace BTCPayServer.Controllers.Greenfield
private readonly EventAggregator _eventAggregator;
private readonly WalletReceiveService _walletReceiveService;
private readonly IFeeProviderFactory _feeProviderFactory;
private readonly LabelFactory _labelFactory;
private readonly UTXOLocker _utxoLocker;
public GreenfieldStoreOnChainWalletsController(
@ -69,7 +68,6 @@ namespace BTCPayServer.Controllers.Greenfield
EventAggregator eventAggregator,
WalletReceiveService walletReceiveService,
IFeeProviderFactory feeProviderFactory,
LabelFactory labelFactory,
UTXOLocker utxoLocker
)
{
@ -86,7 +84,6 @@ namespace BTCPayServer.Controllers.Greenfield
_eventAggregator = eventAggregator;
_walletReceiveService = walletReceiveService;
_feeProviderFactory = feeProviderFactory;
_labelFactory = labelFactory;
_utxoLocker = utxoLocker;
}
@ -202,7 +199,7 @@ namespace BTCPayServer.Controllers.Greenfield
if (!string.IsNullOrWhiteSpace(labelFilter))
{
walletTransactionsInfoAsync.TryGetValue(t.TransactionId.ToString(), out var transactionInfo);
if (transactionInfo?.Labels.ContainsKey(labelFilter) is true)
if (transactionInfo?.LabelColors.ContainsKey(labelFilter) is true)
filteredList.Add(t);
}
if (statusFilter?.Any() is true)
@ -270,36 +267,18 @@ namespace BTCPayServer.Controllers.Greenfield
}
var walletId = new WalletId(storeId, cryptoCode);
var walletTransactionsInfoAsync = _walletRepository.GetWalletTransactionsInfo(walletId);
if (!(await walletTransactionsInfoAsync).TryGetValue(transactionId, out var walletTransactionInfo))
{
walletTransactionInfo = new WalletTransactionInfo();
}
var txObjectId = new WalletObjectId(walletId, WalletObjectData.Types.Tx, transactionId);
if (request.Comment != null)
{
walletTransactionInfo.Comment = request.Comment.Trim().Truncate(WalletTransactionDataExtensions.MaxCommentSize);
await _walletRepository.SetWalletObjectComment(txObjectId, request.Comment);
}
if (request.Labels != null)
{
var walletBlobInfo = await _walletRepository.GetWalletInfo(walletId);
foreach (string label in request.Labels)
{
var rawLabel = await _labelFactory.BuildLabel(
walletBlobInfo,
Request,
walletTransactionInfo,
walletId,
transactionId,
label
);
walletTransactionInfo.Labels.TryAdd(rawLabel.Text, rawLabel);
}
await _walletRepository.AddWalletObjectLabels(txObjectId, request.Labels.ToArray());
}
await _walletRepository.SetWalletTransactionInfo(walletId, transactionId, walletTransactionInfo);
var walletTransactionsInfo =
(await _walletRepository.GetWalletTransactionsInfo(walletId, new[] { transactionId }))
.Values
@ -319,19 +298,20 @@ namespace BTCPayServer.Controllers.Greenfield
var wallet = _btcPayWalletProvider.GetWallet(network);
var walletId = new WalletId(storeId, cryptoCode);
var walletTransactionsInfoAsync = await _walletRepository.GetWalletTransactionsInfo(walletId);
var utxos = await wallet.GetUnspentCoins(derivationScheme.AccountDerivation);
var walletTransactionsInfoAsync = await _walletRepository.GetWalletTransactionsInfo(walletId, utxos.Select(u => u.OutPoint.Hash.ToString()).ToHashSet().ToArray());
return Ok(utxos.Select(coin =>
{
walletTransactionsInfoAsync.TryGetValue(coin.OutPoint.Hash.ToString(), out var info);
var labels = info?.Labels ?? new Dictionary<string, LabelData>();
return new OnChainWalletUTXOData()
{
Outpoint = coin.OutPoint,
Amount = coin.Value.GetValue(network),
Comment = info?.Comment,
Labels = info?.Labels,
#pragma warning disable CS0612 // Type or member is obsolete
Labels = info?.LegacyLabels ?? new Dictionary<string, LabelData>(),
#pragma warning restore CS0612 // Type or member is obsolete
Link = string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink,
coin.OutPoint.Hash.ToString()),
Timestamp = coin.Timestamp,
@ -592,8 +572,7 @@ namespace BTCPayServer.Controllers.Greenfield
payjoinPSBT.Finalize();
var payjoinTransaction = payjoinPSBT.ExtractTransaction();
var hash = payjoinTransaction.GetHash();
_eventAggregator.Publish(new UpdateTransactionLabel(new WalletId(Store.Id, cryptoCode), hash,
UpdateTransactionLabel.PayjoinLabelTemplate()));
await this._walletRepository.AddWalletTransactionAttachment(new WalletId(Store.Id, cryptoCode), hash, Attachment.Payjoin());
broadcastResult = await explorerClient.BroadcastAsync(payjoinTransaction);
if (broadcastResult.Success)
{
@ -676,7 +655,9 @@ namespace BTCPayServer.Controllers.Greenfield
{
TransactionHash = tx.TransactionId,
Comment = walletTransactionsInfoAsync?.Comment ?? string.Empty,
Labels = walletTransactionsInfoAsync?.Labels ?? new Dictionary<string, LabelData>(),
#pragma warning disable CS0612 // Type or member is obsolete
Labels = walletTransactionsInfoAsync?.LegacyLabels ?? new Dictionary<string, LabelData>(),
#pragma warning restore CS0612 // Type or member is obsolete
Amount = tx.BalanceChange.GetValue(wallet.Network),
BlockHash = tx.BlockHash,
BlockHeight = tx.Height,

View File

@ -0,0 +1,204 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Rating;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using RateSource = BTCPayServer.Client.Models.RateSource;
namespace BTCPayServer.Controllers.GreenField
{
[ApiController]
[Route("api/v1/stores/{storeId}/rates/configuration")]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public class GreenfieldStoreRateConfigurationController : ControllerBase
{
private readonly RateFetcher _rateProviderFactory;
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly StoreRepository _storeRepository;
public GreenfieldStoreRateConfigurationController(
RateFetcher rateProviderFactory,
BTCPayNetworkProvider btcPayNetworkProvider,
StoreRepository storeRepository)
{
_rateProviderFactory = rateProviderFactory;
_btcPayNetworkProvider = btcPayNetworkProvider;
_storeRepository = storeRepository;
}
[HttpGet("")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public IActionResult GetStoreRateConfiguration()
{
var data = HttpContext.GetStoreData();
var blob = data.GetStoreBlob();
return Ok(new StoreRateConfiguration()
{
EffectiveScript = blob.GetRateRules(_btcPayNetworkProvider, out var preferredExchange).ToString(),
Spread = blob.Spread * 100.0m,
IsCustomScript = blob.RateScripting,
PreferredSource = preferredExchange ? blob.PreferredExchange : null
});
}
[HttpGet("/misc/rate-sources")]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie + "," + AuthenticationSchemes.Greenfield)]
public ActionResult<List<RateSource>> GetRateSources()
{
return Ok(_rateProviderFactory.RateProviderFactory.GetSupportedExchanges().Select(provider =>
new RateSource() {Id = provider.Id, Name = provider.DisplayName}));
}
[HttpPut("")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public async Task<IActionResult> UpdateStoreRateConfiguration(
StoreRateConfiguration configuration)
{
var storeData = HttpContext.GetStoreData();
var blob = storeData.GetStoreBlob();
ValidateAndSanitizeConfiguration(configuration, blob);
if (!ModelState.IsValid)
return this.CreateValidationError(ModelState);
PopulateBlob(configuration, blob);
storeData.SetStoreBlob(blob);
await _storeRepository.UpdateStore(storeData);
return GetStoreRateConfiguration();
}
[HttpPost("preview")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public async Task<IActionResult> PreviewUpdateStoreRateConfiguration(
StoreRateConfiguration configuration, [FromQuery] string[] currencyPair)
{
var data = HttpContext.GetStoreData();
var blob = data.GetStoreBlob();
var parsedCurrencyPairs = new HashSet<CurrencyPair>();
foreach (var pair in currencyPair ?? Array.Empty<string>())
{
if (!CurrencyPair.TryParse(pair, out var currencyPairParsed))
{
ModelState.AddModelError(nameof(currencyPair),
$"Invalid currency pair '{pair}' (it should be formatted like BTC_USD,BTC_CAD)");
break;
}
parsedCurrencyPairs.Add(currencyPairParsed);
}
ValidateAndSanitizeConfiguration(configuration, blob);
if (!ModelState.IsValid)
return this.CreateValidationError(ModelState);
PopulateBlob(configuration, blob);
var rules = blob.GetRateRules(_btcPayNetworkProvider);
var rateTasks = _rateProviderFactory.FetchRates(parsedCurrencyPairs, rules, CancellationToken.None);
await Task.WhenAll(rateTasks.Values);
var result = new List<StoreRatePreviewResult>();
foreach (var rateTask in rateTasks)
{
var rateTaskResult = rateTask.Value.Result;
result.Add(new StoreRatePreviewResult()
{
CurrencyPair = rateTask.Key.ToString(),
Errors = rateTaskResult.Errors.Select(errors => errors.ToString()).ToList(),
Rate = rateTaskResult.Errors.Any() ? (decimal?)null : rateTaskResult.BidAsk.Bid
});
}
return Ok(result);
}
private void ValidateAndSanitizeConfiguration(StoreRateConfiguration? configuration, StoreBlob storeBlob)
{
if (configuration is null)
{
ModelState.AddModelError("", "Body required");
return;
}
if (configuration.Spread < 0 || configuration.Spread > 100)
{
ModelState.AddModelError(nameof(configuration.Spread),
$"Spread value must be in %, between 0 and 100");
}
if (configuration.IsCustomScript)
{
if (string.IsNullOrEmpty(configuration.EffectiveScript))
{
configuration.EffectiveScript = storeBlob.GetDefaultRateRules(_btcPayNetworkProvider).ToString();
}
if (!RateRules.TryParse(configuration.EffectiveScript, out var r))
{
ModelState.AddModelError(nameof(configuration.EffectiveScript),
$"Script syntax is invalid");
}
else
{
configuration.EffectiveScript = r.ToString();
}
if (!string.IsNullOrEmpty(configuration.PreferredSource))
{
ModelState.AddModelError(nameof(configuration.PreferredSource),
$"You can't set the preferredSource if you are using custom scripts");
}
}
else
{
if (!string.IsNullOrEmpty(configuration.EffectiveScript))
{
ModelState.AddModelError(nameof(configuration.EffectiveScript),
$"You can't set the effectiveScript if you aren't using custom scripts");
}
if (string.IsNullOrEmpty(configuration.PreferredSource))
{
ModelState.AddModelError(nameof(configuration.PreferredSource),
$"The preferredSource is required if you aren't using custom scripts");
}
configuration.PreferredSource = _rateProviderFactory
.RateProviderFactory
.GetSupportedExchanges()
.FirstOrDefault(s =>
s.Id.Equals(configuration.PreferredSource,
StringComparison.InvariantCultureIgnoreCase))?.Id;
if (string.IsNullOrEmpty(configuration.PreferredSource))
{
ModelState.AddModelError(nameof(configuration.PreferredSource),
$"Unsupported source, please check /misc/rate-sources to see valid values ({configuration.PreferredSource})");
}
}
}
private static void PopulateBlob(StoreRateConfiguration configuration, StoreBlob storeBlob)
{
storeBlob.PreferredExchange = configuration.PreferredSource;
storeBlob.Spread = configuration.Spread / 100.0m;
storeBlob.RateScripting = configuration.IsCustomScript;
storeBlob.RateScript = configuration.EffectiveScript;
}
}
}

View File

@ -127,6 +127,8 @@ namespace BTCPayServer.Controllers.Greenfield
//we do not include PaymentMethodCriteria because moving the CurrencyValueJsonConverter to the Client csproj is hard and requires a refactor (#1571 & #1572)
NetworkFeeMode = storeBlob.NetworkFeeMode,
RequiresRefundEmail = storeBlob.RequiresRefundEmail,
CheckoutFormId = storeBlob.CheckoutFormId,
CheckoutType = storeBlob.CheckoutType,
Receipt = InvoiceDataBase.ReceiptOptions.Merge(storeBlob.ReceiptOptions, null),
LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi,
LightningPrivateRouteHints = storeBlob.LightningPrivateRouteHints,
@ -165,6 +167,8 @@ namespace BTCPayServer.Controllers.Greenfield
blob.NetworkFeeMode = restModel.NetworkFeeMode;
blob.DefaultCurrency = restModel.DefaultCurrency;
blob.RequiresRefundEmail = restModel.RequiresRefundEmail;
blob.CheckoutFormId = restModel.CheckoutFormId;
blob.CheckoutType = restModel.CheckoutType;
blob.ReceiptOptions = InvoiceDataBase.ReceiptOptions.Merge(restModel.Receipt, null);
blob.LightningAmountInSatoshi = restModel.LightningAmountInSatoshi;
blob.LightningPrivateRouteHints = restModel.LightningPrivateRouteHints;

View File

@ -351,7 +351,7 @@ namespace BTCPayServer.Controllers.Greenfield
CancellationToken token = default)
{
return GetFromActionResult<LightningNodeBalanceData>(
await GetController<GreenfieldStoreLightningNodeApiController>().GetBalance(cryptoCode));
await GetController<GreenfieldStoreLightningNodeApiController>().GetBalance(cryptoCode, token));
}
public override async Task ConnectToLightningNode(string storeId, string cryptoCode,
@ -381,10 +381,11 @@ namespace BTCPayServer.Controllers.Greenfield
await GetController<GreenfieldStoreLightningNodeApiController>().GetDepositAddress(cryptoCode, token));
}
public override async Task PayLightningInvoice(string storeId, string cryptoCode,
public override async Task<LightningPaymentData> PayLightningInvoice(string storeId, string cryptoCode,
PayLightningInvoiceRequest request, CancellationToken token = default)
{
HandleActionResult(await GetController<GreenfieldStoreLightningNodeApiController>().PayInvoice(cryptoCode, request, token));
return GetFromActionResult<LightningPaymentData>(
await GetController<GreenfieldStoreLightningNodeApiController>().PayInvoice(cryptoCode, request, token));
}
public override async Task<LightningInvoiceData> GetLightningInvoice(string storeId, string cryptoCode,
@ -394,6 +395,13 @@ namespace BTCPayServer.Controllers.Greenfield
await GetController<GreenfieldStoreLightningNodeApiController>().GetInvoice(cryptoCode, invoiceId, token));
}
public override async Task<LightningInvoiceData[]> GetLightningInvoices(string storeId, string cryptoCode,
bool? pendingOnly = null, long? offsetIndex = null, CancellationToken token = default)
{
return GetFromActionResult<LightningInvoiceData[]>(
await GetController<GreenfieldStoreLightningNodeApiController>().GetInvoices(cryptoCode, pendingOnly, offsetIndex, token));
}
public override async Task<LightningInvoiceData> CreateLightningInvoice(string storeId, string cryptoCode,
CreateLightningInvoiceRequest request, CancellationToken token = default)
{
@ -455,6 +463,13 @@ namespace BTCPayServer.Controllers.Greenfield
await GetController<GreenfieldInternalLightningNodeApiController>().GetInvoice(cryptoCode, invoiceId, token));
}
public override async Task<LightningInvoiceData[]> GetLightningInvoices(string cryptoCode,
bool? pendingOnly = null, long? offsetIndex = null, CancellationToken token = default)
{
return GetFromActionResult<LightningInvoiceData[]>(
await GetController<GreenfieldInternalLightningNodeApiController>().GetInvoices(cryptoCode, pendingOnly, offsetIndex, token));
}
public override async Task<LightningInvoiceData> CreateLightningInvoice(string cryptoCode,
CreateLightningInvoiceRequest request,
CancellationToken token = default)
@ -577,6 +592,12 @@ namespace BTCPayServer.Controllers.Greenfield
HandleActionResult(await GetController<GreenfieldPaymentRequestsController>().ArchivePaymentRequest(storeId, paymentRequestId));
}
public override async Task<InvoiceData> PayPaymentRequest(string storeId, string paymentRequestId, PayPaymentRequestRequest request, CancellationToken token = default)
{
return GetFromActionResult<InvoiceData>(
await GetController<GreenfieldPaymentRequestsController>().PayPaymentRequest(storeId, paymentRequestId, request, token));
}
public override async Task<PaymentRequestData> CreatePaymentRequest(string storeId,
CreatePaymentRequestRequest request, CancellationToken token = default)
{
@ -1103,6 +1124,14 @@ namespace BTCPayServer.Controllers.Greenfield
await GetController<GreenfieldAppsController>().CreatePointOfSaleApp(storeId, request));
}
public override async Task<PointOfSaleAppData> UpdatePointOfSaleApp(
string appId,
CreatePointOfSaleAppRequest request, CancellationToken token = default)
{
return GetFromActionResult<PointOfSaleAppData>(
await GetController<GreenfieldAppsController>().UpdatePointOfSaleApp(appId, request));
}
public override async Task<AppDataBase> GetApp(string appId, CancellationToken token = default)
{
return GetFromActionResult<AppDataBase>(
@ -1113,5 +1142,31 @@ namespace BTCPayServer.Controllers.Greenfield
{
HandleActionResult(await GetController<GreenfieldAppsController>().DeleteApp(appId));
}
public override Task<List<RateSource>> GetRateSources(CancellationToken token = default)
{
return Task.FromResult(GetFromActionResult(GetController<GreenfieldStoreRateConfigurationController>().GetRateSources()));
}
public override Task<StoreRateConfiguration> GetStoreRateConfiguration(string storeId, CancellationToken token = default)
{
return Task.FromResult(GetFromActionResult<StoreRateConfiguration>(GetController<GreenfieldStoreRateConfigurationController>().GetStoreRateConfiguration()));
}
public override async Task<List<StoreRatePreviewResult>> PreviewUpdateStoreRateConfiguration(string storeId,
StoreRateConfiguration request,
string[] currencyPair,
CancellationToken token = default)
{
return GetFromActionResult<List<StoreRatePreviewResult>>(
await GetController<GreenfieldStoreRateConfigurationController>().PreviewUpdateStoreRateConfiguration(request,
currencyPair));
}
public override async Task<StoreRateConfiguration> UpdateStoreRateConfiguration(string storeId, StoreRateConfiguration request, CancellationToken token = default)
{
return GetFromActionResult<StoreRateConfiguration>(await GetController<GreenfieldStoreRateConfigurationController>().UpdateStoreRateConfiguration(request));
}
}
}

View File

@ -1,189 +0,0 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Client;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Services.Apps;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers
{
public partial class UIAppsController
{
public class AppUpdated
{
public string AppId { get; set; }
public object Settings { get; set; }
public string StoreId { get; set; }
public override string ToString()
{
return string.Empty;
}
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[HttpGet("{appId}/settings/crowdfund")]
public async Task<IActionResult> UpdateCrowdfund(string appId)
{
var app = GetCurrentApp();
if (app == null)
return NotFound();
var settings = app.GetSettings<CrowdfundSettings>();
var resetEvery = Enum.GetName(typeof(CrowdfundResetEvery), settings.ResetEvery);
var vm = new UpdateCrowdfundViewModel
{
Title = settings.Title,
StoreId = app.StoreDataId,
StoreName = app.StoreData?.StoreName,
StoreDefaultCurrency = await GetStoreDefaultCurrentIfEmpty(app.StoreDataId, settings.TargetCurrency),
AppName = app.Name,
Enabled = settings.Enabled,
EnforceTargetAmount = settings.EnforceTargetAmount,
StartDate = settings.StartDate,
TargetCurrency = settings.TargetCurrency,
Description = settings.Description,
MainImageUrl = settings.MainImageUrl,
EmbeddedCSS = settings.EmbeddedCSS,
EndDate = settings.EndDate,
TargetAmount = settings.TargetAmount,
CustomCSSLink = settings.CustomCSSLink,
NotificationUrl = settings.NotificationUrl,
Tagline = settings.Tagline,
PerksTemplate = settings.PerksTemplate,
DisqusEnabled = settings.DisqusEnabled,
SoundsEnabled = settings.SoundsEnabled,
DisqusShortname = settings.DisqusShortname,
AnimationsEnabled = settings.AnimationsEnabled,
ResetEveryAmount = settings.ResetEveryAmount,
ResetEvery = resetEvery,
IsRecurring = resetEvery != nameof(CrowdfundResetEvery.Never),
UseAllStoreInvoices = app.TagAllInvoices,
AppId = appId,
SearchTerm = app.TagAllInvoices ? $"storeid:{app.StoreDataId}" : $"orderid:{AppService.GetAppOrderId(app)}",
DisplayPerksRanking = settings.DisplayPerksRanking,
DisplayPerksValue = settings.DisplayPerksValue,
SortPerksByPopularity = settings.SortPerksByPopularity,
Sounds = string.Join(Environment.NewLine, settings.Sounds),
AnimationColors = string.Join(Environment.NewLine, settings.AnimationColors)
};
return View(vm);
}
[HttpPost("{appId}/settings/crowdfund")]
public async Task<IActionResult> UpdateCrowdfund(string appId, UpdateCrowdfundViewModel vm, string command)
{
var app = GetCurrentApp();
if (app == null)
return NotFound();
vm.TargetCurrency = await GetStoreDefaultCurrentIfEmpty(app.StoreDataId, vm.TargetCurrency);
if (_currencies.GetCurrencyData(vm.TargetCurrency, false) == null)
ModelState.AddModelError(nameof(vm.TargetCurrency), "Invalid currency");
try
{
vm.PerksTemplate = _appService.SerializeTemplate(_appService.Parse(vm.PerksTemplate, vm.TargetCurrency));
}
catch
{
ModelState.AddModelError(nameof(vm.PerksTemplate), "Invalid template");
}
if (vm.TargetAmount is decimal v && v == 0.0m)
{
vm.TargetAmount = null;
}
if (!vm.IsRecurring)
{
vm.ResetEvery = nameof(CrowdfundResetEvery.Never);
}
if (Enum.Parse<CrowdfundResetEvery>(vm.ResetEvery) != CrowdfundResetEvery.Never && !vm.StartDate.HasValue)
{
ModelState.AddModelError(nameof(vm.StartDate), "A start date is needed when the goal resets every X amount of time.");
}
if (Enum.Parse<CrowdfundResetEvery>(vm.ResetEvery) != CrowdfundResetEvery.Never && vm.ResetEveryAmount <= 0)
{
ModelState.AddModelError(nameof(vm.ResetEveryAmount), "You must reset the goal at a minimum of 1 ");
}
if (vm.DisplayPerksRanking)
{
vm.SortPerksByPopularity = true;
}
var parsedSounds = vm.Sounds?.Split(
new[] { "\r\n", "\r", "\n" },
StringSplitOptions.None
).Select(s => s.Trim()).ToArray();
if (vm.SoundsEnabled && (parsedSounds == null || !parsedSounds.Any()))
{
vm.SoundsEnabled = false;
parsedSounds = new CrowdfundSettings().Sounds;
}
var parsedAnimationColors = vm.AnimationColors?.Split(
new[] { "\r\n", "\r", "\n" },
StringSplitOptions.None
).Select(s => s.Trim()).ToArray();
if (vm.AnimationsEnabled && (parsedAnimationColors == null || !parsedAnimationColors.Any()))
{
vm.AnimationsEnabled = false;
parsedAnimationColors = new CrowdfundSettings().AnimationColors;
}
if (!ModelState.IsValid)
{
return View(vm);
}
app.Name = vm.AppName;
var newSettings = new CrowdfundSettings
{
Title = vm.Title,
Enabled = vm.Enabled,
EnforceTargetAmount = vm.EnforceTargetAmount,
StartDate = vm.StartDate?.ToUniversalTime(),
TargetCurrency = vm.TargetCurrency,
Description = vm.Description,
EndDate = vm.EndDate?.ToUniversalTime(),
TargetAmount = vm.TargetAmount,
CustomCSSLink = vm.CustomCSSLink,
MainImageUrl = vm.MainImageUrl,
EmbeddedCSS = vm.EmbeddedCSS,
NotificationUrl = vm.NotificationUrl,
Tagline = vm.Tagline,
PerksTemplate = vm.PerksTemplate,
DisqusEnabled = vm.DisqusEnabled,
SoundsEnabled = vm.SoundsEnabled,
DisqusShortname = vm.DisqusShortname,
AnimationsEnabled = vm.AnimationsEnabled,
ResetEveryAmount = vm.ResetEveryAmount,
ResetEvery = Enum.Parse<CrowdfundResetEvery>(vm.ResetEvery),
DisplayPerksValue = vm.DisplayPerksValue,
DisplayPerksRanking = vm.DisplayPerksRanking,
SortPerksByPopularity = vm.SortPerksByPopularity,
Sounds = parsedSounds,
AnimationColors = parsedAnimationColors
};
app.TagAllInvoices = vm.UseAllStoreInvoices;
app.SetSettings(newSettings);
await _appService.UpdateOrCreateApp(app);
_eventAggregator.Publish(new AppUpdated()
{
AppId = appId,
StoreId = app.StoreDataId,
Settings = newSettings
});
TempData[WellKnownTempData.SuccessMessage] = "App updated";
return RedirectToAction(nameof(UpdateCrowdfund), new { appId });
}
}
}

View File

@ -1,13 +1,17 @@
using System;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Client;
using BTCPayServer.Components.AppSales;
using BTCPayServer.Components.AppTopItems;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers
{
public partial class UIAppsController
{
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[HttpGet("{appId}/dashboard/app-top-items")]
public IActionResult AppTopItems(string appId)
{
@ -21,6 +25,7 @@ namespace BTCPayServer.Controllers
return ViewComponent("AppTopItems", new { vm });
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[HttpGet("{appId}/dashboard/app-sales")]
public IActionResult AppSales(string appId)
{
@ -34,6 +39,7 @@ namespace BTCPayServer.Controllers
return ViewComponent("AppSales", new { vm });
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[HttpGet("{appId}/dashboard/app-sales/{period}")]
public async Task<IActionResult> AppSales(string appId, AppSalesPeriod period)
{

View File

@ -1,161 +0,0 @@
using System;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Services.Apps;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers
{
public partial class UIAppsController
{
[HttpGet("{appId}/settings/pos")]
public async Task<IActionResult> UpdatePointOfSale(string appId)
{
var app = GetCurrentApp();
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
settings.DefaultView = settings.EnableShoppingCart ? PosViewType.Cart : settings.DefaultView;
settings.EnableShoppingCart = false;
var vm = new UpdatePointOfSaleViewModel
{
Id = appId,
StoreId = app.StoreDataId,
StoreName = app.StoreData?.StoreName,
StoreDefaultCurrency = await GetStoreDefaultCurrentIfEmpty(app.StoreDataId, settings.Currency),
AppName = app.Name,
Title = settings.Title,
DefaultView = settings.DefaultView,
ShowCustomAmount = settings.ShowCustomAmount,
ShowDiscount = settings.ShowDiscount,
EnableTips = settings.EnableTips,
Currency = settings.Currency,
Template = settings.Template,
ButtonText = settings.ButtonText ?? PointOfSaleSettings.BUTTON_TEXT_DEF,
CustomButtonText = settings.CustomButtonText ?? PointOfSaleSettings.CUSTOM_BUTTON_TEXT_DEF,
CustomTipText = settings.CustomTipText ?? PointOfSaleSettings.CUSTOM_TIP_TEXT_DEF,
CustomTipPercentages = settings.CustomTipPercentages != null ? string.Join(",", settings.CustomTipPercentages) : string.Join(",", PointOfSaleSettings.CUSTOM_TIP_PERCENTAGES_DEF),
CustomCSSLink = settings.CustomCSSLink,
EmbeddedCSS = settings.EmbeddedCSS,
Description = settings.Description,
NotificationUrl = settings.NotificationUrl,
RedirectUrl = settings.RedirectUrl,
SearchTerm = app.TagAllInvoices ? $"storeid:{app.StoreDataId}" : $"orderid:{AppService.GetAppOrderId(app)}",
RedirectAutomatically = settings.RedirectAutomatically.HasValue ? settings.RedirectAutomatically.Value ? "true" : "false" : "",
RequiresRefundEmail = settings.RequiresRefundEmail
};
if (HttpContext?.Request != null)
{
var appUrl = HttpContext.Request.GetAbsoluteUri($"/apps/{appId}/pos");
var encoder = HtmlEncoder.Default;
if (settings.ShowCustomAmount)
{
StringBuilder builder = new StringBuilder();
builder.AppendLine(CultureInfo.InvariantCulture, $"<form method=\"POST\" action=\"{encoder.Encode(appUrl)}\">");
builder.AppendLine($" <input type=\"hidden\" name=\"amount\" value=\"100\" />");
builder.AppendLine($" <input type=\"hidden\" name=\"email\" value=\"customer@example.com\" />");
builder.AppendLine($" <input type=\"hidden\" name=\"orderId\" value=\"CustomOrderId\" />");
builder.AppendLine($" <input type=\"hidden\" name=\"notificationUrl\" value=\"https://example.com/callbacks\" />");
builder.AppendLine($" <input type=\"hidden\" name=\"redirectUrl\" value=\"https://example.com/thanksyou\" />");
builder.AppendLine($" <button type=\"submit\">Buy now</button>");
builder.AppendLine($"</form>");
vm.Example1 = builder.ToString();
}
try
{
var items = _appService.Parse(settings.Template, settings.Currency);
var builder = new StringBuilder();
builder.AppendLine(CultureInfo.InvariantCulture, $"<form method=\"POST\" action=\"{encoder.Encode(appUrl)}\">");
builder.AppendLine($" <input type=\"hidden\" name=\"email\" value=\"customer@example.com\" />");
builder.AppendLine($" <input type=\"hidden\" name=\"orderId\" value=\"CustomOrderId\" />");
builder.AppendLine($" <input type=\"hidden\" name=\"notificationUrl\" value=\"https://example.com/callbacks\" />");
builder.AppendLine($" <input type=\"hidden\" name=\"redirectUrl\" value=\"https://example.com/thanksyou\" />");
builder.AppendLine(CultureInfo.InvariantCulture, $" <button type=\"submit\" name=\"choiceKey\" value=\"{items[0].Id}\">Buy now</button>");
builder.AppendLine($"</form>");
vm.Example2 = builder.ToString();
}
catch { }
vm.InvoiceUrl = appUrl + "invoices/SkdsDghkdP3D3qkj7bLq3";
}
vm.ExampleCallback = "{\n \"id\":\"SkdsDghkdP3D3qkj7bLq3\",\n \"url\":\"https://btcpay.example.com/invoice?id=SkdsDghkdP3D3qkj7bLq3\",\n \"status\":\"paid\",\n \"price\":10,\n \"currency\":\"EUR\",\n \"invoiceTime\":1520373130312,\n \"expirationTime\":1520374030312,\n \"currentTime\":1520373179327,\n \"exceptionStatus\":false,\n \"buyerFields\":{\n \"buyerEmail\":\"customer@example.com\",\n \"buyerNotify\":false\n },\n \"paymentSubtotals\": {\n \"BTC\":114700\n },\n \"paymentTotals\": {\n \"BTC\":118400\n },\n \"transactionCurrency\": \"BTC\",\n \"amountPaid\": \"1025900\",\n \"exchangeRates\": {\n \"BTC\": {\n \"EUR\": 8721.690715789999,\n \"USD\": 10817.99\n }\n }\n}";
return View(vm);
}
[HttpPost("{appId}/settings/pos")]
public async Task<IActionResult> UpdatePointOfSale(string appId, UpdatePointOfSaleViewModel vm)
{
var app = GetCurrentApp();
if (app == null)
return NotFound();
if (!ModelState.IsValid)
return View(vm);
vm.Currency = await GetStoreDefaultCurrentIfEmpty(app.StoreDataId, vm.Currency);
if (_currencies.GetCurrencyData(vm.Currency, false) == null)
ModelState.AddModelError(nameof(vm.Currency), "Invalid currency");
try
{
vm.Template = _appService.SerializeTemplate(_appService.Parse(vm.Template, vm.Currency));
}
catch
{
ModelState.AddModelError(nameof(vm.Template), "Invalid template");
}
if (!ModelState.IsValid)
{
return View(vm);
}
app.Name = vm.AppName;
app.SetSettings(new PointOfSaleSettings
{
Title = vm.Title,
DefaultView = vm.DefaultView,
ShowCustomAmount = vm.ShowCustomAmount,
ShowDiscount = vm.ShowDiscount,
EnableTips = vm.EnableTips,
Currency = vm.Currency,
Template = vm.Template,
ButtonText = vm.ButtonText,
CustomButtonText = vm.CustomButtonText,
CustomTipText = vm.CustomTipText,
CustomTipPercentages = ListSplit(vm.CustomTipPercentages),
CustomCSSLink = vm.CustomCSSLink,
NotificationUrl = vm.NotificationUrl,
RedirectUrl = vm.RedirectUrl,
Description = vm.Description,
EmbeddedCSS = vm.EmbeddedCSS,
RedirectAutomatically = string.IsNullOrEmpty(vm.RedirectAutomatically) ? (bool?)null : bool.Parse(vm.RedirectAutomatically),
RequiresRefundEmail = vm.RequiresRefundEmail,
});
await _appService.UpdateOrCreateApp(app);
TempData[WellKnownTempData.SuccessMessage] = "App updated";
return RedirectToAction(nameof(UpdatePointOfSale), new { appId });
}
private int[] ListSplit(string list, string separator = ",")
{
if (string.IsNullOrEmpty(list))
{
return Array.Empty<int>();
}
// Remove all characters except numeric and comma
Regex charsToDestroy = new Regex(@"[^\d|\" + separator + "]");
list = charsToDestroy.Replace(list, "");
return list.Split(separator, StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToArray();
}
}
}

View File

@ -5,10 +5,10 @@ using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client;
using BTCPayServer.Data;
using BTCPayServer.Models;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Plugins.Crowdfund.Controllers;
using BTCPayServer.Plugins.PointOfSale.Controllers;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
@ -16,33 +16,53 @@ using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers
{
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[AutoValidateAntiforgeryToken]
[Route("apps")]
public partial class UIAppsController : Controller
{
public UIAppsController(
UserManager<ApplicationUser> userManager,
EventAggregator eventAggregator,
CurrencyNameTable currencies,
StoreRepository storeRepository,
AppService appService)
{
_userManager = userManager;
_eventAggregator = eventAggregator;
_currencies = currencies;
_storeRepository = storeRepository;
_appService = appService;
}
private readonly UserManager<ApplicationUser> _userManager;
private readonly EventAggregator _eventAggregator;
private readonly CurrencyNameTable _currencies;
private readonly StoreRepository _storeRepository;
private readonly AppService _appService;
public string CreatedAppId { get; set; }
public class AppUpdated
{
public string AppId { get; set; }
public object Settings { get; set; }
public string StoreId { get; set; }
public override string ToString()
{
return string.Empty;
}
}
[HttpGet("/apps/{appId}")]
public async Task<IActionResult> RedirectToApp(string appId)
{
var app = await _appService.GetApp(appId, null);
if (app is null)
return NotFound();
return app.AppType switch
{
nameof(AppType.Crowdfund) => RedirectToAction(nameof(UICrowdfundController.ViewCrowdfund), "UICrowdfund", new { appId }),
nameof(AppType.PointOfSale) => RedirectToAction(nameof(UIPointOfSaleController.ViewPointOfSale), "UIPointOfSale", new { appId }),
_ => NotFound()
};
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[HttpGet("/stores/{storeId}/apps")]
public async Task<IActionResult> ListApps(
string storeId,
@ -88,6 +108,7 @@ namespace BTCPayServer.Controllers
});
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[HttpGet("/stores/{storeId}/apps/create")]
public IActionResult CreateApp(string storeId)
{
@ -97,6 +118,7 @@ namespace BTCPayServer.Controllers
});
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[HttpPost("/stores/{storeId}/apps/create")]
public async Task<IActionResult> CreateApp(string storeId, CreateAppViewModel vm)
{
@ -139,12 +161,13 @@ namespace BTCPayServer.Controllers
return appType switch
{
AppType.PointOfSale => RedirectToAction(nameof(UpdatePointOfSale), new { appId = appData.Id }),
AppType.Crowdfund => RedirectToAction(nameof(UpdateCrowdfund), new { appId = appData.Id }),
AppType.PointOfSale => RedirectToAction(nameof(UIPointOfSaleController.UpdatePointOfSale), "UIPointOfSale", new { appId = appData.Id }),
AppType.Crowdfund => RedirectToAction(nameof(UICrowdfundController.UpdateCrowdfund), "UICrowdfund", new { appId = appData.Id }),
_ => throw new ArgumentOutOfRangeException()
};
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[HttpGet("{appId}/delete")]
public IActionResult DeleteApp(string appId)
{
@ -155,6 +178,7 @@ namespace BTCPayServer.Controllers
return View("Confirm", new ConfirmModel("Delete app", $"The app <strong>{app.Name}</strong> and its settings will be permanently deleted. Are you sure?", "Delete"));
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[HttpPost("{appId}/delete")]
public async Task<IActionResult> DeleteAppPost(string appId)
{

View File

@ -1,428 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Configuration;
using BTCPayServer.Data;
using BTCPayServer.Filters;
using BTCPayServer.ModelBinders;
using BTCPayServer.Models;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Services.Apps;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using NBitpayClient;
using NicolasDorier.RateLimits;
namespace BTCPayServer.Controllers
{
public class UIAppsPublicController : Controller
{
public UIAppsPublicController(AppService appService,
BTCPayServerOptions btcPayServerOptions,
UIInvoiceController invoiceController,
UserManager<ApplicationUser> userManager)
{
_AppService = appService;
_BtcPayServerOptions = btcPayServerOptions;
_InvoiceController = invoiceController;
_UserManager = userManager;
}
private readonly AppService _AppService;
private readonly BTCPayServerOptions _BtcPayServerOptions;
private readonly UIInvoiceController _InvoiceController;
private readonly UserManager<ApplicationUser> _UserManager;
[HttpGet("/apps/{appId}")]
public async Task<IActionResult> RedirectToApp(string appId)
{
var app = await _AppService.GetApp(appId, null);
if (app is null)
return NotFound();
switch (app.AppType)
{
case nameof(AppType.Crowdfund):
return RedirectToAction("ViewCrowdfund", new { appId });
case nameof(AppType.PointOfSale):
return RedirectToAction("ViewPointOfSale", new { appId });
}
return NotFound();
}
[HttpGet]
[Route("/")]
[Route("/apps/{appId}/pos/{viewType?}")]
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
[DomainMappingConstraint(AppType.PointOfSale)]
public async Task<IActionResult> ViewPointOfSale(string appId, PosViewType? viewType = null)
{
var app = await _AppService.GetApp(appId, AppType.PointOfSale);
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
var numberFormatInfo = _AppService.Currencies.GetNumberFormatInfo(settings.Currency) ?? _AppService.Currencies.GetNumberFormatInfo("USD");
double step = Math.Pow(10, -(numberFormatInfo.CurrencyDecimalDigits));
viewType ??= settings.EnableShoppingCart ? PosViewType.Cart : settings.DefaultView;
var store = await _AppService.GetStore(app);
var storeBlob = store.GetStoreBlob();
return View("PointOfSale/" + viewType, new ViewPointOfSaleViewModel()
{
Title = settings.Title,
Step = step.ToString(CultureInfo.InvariantCulture),
ViewType = (PosViewType)viewType,
ShowCustomAmount = settings.ShowCustomAmount,
ShowDiscount = settings.ShowDiscount,
EnableTips = settings.EnableTips,
CurrencyCode = settings.Currency,
CurrencySymbol = numberFormatInfo.CurrencySymbol,
CurrencyInfo = new ViewPointOfSaleViewModel.CurrencyInfoData()
{
CurrencySymbol = string.IsNullOrEmpty(numberFormatInfo.CurrencySymbol) ? settings.Currency : numberFormatInfo.CurrencySymbol,
Divisibility = numberFormatInfo.CurrencyDecimalDigits,
DecimalSeparator = numberFormatInfo.CurrencyDecimalSeparator,
ThousandSeparator = numberFormatInfo.NumberGroupSeparator,
Prefixed = new[] { 0, 2 }.Contains(numberFormatInfo.CurrencyPositivePattern),
SymbolSpace = new[] { 2, 3 }.Contains(numberFormatInfo.CurrencyPositivePattern)
},
Items = _AppService.GetPOSItems(settings.Template, settings.Currency),
ButtonText = settings.ButtonText,
CustomButtonText = settings.CustomButtonText,
CustomTipText = settings.CustomTipText,
CustomTipPercentages = settings.CustomTipPercentages,
CustomCSSLink = settings.CustomCSSLink,
CustomLogoLink = storeBlob.CustomLogo,
AppId = appId,
StoreId = store.Id,
Description = settings.Description,
EmbeddedCSS = settings.EmbeddedCSS,
RequiresRefundEmail = settings.RequiresRefundEmail
});
}
[HttpPost]
[Route("/")]
[Route("/apps/{appId}/pos/{viewType?}")]
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
[IgnoreAntiforgeryToken]
[EnableCors(CorsPolicies.All)]
[DomainMappingConstraint(AppType.PointOfSale)]
[RateLimitsFilter(ZoneLimits.PublicInvoices, Scope = RateLimitsScope.RemoteAddress)]
public async Task<IActionResult> ViewPointOfSale(string appId,
PosViewType viewType,
[ModelBinder(typeof(InvariantDecimalModelBinder))] decimal? amount,
string email,
string orderId,
string notificationUrl,
string redirectUrl,
string choiceKey,
string posData = null,
RequiresRefundEmail requiresRefundEmail = RequiresRefundEmail.InheritFromStore,
CancellationToken cancellationToken = default)
{
var app = await _AppService.GetApp(appId, AppType.PointOfSale);
if (string.IsNullOrEmpty(choiceKey) && amount <= 0)
{
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
}
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
settings.DefaultView = settings.EnableShoppingCart ? PosViewType.Cart : settings.DefaultView;
if (string.IsNullOrEmpty(choiceKey) && !settings.ShowCustomAmount && settings.DefaultView != PosViewType.Cart)
{
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId, viewType = viewType });
}
string title = null;
decimal? price = null;
Dictionary<string, InvoiceSupportedTransactionCurrency> paymentMethods = null;
ViewPointOfSaleViewModel.Item choice = null;
if (!string.IsNullOrEmpty(choiceKey))
{
var choices = _AppService.GetPOSItems(settings.Template, settings.Currency);
choice = choices.FirstOrDefault(c => c.Id == choiceKey);
if (choice == null)
return NotFound();
title = choice.Title;
if (choice.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup)
{
price = null;
}
else
{
price = choice.Price.Value;
if (amount > price)
price = amount;
}
if (choice.Inventory.HasValue)
{
if (choice.Inventory <= 0)
{
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
}
}
if (choice?.PaymentMethods?.Any() is true)
{
paymentMethods = choice?.PaymentMethods.ToDictionary(s => s,
s => new InvoiceSupportedTransactionCurrency() { Enabled = true });
}
}
else
{
if (!settings.ShowCustomAmount && settings.DefaultView != PosViewType.Cart)
return NotFound();
price = amount;
title = settings.Title;
//if cart IS enabled and we detect posdata that matches the cart system's, check inventory for the items
if (!string.IsNullOrEmpty(posData) &&
settings.DefaultView == PosViewType.Cart &&
AppService.TryParsePosCartItems(posData, out var cartItems))
{
var choices = _AppService.GetPOSItems(settings.Template, settings.Currency);
foreach (var cartItem in cartItems)
{
var itemChoice = choices.FirstOrDefault(c => c.Id == cartItem.Key);
if (itemChoice == null)
return NotFound();
if (itemChoice.Inventory.HasValue)
{
switch (itemChoice.Inventory)
{
case int i when i <= 0:
return RedirectToAction(nameof(ViewPointOfSale), new { appId });
case int inventory when inventory < cartItem.Value:
return RedirectToAction(nameof(ViewPointOfSale), new { appId });
}
}
}
}
}
var store = await _AppService.GetStore(app);
try
{
var invoice = await _InvoiceController.CreateInvoiceCore(new BitpayCreateInvoiceRequest()
{
ItemCode = choice?.Id,
ItemDesc = title,
Currency = settings.Currency,
Price = price,
BuyerEmail = email,
OrderId = orderId ?? AppService.GetAppOrderId(app),
NotificationURL =
string.IsNullOrEmpty(notificationUrl) ? settings.NotificationUrl : notificationUrl,
RedirectURL = !string.IsNullOrEmpty(redirectUrl) ? redirectUrl
: !string.IsNullOrEmpty(settings.RedirectUrl) ? settings.RedirectUrl
: Request.GetDisplayUrl(),
FullNotifications = true,
ExtendedNotifications = true,
PosData = string.IsNullOrEmpty(posData) ? null : posData,
RedirectAutomatically = settings.RedirectAutomatically,
SupportedTransactionCurrencies = paymentMethods,
RequiresRefundEmail = requiresRefundEmail == RequiresRefundEmail.InheritFromStore
? store.GetStoreBlob().RequiresRefundEmail
: requiresRefundEmail == RequiresRefundEmail.On,
}, store, HttpContext.Request.GetAbsoluteRoot(),
new List<string>() { AppService.GetAppInternalTag(appId) },
cancellationToken, (entity) =>
{
entity.Metadata.OrderUrl = Request.GetDisplayUrl();
} );
return RedirectToAction(nameof(UIInvoiceController.Checkout), "UIInvoice", new { invoiceId = invoice.Data.Id });
}
catch (BitpayHttpException e)
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Html = e.Message.Replace("\n", "<br />", StringComparison.OrdinalIgnoreCase),
Severity = StatusMessageModel.StatusSeverity.Error,
AllowDismiss = true
});
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
}
}
[HttpGet]
[Route("/")]
[Route("/apps/{appId}/crowdfund")]
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
[DomainMappingConstraintAttribute(AppType.Crowdfund)]
public async Task<IActionResult> ViewCrowdfund(string appId, string statusMessage)
{
var app = await _AppService.GetApp(appId, AppType.Crowdfund, true);
if (app == null)
return NotFound();
var settings = app.GetSettings<CrowdfundSettings>();
var isAdmin = await _AppService.GetAppDataIfOwner(GetUserId(), appId, AppType.Crowdfund) != null;
var hasEnoughSettingsToLoad = !string.IsNullOrEmpty(settings.TargetCurrency);
if (!hasEnoughSettingsToLoad)
{
if (!isAdmin)
return NotFound();
return NotFound("A Target Currency must be set for this app in order to be loadable.");
}
var appInfo = await GetAppInfo(appId);
if (settings.Enabled)
return View(appInfo);
if (!isAdmin)
return NotFound();
return View(appInfo);
}
[HttpPost]
[Route("/")]
[Route("/apps/{appId}/crowdfund")]
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
[IgnoreAntiforgeryToken]
[EnableCors(CorsPolicies.All)]
[DomainMappingConstraintAttribute(AppType.Crowdfund)]
[RateLimitsFilter(ZoneLimits.PublicInvoices, Scope = RateLimitsScope.RemoteAddress)]
public async Task<IActionResult> ContributeToCrowdfund(string appId, ContributeToCrowdfund request, CancellationToken cancellationToken)
{
var app = await _AppService.GetApp(appId, AppType.Crowdfund, true);
if (app == null)
return NotFound();
var settings = app.GetSettings<CrowdfundSettings>();
var isAdmin = await _AppService.GetAppDataIfOwner(GetUserId(), appId, AppType.Crowdfund) != null;
if (!settings.Enabled && !isAdmin)
{
return NotFound("Crowdfund is not currently active");
}
var info = await GetAppInfo(appId);
if (!isAdmin &&
((settings.StartDate.HasValue && DateTime.UtcNow < settings.StartDate) ||
(settings.EndDate.HasValue && DateTime.UtcNow > settings.EndDate) ||
(settings.EnforceTargetAmount &&
(info.Info.PendingProgressPercentage.GetValueOrDefault(0) +
info.Info.ProgressPercentage.GetValueOrDefault(0)) >= 100)))
{
return NotFound("Crowdfund is not currently active");
}
var store = await _AppService.GetStore(app);
var title = settings.Title;
decimal? price = request.Amount;
Dictionary<string, InvoiceSupportedTransactionCurrency> paymentMethods = null;
ViewPointOfSaleViewModel.Item choice = null;
if (!string.IsNullOrEmpty(request.ChoiceKey))
{
var choices = _AppService.GetPOSItems(settings.PerksTemplate, settings.TargetCurrency);
choice = choices.FirstOrDefault(c => c.Id == request.ChoiceKey);
if (choice == null)
return NotFound("Incorrect option provided");
title = choice.Title;
if (choice.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup)
{
price = null;
}
else
{
price = choice.Price.Value;
if (request.Amount > price)
price = request.Amount;
}
if (choice.Inventory.HasValue)
{
if (choice.Inventory <= 0)
{
return NotFound("Option was out of stock");
}
}
if (choice?.PaymentMethods?.Any() is true)
{
paymentMethods = choice?.PaymentMethods.ToDictionary(s => s,
s => new InvoiceSupportedTransactionCurrency() { Enabled = true });
}
}
else
{
if (request.Amount < 0)
{
return NotFound("Please provide an amount greater than 0");
}
price = request.Amount;
}
if (!isAdmin && (settings.EnforceTargetAmount && info.TargetAmount.HasValue && price >
(info.TargetAmount - (info.Info.CurrentAmount + info.Info.CurrentPendingAmount))))
{
return NotFound("Contribution Amount is more than is currently allowed.");
}
try
{
var invoice = await _InvoiceController.CreateInvoiceCore(new BitpayCreateInvoiceRequest()
{
OrderId = AppService.GetAppOrderId(app),
Currency = settings.TargetCurrency,
ItemCode = request.ChoiceKey ?? string.Empty,
ItemDesc = title,
BuyerEmail = request.Email,
Price = price,
NotificationURL = settings.NotificationUrl,
FullNotifications = true,
ExtendedNotifications = true,
SupportedTransactionCurrencies = paymentMethods,
RedirectURL = request.RedirectUrl ?? Request.GetDisplayUrl(),
}, store, HttpContext.Request.GetAbsoluteRoot(),
new List<string>() {AppService.GetAppInternalTag(appId)},
cancellationToken, (entity) =>
{
entity.Metadata.OrderUrl = Request.GetDisplayUrl();
});
if (request.RedirectToCheckout)
{
return RedirectToAction(nameof(UIInvoiceController.Checkout), "UIInvoice",
new {invoiceId = invoice.Data.Id});
}
return Ok(invoice.Data.Id);
}
catch (BitpayHttpException e)
{
return BadRequest(e.Message);
}
}
private async Task<ViewCrowdfundViewModel> GetAppInfo(string appId)
{
var info = (ViewCrowdfundViewModel)await _AppService.GetAppInfo(appId);
info.HubPath = AppHub.GetHubPath(Request);
info.SimpleDisplay = Request.Query.ContainsKey("simple");
return info;
}
private string GetUserId()
{
return _UserManager.GetUserId(User);
}
}
}

View File

@ -1,4 +1,5 @@
using System;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Data;
@ -24,34 +25,35 @@ namespace BTCPayServer.Controllers
public string CryptoCode { get; set; } = "BTC";
}
[HttpPost]
[Route("i/{invoiceId}/test-payment")]
[HttpPost("i/{invoiceId}/test-payment")]
[CheatModeRoute]
public async Task<IActionResult> TestPayment(string invoiceId, FakePaymentRequest request, [FromServices] Cheater cheater)
{
var invoice = await _InvoiceRepository.GetInvoice(invoiceId);
var store = await _StoreRepository.FindStore(invoice.StoreId);
// TODO support altcoins, not just bitcoin
var network = _NetworkProvider.GetNetwork<BTCPayNetwork>(request.CryptoCode);
// TODO support altcoins, not just bitcoin - and make it work for LN-only invoices
var isSats = request.CryptoCode.ToUpper(CultureInfo.InvariantCulture) == "SATS";
var cryptoCode = isSats ? "BTC" : request.CryptoCode;
var network = _NetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
var paymentMethodId = new [] {store.GetDefaultPaymentId()}.Concat(store.GetEnabledPaymentIds(_NetworkProvider))
.FirstOrDefault(p => p!= null && p.CryptoCode == request.CryptoCode && p.PaymentType == PaymentTypes.BTCLike);
.FirstOrDefault(p => p != null && p.CryptoCode == cryptoCode && p.PaymentType == PaymentTypes.BTCLike);
var bitcoinAddressString = invoice.GetPaymentMethod(paymentMethodId).GetPaymentMethodDetails().GetPaymentDestination();
var bitcoinAddressObj = BitcoinAddress.Create(bitcoinAddressString, network.NBitcoinNetwork);
var BtcAmount = request.Amount;
var amount = new Money(request.Amount, isSats ? MoneyUnit.Satoshi : MoneyUnit.BTC);
try
{
var paymentMethod = invoice.GetPaymentMethod(paymentMethodId);
var rate = paymentMethod.Rate;
var txid = cheater.CashCow.SendToAddress(bitcoinAddressObj, new Money(BtcAmount, MoneyUnit.BTC)).ToString();
var txid = (await cheater.CashCow.SendToAddressAsync(bitcoinAddressObj, amount)).ToString();
// TODO The value of totalDue is wrong. How can we get the real total due? invoice.Price is only correct if this is the 2nd payment, not for a 3rd or 4th payment.
var totalDue = invoice.Price;
return Ok(new
{
Txid = txid,
AmountRemaining = (totalDue - (BtcAmount * rate)) / rate,
AmountRemaining = (totalDue - (amount.ToUnit(MoneyUnit.BTC) * rate)) / rate,
SuccessMessage = "Created transaction " + txid
});
}
@ -65,8 +67,7 @@ namespace BTCPayServer.Controllers
}
}
[HttpPost]
[Route("i/{invoiceId}/mine-blocks")]
[HttpPost("i/{invoiceId}/mine-blocks")]
[CheatModeRoute]
public IActionResult MineBlock(string invoiceId, MineBlocksRequest request, [FromServices] Cheater cheater)
{
@ -96,8 +97,7 @@ namespace BTCPayServer.Controllers
}
}
[HttpPost]
[Route("i/{invoiceId}/expire")]
[HttpPost("i/{invoiceId}/expire")]
[CheatModeRoute]
public async Task<IActionResult> TestExpireNow(string invoiceId, [FromServices] Cheater cheater)
{

View File

@ -24,6 +24,7 @@ using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Invoices.Export;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
@ -147,6 +148,7 @@ namespace BTCPayServer.Controllers
var details = InvoicePopulatePayments(invoice);
model.CryptoPayments = details.CryptoPayments;
model.Payments = details.Payments;
model.Overpaid = details.Overpaid;
return View(model);
}
@ -176,7 +178,7 @@ namespace BTCPayServer.Controllers
});
}
JToken? receiptData = null;
i.Metadata?.AdditionalData.TryGetValue("receiptData", out receiptData);
i.Metadata?.AdditionalData?.TryGetValue("receiptData", out receiptData);
return View(new InvoiceReceiptViewModel
{
@ -467,15 +469,25 @@ namespace BTCPayServer.Controllers
private InvoiceDetailsModel InvoicePopulatePayments(InvoiceEntity invoice)
{
return new InvoiceDetailsModel
var overpaid = false;
var model = new InvoiceDetailsModel
{
Archived = invoice.Archived,
Payments = invoice.GetPayments(false),
Overpaid = true,
CryptoPayments = invoice.GetPaymentMethods().Select(
data =>
{
var accounting = data.Calculate();
var paymentMethodId = data.GetId();
var overpaidAmount = accounting.OverpaidHelper.ToDecimal(MoneyUnit.BTC);
if (overpaidAmount > 0)
{
overpaid = true;
}
return new InvoiceDetailsModel.CryptoPayment
{
PaymentMethodId = paymentMethodId,
@ -486,13 +498,16 @@ namespace BTCPayServer.Controllers
accounting.CryptoPaid.ToDecimal(MoneyUnit.BTC),
paymentMethodId.CryptoCode),
Overpaid = _CurrencyNameTable.DisplayFormatCurrency(
accounting.OverpaidHelper.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode),
overpaidAmount, paymentMethodId.CryptoCode),
Address = data.GetPaymentMethodDetails().GetPaymentDestination(),
Rate = ExchangeRate(data),
PaymentMethodRaw = data
};
}).ToList()
};
model.Overpaid = overpaid;
return model;
}
[HttpPost("invoices/{invoiceId}/archive")]
@ -589,23 +604,26 @@ namespace BTCPayServer.Controllers
[HttpGet("i/{invoiceId}/{paymentMethodId}")]
[HttpGet("invoice")]
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest", false)]
[XFrameOptionsAttribute(null)]
[ReferrerPolicyAttribute("origin")]
[XFrameOptions(null)]
[ReferrerPolicy("origin")]
public async Task<IActionResult> Checkout(string? invoiceId, string? id = null, string? paymentMethodId = null,
[FromQuery] string? view = null, [FromQuery] string? lang = null)
{
//Keep compatibility with Bitpay
invoiceId = invoiceId ?? id;
//
// Keep compatibility with Bitpay
invoiceId ??= id;
if (invoiceId is null)
return NotFound();
var model = await GetInvoiceModel(invoiceId, paymentMethodId == null ? null : PaymentMethodId.Parse(paymentMethodId), lang);
if (model == null)
return NotFound();
if (view == "modal")
model.IsModal = true;
return View(nameof(Checkout), model);
var viewName = model.CheckoutType == CheckoutType.V2 ? "CheckoutV2" : nameof(Checkout);
return View(viewName, model);
}
[HttpGet("invoice-noscript")]
@ -717,15 +735,18 @@ namespace BTCPayServer.Controllers
var receiptEnabled = InvoiceDataBase.ReceiptOptions.Merge(storeBlob.ReceiptOptions, invoice.ReceiptOptions).Enabled is true;
var receiptUrl = receiptEnabled? _linkGenerator.GetUriByAction(
nameof(UIInvoiceController.InvoiceReceipt),
nameof(InvoiceReceipt),
"UIInvoice",
new {invoiceId},
Request.Scheme,
Request.Host,
Request.PathBase) : null;
var model = new PaymentModel
{
#if ALTCOINS
AltcoinsBuild = true,
#endif
Activated = paymentMethodDetails.Activated,
CryptoCode = network.CryptoCode,
RootPath = Request.PathBase.Value.WithTrailingSlash(),
@ -734,6 +755,10 @@ namespace BTCPayServer.Controllers
DefaultLang = lang ?? invoice.DefaultLanguage ?? storeBlob.DefaultLang ?? "en",
CustomCSSLink = storeBlob.CustomCSS,
CustomLogoLink = storeBlob.CustomLogo,
LogoFileId = storeBlob.LogoFileId,
BrandColor = storeBlob.BrandColor,
CheckoutFormId = invoice.CheckoutFormId ?? storeBlob.CheckoutFormId,
CheckoutType = invoice.CheckoutType ?? storeBlob.CheckoutType,
HtmlTitle = storeBlob.HtmlTitle ?? "BTCPay Invoice",
CryptoImage = Request.GetRelativePathOrAbsolute(paymentMethodHandler.GetCryptoImage(paymentMethodId)),
BtcAddress = paymentMethodDetails.GetPaymentDestination(),
@ -769,12 +794,19 @@ namespace BTCPayServer.Controllers
IsMultiCurrency = invoice.GetPayments(false).Select(p => p.GetPaymentMethodId()).Concat(new[] { paymentMethod.GetId() }).Distinct().Count() > 1,
StoreId = store.Id,
AvailableCryptos = invoice.GetPaymentMethods()
.Where(i => i.Network != null)
.Where(i => i.Network != null &&
// TODO: These cases and implementation need to be discussed
(storeBlob.CheckoutType == CheckoutType.V1 ||
// Exclude LNURL for non-topup invoices
(invoice.IsUnsetTopUp() || i.GetId().PaymentType is not LNURLPayPaymentType)) &&
// Exclude Lightning if OnChainWithLnInvoiceFallback is active
(!storeBlob.OnChainWithLnInvoiceFallback || i.GetId().PaymentType is not LightningPaymentType)
)
.Select(kv =>
{
var availableCryptoPaymentMethodId = kv.GetId();
var availableCryptoHandler = _paymentMethodHandlerDictionary[availableCryptoPaymentMethodId];
return new PaymentModel.AvailableCrypto()
return new PaymentModel.AvailableCrypto
{
PaymentMethodId = kv.GetId().ToString(),
CryptoCode = kv.Network?.CryptoCode ?? kv.GetId().CryptoCode,
@ -893,6 +925,14 @@ namespace BTCPayServer.Controllers
return Ok("{}");
}
[HttpPost("i/{invoiceId}/Form")]
[HttpPost("invoice/Form")]
public IActionResult UpdateForm(string invoiceId)
{
// TODO: Forms integration
return Ok();
}
[HttpGet("/stores/{storeId}/invoices")]
[HttpGet("invoices")]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanViewInvoices)]
@ -1039,10 +1079,12 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(UIHomeController.Index), "UIHome");
}
var storeBlob = HttpContext.GetStoreData()?.GetStoreBlob();
var vm = new CreateInvoiceModel
{
StoreId = model.StoreId,
Currency = HttpContext.GetStoreData()?.GetStoreBlob().DefaultCurrency,
Currency = storeBlob?.DefaultCurrency,
UseNewCheckout = storeBlob?.CheckoutType is CheckoutType.V2,
AvailablePaymentMethods = GetPaymentMethodsSelectList()
};
@ -1055,8 +1097,11 @@ namespace BTCPayServer.Controllers
[BitpayAPIConstraint(false)]
public async Task<IActionResult> CreateInvoice(CreateInvoiceModel model, CancellationToken cancellationToken)
{
model.AvailablePaymentMethods = GetPaymentMethodsSelectList();
var store = HttpContext.GetStoreData();
var storeBlob = store.GetStoreBlob();
model.UseNewCheckout = storeBlob.CheckoutType == CheckoutType.V2;
model.AvailablePaymentMethods = GetPaymentMethodsSelectList();
if (!ModelState.IsValid)
{
return View(model);
@ -1075,18 +1120,17 @@ namespace BTCPayServer.Controllers
try
{
var result = await CreateInvoiceCore(new BitpayCreateInvoiceRequest()
var result = await CreateInvoiceCore(new BitpayCreateInvoiceRequest
{
Price = model.Amount,
Currency = model.Currency,
PosData = model.PosData,
OrderId = model.OrderId,
//RedirectURL = redirect + "redirect",
NotificationURL = model.NotificationUrl,
ItemDesc = model.ItemDesc,
FullNotifications = true,
BuyerEmail = model.BuyerEmail,
SupportedTransactionCurrencies = model.SupportedTransactionCurrencies?.ToDictionary(s => s, s => new InvoiceSupportedTransactionCurrency()
SupportedTransactionCurrencies = model.SupportedTransactionCurrencies?.ToDictionary(s => s, s => new InvoiceSupportedTransactionCurrency
{
Enabled = true
}),
@ -1094,8 +1138,11 @@ namespace BTCPayServer.Controllers
NotificationEmail = model.NotificationEmail,
ExtendedNotifications = model.NotificationEmail != null,
RequiresRefundEmail = model.RequiresRefundEmail == RequiresRefundEmail.InheritFromStore
? store.GetStoreBlob().RequiresRefundEmail
: model.RequiresRefundEmail == RequiresRefundEmail.On
? storeBlob.RequiresRefundEmail
: model.RequiresRefundEmail == RequiresRefundEmail.On,
CheckoutFormId = model.CheckoutFormId == GenericFormOption.InheritFromStore.ToString()
? storeBlob.CheckoutFormId
: model.CheckoutFormId
}, store, HttpContext.Request.GetAbsoluteRoot(), cancellationToken: cancellationToken);
TempData[WellKnownTempData.SuccessMessage] = $"Invoice {result.Data.Id} just created!";

View File

@ -5,21 +5,25 @@ using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.HostedServices;
using BTCPayServer.Logging;
using BTCPayServer.Models;
using BTCPayServer.Models.PaymentRequestViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Rating;
using BTCPayServer.Security;
using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.PaymentRequests;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Validation;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
@ -138,6 +142,7 @@ namespace BTCPayServer.Controllers
entity.RedirectAutomatically =
invoice.RedirectAutomatically.GetValueOrDefault(storeBlob.RedirectAutomatically);
entity.RequiresRefundEmail = invoice.RequiresRefundEmail;
entity.CheckoutFormId = invoice.CheckoutFormId;
entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy);
IPaymentFilter? excludeFilter = null;
@ -167,6 +172,35 @@ namespace BTCPayServer.Controllers
return await CreateInvoiceCoreRaw(entity, store, excludeFilter, null, cancellationToken, entityManipulator);
}
internal async Task<InvoiceEntity> CreatePaymentRequestInvoice(ViewPaymentRequestViewModel pr, decimal? amount, StoreData storeData, HttpRequest request, CancellationToken cancellationToken)
{
if (pr.AllowCustomPaymentAmounts && amount != null)
amount = Math.Min(pr.AmountDue, amount.Value);
else
amount = pr.AmountDue;
var redirectUrl = _linkGenerator.PaymentRequestLink(pr.Id, request.Scheme, request.Host, request.PathBase);
var invoiceMetadata =
new InvoiceMetadata
{
OrderId = PaymentRequestRepository.GetOrderIdForPaymentRequest(pr.Id),
PaymentRequestId = pr.Id,
BuyerEmail = pr.Email
};
var invoiceRequest =
new CreateInvoiceRequest
{
Metadata = invoiceMetadata.ToJObject(),
Currency = pr.Currency,
Amount = amount,
Checkout = { RedirectURL = redirectUrl }
};
var additionalTags = new List<string> { PaymentRequestRepository.GetInternalTag(pr.Id) };
return await CreateInvoiceCoreRaw(invoiceRequest, storeData, request.GetAbsoluteRoot(), additionalTags, cancellationToken);
}
internal async Task<InvoiceEntity> CreateInvoiceCoreRaw(CreateInvoiceRequest invoice, StoreData store, string serverUrl, List<string>? additionalTags = null, CancellationToken cancellationToken = default, Action<InvoiceEntity>? entityManipulator = null)
{
var storeBlob = store.GetStoreBlob();
@ -193,6 +227,8 @@ namespace BTCPayServer.Controllers
entity.DefaultLanguage = invoice.Checkout.DefaultLanguage;
entity.DefaultPaymentMethod = invoice.Checkout.DefaultPaymentMethod;
entity.RedirectAutomatically = invoice.Checkout.RedirectAutomatically ?? storeBlob.RedirectAutomatically;
entity.CheckoutFormId = invoice.Checkout.CheckoutFormId;
entity.CheckoutType = invoice.Checkout.CheckoutType;
entity.RequiresRefundEmail = invoice.Checkout.RequiresRefundEmail;
IPaymentFilter? excludeFilter = null;
if (invoice.Checkout.PaymentMethods != null)
@ -278,7 +314,6 @@ namespace BTCPayServer.Controllers
if (!noNeedForMethods)
{
// This loop ends with .ToList so we are querying all payment methods at once
// instead of sequentially to improve response time
var x1 = store.GetSupportedPaymentMethods(_NetworkProvider)

View File

@ -19,6 +19,7 @@ using BTCPayServer.Lightning;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Plugins.PointOfSale.Models;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
@ -408,7 +409,7 @@ namespace BTCPayServer
if (i.Type != InvoiceType.TopUp)
{
min = i.GetPaymentMethod(pmi).Calculate().Due.ToDecimal(MoneyUnit.Satoshi);
max = min;
max = item?.Price?.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Minimum ? null : min;
}
if (!string.IsNullOrEmpty(username))
@ -559,9 +560,12 @@ namespace BTCPayServer
LightningInvoice invoice;
try
{
invoice = await client.CreateInvoice(new CreateInvoiceParams(amount.Value,
descriptionHash,
i.ExpirationTime.ToUniversalTime() - DateTimeOffset.UtcNow));
var expiry = i.ExpirationTime.ToUniversalTime() - DateTimeOffset.UtcNow;
var param = new CreateInvoiceParams(amount.Value, descriptionHash, expiry)
{
PrivateRouteHints = blob.LightningPrivateRouteHints
};
invoice = await client.CreateInvoice(param);
if (!BOLT11PaymentRequest.Parse(invoice.BOLT11, network.NBitcoinNetwork)
.VerifyDescriptionHash(metadata))
{

View File

@ -35,7 +35,7 @@ namespace BTCPayServer.Controllers
private readonly EventAggregator _EventAggregator;
private readonly CurrencyNameTable _Currencies;
private readonly InvoiceRepository _InvoiceRepository;
private readonly LinkGenerator _linkGenerator;
private readonly StoreRepository _storeRepository;
public UIPaymentRequestController(
UIInvoiceController invoiceController,
@ -44,8 +44,8 @@ namespace BTCPayServer.Controllers
PaymentRequestService paymentRequestService,
EventAggregator eventAggregator,
CurrencyNameTable currencies,
InvoiceRepository invoiceRepository,
LinkGenerator linkGenerator)
StoreRepository storeRepository,
InvoiceRepository invoiceRepository)
{
_InvoiceController = invoiceController;
_UserManager = userManager;
@ -53,8 +53,8 @@ namespace BTCPayServer.Controllers
_PaymentRequestService = paymentRequestService;
_EventAggregator = eventAggregator;
_Currencies = currencies;
_storeRepository = storeRepository;
_InvoiceRepository = invoiceRepository;
_linkGenerator = linkGenerator;
}
[BitpayAPIConstraint(false)]
@ -213,14 +213,7 @@ namespace BTCPayServer.Controllers
return BadRequest("Payment Request has expired");
}
var stateAllowedToDisplay = new HashSet<InvoiceState>
{
new InvoiceState(InvoiceStatusLegacy.New, InvoiceExceptionStatus.None),
new InvoiceState(InvoiceStatusLegacy.New, InvoiceExceptionStatus.PaidPartial),
};
var currentInvoice = result
.Invoices
.FirstOrDefault(invoice => stateAllowedToDisplay.Contains(invoice.State));
var currentInvoice = result.Invoices.GetReusableInvoice(amount);
if (currentInvoice != null)
{
if (redirectToInvoice)
@ -231,38 +224,10 @@ namespace BTCPayServer.Controllers
return Ok(currentInvoice.Id);
}
if (result.AllowCustomPaymentAmounts && amount != null)
amount = Math.Min(result.AmountDue, amount.Value);
else
amount = result.AmountDue;
var pr = await _PaymentRequestRepository.FindPaymentRequest(payReqId, null, cancellationToken);
var blob = pr.GetBlob();
var store = pr.StoreData;
try
{
var redirectUrl = _linkGenerator.PaymentRequestLink(payReqId, Request.Scheme, Request.Host, Request.PathBase);
var invoiceMetadata =
new InvoiceMetadata
{
OrderId = PaymentRequestRepository.GetOrderIdForPaymentRequest(payReqId),
PaymentRequestId = payReqId,
BuyerEmail = result.Email
};
var invoiceRequest =
new CreateInvoiceRequest
{
Metadata = invoiceMetadata.ToJObject(),
Currency = blob.Currency,
Amount = amount.Value,
Checkout = { RedirectURL = redirectUrl }
};
var additionalTags = new List<string> { PaymentRequestRepository.GetInternalTag(payReqId) };
var newInvoice = await _InvoiceController.CreateInvoiceCoreRaw(invoiceRequest, store, Request.GetAbsoluteRoot(), additionalTags, cancellationToken);
var store = await _storeRepository.FindStore(result.StoreId);
var newInvoice = await _InvoiceController.CreatePaymentRequestInvoice(result, amount, store, Request, cancellationToken);
if (redirectToInvoice)
{
return RedirectToAction("Checkout", "UIInvoice", new { invoiceId = newInvoice.Id });

View File

@ -13,7 +13,6 @@ using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers
{
[Route("embed/{storeId}/{cryptoCode}/ln")]
[AllowAnonymous]
public class UIPublicLightningNodeInfoController : Controller
@ -43,11 +42,11 @@ namespace BTCPayServer.Controllers
var paymentMethodDetails = GetExistingLightningSupportedPaymentMethod(cryptoCode, store);
var network = _BtcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
var nodeInfo =
await _LightningLikePaymentHandler.GetNodeInfo(paymentMethodDetails, network, new InvoiceLogs());
await _LightningLikePaymentHandler.GetNodeInfo(paymentMethodDetails, network, new InvoiceLogs(), throws: true);
return View(new ShowLightningNodeInfoViewModel
{
Available = nodeInfo.Any(),
Available = true,
NodeInfo = nodeInfo.Select(n => new ShowLightningNodeInfoViewModel.NodeData(n)).ToArray(),
CryptoCode = cryptoCode,
CryptoImage = GetImage(paymentMethodDetails.PaymentId, network),

View File

@ -83,11 +83,11 @@ namespace BTCPayServer.Controllers
[HttpPost("server/plugins/install")]
public async Task<IActionResult> InstallPlugin(
[FromServices] PluginService pluginService, string plugin, bool update = false, string path ="")
[FromServices] PluginService pluginService, string plugin, bool update = false, string version = null)
{
try
{
await pluginService.DownloadRemotePlugin(plugin, path);
await pluginService.DownloadRemotePlugin(plugin, version);
if (update)
{
pluginService.UpdatePlugin(plugin);

View File

@ -33,6 +33,7 @@ namespace BTCPayServer.Controllers
StoreId = CurrentStore.Id,
StoreName = CurrentStore.StoreName,
CryptoCode = cryptoCode,
Network = _NetworkProvider.DefaultNetwork,
IsSetUp = walletEnabled || lightningEnabled
};

View File

@ -160,7 +160,16 @@ namespace BTCPayServer.Controllers
{
CryptoCode = paymentMethodId.CryptoCode
};
paymentMethod.SetLightningUrl(connectionString);
try
{
paymentMethod.SetLightningUrl(connectionString);
}
catch (Exception ex)
{
ModelState.AddModelError(nameof(vm.ConnectionString), ex.Message);
return View(vm);
}
}
switch (command)
@ -185,12 +194,15 @@ namespace BTCPayServer.Controllers
try
{
var info = await handler.GetNodeInfo(paymentMethod, network, new InvoiceLogs(), Request.IsOnion(), true);
if (!vm.SkipPortTest)
var hasPublicAddress = info.Any();
if (!vm.SkipPortTest && hasPublicAddress)
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20));
await handler.TestConnection(info.First(), cts.Token);
}
TempData[WellKnownTempData.SuccessMessage] = $"Connection to the Lightning node successful. Your node address: {info.First()}";
TempData[WellKnownTempData.SuccessMessage] = "Connection to the Lightning node successful" + (hasPublicAddress
? $". Your node address: {info.First()}"
: ", but no public address has been configured");
}
catch (Exception ex)
{
@ -216,7 +228,7 @@ namespace BTCPayServer.Controllers
var lightning = GetExistingLightningSupportedPaymentMethod(cryptoCode, store);
if (lightning == null)
{
TempData[WellKnownTempData.ErrorMessage] = $"You need to connect to a Lightning node before adjusting its settings.";
TempData[WellKnownTempData.ErrorMessage] = "You need to connect to a Lightning node before adjusting its settings.";
return RedirectToAction(nameof(SetupLightningNode), new { storeId, cryptoCode });
}

View File

@ -119,17 +119,16 @@ namespace BTCPayServer.Controllers
accountSettings.AccountKeyPath =
vm.KeyPath == null ? null : KeyPath.Parse(vm.KeyPath);
accountSettings.RootFingerprint = string.IsNullOrEmpty(vm.RootFingerprint)
? (HDFingerprint?)null
: new HDFingerprint(
NBitcoin.DataEncoders.Encoders.Hex.DecodeData(vm.RootFingerprint));
? null
: new HDFingerprint(Encoders.Hex.DecodeData(vm.RootFingerprint));
}
}
vm.DerivationScheme = strategy.AccountDerivation.ToString();
ModelState.Remove(nameof(vm.DerivationScheme));
}
catch
catch (Exception ex)
{
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid wallet format");
ModelState.AddModelError(nameof(vm.DerivationScheme), $"Invalid wallet format: {ex.Message}");
return View(vm.ViewName, vm);
}
}

View File

@ -3,9 +3,11 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client;
using BTCPayServer.Configuration;
@ -59,6 +61,7 @@ namespace BTCPayServer.Controllers
IAuthorizationService authorizationService,
EventAggregator eventAggregator,
AppService appService,
IFileService fileService,
WebhookSender webhookNotificationManager,
IDataProtectionProvider dataProtector,
IOptions<ExternalServicesOptions> externalServiceOptions)
@ -74,6 +77,7 @@ namespace BTCPayServer.Controllers
_policiesSettings = policiesSettings;
_authorizationService = authorizationService;
_appService = appService;
_fileService = fileService;
DataProtector = dataProtector.CreateProtector("ConfigProtector");
WebhookNotificationManager = webhookNotificationManager;
_EventAggregator = eventAggregator;
@ -101,9 +105,14 @@ namespace BTCPayServer.Controllers
private readonly PoliciesSettings _policiesSettings;
private readonly IAuthorizationService _authorizationService;
private readonly AppService _appService;
private readonly IFileService _fileService;
private readonly EventAggregator _EventAggregator;
private readonly IOptions<ExternalServicesOptions> _externalServiceOptions;
public string? GeneratedPairingCode { get; set; }
public WebhookSender WebhookNotificationManager { get; }
public IDataProtector DataProtector { get; }
[TempData]
public bool StoreNotConfigured
{
@ -356,20 +365,11 @@ namespace BTCPayServer.Controllers
.Where(s => s.PaymentId.PaymentType != PaymentTypes.LNURLPay)
.Select(method =>
{
var existing =
storeBlob.PaymentMethodCriteria.SingleOrDefault(criteria =>
var existing = storeBlob.PaymentMethodCriteria.SingleOrDefault(criteria =>
criteria.PaymentMethod == method.PaymentId);
if (existing is null)
{
return new PaymentMethodCriteriaViewModel()
{
PaymentMethod = method.PaymentId.ToString(),
Value = ""
};
}
else
{
return new PaymentMethodCriteriaViewModel()
return existing is null
? new PaymentMethodCriteriaViewModel { PaymentMethod = method.PaymentId.ToString(), Value = "" }
: new PaymentMethodCriteriaViewModel
{
PaymentMethod = existing.PaymentMethod.ToString(),
Type = existing.Above
@ -377,9 +377,11 @@ namespace BTCPayServer.Controllers
: PaymentMethodCriteriaViewModel.CriteriaType.LessThan,
Value = existing.Value?.ToString() ?? ""
};
}
}).ToList();
vm.UseNewCheckout = storeBlob.CheckoutType == Client.Models.CheckoutType.V2;
vm.CheckoutFormId = storeBlob.CheckoutFormId;
vm.OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback;
vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail;
vm.LazyPaymentMethods = storeBlob.LazyPaymentMethods;
vm.RedirectAutomatically = storeBlob.RedirectAutomatically;
@ -495,6 +497,14 @@ namespace BTCPayServer.Controllers
PaymentMethod = paymentMethodId
});
}
blob.CheckoutType = model.UseNewCheckout ? Client.Models.CheckoutType.V2 : Client.Models.CheckoutType.V1;
if (blob.CheckoutType == Client.Models.CheckoutType.V2)
{
blob.CheckoutFormId = model.CheckoutFormId;
blob.OnChainWithLnInvoiceFallback = model.OnChainWithLnInvoiceFallback;
}
blob.RequiresRefundEmail = model.RequiresRefundEmail;
blob.LazyPaymentMethods = model.LazyPaymentMethods;
blob.RedirectAutomatically = model.RedirectAutomatically;
@ -592,6 +602,8 @@ namespace BTCPayServer.Controllers
Id = store.Id,
StoreName = store.StoreName,
StoreWebsite = store.StoreWebsite,
LogoFileId = storeBlob.LogoFileId,
BrandColor = storeBlob.BrandColor,
NetworkFeeMode = storeBlob.NetworkFeeMode,
AnyoneCanCreateInvoice = storeBlob.AnyoneCanInvoice,
PaymentTolerance = storeBlob.PaymentTolerance,
@ -619,7 +631,7 @@ namespace BTCPayServer.Controllers
needUpdate = true;
CurrentStore.StoreWebsite = model.StoreWebsite;
}
var blob = CurrentStore.GetStoreBlob();
blob.AnyoneCanInvoice = model.AnyoneCanCreateInvoice;
blob.NetworkFeeMode = model.NetworkFeeMode;
@ -627,6 +639,43 @@ namespace BTCPayServer.Controllers
blob.DefaultCurrency = model.DefaultCurrency;
blob.InvoiceExpiration = TimeSpan.FromMinutes(model.InvoiceExpiration);
blob.RefundBOLT11Expiration = TimeSpan.FromDays(model.BOLT11Expiration);
if (!string.IsNullOrEmpty(model.BrandColor) && !ColorPalette.IsValid(model.BrandColor))
{
ModelState.AddModelError(nameof(model.BrandColor), "Invalid color");
return View(model);
}
blob.BrandColor = model.BrandColor;
if (model.LogoFile != null)
{
if (model.LogoFile.ContentType.StartsWith("image/", StringComparison.InvariantCulture))
{
var userId = GetUserId();
if (userId is null)
return NotFound();
// delete existing image
if (!string.IsNullOrEmpty(blob.LogoFileId))
{
await _fileService.RemoveFile(blob.LogoFileId, userId);
}
// add new image
try
{
var storedFile = await _fileService.AddFile(model.LogoFile, userId);
blob.LogoFileId = storedFile.Id;
}
catch (Exception e)
{
TempData[WellKnownTempData.ErrorMessage] = $"Could not save logo: {e.Message}";
}
}
else
{
TempData[WellKnownTempData.ErrorMessage] = "The uploaded logo file needs to be an image";
}
}
if (CurrentStore.SetStoreBlob(blob))
{
@ -672,10 +721,10 @@ namespace BTCPayServer.Controllers
private DerivationSchemeSettings ParseDerivationStrategy(string derivationScheme, BTCPayNetwork network)
{
var parser = new DerivationSchemeParser(network);
try
var isOD = Regex.Match(derivationScheme, @"\(.*?\)");
if (isOD.Success)
{
var derivationSchemeSettings = new DerivationSchemeSettings();
derivationSchemeSettings.Network = network;
var derivationSchemeSettings = new DerivationSchemeSettings { Network = network };
var result = parser.ParseOutputDescriptor(derivationScheme);
derivationSchemeSettings.AccountOriginal = derivationScheme.Trim();
derivationSchemeSettings.AccountDerivation = result.Item1;
@ -687,16 +736,12 @@ namespace BTCPayServer.Controllers
}).ToArray() ?? new AccountKeySettings[result.Item1.GetExtPubKeys().Count()];
return derivationSchemeSettings;
}
catch (Exception)
{
// ignored
}
return new DerivationSchemeSettings(parser.Parse(derivationScheme), network);
var strategy = parser.Parse(derivationScheme);
return new DerivationSchemeSettings(strategy, network);
}
[HttpGet]
[Route("{storeId}/Tokens")]
[HttpGet("{storeId}/tokens")]
public async Task<IActionResult> ListTokens()
{
var model = new TokensViewModel();
@ -739,8 +784,7 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(ListTokens), new { storeId = token?.StoreId });
}
[HttpGet]
[Route("{storeId}/tokens/{tokenId}")]
[HttpGet("{storeId}/tokens/{tokenId}")]
public async Task<IActionResult> ShowToken(string tokenId)
{
var token = await _TokenRepository.GetToken(tokenId);
@ -749,8 +793,18 @@ namespace BTCPayServer.Controllers
return View(token);
}
[HttpPost]
[Route("{storeId}/Tokens/Create")]
[HttpGet("{storeId}/tokens/create")]
public IActionResult CreateToken(string storeId)
{
var model = new CreateTokenViewModel();
ViewBag.HidePublicKey = storeId == null;
ViewBag.ShowStores = storeId == null;
ViewBag.ShowMenu = storeId != null;
model.StoreId = storeId;
return View(model);
}
[HttpPost("{storeId}/tokens/create")]
public async Task<IActionResult> CreateToken(string storeId, CreateTokenViewModel model)
{
if (!ModelState.IsValid)
@ -791,29 +845,12 @@ namespace BTCPayServer.Controllers
GeneratedPairingCode = pairingCode;
return RedirectToAction(nameof(RequestPairing), new
{
pairingCode = pairingCode,
pairingCode,
selectedStore = storeId
});
}
public string? GeneratedPairingCode { get; set; }
public WebhookSender WebhookNotificationManager { get; }
public IDataProtector DataProtector { get; }
[HttpGet]
[Route("{storeId}/Tokens/Create")]
public IActionResult CreateToken(string storeId)
{
var model = new CreateTokenViewModel();
ViewBag.HidePublicKey = storeId == null;
ViewBag.ShowStores = storeId == null;
ViewBag.ShowMenu = storeId != null;
model.StoreId = storeId;
return View(model);
}
[HttpGet]
[Route("/api-tokens")]
[HttpGet("/api-tokens")]
[AllowAnonymous]
public async Task<IActionResult> CreateToken()
{
@ -834,16 +871,14 @@ namespace BTCPayServer.Controllers
return View(model);
}
[HttpPost]
[Route("/api-tokens")]
[HttpPost("/api-tokens")]
[AllowAnonymous]
public Task<IActionResult> CreateToken2(CreateTokenViewModel model)
{
return CreateToken(model.StoreId, model);
}
[HttpPost]
[Route("{storeId}/tokens/apikey")]
[HttpPost("{storeId}/tokens/apikey")]
public async Task<IActionResult> GenerateAPIKey(string storeId, string command = "")
{
var store = HttpContext.GetStoreData();
@ -866,50 +901,48 @@ namespace BTCPayServer.Controllers
});
}
[HttpGet]
[Route("/api-access-request")]
[HttpGet("/api-access-request")]
[AllowAnonymous]
public async Task<IActionResult> RequestPairing(string pairingCode, string? selectedStore = null)
{
var userId = GetUserId();
if (userId == null)
return Challenge(AuthenticationSchemes.Cookie);
if (pairingCode == null)
return NotFound();
if (selectedStore != null)
{
var store = await _Repo.FindStore(selectedStore, userId);
if (store == null)
return NotFound();
HttpContext.SetStoreData(store);
ViewBag.ShowStores = false;
}
var pairing = await _TokenRepository.GetPairingAsync(pairingCode);
if (pairing == null)
{
TempData[WellKnownTempData.ErrorMessage] = "Unknown pairing code";
return RedirectToAction(nameof(UIHomeController.Index), "UIHome");
}
else
var stores = await _Repo.GetStoresByUserId(userId);
return View(new PairingModel
{
var stores = await _Repo.GetStoresByUserId(userId);
return View(new PairingModel
Id = pairing.Id,
Label = pairing.Label,
SIN = pairing.SIN ?? "Server-Initiated Pairing",
StoreId = selectedStore ?? stores.FirstOrDefault()?.Id,
Stores = stores.Where(u => u.Role == StoreRoles.Owner).Select(s => new PairingModel.StoreViewModel
{
Id = pairing.Id,
Label = pairing.Label,
SIN = pairing.SIN ?? "Server-Initiated Pairing",
StoreId = selectedStore ?? stores.FirstOrDefault()?.Id,
Stores = stores.Where(u => u.Role == StoreRoles.Owner).Select(s => new PairingModel.StoreViewModel()
{
Id = s.Id,
Name = string.IsNullOrEmpty(s.StoreName) ? s.Id : s.StoreName
}).ToArray()
});
}
Id = s.Id,
Name = string.IsNullOrEmpty(s.StoreName) ? s.Id : s.StoreName
}).ToArray()
});
}
[HttpPost]
[Route("/api-access-request")]
[HttpPost("/api-access-request")]
public async Task<IActionResult> Pair(string pairingCode, string storeId)
{
if (pairingCode == null)

View File

@ -460,7 +460,7 @@ namespace BTCPayServer.Controllers
vm.SigningContext.OriginalPSBT = psbt.ToBase64();
proposedPayjoin.Finalize();
var hash = proposedPayjoin.ExtractTransaction().GetHash();
_EventAggregator.Publish(new UpdateTransactionLabel(walletId, hash, UpdateTransactionLabel.PayjoinLabelTemplate()));
await WalletRepository.AddWalletTransactionAttachment(walletId, hash, Attachment.Payjoin());
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Success,

View File

@ -39,6 +39,8 @@ using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
using Newtonsoft.Json;
using StoreData = BTCPayServer.Data.StoreData;
using Microsoft.AspNetCore.Routing;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Controllers
{
@ -60,11 +62,10 @@ namespace BTCPayServer.Controllers
private readonly IFeeProviderFactory _feeRateProvider;
private readonly BTCPayWalletProvider _walletProvider;
private readonly WalletReceiveService _walletReceiveService;
private readonly EventAggregator _EventAggregator;
private readonly SettingsRepository _settingsRepository;
private readonly DelayedTransactionBroadcaster _broadcaster;
private readonly PayjoinClient _payjoinClient;
private readonly LabelFactory _labelFactory;
private readonly LinkGenerator _linkGenerator;
private readonly PullPaymentHostedService _pullPaymentHostedService;
private readonly UTXOLocker _utxoLocker;
private readonly WalletHistogramService _walletHistogramService;
@ -84,16 +85,16 @@ namespace BTCPayServer.Controllers
IFeeProviderFactory feeRateProvider,
BTCPayWalletProvider walletProvider,
WalletReceiveService walletReceiveService,
EventAggregator eventAggregator,
SettingsRepository settingsRepository,
DelayedTransactionBroadcaster broadcaster,
PayjoinClient payjoinClient,
LabelFactory labelFactory,
IServiceProvider serviceProvider,
PullPaymentHostedService pullPaymentHostedService,
UTXOLocker utxoLocker)
UTXOLocker utxoLocker,
LinkGenerator linkGenerator)
{
_currencyTable = currencyTable;
_linkGenerator = linkGenerator;
Repository = repo;
WalletRepository = walletRepository;
RateFetcher = rateProvider;
@ -105,11 +106,9 @@ namespace BTCPayServer.Controllers
_feeRateProvider = feeRateProvider;
_walletProvider = walletProvider;
_walletReceiveService = walletReceiveService;
_EventAggregator = eventAggregator;
_settingsRepository = settingsRepository;
_broadcaster = broadcaster;
_payjoinClient = payjoinClient;
_labelFactory = labelFactory;
_pullPaymentHostedService = pullPaymentHostedService;
_utxoLocker = utxoLocker;
ServiceProvider = serviceProvider;
@ -146,58 +145,19 @@ namespace BTCPayServer.Controllers
if (paymentMethod == null)
return NotFound();
var walletBlobInfoAsync = WalletRepository.GetWalletInfo(walletId);
var walletTransactionsInfoAsync = WalletRepository.GetWalletTransactionsInfo(walletId);
var txObjId = new WalletObjectId(walletId, WalletObjectData.Types.Tx, transactionId);
var wallet = _walletProvider.GetWallet(paymentMethod.Network);
var walletBlobInfo = await walletBlobInfoAsync;
var walletTransactionsInfo = await walletTransactionsInfoAsync;
if (addlabel != null)
{
if (!walletTransactionsInfo.TryGetValue(transactionId, out var walletTransactionInfo))
{
walletTransactionInfo = new WalletTransactionInfo();
}
var rawLabel = await _labelFactory.BuildLabel(
walletBlobInfo,
Request!,
walletTransactionInfo,
walletId,
transactionId,
addlabel
);
if (walletTransactionInfo.Labels.TryAdd(rawLabel.Text, rawLabel))
{
await WalletRepository.SetWalletTransactionInfo(walletId, transactionId, walletTransactionInfo);
}
await WalletRepository.AddWalletObjectLabels(txObjId, addlabel);
}
else if (removelabel != null)
{
removelabel = removelabel.Trim();
if (walletTransactionsInfo.TryGetValue(transactionId, out var walletTransactionInfo))
{
if (walletTransactionInfo.Labels.Remove(removelabel))
{
var canDeleteColor =
!walletTransactionsInfo.Any(txi => txi.Value.Labels.ContainsKey(removelabel));
if (canDeleteColor)
{
walletBlobInfo.LabelColors.Remove(removelabel);
await WalletRepository.SetWalletInfo(walletId, walletBlobInfo);
}
await WalletRepository.SetWalletTransactionInfo(walletId, transactionId, walletTransactionInfo);
}
}
await WalletRepository.RemoveWalletObjectLabels(txObjId, removelabel);
}
else if (addcomment != null)
{
addcomment = addcomment.Trim().Truncate(WalletTransactionDataExtensions.MaxCommentSize);
if (!walletTransactionsInfo.TryGetValue(transactionId, out var walletTransactionInfo))
{
walletTransactionInfo = new WalletTransactionInfo();
}
walletTransactionInfo.Comment = addcomment;
await WalletRepository.SetWalletTransactionInfo(walletId, transactionId, walletTransactionInfo);
await WalletRepository.SetWalletObjectComment(txObjId, addcomment);
}
return RedirectToAction(nameof(WalletTransactions), new { walletId = walletId.ToString() });
}
@ -267,15 +227,17 @@ namespace BTCPayServer.Controllers
return NotFound();
var wallet = _walletProvider.GetWallet(paymentMethod.Network);
var walletBlobAsync = WalletRepository.GetWalletInfo(walletId);
var walletTransactionsInfoAsync = WalletRepository.GetWalletTransactionsInfo(walletId);
// We can't filter at the database level if we need to apply label filter
var preFiltering = string.IsNullOrEmpty(labelFilter);
var transactions = await wallet.FetchTransactionHistory(paymentMethod.AccountDerivation, preFiltering ? skip : null, preFiltering ? count : null);
var walletBlob = await walletBlobAsync;
var walletTransactionsInfo = await walletTransactionsInfoAsync;
var walletTransactionsInfo = await WalletRepository.GetWalletTransactionsInfo(walletId, transactions.Select(t => t.TransactionId.ToString()).ToArray());
var model = new ListTransactionsViewModel { Skip = skip, Count = count };
model.Labels.AddRange(
(await WalletRepository.GetWalletLabels(walletId))
.Select(c => (c.Label, c.Color, ColorPalette.Default.TextColor(c.Color)))
);
if (labelFilter != null)
{
model.PaginationQuery = new Dictionary<string, object> { { "labelFilter", labelFilter } };
@ -305,25 +267,30 @@ namespace BTCPayServer.Controllers
if (walletTransactionsInfo.TryGetValue(tx.TransactionId.ToString(), out var transactionInfo))
{
var labels = _labelFactory.ColorizeTransactionLabels(walletBlob, transactionInfo, Request);
vm.Labels.AddRange(labels);
model.Labels.AddRange(labels);
var labels = CreateTransactionTagModels(transactionInfo);
vm.Tags.AddRange(labels);
vm.Comment = transactionInfo.Comment;
}
if (labelFilter == null ||
vm.Labels.Any(l => l.Text.Equals(labelFilter, StringComparison.OrdinalIgnoreCase)))
vm.Tags.Any(l => l.Text.Equals(labelFilter, StringComparison.OrdinalIgnoreCase)))
model.Transactions.Add(vm);
}
model.Total = preFiltering ? null : model.Transactions.Count;
model.Transactions = model.Transactions.Skip(skip).Take(count)
.ToList();
// if we couldn't filter at the db level, we need to apply skip and count
if (!preFiltering)
{
model.Transactions = model.Transactions.Skip(skip).Take(count).ToList();
}
}
model.CryptoCode = walletId.CryptoCode;
return View(model);
//If ajax call then load the partial view
return Request.Headers["X-Requested-With"] == "XMLHttpRequest"
? PartialView("_WalletTransactionsList", model)
: View(model);
}
[HttpGet("{walletId}/histogram/{type}")]
@ -607,17 +574,15 @@ namespace BTCPayServer.Controllers
var schemeSettings = GetDerivationSchemeSettings(walletId);
if (schemeSettings is null)
return NotFound();
var walletBlobAsync = await WalletRepository.GetWalletInfo(walletId);
var walletTransactionsInfoAsync = await WalletRepository.GetWalletTransactionsInfo(walletId);
var utxos = await _walletProvider.GetWallet(network)
.GetUnspentCoins(schemeSettings.AccountDerivation, false, cancellation);
var walletTransactionsInfoAsync = await this.WalletRepository.GetWalletTransactionsInfo(walletId, utxos.Select(u => u.OutPoint.Hash.ToString()).Distinct().ToArray());
vm.InputsAvailable = utxos.Select(coin =>
{
walletTransactionsInfoAsync.TryGetValue(coin.OutPoint.Hash.ToString(), out var info);
var labels = info?.Labels == null
? new List<ColoredLabel>()
: _labelFactory.ColorizeTransactionLabels(walletBlobAsync, info, Request).ToList();
var labels = CreateTransactionTagModels(info).ToList();
return new WalletSendModel.InputSelectionOption()
{
Outpoint = coin.OutPoint.ToString(),
@ -1353,6 +1318,117 @@ namespace BTCPayServer.Controllers
private string GetUserId() => _userManager.GetUserId(User);
private StoreData GetCurrentStore() => HttpContext.GetStoreData();
public IEnumerable<TransactionTagModel> CreateTransactionTagModels(WalletTransactionInfo? transactionInfo)
{
if (transactionInfo is null)
return Array.Empty<TransactionTagModel>();
string PayoutTooltip(IGrouping<string, string>? payoutsByPullPaymentId = null)
{
if (payoutsByPullPaymentId is null)
{
return "Paid a payout";
}
else if (payoutsByPullPaymentId.Count() == 1)
{
var pp = payoutsByPullPaymentId.Key;
var payout = payoutsByPullPaymentId.First();
if (!string.IsNullOrEmpty(pp))
return $"Paid a payout ({payout}) of a pull payment ({pp})";
else
return $"Paid a payout {payout}";
}
else
{
var pp = payoutsByPullPaymentId.Key;
if (!string.IsNullOrEmpty(pp))
return $"Paid {payoutsByPullPaymentId.Count()} payouts of a pull payment ({pp})";
else
return $"Paid {payoutsByPullPaymentId.Count()} payouts";
}
}
var models = new Dictionary<string, TransactionTagModel>();
foreach (var tag in transactionInfo.Attachments)
{
if (models.ContainsKey(tag.Type))
continue;
if (!transactionInfo.LabelColors.TryGetValue(tag.Type, out var color))
continue;
var model = new TransactionTagModel
{
Text = tag.Type,
Color = color,
TextColor = ColorPalette.Default.TextColor(color)
};
models.Add(tag.Type, model);
if (tag.Type == "payout")
{
var payoutsByPullPaymentId =
transactionInfo.Attachments.Where(t => t.Type == "payout")
.GroupBy(t => t.Data?["pullPaymentId"]?.Value<string>() ?? "",
k => k.Id).ToList();
model.Tooltip = payoutsByPullPaymentId.Count switch
{
0 => PayoutTooltip(),
1 => PayoutTooltip(payoutsByPullPaymentId.First()),
_ =>
$"<ul>{string.Join(string.Empty, payoutsByPullPaymentId.Select(pair => $"<li>{PayoutTooltip(pair)}</li>"))}</ul>"
};
model.Link = _linkGenerator.PayoutLink(transactionInfo.WalletId.ToString(), null, PayoutState.Completed, Request.Scheme, Request.Host,
Request.PathBase);
}
else if (tag.Type == "payjoin")
{
model.Tooltip = $"This UTXO was part of a PayJoin transaction.";
}
else if (tag.Type == "invoice")
{
model.Tooltip = $"Received through an invoice {tag.Id}";
model.Link = string.IsNullOrEmpty(tag.Id)
? null
: _linkGenerator.InvoiceLink(tag.Id, Request.Scheme, Request.Host, Request.PathBase);
}
else if (tag.Type == "payment-request")
{
model.Tooltip = $"Received through a payment request {tag.Id}";
model.Link = _linkGenerator.PaymentRequestLink(tag.Id, Request.Scheme, Request.Host, Request.PathBase);
}
else if (tag.Type == "app")
{
model.Tooltip = $"Received through an app {tag.Id}";
model.Link = _linkGenerator.AppLink(tag.Id, Request.Scheme, Request.Host, Request.PathBase);
}
else if (tag.Type == "pj-exposed")
{
if (tag.Id.Length != 0)
{
model.Tooltip = $"This UTXO was exposed through a PayJoin proposal for an invoice ({tag.Id})";
model.Link = _linkGenerator.InvoiceLink(tag.Id, Request.Scheme, Request.Host, Request.PathBase);
}
else
{
model.Tooltip = $"This UTXO was exposed through a PayJoin proposal";
}
}
else if (tag.Type == "payjoin")
{
model.Tooltip = $"This UTXO was part of a PayJoin transaction.";
}
}
foreach (var label in transactionInfo.LabelColors)
models.TryAdd(label.Key, new TransactionTagModel
{
Text = label.Key,
Color = label.Value,
TextColor = ColorPalette.Default.TextColor(label.Value)
});
return models.Values.OrderBy(v => v.Text);
}
}
public class WalletReceiveViewModel

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