Compare commits

...

239 Commits

Author SHA1 Message Date
71c2bfffc1 Remove untested and unfinished code 2022-11-25 10:02:01 +09:00
4b952648a4 Refactoring: Use PostRedirect instead of TempData for data transfer 2022-11-25 00:24:02 +01:00
8b7b772d34 Add index - needs migration 2022-11-24 19:54:55 +01:00
713b87d07b Update test 2022-11-24 19:43:28 +01:00
82a690645e More minor syntax cleanups 2022-11-24 19:33:43 +01:00
dcade8b4a9 Remove custom form options from select for now 2022-11-24 19:09:41 +01:00
ad17234a86 Minor cleanups 2022-11-24 19:03:00 +01:00
7218c3077d Hide custom form feature in UI 2022-11-24 17:14:23 +01:00
3e1511c0a8 Merge branch 'master' into form_builder 2022-11-24 17:03:47 +01:00
bb60c2ac48 Checkout v2: Minor fixes (#4345)
* Do not show remaining amount for topup invoices in expiry message

As [reported by @petzsch](https://chat.btcpayserver.org/btcpayserver/pl/gg1zy8t5h3dq7nme1nom93migo).

* Fix links on result page in Checkout Classic

Closes #4344.

* Better way to exclude Lightning if BIP21 is active and we have both PMs


Unify margins
2022-11-24 23:14:56 +09:00
3cb7c64321 Remove useless storeId parameter 2022-11-24 21:36:24 +09:00
9c88c53798 Remove storeId from step form 2022-11-24 21:15:55 +09:00
50d08de78b Fix modify 2022-11-24 21:15:27 +09:00
15ab7c051b Fix query to forms, ensure no permission bypass 2022-11-24 20:34:05 +09:00
4e8126a734 Fix ef request 2022-11-24 18:07:08 +09:00
8195acbbf7 Fix warnings 2022-11-24 17:53:43 +09:00
ec35b75324 Put the Authorize at controller level on UIForms 2022-11-24 17:48:21 +09:00
d65835b0fe Refactor FormQuery to only be able to query single store and single form 2022-11-24 17:42:55 +09:00
426a65d3fe Merge remote-tracking branch 'origin/master' into form_builder
# Conflicts:
#	BTCPayServer/Controllers/UIInvoiceController.UI.cs
#	BTCPayServer/Models/StoreViewModels/CheckoutAppearanceViewModel.cs
#	BTCPayServer/Views/UIInvoice/CheckoutV2.cshtml
#	BTCPayServer/Views/UIStores/CheckoutAppearance.cshtml
2022-11-24 07:23:32 +01:00
a4ee1e9805 Checkout v2 finetuning (#4276)
* Indent all JSON files with two spaces

* Upgrade Vue.js

* Cheat mode improvements

* Show payment details in case of expired invoice

* Add logo size recommendation

* Show clipboard copy hint cursor

* Improve info area and wording

* Update BIP21 wording

* Invoice details adjustments

* Remove form; switch payment methods via AJAX

* UI updates

* Decrease paddings to gain space

* Tighten up padding between logo mark and the store title text

* Add drop-shadow to the containers

* Wording

* Cheating improvements

* Improve footer spacing

* Cheating improvements

* Display addresses

* More improvements

* Expire invoices

* Customize invoice expiry

* Footer improvements

* Remove theme switch

* Remove non-existing sourcemap references

* Move inline JS to checkout.js file

* Plugin compatibility

See Kukks/btcpayserver#8

* Test fix

* Upgrade vue-i18next

* Extract translations into a separate file

* Round QR code borders

* Remove "Pay with Bitcoin" title in BIP21 case

* Add copy hint to payment details

* Cheating: Reduce margins

* Adjust dt color

* Hide addresses for first iteration

* Improve View Details button

* Make info section collapsible

* Revert original en locale file

* Checkout v2 tests

* Result view link fixes

* Fix BIP21 + lazy payment methods case

* More result page link improvements

* minor visual improvements

* Update clipboard code

Remove fallback for old browsers. https://caniuse.com/?search=navigator.clipboard

* Transition copy symbol

* Update info text color

* Invert dark neutral colors

Simplifies the dark theme quite a bit.

* copy adjustments

* updates QR border-radius

* Add option to remove logo

* More checkout v2 test cases

* JS improvements

* Remove leftovers

* Update test

* Fix links

* Update tests

* Update plugins integration

* Remove obsolete url code

* Minor view update

* Update JS to not use arrow functions

* Remove FormId from Checkout Appearance settings

* Add English-only hint and feedback link

* Checkout Appearance: Make options clearer, remove Custom CSS for v2

* Clipboard copy full URL instead of just address/BOLT11

* Upgrade JS libs, add content checks

* Add test for BIP21 setting with zero amount invoice

Co-authored-by: dstrukt <gfxdsign@gmail.com>
2022-11-24 08:53:32 +09:00
2fe2efcb7a make pay request form submission redirect to invoice 2022-11-23 21:53:08 +01:00
bf0a8c1e62 Fix flaky test ln payout (#4334) 2022-11-23 21:02:47 +09:00
f671f8fd26 fix 2022-11-23 07:43:39 +01:00
e0f15c8792 fix migration for forms 2022-11-22 22:58:37 +01:00
4e828f4398 Merge remote-tracking branch 'origin/master' into form_builder
# Conflicts:
#	BTCPayServer/Services/BTCPayServerEnvironment.cs
2022-11-22 22:52:44 +01:00
399fbe2827 add form test 2022-11-22 22:47:01 +01:00
6f547750ae Remove invoice and store level form 2022-11-22 22:04:50 +01:00
f0f09180d5 Fix flaky test (#4330)
* Fix flaky test

* Update BTCPayServer/PayoutProcessors/BaseAutomatedPayoutProcessor.cs

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

Co-authored-by: d11n <mail@dennisreimann.de>
2022-11-22 22:04:34 +01:00
95c6817d2a Add documentation link to plugins (#4329)
* Add documentation link to plugins

* Minor UI updates

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2022-11-22 22:04:34 +01:00
bb9c0989c6 POS: Fix null pointer
Introduced in #4307, the referenced object needs to be `itemChoice` instead of `choice`.
2022-11-22 22:04:34 +01:00
fa2a3dd7f3 Change confirmed to settled. (#4328) 2022-11-22 22:04:34 +01:00
86361d0f75 Server Settings: Update Policies page (#4326)
Handles the multiple submit buttons on that page and closes #4319.

Contains some UI unifications with other pages and also shows the block explorers without needing to toggle the section via JS.
2022-11-22 22:04:33 +01:00
a056e9b570 fix monero issue 2022-11-22 22:04:33 +01:00
b89918ff5d Fix: If reverse proxy wasn't well configured, and error message should have been displayed (#4322) 2022-11-22 22:04:33 +01:00
37693399d6 Fix warnings in Form builder (#4331)
* Fix build warnings about string?

Enable nullable on UIFormsController.cs
Fixes CS8632 The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

* Clean up lack of space in injected services in Submit() of UIFormsController.cs

* Remove unused variables (CS0219) and assignment of nullable value to nullable type (CS8600)

* Cleanup double semicolons while we're at tit
2022-11-22 20:26:25 +01:00
d2b10ef4e6 CanUseTorClient fail no more 2022-11-22 21:58:15 +09:00
9f3fca8fd7 Show the git commit of the current build of BTCPay (#4320)
* Show the git commit of the current build of BTCPay

* Fix build

Co-authored-by: d11n <mail@dennisreimann.de>
2022-11-22 21:37:07 +09:00
9404819dbe Fix flaky test (#4330)
* Fix flaky test

* Update BTCPayServer/PayoutProcessors/BaseAutomatedPayoutProcessor.cs

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

Co-authored-by: d11n <mail@dennisreimann.de>
2022-11-22 12:17:29 +01:00
d959f5096b Add documentation link to plugins (#4329)
* Add documentation link to plugins

* Minor UI updates

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2022-11-22 11:06:23 +01:00
bd3710a60f Merge pull request #4327 from btcpayserver/dennisreimann-patch-1 2022-11-22 06:58:10 +01:00
3d43f3a2b3 Change confirmed to settled. (#4328) 2022-11-22 11:10:21 +09:00
6194c156bd Server Settings: Update Policies page (#4326)
Handles the multiple submit buttons on that page and closes #4319.

Contains some UI unifications with other pages and also shows the block explorers without needing to toggle the section via JS.
2022-11-22 10:27:27 +09:00
bc652507d8 UI updates 2022-11-21 22:44:14 +01:00
850c26dc13 POS: Fix null pointer
Introduced in #4307, the referenced object needs to be `itemChoice` instead of `choice`.
2022-11-21 22:07:46 +01:00
eda0f7327e fix monero issue 2022-11-21 19:55:19 +01:00
bf597495ff Fix: If reverse proxy wasn't well configured, and error message should have been displayed (#4322) 2022-11-21 19:32:19 +01:00
d8094e9fee fix pav bug 2022-11-21 11:55:42 +01:00
33c92c1428 general fixes for form system 2022-11-21 11:51:46 +01:00
6747f91d29 move checkoutform id in checkout appearance outside of checkotu v2 toggle 2022-11-21 11:51:46 +01:00
469ff5b11f fix up code 2022-11-21 11:51:46 +01:00
b3bc4e5745 Do not request additional forms on invoice from pay request 2022-11-21 11:51:46 +01:00
44b0addff5 Display form name in inherit from store setting 2022-11-21 11:51:46 +01:00
383520c453 invoice form through receipt page 2022-11-21 11:51:45 +01:00
0c3b345d1b pay request form rough support 2022-11-21 11:51:45 +01:00
3513e602f4 Add support for pos app + forms 2022-11-21 11:51:45 +01:00
cec83ab4ec Make predefined forms usable statically 2022-11-21 11:51:45 +01:00
16537509d9 Update UIFormsController.cs 2022-11-21 11:51:45 +01:00
6bd6a045f4 UI updates 2022-11-21 11:51:45 +01:00
5de665dfff Cleanups 2022-11-21 11:51:45 +01:00
e329368e21 wip 2022-11-21 11:51:38 +01:00
20025f254c Use the plugin builder website instead of docker to fetch plugins (#4285) 2022-11-21 10:23:25 +09:00
ec76acd3a6 Code analysis (#4293)
* Enable NETAnalyzers for whole project

- remove obsolete analyzers so that the .NET Core SDK NETAnalyzers can be used
- enable NETAnalyzers for all projects so that developers can use them by defining the AnalysisMode on individual projects

This is because if we set AnalysisMode to minimal, recommended or all it would spam with warning.
The idea is to be able to turn them on during development to fix recommended stuff without polluting the build output.

Following commits will implement some of the Code Analysis findings

* Performance hints for using char overloads for single characters (CA1834 and CA1847)

CA1834: Use StringBuilder.Append(char) for single character strings
CA1847: Use string.Contains(char) instead of string.Contains(string) with single characters
2022-11-20 17:42:36 +09:00
1e2acfb296 Disable internal node options if no internal node configured (#4315) 2022-11-20 14:22:36 +09:00
2bd4a680ad Fix tests 2022-11-20 14:19:48 +09:00
4ce504a1e1 Add index to WalletObjects + allow additional queries 2022-11-19 23:39:41 +09:00
d2f071b8b2 Make store creation field same width (#4311)
* Make store creation field same width

Closes #4306.

* Update CreateStore.cshtml
2022-11-19 11:29:34 +01:00
fdbd7b977a Merge pull request #4312 from btcpayserver/non-admin-wallet-warning
Refine non-admin wallet warning copy
2022-11-19 09:44:22 +01:00
d19961b7a0 Refine non-admin wallet warning copy
As [discussed on Mattermost](https://chat.btcpayserver.org/btcpayserver/pl/us4kscqsw7rzmnng7aarxczd5r).
2022-11-18 22:12:53 +01:00
c156254600 Validate cart cost with explicit amount 2022-11-18 16:50:26 +01:00
9b5c6ece90 Refactor walletobj API, make wallet object graph directionless (#4297) 2022-11-19 00:04:46 +09:00
bf91efc756 Add Lightning Service Torq (#4296) 2022-11-18 12:19:01 +01:00
a253fd5001 Small doc improve 2022-11-18 14:22:59 +09:00
52af129c8b Add crowdfund app create endpoint (#4068)
* Add crowdfund app create endpoint

* replace DateTimeJsonConverter with NBitcoin.JsonConverters.DateTimeToUnixTimeConverter

* Use DateTimeOffset instead of DateTime

* Use array instead of CSV

* update "startDate" and "endDate" docs definition

* update docs
2022-11-18 14:20:07 +09:00
3942463ac9 UI: Unify payment request list with invoices (#4294)
Some quick win updates to the payment requests list that unify the display with the invoices list:

- Status is displayed as badge
- Amount is properly formatted
- Expiry date format and ability to switch to relative date
2022-11-18 13:24:57 +09:00
ff572eef7f Use constants rather than magic strings in transaction attachments 2022-11-17 10:24:49 +09:00
2740dfea87 Greenfield: Wallet Objects (#4274)
Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com>
2022-11-16 12:11:17 +09:00
324112b73b Receipts: Fix amount paid discrepancy (#4287)
Displays the actual amount paid instead of the total invoice price. Fixes #4242.
2022-11-16 11:35:49 +09:00
1d7dee8314 Fix NRE and do nto activate onchain if node unavailable even when lazy payments (#4291)
fixes #4289
2022-11-16 09:04:51 +09:00
2d23819944 Greenfield: Allow marking payout status and payment proofs (#4244)
This allows external services to integrate with the payouts system to process payouts. This is also  a step to allow plugins to provide payout processors.

* It provides the payment proof through the greenfield payoust api.
* It allows you to set the state of a payout outside of the usual flow:
  * When state is awaiting payment, allow setting to In progess or completed
  * When state is in progress, allow setting back to awaiting payment
2022-11-15 18:40:57 +09:00
17f3b4125b Add option to customize the instance logo (#4258)
* Add option to customize the instance logo


Custom logo for BTCPay instances

* Incorporate SVGUse helper
2022-11-14 22:29:23 +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
4aa94d5a13 bump btcpay to revert bug 2022-08-18 13:21:21 +02:00
b0253e11bf Revert "Allow bind and port for http too (#4031)"
This reverts commit a51c9d2b2dfd1b78ce99cb9c527f17a559660e43.
2022-08-18 13:12:29 +02:00
1a4a3714c7 Changelog for 1.6.8 2022-08-18 08:16:22 +02:00
999090dbdb Add Yadio exchange rate provider
Yadio is a service which provides exchange rates for many currencies such as the Lebanese Pound which have the real exchange rate which differs from the official rate significantly (32,525 LBP for USD real rate vs 1,510 official rate).

See more details here: https://yadio.io/info.html

See discussions here:
https://github.com/btcpayserver/btcpayserver/discussions/4001
https://github.com/btcpayserver/btcpayserver/discussions/2489#discussioncomment-3102370
2022-08-18 08:08:55 +02:00
13e3b515c9 Cleanse objects from obsolete altcoins 2022-08-17 21:23:16 +02:00
b6062a94b9 Explicitly disable fsize limit for some routes (#4045) 2022-08-17 10:18:30 +02:00
d0b26e9f69 Refactor Payouts (#4032)
Co-authored-by: d11n <mail@dennisreimann.de>
2022-08-17 09:45:51 +02:00
d6ae34929e Update launchSettings.json
Remove `BTCPAY_BTCEXTERNALLNDGRPC` because it is unsupported and not used anywhere in the app.

Use `http` for `BTCPAY_BTCEXTERNALLNDREST` because the HTTPS connection cannot be established.
2022-08-17 09:28:46 +02:00
a51c9d2b2d Allow bind and port for http too (#4031) 2022-08-17 09:11:13 +02:00
6c45ccc73d Add no rate found error message in Invoices (#4039) 2022-08-16 09:04:13 +02:00
6459c7bfad fix altcoin only dashboard crash
closes #4038
2022-08-15 20:22:31 +02:00
e6c1b0cf54 Merge pull request #4041 from dennisreimann/swagger-validation 2022-08-15 17:27:30 +02:00
c8558810ad Fix swagger validation errors
Combined the files with the same approach as in the docs:

`jq -rs 'reduce .[] as $item ({}; . * $item)' swagger.template.* > openapi.json`

Afterwards the bundled version can be validated using these commands:

`npx swagger-cli validate openapi.json && npx @redocly/cli lint openapi.json`
2022-08-15 15:59:26 +02:00
b0eb0b1de7 Merge pull request #4034 from dennisreimann/lnurl-controller-refactoring 2022-08-15 10:00:50 +02:00
6bdf31efda API docs: Fix duplicate speed policy schema
See the additional discussion in #3877. As we merged the Swagger JSON files for the docs, the duplicate store speed policy schema overwrote the updated invoice speed policy schema. This is now fixed by using a unified schema.
2022-08-15 06:20:10 +02:00
3cd2971cec Remove link to article that is no longer available 2022-08-14 11:19:25 +02:00
58784ebbfc Update ChromeDriver 2022-08-13 12:54:18 +02:00
de24a6e71b LNURL controller refactoring
By moving the `amount is null` check up, this prevents cases in which the `paymentMethodDetails.GeneratedBoltAmount != amount` check fails because of amount being null.
2022-08-12 20:10:44 +02:00
8c8a5a4f5e Edit view for Pull Payments (#4016)
Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2022-08-11 14:30:42 +02:00
7f41a1ef09 bump and changelog for 1.6.7 2022-08-10 10:07:15 +02:00
15e1169f62 Fix Kraken rate provider 2022-08-10 08:20:41 +02:00
de92677b69 Custodian Account Deposit UI (#4024)
Co-authored-by: d11n <mail@dennisreimann.de>
2022-08-09 20:03:55 +02:00
377ed95130 Upgrade Lightning library 2022-08-09 19:57:43 +02:00
1b1c7ad3b1 Bind app server to 0.0.0.0 to make it accessible on the local network
This way it can be accessed with other devices on the local network. It helps test with mobile devices for instance.
2022-08-08 11:15:24 +02:00
e4e0fb0f35 Ensure store button shows up correctly before store is created
fix #3972
2022-08-07 20:44:28 +02:00
bf0cbf24e4 bump and changelog for 1.6.6 2022-08-06 17:07:41 +02:00
7d454a4c7b make sure entity state gets modified in payout processors 2022-08-06 17:05:29 +02:00
680f1470cf Update app store links, add Authy as two factor app 2022-08-04 12:49:53 +09:00
1854fd307f Fix tests and warnigns 2022-08-04 12:42:15 +09:00
6239f9da75 Bump 2022-08-04 12:11:21 +09:00
40e39b82e8 Fix several potential NullReferenceException (Fix #4017) 2022-08-04 12:08:13 +09:00
c71e671311 Added custodian account trade support (#3978)
* Added custodian account trade support

* UI updates

* Improved UI spacing and field sizes + Fixed input validation

* Reset error message when opening trade modal

* Better error handing + test + surface error in trade modal in UI

* Add delete confirmation modal

* Fixed duplicate ID in site nav

* Replace jQuery.ajax with fetch for onTradeSubmit

* Added support for minimumTradeQty to trading pairs

* Fixed LocalBTCPayServerClient after previous refactoring

* Handling dust amounts + minor API change

* Replaced jQuery with Fetch API + UX improvements + more TODOs

* Moved namespace because Rider was unhappy

* Major UI improvements when swapping or changing assets, fixed bugs in min trade qty, fixed initial qty after an asset change etc

* Commented out code for easier debugging

* Fixed missing default values

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2022-08-04 11:38:49 +09:00
478 changed files with 16024 additions and 17248 deletions

View File

@ -41,9 +41,10 @@ jobs:
- run:
command: |
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
GIT_COMMIT=$(git rev-parse HEAD)
#
sudo docker build --pull -t $DOCKERHUB_REPO:$LATEST_TAG-amd64 -f amd64.Dockerfile .
sudo docker build --pull --build-arg CONFIGURATION_NAME=Altcoins-Release -t $DOCKERHUB_REPO:$LATEST_TAG-altcoins-amd64 -f amd64.Dockerfile .
sudo docker build --build-arg GIT_COMMIT=${GIT_COMMIT} --pull -t $DOCKERHUB_REPO:$LATEST_TAG-amd64 -f amd64.Dockerfile .
sudo docker build --build-arg GIT_COMMIT=${GIT_COMMIT} --pull --build-arg CONFIGURATION_NAME=Altcoins-Release -t $DOCKERHUB_REPO:$LATEST_TAG-altcoins-amd64 -f amd64.Dockerfile .
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-amd64
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-altcoins-amd64
@ -57,9 +58,10 @@ jobs:
command: |
sudo docker run --rm --privileged multiarch/qemu-user-static:register --reset
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
GIT_COMMIT=$(git rev-parse HEAD)
#
sudo docker build --pull -t $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 -f arm32v7.Dockerfile .
sudo docker build --pull --build-arg CONFIGURATION_NAME=Altcoins-Release -t $DOCKERHUB_REPO:$LATEST_TAG-altcoins-arm32v7 -f arm32v7.Dockerfile .
sudo docker build --build-arg GIT_COMMIT=${GIT_COMMIT} --pull -t $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 -f arm32v7.Dockerfile .
sudo docker build --build-arg GIT_COMMIT=${GIT_COMMIT} --pull --build-arg CONFIGURATION_NAME=Altcoins-Release -t $DOCKERHUB_REPO:$LATEST_TAG-altcoins-arm32v7 -f arm32v7.Dockerfile .
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-arm32v7
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-altcoins-arm32v7
@ -73,9 +75,10 @@ jobs:
command: |
sudo docker run --rm --privileged multiarch/qemu-user-static:register --reset
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
GIT_COMMIT=$(git rev-parse HEAD)
#
sudo docker build --pull -t $DOCKERHUB_REPO:$LATEST_TAG-arm64v8 -f arm64v8.Dockerfile .
sudo docker build --build-arg CONFIGURATION_NAME=Altcoins-Release --pull -t $DOCKERHUB_REPO:$LATEST_TAG-altcoins-arm64v8 -f arm64v8.Dockerfile .
sudo docker build --build-arg GIT_COMMIT=${GIT_COMMIT} --pull -t $DOCKERHUB_REPO:$LATEST_TAG-arm64v8 -f arm64v8.Dockerfile .
sudo docker build --build-arg GIT_COMMIT=${GIT_COMMIT} --build-arg CONFIGURATION_NAME=Altcoins-Release --pull -t $DOCKERHUB_REPO:$LATEST_TAG-altcoins-arm64v8 -f arm64v8.Dockerfile .
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-arm64v8
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-altcoins-arm64v8

View File

@ -12,7 +12,7 @@ indent_style = space
indent_size = 4
charset = utf-8
[launchSettings.json]
[*.json]
indent_size = 2
# C# files

6
.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
@ -300,3 +296,5 @@ BTCPayServer/testpwd
.DS_Store
Packed Plugins
Plugins/packed
BTCPayServer/wwwroot/swagger/v1/openapi.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,18 +1,19 @@
namespace BTCPayServer.Abstractions.Custodians;
namespace BTCPayServer.Abstractions.Custodians.Client;
public class AssetQuoteResult
{
public string FromAsset { get; }
public string ToAsset { get; }
public decimal Bid { get; }
public decimal Ask { get; }
public string FromAsset { get; set; }
public string ToAsset { get; set; }
public decimal Bid { get; set; }
public decimal Ask { get; set; }
public AssetQuoteResult() { }
public AssetQuoteResult(string fromAsset, string toAsset,decimal bid, decimal ask)
{
this.FromAsset = fromAsset;
this.ToAsset = toAsset;
this.Bid = bid;
this.Ask = ask;
FromAsset = fromAsset;
ToAsset = toAsset;
Bid = bid;
Ask = ask;
}
}

View File

@ -1,7 +1,7 @@
using System.Collections.Generic;
using BTCPayServer.Client.Models;
namespace BTCPayServer.Abstractions.Custodians;
namespace BTCPayServer.Abstractions.Custodians.Client;
/**
* The result of a market trade. Used as a return type for custodians implementing ICanTrade

View File

@ -2,7 +2,7 @@ using System;
using System.Collections.Generic;
using BTCPayServer.Client.Models;
namespace BTCPayServer.Abstractions.Custodians;
namespace BTCPayServer.Abstractions.Custodians.Client;
public class WithdrawResult
{

View File

@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Custodians.Client;
using BTCPayServer.Client.Models;
using Newtonsoft.Json.Linq;

View File

@ -1,5 +1,6 @@
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Custodians.Client;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Abstractions.Custodians;

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

@ -1,35 +1,34 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Abstractions.Form;
public abstract class Field
public class Field
{
// HTML5 compatible type string like "text", "textarea", "email", "password", etc. Each type is a class and may contain more fields (i.e. "select" would have options).
public string Type;
// The name of the HTML5 node. Should be used as the key for the posted data.
public string Name;
// The translated label of the field.
public string Label;
// HTML5 compatible type string like "text", "textarea", "email", "password", etc. Each type is a class and may contain more fields (i.e. "select" would have options).
public string Type;
// The value field is what is currently in the DB or what the user entered, but possibly not saved yet due to validation errors.
// If this is the first the user sees the form, then value and original value are the same. Value changes as the user starts interacting with the form.
public string Value;
// The original value is the value that is currently saved in the backend. A "reset" button can be used to revert back to this. Should only be set from the constructor.
public string OriginalValue;
// A useful note shown below the field or via a tooltip / info icon. Should be translated for the user.
public string HelpText;
// The field is considered "valid" if there are no validation errors
public List<string> ValidationErrors = new List<string>();
public bool Required = false;
public bool IsValid()
public virtual bool IsValid()
{
return ValidationErrors.Count == 0;
return ValidationErrors.Count == 0 && Fields.All(field => field.IsValid());
}
[JsonExtensionData] public IDictionary<string, JToken> AdditionalData { get; set; }
public List<Field> Fields { get; set; } = new();
}

View File

@ -1,14 +1,12 @@
using System.Collections.Generic;
namespace BTCPayServer.Abstractions.Form;
public class Fieldset
public class Fieldset : Field
{
public bool Hidden { get; set; }
public string Label { get; set; }
public Fieldset()
{
this.Fields = new List<Field>();
Type = "fieldset";
}
public string Label { get; set; }
public List<Field> Fields { get; set; }
}

View File

@ -1,60 +1,143 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Abstractions.Form;
public class Form
{
// Messages to be shown at the top of the form indicating user feedback like "Saved successfully" or "Please change X because of Y." or a warning, etc...
public List<AlertMessage> TopMessages { get; set; } = new();
// Groups of fields in the form
public List<Fieldset> Fieldsets { get; set; } = new();
public List<Field> Fields { get; set; } = new();
// Are all the fields valid in the form?
public bool IsValid()
{
foreach (var fieldset in Fieldsets)
{
foreach (var field in fieldset.Fields)
{
if (!field.IsValid())
{
return false;
}
}
}
return true;
return Fields.All(field => field.IsValid());
}
public Field GetFieldByName(string name)
{
foreach (var fieldset in Fieldsets)
return GetFieldByName(name, Fields, null);
}
private static Field GetFieldByName(string name, List<Field> fields, string prefix)
{
prefix ??= string.Empty;
foreach (var field in fields)
{
foreach (var field in fieldset.Fields)
var currentPrefix = prefix;
if (!string.IsNullOrEmpty(field.Name))
{
if (name.Equals(field.Name))
currentPrefix = $"{prefix}{field.Name}";
if (currentPrefix.Equals(name, StringComparison.InvariantCultureIgnoreCase))
{
return field;
}
currentPrefix += "_";
}
var subFieldResult = GetFieldByName(name, field.Fields, currentPrefix);
if (subFieldResult is not null)
{
return subFieldResult;
}
}
return null;
}
public List<string> GetAllNames()
{
return GetAllNames(Fields);
}
private static List<string> GetAllNames(List<Field> fields)
{
var names = new List<string>();
foreach (var fieldset in Fieldsets)
foreach (var field in fields)
{
foreach (var field in fieldset.Fields)
string prefix = string.Empty;
if (!string.IsNullOrEmpty(field.Name))
{
names.Add(field.Name);
prefix = $"{field.Name}_";
}
if (field.Fields.Any())
{
names.AddRange(GetAllNames(field.Fields).Select(s => $"{prefix}{s}" ));
}
}
return names;
}
public void ApplyValuesFromOtherForm(Form form)
{
foreach (var fieldset in Fields)
{
foreach (var field in fieldset.Fields)
{
field.Value = form
.GetFieldByName(
$"{(string.IsNullOrEmpty(fieldset.Name) ? string.Empty : fieldset.Name + "_")}{field.Name}")
?.Value;
}
}
}
public void ApplyValuesFromForm(IFormCollection form)
{
var names = GetAllNames();
foreach (var name in names)
{
var field = GetFieldByName(name);
if (field is null || !form.TryGetValue(name, out var val))
{
continue;
}
field.Value = val;
}
}
public Dictionary<string, object> GetValues()
{
return GetValues(Fields);
}
private static Dictionary<string, object> GetValues(List<Field> fields)
{
var result = new Dictionary<string, object>();
foreach (Field field in fields)
{
var name = field.Name ?? string.Empty;
if (field.Fields.Any())
{
var values = GetValues(fields);
values.Remove(string.Empty, out var keylessValue);
result.TryAdd(name, values);
if (keylessValue is not Dictionary<string, object> dict) continue;
foreach (KeyValuePair<string,object> keyValuePair in dict)
{
result.TryAdd(keyValuePair.Key, keyValuePair.Value);
}
}
else
{
result.TryAdd(name, field.Value);
}
}
return result;
}
}

View File

@ -0,0 +1,27 @@
namespace BTCPayServer.Abstractions.Form;
public class HtmlInputField : Field
{
// The translated label of the field.
public string Label;
// The original value is the value that is currently saved in the backend. A "reset" button can be used to revert back to this. Should only be set from the constructor.
public string OriginalValue;
// A useful note shown below the field or via a tooltip / info icon. Should be translated for the user.
public string HelpText;
public bool Required;
public HtmlInputField(string label, string name, string value, bool required, string helpText, string type = "text")
{
Label = label;
Name = name;
Value = value;
OriginalValue = value;
Required = required;
HelpText = helpText;
Type = type;
}
// TODO JSON parsing from string to objects again probably won't work out of the box because of the different field types.
}

View File

@ -1,19 +0,0 @@
namespace BTCPayServer.Abstractions.Form;
public class TextField : Field
{
public TextField(string label, string name, string value, bool required, string helpText)
{
this.Label = label;
this.Name = name;
this.Value = value;
this.OriginalValue = value;
this.Required = required;
this.HelpText = helpText;
this.Type = "text";
}
// TODO JSON parsing from string to objects again probably won't work out of the box because of the different field types.
}

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

@ -19,6 +19,28 @@ namespace BTCPayServer.Client
method: HttpMethod.Post), token);
return await HandleResponse<PointOfSaleAppData>(response);
}
public virtual async Task<CrowdfundAppData> CreateCrowdfundApp(string storeId,
CreateCrowdfundAppRequest request, CancellationToken token = default)
{
if (request == null)
throw new ArgumentNullException(nameof(request));
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/apps/crowdfund", bodyPayload: request,
method: HttpMethod.Post), token);
return await HandleResponse<CrowdfundAppData>(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)
{

View File

@ -56,9 +56,14 @@ namespace BTCPayServer.Client
return await HandleResponse<DepositAddressData>(response);
}
public virtual async Task<MarketTradeResponseData> TradeMarket(string storeId, string accountId, TradeRequestData request, CancellationToken token = default)
public virtual async Task<MarketTradeResponseData> MarketTradeCustodianAccountAsset(string storeId, string accountId, TradeRequestData request, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}/trades/market", bodyPayload: request, method: HttpMethod.Post), token);
//var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/users", null, request, HttpMethod.Post), token);
//return await HandleResponse<ApplicationUserData>(response);
var internalRequest = CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}/trades/market", null,
request, HttpMethod.Post);
var response = await _httpClient.SendAsync(internalRequest, token);
return await HandleResponse<MarketTradeResponseData>(response);
}
@ -73,7 +78,6 @@ namespace BTCPayServer.Client
var queryPayload = new Dictionary<string, object>();
queryPayload.Add("fromAsset", fromAsset);
queryPayload.Add("toAsset", toAsset);
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}/trades/quote", queryPayload), token);
return await HandleResponse<TradeQuoteResponseData>(response);
}

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

@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
using NBitcoin;
namespace BTCPayServer.Client
{
public partial class BTCPayServerClient
{
public virtual async Task<OnChainWalletObjectData> GetOnChainWalletObject(string storeId, string cryptoCode, OnChainWalletObjectId objectId, bool? includeNeighbourData = null, CancellationToken token = default)
{
Dictionary<string, object> parameters = new Dictionary<string, object>();
if (includeNeighbourData is bool v)
parameters.Add("includeNeighbourData", v);
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects/{objectId.Type}/{objectId.Id}", parameters, method: HttpMethod.Get), token);
try
{
return await HandleResponse<OnChainWalletObjectData>(response);
}
catch (GreenfieldAPIException err) when (err.APIError.Code == "wallet-object-not-found")
{
return null;
}
}
public virtual async Task<OnChainWalletObjectData[]> GetOnChainWalletObjects(string storeId, string cryptoCode, GetWalletObjectsRequest query = null, CancellationToken token = default)
{
Dictionary<string, object> parameters = new Dictionary<string, object>();
if (query?.Type is string s)
parameters.Add("type", s);
if (query?.Ids is string[] ids)
parameters.Add("ids", ids);
if (query?.IncludeNeighbourData is bool v)
parameters.Add("includeNeighbourData", v);
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects", parameters, method:HttpMethod.Get), token);
return await HandleResponse<OnChainWalletObjectData[]>(response);
}
public virtual async Task RemoveOnChainWalletObject(string storeId, string cryptoCode, OnChainWalletObjectId objectId,
CancellationToken token = default)
{
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects/{objectId.Type}/{objectId.Id}", method:HttpMethod.Delete), token);
await HandleResponse(response);
}
public virtual async Task<OnChainWalletObjectData> AddOrUpdateOnChainWalletObject(string storeId, string cryptoCode, AddOnChainWalletObjectRequest request,
CancellationToken token = default)
{
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects", method:HttpMethod.Post, bodyPayload: request), token);
return await HandleResponse<OnChainWalletObjectData>(response);
}
public virtual async Task AddOrUpdateOnChainWalletLink(string storeId, string cryptoCode,
OnChainWalletObjectId objectId,
AddOnChainWalletObjectLinkRequest request = null,
CancellationToken token = default)
{
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects/{objectId.Type}/{objectId.Id}/links", method:HttpMethod.Post, bodyPayload: request), token);
await HandleResponse(response);
}
public virtual async Task RemoveOnChainWalletLinks(string storeId, string cryptoCode,
OnChainWalletObjectId objectId,
OnChainWalletObjectId link,
CancellationToken token = default)
{
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects/{objectId.Type}/{objectId.Id}/links/{link.Type}/{link.Id}", method:HttpMethod.Delete), token);
await HandleResponse(response);
}
}
}

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

@ -53,6 +53,16 @@ namespace BTCPayServer.Client
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/pull-payments/{HttpUtility.UrlEncode(pullPaymentId)}/payouts", bodyPayload: payoutRequest, method: HttpMethod.Post), cancellationToken);
return await HandleResponse<PayoutData>(response);
}
public virtual async Task<PayoutData> GetPullPaymentPayout(string pullPaymentId, string payoutId, CancellationToken cancellationToken = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/pull-payments/{HttpUtility.UrlEncode(pullPaymentId)}/payouts/{payoutId}", method: HttpMethod.Get), cancellationToken);
return await HandleResponse<PayoutData>(response);
}
public virtual async Task<PayoutData> GetStorePayout(string storeId, string payoutId, CancellationToken cancellationToken = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/payouts/{payoutId}", method: HttpMethod.Get), cancellationToken);
return await HandleResponse<PayoutData>(response);
}
public virtual async Task<PayoutData> CreatePayout(string storeId, CreatePayoutThroughStoreRequest payoutRequest, CancellationToken cancellationToken = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/payouts", bodyPayload: payoutRequest, method: HttpMethod.Post), cancellationToken);
@ -69,7 +79,7 @@ namespace BTCPayServer.Client
return await HandleResponse<PayoutData>(response);
}
public async Task MarkPayoutPaid(string storeId, string payoutId,
public virtual async Task MarkPayoutPaid(string storeId, string payoutId,
CancellationToken cancellationToken = default)
{
var response = await _httpClient.SendAsync(
@ -78,5 +88,14 @@ namespace BTCPayServer.Client
method: HttpMethod.Post), cancellationToken);
await HandleResponse(response);
}
public virtual async Task MarkPayout(string storeId, string payoutId, MarkPayoutRequest request,
CancellationToken cancellationToken = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest(
$"api/v1/stores/{HttpUtility.UrlEncode(storeId)}/payouts/{HttpUtility.UrlEncode(payoutId)}/mark",
method: HttpMethod.Post, bodyPayload: request), cancellationToken);
await HandleResponse(response);
}
}
}

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

@ -1,20 +1,31 @@
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models;
[JsonObject(MemberSerialization.OptIn)]
public class AssetPairData
{
public AssetPairData()
{
}
public AssetPairData(string assetBought, string assetSold)
public AssetPairData(string assetBought, string assetSold, decimal minimumTradeQty)
{
AssetBought = assetBought;
AssetSold = assetSold;
MinimumTradeQty = minimumTradeQty;
}
[JsonProperty]
public string AssetBought { set; get; }
[JsonProperty]
public string AssetSold { set; get; }
[JsonProperty]
public decimal MinimumTradeQty { set; get; }
public override string ToString()
{
return AssetBought + "/" + AssetSold;

View File

@ -1,3 +1,4 @@
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
@ -25,7 +26,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 +37,48 @@ namespace BTCPayServer.Client.Models
public string RedirectUrl { get; set; } = null;
public bool? RedirectAutomatically { get; set; } = null;
public bool? RequiresRefundEmail { get; set; } = null;
public string FormId { get; set; } = null;
public string EmbeddedCSS { get; set; } = null;
public CheckoutType? CheckoutType { get; set; } = null;
}
public enum CrowdfundResetEvery
{
Never,
Hour,
Day,
Month,
Year
}
public class CreateCrowdfundAppRequest : CreateAppRequest
{
public string Title { get; set; } = null;
public bool? Enabled { get; set; } = null;
public bool? EnforceTargetAmount { get; set; } = null;
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
public DateTimeOffset? StartDate { get; set; } = null;
public string TargetCurrency { get; set; } = null;
public string Description { get; set; } = null;
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
public DateTimeOffset? EndDate { get; set; } = null;
public decimal? TargetAmount { get; set; } = null;
public string CustomCSSLink { get; set; } = null;
public string MainImageUrl { get; set; } = null;
public string EmbeddedCSS { get; set; } = null;
public string NotificationUrl { get; set; } = null;
public string Tagline { get; set; } = null;
public string PerksTemplate { get; set; } = null;
public bool? SoundsEnabled { get; set; } = null;
public string DisqusShortname { get; set; } = null;
public bool? AnimationsEnabled { get; set; } = null;
public int? ResetEveryAmount { get; set; } = null;
[JsonConverter(typeof(StringEnumConverter))]
public CrowdfundResetEvery ResetEvery { get; set; } = CrowdfundResetEvery.Never;
public bool? DisplayPerksValue { get; set; } = null;
public bool? DisplayPerksRanking { get; set; } = null;
public bool? SortPerksByPopularity { get; set; } = null;
public string[] Sounds { get; set; } = null;
public string[] AnimationColors { get; set; } = null;
}
}

View File

@ -1,10 +1,12 @@
using System.Collections.Generic;
namespace BTCPayServer.Client.Models;
public class CustodianData
{
public string Code { get; set; }
public string Name { get; set; }
public string[] TradableAssetPairs { get; set; }
public Dictionary<string, AssetPairData> TradableAssetPairs { get; set; }
public string[] WithdrawablePaymentMethods { get; set; }
public string[] DepositablePaymentMethods { get; set; }

View File

@ -85,6 +85,7 @@ namespace BTCPayServer.Client.Models
public bool? RedirectAutomatically { get; set; }
public bool? RequiresRefundEmail { get; set; } = null;
public string DefaultLanguage { 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

@ -0,0 +1,14 @@
#nullable enable
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client.Models;
public class MarkPayoutRequest
{
[JsonConverter(typeof(StringEnumConverter))]
public PayoutState State { get; set; } = PayoutState.Completed;
public JObject? PaymentProof { get; set; }
}

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

@ -4,16 +4,94 @@ using BTCPayServer.JsonConverters;
using NBitcoin;
using NBitcoin.JsonConverters;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client.Models
{
public class OnChainWalletObjectId
{
public OnChainWalletObjectId()
{
}
public OnChainWalletObjectId(string type, string id)
{
Type = type;
Id = id;
}
public string Type { get; set; }
public string Id { get; set; }
}
public class AddOnChainWalletObjectLinkRequest : OnChainWalletObjectId
{
public AddOnChainWalletObjectLinkRequest()
{
}
public AddOnChainWalletObjectLinkRequest(string objectType, string objectId) : base(objectType, objectId)
{
}
public JObject Data { get; set; }
}
public class GetWalletObjectsRequest
{
public string Type { get; set; }
public string[] Ids { get; set; }
public bool? IncludeNeighbourData { get; set; }
}
public class AddOnChainWalletObjectRequest : OnChainWalletObjectId
{
public AddOnChainWalletObjectRequest()
{
}
public AddOnChainWalletObjectRequest(string objectType, string objectId) : base(objectType, objectId)
{
}
public JObject Data { get; set; }
}
public class OnChainWalletObjectData : OnChainWalletObjectId
{
public OnChainWalletObjectData()
{
}
public OnChainWalletObjectData(string type, string id) : base(type, id)
{
}
public class OnChainWalletObjectLink : OnChainWalletObjectId
{
public OnChainWalletObjectLink()
{
}
public OnChainWalletObjectLink(string type, string id) : base(type, id)
{
}
public JObject LinkData { get; set; }
public JObject ObjectData { get; set; }
}
public JObject Data { get; set; }
public OnChainWalletObjectLink[] Links { get; set; }
}
public class OnChainWalletTransactionData
{
[JsonConverter(typeof(UInt256JsonConverter))]
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

@ -24,5 +24,9 @@ namespace BTCPayServer.Client.Models
[JsonExtensionData]
public IDictionary<string, JToken> AdditionalData { get; set; }
public string FormId { get; set; }
public string FormResponse { get; set; }
}
}

View File

@ -12,7 +12,6 @@ namespace BTCPayServer.Client.Models
public DateTimeOffset CreatedTime { get; set; }
public string Id { get; set; }
public bool Archived { get; set; }
public enum PaymentRequestStatus
{
Pending = 0,

View File

@ -2,6 +2,7 @@ using System;
using BTCPayServer.JsonConverters;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client.Models
{
@ -29,5 +30,6 @@ namespace BTCPayServer.Client.Models
[JsonConverter(typeof(StringEnumConverter))]
public PayoutState State { get; set; }
public int Revision { get; set; }
public JObject PaymentProof { get; set; }
}
}

View File

@ -17,4 +17,9 @@ namespace BTCPayServer.Client.Models
{
// We can add POS specific things here later
}
public class CrowdfundAppData : AppDataBase
{
// We can add Crowdfund specific things here later
}
}

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,8 @@ namespace BTCPayServer.Client.Models
public bool AnyoneCanCreateInvoice { get; set; }
public string DefaultCurrency { get; set; }
public bool RequiresRefundEmail { 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 +68,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

@ -137,6 +137,7 @@ namespace BTCPayServer
public string CryptoImagePath { get; set; }
public string[] DefaultRateRules { get; set; } = Array.Empty<string>();
public override string ToString()
{
return CryptoCode;

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

@ -180,7 +180,7 @@ namespace BTCPayServer.Logging
logBuilder.Append(": ");
var lenAfter = logBuilder.ToString().Length;
while (lenAfter++ < 18)
logBuilder.Append(" ");
logBuilder.Append(' ');
// scope information
GetScopeInformation(logBuilder);

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

@ -0,0 +1,12 @@
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
namespace BTCPayServer.Data.Data;
public class FormData
{
public string Id { get; set; }
public string Name { get; set; }
public string Config { get; set; }
}

View File

@ -21,4 +21,9 @@ public class PayoutProcessorData
.HasOne(o => o.Store)
.WithMany(data => data.PayoutProcessors).OnDelete(DeleteBehavior.Cascade);
}
public override string ToString()
{
return $"{Processor} {PaymentMethod} {StoreId}";
}
}

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,87 @@
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 const string Payjoin = "payjoin";
public const string Invoice = "invoice";
public const string PaymentRequest = "payment-request";
public const string App = "app";
public const string PayjoinExposed = "pj-exposed";
public const string Payout = "payout";
public const string PullPayment = "pull-payment";
}
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; }
public IEnumerable<(string type, string id, string linkdata, string objectdata)> GetLinks()
{
if (ChildLinks is not null)
foreach (var c in ChildLinks)
{
yield return (c.ChildType, c.ChildId, c.Data, c.Child?.Data);
}
if (ParentLinks is not null)
foreach (var c in ParentLinks)
{
yield return (c.ParentType, c.ParentId, c.Data, c.Parent?.Data);
}
}
public IEnumerable<WalletObjectData> GetNeighbours()
{
if (ChildLinks != null)
foreach (var c in ChildLinks)
{
if (c.Child != null)
yield return c.Child;
}
if (ParentLinks != null)
foreach (var c in ParentLinks)
{
if (c.Parent != null)
yield return c.Parent;
}
}
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
{
builder.Entity<WalletObjectData>().HasKey(o =>
new
{
o.WalletId,
o.Type,
o.Id,
});
builder.Entity<WalletObjectData>().HasIndex(o =>
new
{
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,81 @@
// <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.CreateIndex(
name: "IX_WalletObjects_Type_Id",
table: "WalletObjects",
columns: new[] { "Type", "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.9");
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,54 @@ 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.HasIndex("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 +1382,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 +1543,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

@ -168,7 +168,7 @@ namespace BTCPayServer.Services.Rates
sb.Append(url);
if (payload != null)
{
sb.Append("?");
sb.Append('?');
sb.Append(String.Join('&', payload.Select(kv => $"{kv.Key}={kv.Value}").OfType<object>().ToArray()));
}
var request = new HttpRequestMessage(HttpMethod.Get, sb.ToString());
@ -177,9 +177,15 @@ namespace BTCPayServer.Services.Rates
var result = JsonConvert.DeserializeObject<T>(stringResult);
if (result is JToken json)
{
if (!(json is JArray) && json["result"] is JObject {Count: > 0} pairResult)
{
return (T)(object)(pairResult);
}
if (!(json is JArray) && json["error"] is JArray error && error.Count != 0)
{
throw new APIException(error[0].ToStringInvariant());
throw new APIException(string.Join("\n",
error.Select(token => token.ToStringInvariant()).Distinct()));
}
result = (T)(object)(json["result"] ?? json);
}

View File

@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Rating;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json;
namespace BTCPayServer.Services.Rates
{
public class YadioRateProvider : IRateProvider
{
private readonly HttpClient _httpClient;
public YadioRateProvider(HttpClient httpClient)
{
_httpClient = httpClient ?? new HttpClient();
}
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{
var response = await _httpClient.GetAsync("https://api.yadio.io/exrates/BTC", cancellationToken);
response.EnsureSuccessStatusCode();
var jobj = await response.Content.ReadAsAsync<JObject>(cancellationToken);
var results = jobj["BTC"];
var list = new List<PairRate>();
foreach (var item in results)
{
string name = ((JProperty)item).Name;
int value = results[name].Value<int>();
list.Add(new PairRate(new CurrencyPair("BTC", name), new BidAsk(value)));
}
return list.ToArray();
}
}
}

View File

@ -84,6 +84,7 @@ namespace BTCPayServer.Services.Rates
yield return new AvailableRateProvider("coinbasepro", "Coinbase Pro", "https://api.pro.coinbase.com/products");
yield return new AvailableRateProvider("argoneum", "Argoneum", "https://rates.argoneum.net/rates");
yield return new AvailableRateProvider("yadio", "Yadio", "https://api.yadio.io/exrates/BTC");
}
void InitExchanges()
{
@ -104,6 +105,7 @@ namespace BTCPayServer.Services.Rates
Providers.Add("ripio", new RipioExchangeProvider(_httpClientFactory?.CreateClient("EXCHANGE_RIPIO")));
Providers.Add("cryptomarket", new CryptoMarketExchangeRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_CRYPTOMARKET")));
Providers.Add("bitflyer", new BitflyerRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BITFLYER")));
Providers.Add("yadio", new YadioRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_YADIO")));
// Providers.Add("argoneum", new ArgoneumRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_ARGONEUM")));

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="103.0.5060.5300" />
<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

@ -30,7 +30,7 @@ namespace BTCPayServer.Tests
s.AddDerivationScheme();
s.GoToStore(StoreNavPages.CheckoutAppearance);
s.Driver.FindElement(By.Id("RequiresRefundEmail")).Click();
s.Driver.FindElement(By.Name("command")).Click();
s.Driver.FindElement(By.Id("Save")).Click();
var emailAlreadyThereInvoiceId = s.CreateInvoice(100, "USD", "a@g.com");
s.GoToInvoiceCheckout(emailAlreadyThereInvoiceId);

View File

@ -0,0 +1,212 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Payments;
using BTCPayServer.Tests.Logging;
using BTCPayServer.Views.Stores;
using NBitcoin;
using OpenQA.Selenium;
using Xunit;
using Xunit.Abstractions;
namespace BTCPayServer.Tests
{
[Trait("Selenium", "Selenium")]
[Collection(nameof(NonParallelizableCollectionDefinition))]
public class CheckoutV2Tests : UnitTestBase
{
private const int TestTimeout = TestUtils.TestTimeout;
public CheckoutV2Tests(ITestOutputHelper helper) : base(helper)
{
}
[Fact(Timeout = TestTimeout)]
[Trait("Lightning", "Lightning")]
public async Task CanConfigureCheckout()
{
using var s = CreateSeleniumTester();
s.Server.ActivateLightning();
await s.StartAsync();
s.GoToRegister();
s.RegisterNewUser(true);
s.CreateNewStore();
s.EnableCheckoutV2();
s.AddLightningNode();
s.AddDerivationScheme();
// Configure store url
var storeUrl = "https://satoshisteaks.com/";
s.GoToStore();
s.Driver.FindElement(By.Id("StoreWebsite")).SendKeys(storeUrl);
s.Driver.FindElement(By.Id("Save")).Click();
Assert.Contains("Store successfully updated", s.FindAlertMessage().Text);
// Default payment method
var invoiceId = s.CreateInvoice(defaultPaymentMethod: "BTC_LightningLike");
s.GoToInvoiceCheckout(invoiceId);
Assert.Equal(2, s.Driver.FindElements(By.CssSelector(".payment-method")).Count);
Assert.Contains("Lightning", s.Driver.WaitForElement(By.CssSelector(".payment-method.active")).Text);
Assert.DoesNotContain("LNURL", s.Driver.PageSource);
var payUrl = s.Driver.FindElement(By.CssSelector(".btn-primary")).GetAttribute("href");
Assert.StartsWith("lightning:", payUrl);
// Lightning amount in Sats
Assert.Contains("BTC", s.Driver.FindElement(By.Id("AmountDue")).Text);
s.GoToHome();
s.GoToLightningSettings();
s.Driver.SetCheckbox(By.Id("LightningAmountInSatoshi"), true);
s.Driver.FindElement(By.Id("save")).Click();
Assert.Contains("BTC Lightning settings successfully updated", s.FindAlertMessage().Text);
s.GoToInvoiceCheckout(invoiceId);
Assert.Contains("Sats", s.Driver.FindElement(By.Id("AmountDue")).Text);
// Expire
var expirySeconds = s.Driver.FindElement(By.Id("ExpirySeconds"));
expirySeconds.Clear();
expirySeconds.SendKeys("3");
s.Driver.FindElement(By.Id("Expire")).Click();
var paymentInfo = s.Driver.WaitForElement(By.Id("PaymentInfo"));
Assert.Contains("This invoice will expire in", paymentInfo.Text);
Assert.DoesNotContain("Please send", paymentInfo.Text);
TestUtils.Eventually(() =>
{
var expiredSection = s.Driver.FindElement(By.Id("expired"));
Assert.True(expiredSection.Displayed);
Assert.Contains("Invoice Expired", expiredSection.Text);
});
Assert.True(s.Driver.ElementDoesNotExist(By.Id("ReceiptLink")));
Assert.Equal(storeUrl, s.Driver.FindElement(By.Id("StoreLink")).GetAttribute("href"));
// Test payment
s.GoToHome();
invoiceId = s.CreateInvoice();
s.GoToInvoiceCheckout(invoiceId);
// Details
s.Driver.ToggleCollapse("PaymentDetails");
var details = s.Driver.FindElement(By.CssSelector(".payment-details"));
Assert.Contains("Total Price", details.Text);
Assert.Contains("Total Fiat", details.Text);
Assert.Contains("Exchange Rate", details.Text);
Assert.Contains("Amount Due", details.Text);
Assert.Contains("Recommended Fee", details.Text);
// Pay partial amount
await Task.Delay(200);
var address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-destination");
var amountFraction = "0.00001";
await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(address, Network.RegTest),
Money.Parse(amountFraction));
await s.Server.ExplorerNode.GenerateAsync(1);
// Fake Pay
s.Driver.FindElement(By.Id("FakePayAmount")).FillIn(amountFraction);
s.Driver.FindElement(By.Id("FakePay")).Click();
TestUtils.Eventually(() =>
{
Assert.Contains("Created transaction",
s.Driver.WaitForElement(By.Id("CheatSuccessMessage")).Text);
s.Server.ExplorerNode.Generate(1);
paymentInfo = s.Driver.WaitForElement(By.Id("PaymentInfo"));
Assert.Contains("The invoice hasn't been paid in full", paymentInfo.Text);
Assert.Contains("Please send", paymentInfo.Text);
});
// Mine
s.Driver.FindElement(By.Id("Mine")).Click();
TestUtils.Eventually(() =>
{
Assert.Contains("Mined 1 block",
s.Driver.WaitForElement(By.Id("CheatSuccessMessage")).Text);
});
// Pay full amount
var amountDue = s.Driver.FindElement(By.Id("AmountDue")).GetAttribute("data-amount-due");
s.Driver.FindElement(By.Id("FakePayAmount")).FillIn(amountDue);
s.Driver.FindElement(By.Id("FakePay")).Click();
TestUtils.Eventually(() =>
{
s.Server.ExplorerNode.Generate(1);
var paidSection = s.Driver.WaitForElement(By.Id("paid"));
Assert.True(paidSection.Displayed);
Assert.Contains("Invoice Paid", paidSection.Text);
});
s.Driver.FindElement(By.Id("ReceiptLink"));
Assert.Equal(storeUrl, s.Driver.FindElement(By.Id("StoreLink")).GetAttribute("href"));
// BIP21
s.GoToHome();
s.GoToStore(StoreNavPages.CheckoutAppearance);
s.Driver.SetCheckbox(By.Id("OnChainWithLnInvoiceFallback"), true);
s.Driver.FindElement(By.Id("Save")).Click();
Assert.Contains("Store successfully updated", s.FindAlertMessage().Text);
invoiceId = s.CreateInvoice();
s.GoToInvoiceCheckout(invoiceId);
Assert.Empty(s.Driver.FindElements(By.CssSelector(".payment-method")));
payUrl = s.Driver.FindElement(By.CssSelector(".btn-primary")).GetAttribute("href");
Assert.StartsWith("bitcoin:", payUrl);
Assert.Contains("&LIGHTNING=", payUrl);
// BIP21 with topup invoice (which is only available with Bitcoin onchain)
s.GoToHome();
invoiceId = s.CreateInvoice(amount: null);
s.GoToInvoiceCheckout(invoiceId);
Assert.Empty(s.Driver.FindElements(By.CssSelector(".payment-method")));
payUrl = s.Driver.FindElement(By.CssSelector(".btn-primary")).GetAttribute("href");
Assert.StartsWith("bitcoin:", payUrl);
Assert.DoesNotContain("&LIGHTNING=", payUrl);
// Expiry message should not show amount for topup invoice
expirySeconds = s.Driver.FindElement(By.Id("ExpirySeconds"));
expirySeconds.Clear();
expirySeconds.SendKeys("5");
s.Driver.FindElement(By.Id("Expire")).Click();
paymentInfo = s.Driver.WaitForElement(By.Id("PaymentInfo"));
Assert.Contains("This invoice will expire in", paymentInfo.Text);
Assert.DoesNotContain("Please send", paymentInfo.Text);
}
[Fact(Timeout = TestTimeout)]
public async Task CanUseCheckoutAsModal()
{
using var s = CreateSeleniumTester();
await s.StartAsync();
s.GoToRegister();
s.RegisterNewUser();
s.CreateNewStore();
s.EnableCheckoutV2();
s.GoToStore();
s.AddDerivationScheme();
var invoiceId = s.CreateInvoice(0.001m, "BTC", "a@x.com");
var invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice(invoiceId);
s.Driver.Navigate()
.GoToUrl(new Uri(s.ServerUri, $"tests/index.html?invoice={invoiceId}"));
TestUtils.Eventually(() =>
{
Assert.True(s.Driver.FindElement(By.Name("btcpay")).Displayed);
});
await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(invoice
.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike))
.GetPaymentMethodDetails().GetPaymentDestination(), Network.RegTest),
new Money(0.001m, MoneyUnit.BTC));
IWebElement closebutton = null;
TestUtils.Eventually(() =>
{
var frameElement = s.Driver.FindElement(By.Name("btcpay"));
var iframe = s.Driver.SwitchTo().Frame(frameElement);
closebutton = iframe.FindElement(By.Id("close"));
Assert.True(closebutton.Displayed);
});
closebutton.Click();
s.Driver.AssertElementNotFound(By.Name("btcpay"));
Assert.Equal(s.Driver.Url,
new Uri(s.ServerUri, $"tests/index.html?invoice={invoiceId}").ToString());
}
}
}

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

@ -125,6 +125,12 @@ retry:
return el;
}
public static void FillIn(this IWebElement el, string text)
{
el.Clear();
el.SendKeys(text);
}
public static void ScrollTo(this IWebDriver driver, IWebElement element)
{

View File

@ -33,12 +33,14 @@ using Microsoft.Extensions.Options;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBitcoin.Scripting.Parser;
using NBXplorer;
using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Xunit;
using Xunit.Abstractions;
using StoreData = BTCPayServer.Data.StoreData;
namespace BTCPayServer.Tests
{
@ -481,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()
{
@ -1292,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");
@ -1387,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
@ -1808,5 +1726,32 @@ namespace BTCPayServer.Tests
}
}
}
[Fact]
public void PaymentMethodIdConverterIsGraceful()
{
var pmi = "\"BTC_hasjdfhasjkfjlajn\"";
JsonTextReader reader = new(new StringReader(pmi));
reader.Read();
Assert.Null(new PaymentMethodIdJsonConverter().ReadJson(reader, typeof(PaymentMethodId), null,
JsonSerializer.CreateDefault()));
}
[Fact]
public void CanBeBracefulAfterObsoleteShitcoin()
{
var blob = new StoreBlob();
blob.PaymentMethodCriteria = new List<PaymentMethodCriteria>()
{
new()
{
Above = true,
Value = new CurrencyValue() {Currency = "BTC", Value = 0.1m},
PaymentMethod = new PaymentMethodId("BTC", PaymentTypes.BTCLike)
}
};
var newBlob = Encoding.UTF8.GetBytes(
new Serializer(null).ToString(blob).Replace( "paymentMethod\":\"BTC\"","paymentMethod\":\"ETH_ZYC\""));
Assert.Empty(StoreDataExtensions.GetStoreBlob(new StoreData() {StoreBlob = newBlob}).PaymentMethodCriteria);
}
}
}

View File

@ -14,10 +14,12 @@ using BTCPayServer.Events;
using BTCPayServer.Lightning;
using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
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;
@ -87,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)]
@ -194,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();
@ -202,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);
@ -219,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 () =>
{
@ -231,6 +288,117 @@ namespace BTCPayServer.Tests
await client.GetApp(retrievedApp.Id);
});
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanCreateCrowdfundApp()
{
using var tester = CreateServerTester();
await tester.StartAsync();
var user = tester.NewAccount();
await user.RegisterDerivationSchemeAsync("BTC");
var client = await user.CreateClient();
// Test validation for creating the app
await AssertValidationError(new[] { "AppName" },
async () => await client.CreateCrowdfundApp(user.StoreId, new CreateCrowdfundAppRequest() {}));
await AssertValidationError(new[] { "AppName" },
async () => await client.CreateCrowdfundApp(
user.StoreId,
new CreateCrowdfundAppRequest()
{
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[] { "TargetCurrency" },
async () => await client.CreateCrowdfundApp(
user.StoreId,
new CreateCrowdfundAppRequest()
{
AppName = "good name",
TargetCurrency = "fake currency"
}
)
);
await AssertValidationError(new[] { "PerksTemplate" },
async () => await client.CreateCrowdfundApp(
user.StoreId,
new CreateCrowdfundAppRequest()
{
AppName = "good name",
PerksTemplate = "lol invalid template"
}
)
);
await AssertValidationError(new[] { "AppName", "TargetCurrency", "PerksTemplate" },
async () => await client.CreateCrowdfundApp(
user.StoreId,
new CreateCrowdfundAppRequest()
{
TargetCurrency = "fake currency",
PerksTemplate = "lol invalid template"
}
)
);
await AssertValidationError(new[] { "AnimationColors" },
async () => await client.CreateCrowdfundApp(
user.StoreId,
new CreateCrowdfundAppRequest()
{
AppName = "good name",
AnimationColors = new string[] {}
}
)
);
await AssertValidationError(new[] { "AnimationColors" },
async () => await client.CreateCrowdfundApp(
user.StoreId,
new CreateCrowdfundAppRequest()
{
AppName = "good name",
AnimationColors = new string[] { " ", " " }
}
)
);
await AssertValidationError(new[] { "Sounds" },
async () => await client.CreateCrowdfundApp(
user.StoreId,
new CreateCrowdfundAppRequest()
{
AppName = "good name",
Sounds = new string[] { " " }
}
)
);
await AssertValidationError(new[] { "Sounds" },
async () => await client.CreateCrowdfundApp(
user.StoreId,
new CreateCrowdfundAppRequest()
{
AppName = "good name",
Sounds = new string[] { " ", " ", " " }
}
)
);
await AssertValidationError(new[] { "EndDate" },
async () => await client.CreateCrowdfundApp(
user.StoreId,
new CreateCrowdfundAppRequest()
{
AppName = "good name",
StartDate = DateTime.Parse("1998-01-01"),
EndDate = DateTime.Parse("1997-12-31")
}
)
);
// Test creating a crowdfund app
var app = await client.CreateCrowdfundApp(user.StoreId, new CreateCrowdfundAppRequest() { AppName = "test app from API" });
Assert.Equal("test app from API", app.Name);
Assert.Equal(user.StoreId, app.StoreId);
Assert.Equal("Crowdfund", app.AppType);
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
@ -740,6 +908,100 @@ namespace BTCPayServer.Tests
await AssertAPIError("invalid-state", async () => await client.MarkPayoutPaid(storeId, payout.Id));
}
[Fact]
[Trait("Integration", "Integration")]
public async Task CanProcessPayoutsExternally()
{
using var tester = CreateServerTester();
await tester.StartAsync();
var acc = tester.NewAccount();
acc.Register();
await acc.CreateStoreAsync();
var storeId = (await acc.RegisterDerivationSchemeAsync("BTC", importKeysToNBX: true)).StoreId;
var client = await acc.CreateClient();
var address = await tester.ExplorerNode.GetNewAddressAsync();
var payout = await client.CreatePayout(storeId, new CreatePayoutThroughStoreRequest()
{
Approved = false,
PaymentMethod = "BTC",
Amount = 0.0001m,
Destination = address.ToString()
});
await AssertAPIError("invalid-state", async () =>
{
await client.MarkPayout(storeId, payout.Id, new MarkPayoutRequest() {State = PayoutState.Completed});
});
await client.ApprovePayout(storeId, payout.Id, new ApprovePayoutRequest());
await client.MarkPayout(storeId, payout.Id, new MarkPayoutRequest() {State = PayoutState.Completed});
Assert.Equal(PayoutState.Completed,(await client.GetStorePayouts(storeId,false)).Single(data => data.Id == payout.Id ).State );
Assert.Null((await client.GetStorePayouts(storeId,false)).Single(data => data.Id == payout.Id ).PaymentProof );
foreach (var state in new []{ PayoutState.AwaitingApproval, PayoutState.Cancelled, PayoutState.Completed, PayoutState.AwaitingApproval, PayoutState.InProgress})
{
await AssertAPIError("invalid-state", async () =>
{
await client.MarkPayout(storeId, payout.Id, new MarkPayoutRequest() {State = state});
});
}
payout = await client.CreatePayout(storeId, new CreatePayoutThroughStoreRequest()
{
Approved = true,
PaymentMethod = "BTC",
Amount = 0.0001m,
Destination = address.ToString()
});
payout = await client.GetStorePayout(storeId, payout.Id);
Assert.NotNull(payout);
Assert.Equal(PayoutState.AwaitingPayment, payout.State);
await AssertValidationError(new []{"PaymentProof"}, async () =>
{
await client.MarkPayout(storeId, payout.Id, new MarkPayoutRequest() {State = PayoutState.Completed, PaymentProof = JObject.FromObject(new
{
test = "zyx"
})});
});
await client.MarkPayout(storeId, payout.Id, new MarkPayoutRequest() {State = PayoutState.InProgress, PaymentProof = JObject.FromObject(new
{
proofType = "external-proof"
})});
payout = await client.GetStorePayout(storeId, payout.Id);
Assert.NotNull(payout);
Assert.Equal(PayoutState.InProgress, payout.State);
Assert.True(payout.PaymentProof.TryGetValue("proofType", out var savedType));
Assert.Equal("external-proof",savedType);
await client.MarkPayout(storeId, payout.Id, new MarkPayoutRequest() {State = PayoutState.AwaitingPayment, PaymentProof = JObject.FromObject(new
{
proofType = "external-proof",
id="finality proof",
link="proof.com"
})});
payout = await client.GetStorePayout(storeId, payout.Id);
Assert.NotNull(payout);
Assert.Null(payout.PaymentProof);
Assert.Equal(PayoutState.AwaitingPayment, payout.State);
await client.MarkPayout(storeId, payout.Id, new MarkPayoutRequest() {State = PayoutState.Completed, PaymentProof = JObject.FromObject(new
{
proofType = "external-proof",
id="finality proof",
link="proof.com"
})});
payout = await client.GetStorePayout(storeId, payout.Id);
Assert.NotNull(payout);
Assert.Equal(PayoutState.Completed, payout.State);
Assert.True(payout.PaymentProof.TryGetValue("proofType", out savedType));
Assert.True(payout.PaymentProof.TryGetValue("link", out var savedLink));
Assert.True(payout.PaymentProof.TryGetValue("id", out var savedId));
Assert.Equal("external-proof",savedType);
Assert.Equal("finality proof",savedId);
Assert.Equal("proof.com",savedLink);
}
private DateTimeOffset RoundSeconds(DateTimeOffset dateTimeOffset)
{
return new DateTimeOffset(dateTimeOffset.Year, dateTimeOffset.Month, dateTimeOffset.Day, dateTimeOffset.Hour, dateTimeOffset.Minute, dateTimeOffset.Second, dateTimeOffset.Offset);
@ -1112,25 +1374,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)]
@ -1268,7 +1598,7 @@ namespace BTCPayServer.Tests
Checkout = new CreateInvoiceRequest.CheckoutOptions()
{
RedirectAutomatically = true,
RequiresRefundEmail = true
RequiresRefundEmail = true,
},
AdditionalSearchTerms = new string[] { "Banana" }
});
@ -1597,25 +1927,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",
@ -1623,9 +1963,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"));
@ -1643,10 +1991,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"
@ -1663,6 +2023,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);
@ -1670,7 +2039,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");
@ -1680,7 +2048,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()
@ -2207,6 +2575,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
@ -2227,7 +2596,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);
@ -2455,6 +2824,50 @@ namespace BTCPayServer.Tests
await newUserBasicClient.GetCurrentUser();
}
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")]
[Trait("Lightning", "Lightning")]
public async Task CanUseLNPayoutProcessor()
{
LightningPendingPayoutListener.SecondsDelay = 0;
using var tester = CreateServerTester();
tester.ActivateLightning();
await tester.StartAsync();
await tester.EnsureChannelsSetup();
var admin = tester.NewAccount();
await admin.GrantAccessAsync(true);
var adminClient = await admin.CreateClient(Policies.Unrestricted);
admin.RegisterLightningNode("BTC", LightningConnectionType.LndREST);
var payoutAmount = LightMoney.Satoshis(1000);
var inv = await tester.MerchantLnd.Client.CreateInvoice(payoutAmount, "Donation to merchant", TimeSpan.FromHours(1), default);
var resp = await tester.CustomerLightningD.Pay(inv.BOLT11);
Assert.Equal(PayResult.Ok, resp.Result);
var customerInvoice = await tester.CustomerLightningD.CreateInvoice(LightMoney.FromUnit(10, LightMoneyUnit.Satoshi),
Guid.NewGuid().ToString(), TimeSpan.FromDays(40));
var payout = await adminClient.CreatePayout(admin.StoreId,
new CreatePayoutThroughStoreRequest()
{
Approved = true, PaymentMethod = "BTC_LightningNetwork", Destination = customerInvoice.BOLT11
});
Assert.Empty(await adminClient.GetStoreLightningAutomatedPayoutProcessors(admin.StoreId, "BTC_LightningNetwork"));
await adminClient.UpdateStoreLightningAutomatedPayoutProcessors(admin.StoreId, "BTC_LightningNetwork",
new LightningAutomatedPayoutSettings() {IntervalSeconds = TimeSpan.FromSeconds(2)});
Assert.Equal(2, Assert.Single( await adminClient.GetStoreLightningAutomatedPayoutProcessors(admin.StoreId, "BTC_LightningNetwork")).IntervalSeconds.TotalSeconds);
await TestUtils.EventuallyAsync(async () =>
{
var payoutC =
(await adminClient.GetStorePayouts(admin.StoreId, false)).Single(data => data.Id == payout.Id);
Assert.Equal(PayoutState.Completed , payoutC.State);
});
}
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")]
public async Task CanUsePayoutProcessorsThroughAPI()
@ -2574,7 +2987,134 @@ namespace BTCPayServer.Tests
Assert.Empty(payouts.Where(data => data.State != PayoutState.InProgress));
});
}
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")]
public async Task CanUseWalletObjectsAPI()
{
using var tester = CreateServerTester();
await tester.StartAsync();
var admin = tester.NewAccount();
await admin.GrantAccessAsync(true);
var client = await admin.CreateClient(Policies.Unrestricted);
Assert.Empty(await client.GetOnChainWalletObjects(admin.StoreId, "BTC"));
var test = new OnChainWalletObjectId("test", "test");
Assert.NotNull(await client.AddOrUpdateOnChainWalletObject(admin.StoreId, "BTC", new AddOnChainWalletObjectRequest(test.Type, test.Id)));
Assert.Single(await client.GetOnChainWalletObjects(admin.StoreId, "BTC"));
Assert.NotNull(await client.GetOnChainWalletObject(admin.StoreId, "BTC", test));
Assert.Null(await client.GetOnChainWalletObject(admin.StoreId, "BTC", new OnChainWalletObjectId("test-wrong", "test")));
Assert.Null(await client.GetOnChainWalletObject(admin.StoreId, "BTC", new OnChainWalletObjectId("test", "test-wrong")));
await client.RemoveOnChainWalletObject(admin.StoreId, "BTC", new OnChainWalletObjectId("test", "test"));
Assert.Empty(await client.GetOnChainWalletObjects(admin.StoreId, "BTC"));
var test1 = new OnChainWalletObjectId("test", "test1");
var test2 = new OnChainWalletObjectId("test", "test2");
await client.AddOrUpdateOnChainWalletObject(admin.StoreId, "BTC", new AddOnChainWalletObjectRequest(test.Type, test.Id));
// Those links don't exists
await AssertAPIError("wallet-object-not-found", () => client.AddOrUpdateOnChainWalletLink(admin.StoreId, "BTC", test, new AddOnChainWalletObjectLinkRequest(test1.Type, test1.Id)));
await AssertAPIError("wallet-object-not-found", () => client.AddOrUpdateOnChainWalletLink(admin.StoreId, "BTC", test, new AddOnChainWalletObjectLinkRequest(test2.Type, test2.Id)));
Assert.Single(await client.GetOnChainWalletObjects(admin.StoreId, "BTC"));
await client.AddOrUpdateOnChainWalletObject(admin.StoreId, "BTC", new AddOnChainWalletObjectRequest(test1.Type, test1.Id));
await client.AddOrUpdateOnChainWalletObject(admin.StoreId, "BTC", new AddOnChainWalletObjectRequest(test2.Type, test2.Id));
await client.AddOrUpdateOnChainWalletLink(admin.StoreId, "BTC", test, new AddOnChainWalletObjectLinkRequest(test1.Type, test1.Id));
await client.AddOrUpdateOnChainWalletLink(admin.StoreId, "BTC", test, new AddOnChainWalletObjectLinkRequest(test2.Type, test2.Id));
var objs = await client.GetOnChainWalletObjects(admin.StoreId, "BTC");
Assert.Equal(3, objs.Length);
var middleObj = objs.Single(data => data.Id == "test" && data.Type == "test");
Assert.Equal(2, middleObj.Links.Length);
Assert.Contains("test1", middleObj.Links.Select(l => l.Id));
Assert.Contains("test2", middleObj.Links.Select(l => l.Id));
var test1Obj = objs.Single(data => data.Id == "test1" && data.Type == "test");
var test2Obj = objs.Single(data => data.Id == "test2" && data.Type == "test");
Assert.Single(test1Obj.Links.Select(l => l.Id), l => l == "test");
Assert.Single(test2Obj.Links.Select(l => l.Id), l => l == "test");
await client.RemoveOnChainWalletLinks(admin.StoreId, "BTC",
test1,
test);
var testObj = await client.GetOnChainWalletObject(admin.StoreId, "BTC", test);
Assert.Single(testObj.Links.Select(l => l.Id), l => l == "test2");
Assert.Single(testObj.Links);
test1Obj = await client.GetOnChainWalletObject(admin.StoreId, "BTC", test1);
Assert.Empty(test1Obj.Links);
await client.AddOrUpdateOnChainWalletLink(admin.StoreId, "BTC",
test1,
new AddOnChainWalletObjectLinkRequest(test.Type, test.Id) { Data = new JObject() { ["testData"] = "lol" } });
// Add some data to test1
await client.AddOrUpdateOnChainWalletObject(admin.StoreId, "BTC", new AddOnChainWalletObjectRequest() { Type = test1.Type, Id = test1.Id, Data = new JObject() { ["testData"] = "test1" } });
// Create a new type
await client.AddOrUpdateOnChainWalletObject(admin.StoreId, "BTC", new AddOnChainWalletObjectRequest() { Type = "newtype", Id = test1.Id });
testObj = await client.GetOnChainWalletObject(admin.StoreId, "BTC", test);
Assert.Single(testObj.Links.Where(l => l.Id == "test1" && l.LinkData["testData"]?.Value<string>() == "lol"));
Assert.Single(testObj.Links.Where(l => l.Id == "test1" && l.ObjectData["testData"]?.Value<string>() == "test1"));
testObj = await client.GetOnChainWalletObject(admin.StoreId, "BTC", test, false);
Assert.Single(testObj.Links.Where(l => l.Id == "test1" && l.LinkData["testData"]?.Value<string>() == "lol"));
Assert.Single(testObj.Links.Where(l => l.Id == "test1" && l.ObjectData is null));
async Task TestWalletRepository(bool useInefficient)
{
// We should have 4 nodes, two `test` type and one `newtype`
// Only the node `test` `test` is connected to `test1`
var wid = new WalletId(admin.StoreId, "BTC");
var repo = tester.PayTester.GetService<WalletRepository>();
var allObjects = await repo.GetWalletObjects((new(wid, null) { UseInefficientPath = useInefficient }));
var allObjectsNoWallet = await repo.GetWalletObjects((new() { UseInefficientPath = useInefficient }));
var allObjectsNoWalletAndType = await repo.GetWalletObjects((new() { Type = "test", UseInefficientPath = useInefficient }));
var allTests = await repo.GetWalletObjects((new(wid, "test") { UseInefficientPath = useInefficient }));
var twoTests2 = await repo.GetWalletObjects((new(wid, "test", new[] { "test1", "test2", "test-unk" }) { UseInefficientPath = useInefficient }));
var oneTest = await repo.GetWalletObjects((new(wid, "test", new[] { "test" }) { UseInefficientPath = useInefficient }));
var oneTestWithoutData = await repo.GetWalletObjects((new(wid, "test", new[] { "test" }) { UseInefficientPath = useInefficient, IncludeNeighbours = false }));
var idsTypes = await repo.GetWalletObjects((new(wid) { TypesIds = new[] { new ObjectTypeId("test", "test1"), new ObjectTypeId("test", "test2") }, UseInefficientPath = useInefficient }));
Assert.Equal(4, allObjects.Count);
// We are reusing a db in this test, as such we may have other wallets here.
Assert.True(allObjectsNoWallet.Count >= 4);
Assert.True(allObjectsNoWalletAndType.Count >= 3);
Assert.Equal(3, allTests.Count);
Assert.Equal(2, twoTests2.Count);
Assert.Single(oneTest);
Assert.NotNull(oneTest.First().Value.GetNeighbours().Select(n => n.Data).FirstOrDefault());
Assert.Single(oneTestWithoutData);
Assert.Null(oneTestWithoutData.First().Value.GetNeighbours().Select(n => n.Data).FirstOrDefault());
Assert.Equal(2, idsTypes.Count);
}
await TestWalletRepository(false);
await TestWalletRepository(true);
{
var allObjects = await client.GetOnChainWalletObjects(admin.StoreId, "BTC");
var allTests = await client.GetOnChainWalletObjects(admin.StoreId, "BTC", new GetWalletObjectsRequest() { Type = "test" });
var twoTests2 = await client.GetOnChainWalletObjects(admin.StoreId, "BTC", new GetWalletObjectsRequest() { Type = "test", Ids = new[] { "test1", "test2", "test-unk" } });
var oneTest = await client.GetOnChainWalletObjects(admin.StoreId, "BTC", new GetWalletObjectsRequest() { Type = "test", Ids=new[] { "test" } });
var oneTestWithoutData = await client.GetOnChainWalletObjects(admin.StoreId, "BTC", new GetWalletObjectsRequest() { Type = "test", Ids = new[] { "test" }, IncludeNeighbourData = false });
Assert.Equal(4, allObjects.Length);
Assert.Equal(3, allTests.Length);
Assert.Equal(2, twoTests2.Length);
Assert.Single(oneTest);
Assert.NotNull(oneTest.First().Links.Select(n => n.ObjectData).FirstOrDefault());
Assert.Single(oneTestWithoutData);
Assert.Null(oneTestWithoutData.First().Links.Select(n => n.ObjectData).FirstOrDefault());
}
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
@ -2593,8 +3133,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()
@ -2859,13 +3458,13 @@ namespace BTCPayServer.Tests
// Test: Trade, unauth
var tradeRequest = new TradeRequestData {FromAsset = MockCustodian.TradeFromAsset, ToAsset = MockCustodian.TradeToAsset, Qty = MockCustodian.TradeQtyBought.ToString(CultureInfo.InvariantCulture)};
await AssertHttpError(401, async () => await unauthClient.TradeMarket(storeId, accountId, tradeRequest));
await AssertHttpError(401, async () => await unauthClient.MarketTradeCustodianAccountAsset(storeId, accountId, tradeRequest));
// Test: Trade, auth, but wrong permission
await AssertHttpError(403, async () => await managerClient.TradeMarket(storeId, accountId, tradeRequest));
await AssertHttpError(403, async () => await managerClient.MarketTradeCustodianAccountAsset(storeId, accountId, tradeRequest));
// Test: Trade, correct permission, correct assets, correct amount
var newTradeResult = await tradeClient.TradeMarket(storeId, accountId, tradeRequest);
var newTradeResult = await tradeClient.MarketTradeCustodianAccountAsset(storeId, accountId, tradeRequest);
Assert.NotNull(newTradeResult);
Assert.Equal(accountId, newTradeResult.AccountId);
Assert.Equal(mockCustodian.Code, newTradeResult.CustodianCode);
@ -2886,23 +3485,27 @@ namespace BTCPayServer.Tests
// Test: GetTradeQuote, SATS
var satsTradeRequest = new TradeRequestData {FromAsset = MockCustodian.TradeFromAsset, ToAsset = "SATS", Qty = MockCustodian.TradeQtyBought.ToString(CultureInfo.InvariantCulture)};
await AssertApiError(400, "use-asset-synonym", async () => await tradeClient.TradeMarket(storeId, accountId, satsTradeRequest));
await AssertApiError(400, "use-asset-synonym", async () => await tradeClient.MarketTradeCustodianAccountAsset(storeId, accountId, satsTradeRequest));
// TODO Test: Trade with percentage qty
// Test: Trade with wrong decimal format (example: JavaScript scientific format)
var wrongQtyTradeRequest = new TradeRequestData {FromAsset = MockCustodian.TradeFromAsset, ToAsset = MockCustodian.TradeToAsset, Qty = "6.1e-7"};
await AssertApiError(400,"bad-qty-format", async () => await tradeClient.MarketTradeCustodianAccountAsset(storeId, accountId, wrongQtyTradeRequest));
// Test: Trade, wrong assets method
var wrongAssetsTradeRequest = new TradeRequestData {FromAsset = "WRONG", ToAsset = MockCustodian.TradeToAsset, Qty = MockCustodian.TradeQtyBought.ToString(CultureInfo.InvariantCulture)};
await AssertHttpError(WrongTradingPairException.HttpCode, async () => await tradeClient.TradeMarket(storeId, accountId, wrongAssetsTradeRequest));
await AssertHttpError(WrongTradingPairException.HttpCode, async () => await tradeClient.MarketTradeCustodianAccountAsset(storeId, accountId, wrongAssetsTradeRequest));
// Test: wrong account ID
await AssertHttpError(404, async () => await tradeClient.TradeMarket(storeId, "WRONG-ACCOUNT-ID", tradeRequest));
await AssertHttpError(404, async () => await tradeClient.MarketTradeCustodianAccountAsset(storeId, "WRONG-ACCOUNT-ID", tradeRequest));
// Test: wrong store ID
await AssertHttpError(403, async () => await tradeClient.TradeMarket("WRONG-STORE-ID", accountId, tradeRequest));
await AssertHttpError(403, async () => await tradeClient.MarketTradeCustodianAccountAsset("WRONG-STORE-ID", accountId, tradeRequest));
// Test: Trade, correct assets, wrong amount
var wrongQtyTradeRequest = new TradeRequestData {FromAsset = MockCustodian.TradeFromAsset, ToAsset = MockCustodian.TradeToAsset, Qty = "0.01"};
await AssertApiError(400, "insufficient-funds", async () => await tradeClient.TradeMarket(storeId, accountId, wrongQtyTradeRequest));
var insufficientFundsTradeRequest = new TradeRequestData {FromAsset = MockCustodian.TradeFromAsset, ToAsset = MockCustodian.TradeToAsset, Qty = "0.01"};
await AssertApiError(400, "insufficient-funds", async () => await tradeClient.MarketTradeCustodianAccountAsset(storeId, accountId, insufficientFundsTradeRequest));
// Test: GetTradeQuote, unauth

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Custodians;
using BTCPayServer.Abstractions.Custodians.Client;
using BTCPayServer.Abstractions.Form;
using BTCPayServer.Client.Models;
using Newtonsoft.Json.Linq;
@ -76,7 +77,7 @@ public class MockCustodian : ICustodian, ICanDeposit, ICanTrade, ICanWithdraw
public List<AssetPairData> GetTradableAssetPairs()
{
var r = new List<AssetPairData>();
r.Add(new AssetPairData("BTC", "EUR"));
r.Add(new AssetPairData("BTC", "EUR", (decimal) 0.0001));
return r;
}

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

@ -185,6 +185,15 @@ namespace BTCPayServer.Tests
return (name, storeId);
}
public void EnableCheckoutV2(bool bip21 = false)
{
GoToStore(StoreNavPages.CheckoutAppearance);
Driver.SetCheckbox(By.Id("UseNewCheckout"), true);
Driver.WaitForElement(By.Id("OnChainWithLnInvoiceFallback"));
Driver.SetCheckbox(By.Id("OnChainWithLnInvoiceFallback"), bip21);
Driver.FindElement(By.Id("Save")).Click();
}
public Mnemonic GenerateWallet(string cryptoCode = "BTC", string seed = "", bool? importkeys = null, bool isHotWallet = false, ScriptPubKeyType format = ScriptPubKeyType.Segwit)
{
var isImport = !string.IsNullOrEmpty(seed);

View File

@ -30,7 +30,6 @@ using Newtonsoft.Json.Linq;
using OpenQA.Selenium;
using OpenQA.Selenium.Support.Extensions;
using OpenQA.Selenium.Support.UI;
using Renci.SshNet.Security.Cryptography;
using Xunit;
using Xunit.Abstractions;
@ -65,6 +64,61 @@ namespace BTCPayServer.Tests
s.Driver.Quit();
}
[Fact(Timeout = TestTimeout)]
public async Task CanUseForms()
{
using var s = CreateSeleniumTester();
await s.StartAsync();
s.RegisterNewUser(true);
s.CreateNewStore();
s.GenerateWallet(isHotWallet: true);
// Point Of Sale
s.Driver.FindElement(By.Id("StoreNav-CreateApp")).Click();
new SelectElement(s.Driver.FindElement(By.Id("SelectedAppType"))).SelectByValue("PointOfSale");
s.Driver.FindElement(By.Id("AppName")).SendKeys(Guid.NewGuid().ToString());
s.Driver.FindElement(By.Id("Create")).Click();
Assert.Contains("App successfully created", s.FindAlertMessage().Text);
new SelectElement(s.Driver.FindElement(By.Id("FormId"))).SelectByValue("Email");
s.Driver.FindElement(By.Id("SaveSettings")).Click();
Assert.Contains("App updated", s.FindAlertMessage().Text);
s.Driver.FindElement(By.Id("ViewApp")).Click();
var windows = s.Driver.WindowHandles;
Assert.Equal(2, windows.Count);
s.Driver.SwitchTo().Window(windows[1]);
s.Driver.FindElement(By.CssSelector("button[type='submit']")).Click();
Assert.Contains("Enter your email", s.Driver.PageSource);
s.Driver.FindElement(By.Name("buyerEmail")).SendKeys("aa@aa.com");
s.Driver.FindElement(By.CssSelector("input[type='submit']")).Click();
s.PayInvoice(true);
var invoiceId = s.Driver.Url[(s.Driver.Url.LastIndexOf("/", StringComparison.Ordinal) + 1)..];
s.GoToInvoice(invoiceId);
Assert.Contains("aa@aa.com", s.Driver.PageSource);
// Payment Request
s.Driver.FindElement(By.Id("StoreNav-PaymentRequests")).Click();
s.Driver.FindElement(By.Id("CreatePaymentRequest")).Click();
s.Driver.FindElement(By.Id("Title")).SendKeys("Pay123");
s.Driver.FindElement(By.Id("Amount")).SendKeys("700");
new SelectElement(s.Driver.FindElement(By.Id("FormId"))).SelectByValue("Email");
s.Driver.FindElement(By.Id("SaveButton")).Click();
s.Driver.FindElement(By.XPath("//a[starts-with(@id, 'Edit-')]")).Click();
var editUrl = s.Driver.Url;
s.Driver.FindElement(By.Id("ViewPaymentRequest")).Click();
s.Driver.FindElement(By.CssSelector("[data-test='form-button']")).Click();
Assert.Contains("Enter your email", s.Driver.PageSource);
s.Driver.FindElement(By.Name("buyerEmail")).SendKeys("aa@aa.com");
s.Driver.FindElement(By.CssSelector("input[type='submit']")).Click();
s.Driver.Navigate().GoToUrl(editUrl);
Assert.Contains("aa@aa.com", s.Driver.PageSource);
}
[Fact(Timeout = TestTimeout)]
public async Task CanUseCPFP()
{
@ -448,9 +502,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();
@ -473,7 +530,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();
@ -481,9 +542,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();
@ -637,15 +699,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();
@ -801,6 +874,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);
@ -843,9 +926,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'");
@ -863,8 +945,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);
@ -993,10 +1080,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();
@ -1027,6 +1114,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"));
@ -1300,6 +1388,46 @@ namespace BTCPayServer.Tests
s.Driver.AssertElementNotFound(By.Id("ActionsDropdownToggle"));
}
[Fact]
[Trait("Selenium", "Selenium")]
[Trait("Lightning", "Lightning")]
public async Task CanEditPullPaymentUI()
{
using var s = CreateSeleniumTester();
s.Server.ActivateLightning(LightningConnectionType.LndREST);
await s.StartAsync();
await s.Server.EnsureChannelsSetup();
s.RegisterNewUser(true);
s.CreateNewStore();
s.GenerateWallet("BTC", "", true, true);
await s.Server.ExplorerNode.GenerateAsync(1);
await s.FundStoreWallet(denomination: 50.0m);
s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
s.Driver.FindElement(By.Id("NewPullPayment")).Click();
s.Driver.FindElement(By.Id("Name")).SendKeys("PP1");
s.Driver.FindElement(By.Id("Amount")).Clear();
s.Driver.FindElement(By.Id("Amount")).SendKeys("99.0");
s.Driver.FindElement(By.Id("Create")).Click();
s.Driver.FindElement(By.LinkText("View")).Click();
s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
s.Driver.FindElement(By.LinkText("PP1")).Click();
var name = s.Driver.FindElement(By.Id("Name"));
name.Clear();
name.SendKeys("PP1 Edited");
var description = s.Driver.FindElement(By.ClassName("card-block"));
description.SendKeys("Description Edit");
s.Driver.FindElement(By.Id("SaveButton")).Click();
s.Driver.FindElement(By.LinkText("PP1 Edited")).Click();
Assert.Contains("Description Edit", s.Driver.PageSource);
Assert.Contains("PP1 Edited", s.Driver.PageSource);
}
[Fact]
[Trait("Selenium", "Selenium")]
[Trait("Lightning", "Lightning")]

View File

@ -72,14 +72,14 @@ namespace BTCPayServer.Tests
}
[Fact]
public void CanQueryDirectProviders()
public async Task CanQueryDirectProviders()
{
// TODO: Check once in a while whether or not they are working again
string[] brokenShitcoinCasinos = { "okex" };
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);
@ -126,6 +138,12 @@ namespace BTCPayServer.Tests
e => e.CurrencyPair == new CurrencyPair("BTC", "CLP") &&
e.BidAsk.Bid > 1.0m); // 1 BTC will always be more than 1 CLP
}
else if (name == "yadio")
{
Assert.Contains(exchangeRates.ByExchange[name],
e => e.CurrencyPair == new CurrencyPair("BTC", "LBP") &&
e.BidAsk.Bid > 1.0m); // 1 BTC will always be more than 1 LBP (I hope)
}
else
{
if (name == "kraken")
@ -154,12 +172,12 @@ namespace BTCPayServer.Tests
// Kraken emit one request only after first GetRates
factory.Providers["kraken"].GetRatesAsync(default).GetAwaiter().GetResult();
using (var c = new HttpClient())
{
var p = new ExchangeSharpRateProvider<ExchangeSharp.ExchangeKrakenAPI>(c);
var rates = p.GetRatesAsync(default).GetAwaiter().GetResult();
Assert.Contains(rates, e => e.CurrencyPair == new CurrencyPair("XXMR", "XXBT") && e.BidAsk.Bid < 1.0m);
}
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]
@ -293,17 +311,39 @@ namespace BTCPayServer.Tests
{
// This test verify that no malicious js is added in the minified files.
// We should extend the tests to other js files, but we can do as we go...
using HttpClient client = new HttpClient();
var actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "bootstrap", "bootstrap.bundle.min.js");
using var client = new HttpClient();
var actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "bootstrap", "bootstrap.bundle.min.js").Trim();
var version = Regex.Match(actual, "Bootstrap v([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value;
var expected = await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/bootstrap@{version}/dist/js/bootstrap.bundle.min.js")).Content.ReadAsStringAsync();
Assert.Equal(expected, actual.Replace("\r\n", "\n", StringComparison.OrdinalIgnoreCase));
var expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/bootstrap@{version}/dist/js/bootstrap.bundle.min.js")).Content.ReadAsStringAsync()).Trim();
Assert.Equal(expected, actual);
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "clipboard.js", "clipboard.js");
expected = await (await client.GetAsync($"https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.8/clipboard.js")).Content.ReadAsStringAsync();
Assert.Equal(expected, actual.Replace("\r\n", "\n", StringComparison.OrdinalIgnoreCase));
expected = (await (await client.GetAsync("https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.8/clipboard.js")).Content.ReadAsStringAsync()).Trim();
Assert.Equal(expected, actual);
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "vuejs", "vue.min.js").Trim();
version = Regex.Match(actual, "Vue\\.js v([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value;
expected = (await (await client.GetAsync($"https://cdnjs.cloudflare.com/ajax/libs/vue/{version}/vue.min.js")).Content.ReadAsStringAsync()).Trim();
Assert.Equal(expected, actual);
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "i18next", "i18next.min.js").Trim();
expected = (await (await client.GetAsync("https://cdnjs.cloudflare.com/ajax/libs/i18next/22.0.6/i18next.min.js")).Content.ReadAsStringAsync()).Trim();
Assert.Equal(expected, actual);
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "i18next", "i18nextHttpBackend.min.js").Trim();
expected = (await (await client.GetAsync("https://cdnjs.cloudflare.com/ajax/libs/i18next-http-backend/2.0.1/i18nextHttpBackend.min.js")).Content.ReadAsStringAsync()).Trim();
Assert.Equal(expected, actual);
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "i18next", "vue-i18next.js").Trim();
expected = (await (await client.GetAsync("https://unpkg.com/@panter/vue-i18next@0.15.2/dist/vue-i18next.js")).Content.ReadAsStringAsync()).Trim();
Assert.Equal(expected, actual);
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "vue-qrcode", "vue-qrcode.min.js").Trim();
version = Regex.Match(actual, "vue-qrcode v([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value;
expected = (await (await client.GetAsync($"https://unpkg.com/@chenfengyuan/vue-qrcode@{version}/dist/vue-qrcode.min.js")).Content.ReadAsStringAsync()).Trim();
Assert.Equal(expected, actual);
}
string GetFileContent(params string[] path)
{
var l = path.ToList();

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,8 +654,8 @@ namespace BTCPayServer.Tests
(string)store2.TempData[WellKnownTempData.ErrorMessage], StringComparison.CurrentCultureIgnoreCase);
}
[Fact(Timeout = LongRunningTestTimeout)]
[Trait("Integration", "Integration")]
[Fact(Timeout = LongRunningTestTimeout * 2)]
[Trait("Flaky", "Flaky")]
public async Task CanUseTorClient()
{
using var tester = CreateServerTester();
@ -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

@ -0,0 +1,16 @@
#!/bin/bash
PREIMAGE=$(cat /dev/urandom | tr -dc 'a-f0-9' | fold -w 64 | head -n 1)
HASH=`node -e "console.log(require('crypto').createHash('sha256').update(Buffer.from('$PREIMAGE', 'hex')).digest('hex'))"`
PAYREQ=$(./docker-customer-lncli.sh addholdinvoice $HASH $@ | jq -r ".payment_request")
echo "HASH: $HASH"
echo "PREIMAGE: $PREIMAGE"
echo "PAY REQ: $PAYREQ"
echo ""
echo "SETTLE: ./docker-customer-lncli.sh settleinvoice $PREIMAGE"
echo "CANCEL: ./docker-customer-lncli.sh cancelinvoice $HASH"
echo "LOOKUP: ./docker-customer-lncli.sh lookupinvoice $HASH"
echo ""
echo "TRACK: ./docker-merchant-lncli.sh trackpayment $HASH"
echo "PAY: ./docker-merchant-lncli.sh payinvoice $PAYREQ"

View File

@ -4,21 +4,23 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<PreserveCompilationContext>true</PreserveCompilationContext>
<RunAnalyzersDuringLiveAnalysis>False</RunAnalyzersDuringLiveAnalysis>
<RunAnalyzersDuringBuild>False</RunAnalyzersDuringBuild>
</PropertyGroup>
<ItemGroup>
<AssemblyAttribute Condition="'$(GitCommit)' != ''" Include="BTCPayServer.GitCommitAttribute">
<_Parameter1>$(GitCommit)</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
<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 +35,6 @@
<ItemGroup>
<None Remove="Currencies.txt" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="bundleconfig.json" />
</ItemGroup>
<ItemGroup Condition="'$(Altcoins)' != 'true'">
<Content Remove="Services\Altcoins\**\*" />
@ -48,23 +47,15 @@
<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.12" />
<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" />
<PackageReference Include="Fido2.AspNet" Version="2.0.2" />
<PackageReference Include="HtmlSanitizer" Version="5.0.372" />
<PackageReference Include="LNURL" Version="0.0.24" />
<PackageReference Include="LNURL" Version="0.0.26" />
<PackageReference Include="MailKit" Version="3.3.0" />
<PackageReference Include="McMaster.NETCore.Plugins.Mvc" Version="1.4.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="3.3.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="QRCoder" Version="1.4.3" />
<PackageReference Include="System.IO.Pipelines" Version="6.0.3" />
<PackageReference Include="NBitpayClient" Version="1.0.0.39" />
@ -72,15 +63,10 @@
<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" />
<PackageReference Include="SSH.NET" Version="2020.0.2" />
<PackageReference Include="Text.Analyzers" Version="3.3.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
@ -90,8 +76,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

@ -0,0 +1,20 @@
@using BTCPayServer.Services
@using BTCPayServer.Abstractions.Contracts
@using BTCPayServer.Abstractions.Extensions
@using BTCPayServer.Abstractions.TagHelpers
@inject ThemeSettings Theme
@inject IFileService FileService
@model BTCPayServer.Components.MainLogo.MainLogoViewModel
@if (!string.IsNullOrEmpty(Theme.LogoFileId))
{
var logoSrc = await FileService.GetFileUrl(Context.Request.GetAbsoluteRootUri(), Theme.LogoFileId);
<img src="@logoSrc" alt="BTCPay Server" class="main-logo main-logo-custom @Model.CssClass" />
}
else
{
<svg xmlns="http://www.w3.org/2000/svg" role="img" alt="BTCPay Server" class="main-logo main-logo-btcpay @Model.CssClass">
<use href="/img/logo.svg#small" class="main-logo-btcpay--small"/>
<use href="/img/logo.svg#large" class="main-logo-btcpay--large"/>
</svg>
}

View File

@ -0,0 +1,16 @@
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Components.MainLogo
{
public class MainLogo : ViewComponent
{
public IViewComponentResult Invoke(string cssClass = null)
{
var vm = new MainLogoViewModel
{
CssClass = cssClass,
};
return View(vm);
}
}
}

View File

@ -0,0 +1,7 @@
namespace BTCPayServer.Components.MainLogo
{
public class MainLogoViewModel
{
public string CssClass { get; set; }
}
}

View File

@ -7,11 +7,9 @@
@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 Microsoft.AspNetCore.Http.IHttpContextAccessor HttpContext;
@inject BTCPayServerEnvironment Env
@inject SignInManager<ApplicationUser> SignInManager
@inject PoliciesSettings PoliciesSettings
@ -35,7 +33,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>
@ -93,23 +91,21 @@
</li>
}
<vc:ui-extension-point location="store-wallets-nav" model="@Model"/>
@if (PoliciesSettings.Experimental)
{
@foreach (var custodianAccount in Model.CustodianAccounts)
{
<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>
</li>
}
<li class="nav-item">
<a asp-area="" asp-controller="UICustodianAccounts" asp-action="CreateCustodianAccount" asp-route-storeId="@Model.Store.Id" class="nav-link js-scroll-trigger @ViewData.IsActivePage(CustodianAccountsNavPages.Create)" id="StoreNav-CreateApp">
<a asp-area="" asp-controller="UICustodianAccounts" asp-action="CreateCustodianAccount" asp-route-storeId="@Model.Store.Id" class="nav-link js-scroll-trigger @ViewData.IsActivePage(CustodianAccountsNavPages.Create)" id="StoreNav-CreateCustodianAccount">
<vc:icon symbol="new"/>
<span>Add Custodian</span>
<span class="badge bg-warning ms-1" style="font-size:10px;">Experimental</span>
@ -171,12 +167,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">
@ -237,7 +228,7 @@
})()
</script>
}
else if (Env.IsSecure)
else if (Env.IsSecure(HttpContext.HttpContext))
{
<ul class="navbar-nav">
@if (!PoliciesSettings.LockSubscription)

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