Compare commits

..

342 Commits

Author SHA1 Message Date
3c7751ae80 Move plugins to separate repos 2022-12-05 11:26:47 +01:00
6d9bbb50d6 Merge branch 'master' into plugins 2022-12-05 09:26:26 +01:00
948bae9f95 Wallet import: Surface detailed error messages ()
* Wallet import: Surface detailed error messages

Similar to , this checks if the input is an output descriptor and display more detailed information about why an import might fail.

* Add test cases
2022-12-05 17:06:05 +09:00
a1c10b4ea3 Fix store selector transition () 2022-12-05 08:47:51 +01:00
f36df81d9a bump lightning lib (Fix ) 2022-12-05 11:37:03 +09:00
2fd9eb6c68 Adapt ln payouts to handle unknown status ()
Co-authored-by: d11n <mail@dennisreimann.de>
2022-12-04 13:23:59 +01:00
508002503e Merge branch 'master' into plugins 2022-12-04 12:50:38 +01:00
8894d14130 Upgrade Bootstrap to v5.2.3; Design System improvements () 2022-12-04 10:01:38 +01:00
4039e74a82 Do not run label migration for new instances 2022-12-01 19:09:51 +09:00
0af3faf6ff Wallet object scripts ()
* Wallet object scripts

* Adjust comment

Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2022-12-01 09:54:55 +09:00
0520b69c18 Update Changelog 2022-11-29 11:55:59 +09:00
e11a775bed fix migration 2022-11-29 11:29:35 +09:00
b4ed4623e1 bump 2022-11-29 11:20:14 +09:00
9ee9653c7d Checkout fixes ()
* Round buttons on results view

* Checkout v2: Fix for BIP21 case with LN as default payment method

Fixes .

* Update changelog

* Add test for fix
2022-11-29 11:19:23 +09:00
e55a16d917 bump bitcoin core in tests () 2022-11-28 22:18:19 +09:00
3458a0b22c Changelog 1.7.1 2022-11-28 21:08:33 +09:00
ddcfa735e0 Improve documentation of Refund API in Greenfield () 2022-11-28 20:58:18 +09:00
3370240541 Udpate langs 2022-11-28 20:57:31 +09:00
c0cec4716e Fix error HTTP 500 happening on Point of Sale (Fix: ) () 2022-11-28 20:50:09 +09:00
08b239e87a Change some table type from TEXT to JSONB ()
* Change some table type from TEXT to JSONB

* Deprecate mysql and sqlite backend
2022-11-28 20:36:18 +09:00
84132e794a POS: Fix manifest ()
- Manifest v1 doesn't support HEX colors
- Make icon URLs absolute

Closes .
2022-11-28 20:35:52 +09:00
425d70f261 Add Greenfield invoice refund endpoint ()
* Add Greenfield invoice refund endpoint

See discussion here: https://github.com/btcpayserver/btcpayserver/discussions/4181

* add test

* add docs
2022-11-28 17:53:08 +09:00
420954ed00 Add metadata to invoice webhook event ()
close 
2022-11-28 17:50:52 +09:00
45edd330f5 Fix logos when rootPath is used ()
* Fix logos when rootPath is used

* Fix close buttons used in JS

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2022-11-28 09:25:18 +01:00
6a0e2bcad3 Adjust currency name to be standard () 2022-11-28 15:00:35 +09:00
d67d3e0167 Small README changes () 2022-11-27 23:35:29 +09:00
031bb7b224 Merge branch 'master' into plugins 2022-11-27 15:31:39 +01:00
cd4f3d9a66 Fix: Calling GetPayment more than once on aLND client would fail 2022-11-26 22:42:08 +09:00
5c6db35c9b Cleanups () 2022-11-26 13:01:00 +09:00
887bea4328 bump BTCPayServer.Client 2022-11-26 00:22:09 +09:00
def5095d77 Changelog for v1.7 ()
Includes updates from v1.6.12 to a6ee92fbd59a702dd56520126b4aea82d54cfb6c.
2022-11-25 22:37:20 +09:00
ab66662ff6 Update What's New ()
Add v1.7 info and hide the button in case the store isn't set up, yet.

Closes .
2022-11-25 22:31:59 +09:00
2d84433a62 bump 2022-11-25 22:26:29 +09:00
b8e61787d4 Merge pull request from btcpayserver/woirnew
Some adjustment for Forms
2022-11-25 22:22:25 +09:00
669825a35d Ensure redirecturl is local for form builder 2022-11-25 19:28:46 +09:00
31b25ca169 Propagate the ModelState errors on dynamic forms 2022-11-25 18:32:40 +09:00
a6ee92fbd5 Update incorrect "monitoringTime" field for invoice API docs () 2022-11-25 09:12:55 +01:00
5ff1a59a99 Make sure the form is properly validated 2022-11-25 16:11:13 +09:00
4f65eb4d65 Remove dead code, fix dups form value 2022-11-25 15:14:54 +09:00
39328c7368 Rename walletobjects Parent/Child to A/B () 2022-11-25 12:06:57 +09:00
2f5f3e1b51 Do not enable receipts for payment requests ()
Payment requests have a receipt-ish style by default. Receipts for each individual invoice of a payment request can be quite confusing as individually they do not prove the pay request was settled.
2022-11-25 11:04:34 +09:00
022285806b Form Builder ()
* wip

* Cleanups

* UI updates

* Update UIFormsController.cs

* Make predefined forms usable statically

* Add support for pos app + forms

* pay request form rough support

* invoice form through receipt page

* Display form name in inherit from store setting

* Do not request additional forms on invoice from pay request

* fix up code

* move checkoutform id in checkout appearance outside of checkotu v2 toggle

* general fixes for form system

* fix pav bug

* UI updates

* Fix warnings in Form builder ()

* 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

* Fix: If reverse proxy wasn't well configured, and error message should have been displayed ()

* fix monero issue

* Server Settings: Update Policies page ()

Handles the multiple submit buttons on that page and closes .

Contains some UI unifications with other pages and also shows the block explorers without needing to toggle the section via JS.

* Change confirmed to settled. ()

* POS: Fix null pointer

Introduced in , the referenced object needs to be `itemChoice` instead of `choice`.

* Add documentation link to plugins ()

* Add documentation link to plugins

* Minor UI updates

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

* Fix flaky test ()

* Fix flaky test

* Update BTCPayServer/PayoutProcessors/BaseAutomatedPayoutProcessor.cs

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

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

* Remove invoice and store level form

* add form test

* fix migration for forms

* fix

* make pay request form submission redirect to invoice

* Refactor FormQuery to only be able to query single store and single form

* Put the Authorize at controller level on UIForms

* Fix warnings

* Fix ef request

* Fix query to forms, ensure no permission bypass

* Fix modify

* Remove storeId from step form

* Remove useless storeId parameter

* Hide custom form feature in UI

* Minor cleanups

* Remove custom form options from select for now

* More minor syntax cleanups

* Update test

* Add index - needs migration

* Refactoring: Use PostRedirect instead of TempData for data transfer

* Remove untested and unfinished code

* formResponse should be a JObject, not a string

* Fix case for Form type

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
Co-authored-by: JesterHodl <103882255+jesterhodl@users.noreply.github.com>
Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com>
Co-authored-by: Andreas Tasch <andy.tasch@gmail.com>
2022-11-25 10:42:55 +09:00
2d6827dd19 Merge branch 'master' into plugins 2022-11-14 19:05:13 +01:00
5e00bc43d5 Merge branch 'master' into plugins 2022-11-05 12:22:46 +01:00
db8a2930a4 Merge branch 'master' into plugins 2022-11-02 13:44:33 +01:00
7325610ac5 Merge branch 'master' into plugins 2022-10-27 09:17:17 +02:00
36b064b50f Merge branch 'master' into plugins 2022-10-24 17:40:38 +02:00
7ecf6504d0 LNbank: IMprove exception handling 2022-10-08 19:51:39 +02:00
7c971df109 PodServer v0.1.4 2022-10-08 18:14:54 +02:00
c7f6784364 Update changelogs 2022-10-08 18:12:21 +02:00
b816c4462e Merge branch 'master' into plugins 2022-10-08 18:07:35 +02:00
ca750f4152 LNbank: Date display improvements 2022-10-05 11:34:37 +02:00
6be968f7bf PodServer: Fix publish date in feed
Fixes .
2022-10-05 11:34:37 +02:00
89cd1079fe Merge branch 'master' into plugins 2022-10-04 18:48:07 +02:00
fa5470c587 LNbank v1.3.4 2022-09-30 22:47:40 +02:00
820421fcbc PodServer v0.1.3 2022-09-30 22:44:40 +02:00
a4ab2dcd84 Merge branch 'lnurl-flow' into plugins 2022-09-30 22:43:06 +02:00
d14f4f8173 PodServer: Fix podcast lookup on public pages
Fixes .
2022-09-30 22:42:14 +02:00
386c18b897 Minor Send and Receive view improvements 2022-09-30 18:09:07 +02:00
5d98fe7b34 LNbank: Improve LNURL payment flow 2022-09-30 18:04:23 +02:00
2008d15dda LNbank: Improve invoice canceling and invalidating 2022-09-29 22:10:45 +02:00
d206a88e3a PodServer v0.1.2 2022-09-29 18:55:40 +02:00
bbce88f269 PodServer: Fix permissions
Fixes .
2022-09-29 18:54:40 +02:00
edbfafcaf2 PodServer v0.1.1 2022-09-29 18:07:49 +02:00
b64ddab08b LNbank: Invalidate unresolved payments 2022-09-29 16:11:29 +02:00
c34b4d64e5 Use LND as default LN node; bind to 0.0.0.0 2022-09-29 14:44:03 +02:00
90308ce6b9 LNbank v1.3.3 2022-09-29 14:37:22 +02:00
a3b702c360 Merge branch 'master' into plugins 2022-09-29 14:32:26 +02:00
4f9820ae1b Update bug report template 2022-09-29 14:29:43 +02:00
6884db431f LNbank: Fix setting LNbank wallet in WebKit-based browsers
Fixes .
2022-09-29 14:29:22 +02:00
349dbdc85d LNbank: Refactor Lightning invoice watcher 2022-09-29 13:53:38 +02:00
dc795311fa PodServer: Fix public podcast page without episodes
Fixes .
2022-09-29 13:29:11 +02:00
7ff2f58f34 LNbank: Break long words in description text 2022-09-29 13:28:37 +02:00
c736babd40 LNbank: Fix send error in LNDhub API 2022-09-29 13:28:13 +02:00
b5174dcbcd PodServer: Fix public podcast page without episodes
Fixes .
2022-09-28 14:25:29 +02:00
ffe40daaa6 Merge branch 'master' into plugins 2022-09-28 14:08:36 +02:00
43649026c1 LNbank v1.3.2 2022-09-27 17:38:01 +02:00
84212de830 Update docs 2022-09-27 17:35:54 +02:00
d958ef2ec7 LNbank: Update changelog 2022-09-27 15:53:55 +02:00
b60dcb6927 LNbank: Add invoices API for Lightning client 2022-09-27 15:53:29 +02:00
9af32e7089 LNbank: Upgrade LNDhub integration
Fixes .
2022-09-27 15:53:29 +02:00
7b10067ce5 Remove unused icon sprites 2022-09-27 14:17:14 +02:00
7746e5beef LNbank v1.3.1 2022-09-26 20:00:59 +02:00
38fd1d338b LNbank: Update changelog 2022-09-26 19:58:42 +02:00
b0e4186733 LNbank: Fix LNURL metadata
Fixes .
2022-09-26 19:56:39 +02:00
c6c0a7b6ac LNbank: Potential migration fix
Might fix .
2022-09-26 19:50:40 +02:00
f2e35529e9 PodServer v0.1.0 2022-09-26 17:46:38 +02:00
1deddbcf14 PodServer: Update changelog and release script 2022-09-26 17:42:47 +02:00
c14f26340e LNbank v1.3.0 2022-09-26 17:37:04 +02:00
ab5497eb3d LNbank: Update changelog 2022-09-26 17:29:32 +02:00
1b1fdd4360 LNbank: Add custom invoice expiry 2022-09-26 17:29:21 +02:00
fbc4dde974 LNbank: Expire invoices explicitely 2022-09-26 16:50:47 +02:00
4d1f8c1d16 LNbank: Update LightningInvoiceWatcher logging and error handling 2022-09-26 16:28:06 +02:00
524ad8393d Bump BTCPay version requirements 2022-09-26 16:00:40 +02:00
0def52416d Merge branch 'master' into plugins 2022-09-26 15:57:19 +02:00
375405ba51 Merge branch 'master' into plugins 2022-09-26 10:38:22 +02:00
9ce1ba0019 Merge branch 'master' into plugins 2022-09-20 13:36:20 +02:00
ff6043cf67 PodServer: Refactoring: Services and repos 2022-09-20 13:35:58 +02:00
e6e3efb709 Merge branch 'master' into plugins 2022-09-20 13:03:56 +02:00
ec029fa5cd PodServer: Add editors and policies 2022-09-19 15:59:58 +02:00
5f9926896a Merge branch 'master' into plugins 2022-09-16 11:57:28 +02:00
e68cb1805f Merge branch 'master' into plugins 2022-09-13 20:37:16 +02:00
f554478254 PodServer: Feed updates 2022-09-13 20:35:29 +02:00
45a8852799 PodServer: View updates 2022-09-13 20:35:14 +02:00
92dae8dff6 PodServer: Import updates 2022-09-13 20:34:52 +02:00
beb58cb90b PodServer: Minor view updates 2022-09-13 16:04:47 +02:00
f379593a2f PodServer: Various contribution-related updates 2022-09-12 18:05:00 +02:00
15ddf9d619 Merge branch 'master' into plugins 2022-09-12 14:57:17 +02:00
82b479cc7c Merge branch 'lnbank-lndhub' into plugins 2022-09-09 12:57:47 +02:00
ba2db52859 Updates for Zeus compatibility
See https://github.com/ZeusLN/zeus/tree/master/stores
2022-09-09 12:36:46 +02:00
8f5af29f23 Updates for Alby compatibility
See https://github.com/getAlby/lightning-browser-extension/blob/master/src/extension/background-script/connectors/lndhub.ts
2022-09-09 12:36:46 +02:00
7b5049099c Use models from LNDhub library 2022-09-09 12:36:46 +02:00
5d37f31d72 LNbank: Add LNDhub compatibility
Closes .
2022-09-09 12:36:46 +02:00
05aac2801c Merge branch 'master' into plugins 2022-09-09 12:36:24 +02:00
160a19f6c7 PodServer: Update public pages 2022-09-08 17:32:04 +02:00
afedb48eeb Merge branch 'master' into plugins 2022-08-31 19:15:57 +02:00
25716c8ab7 Merge branch 'master' into plugins 2022-08-29 18:05:45 +02:00
61c9466374 PodServer: Various updates 2022-08-26 19:48:09 +02:00
4b3d7fd004 PodServer: Contribution basics 2022-08-26 17:01:09 +02:00
e8657da952 PodServer: Update image upload UI 2022-08-25 20:02:38 +02:00
b4e3c8f0b5 Merge branch 'master' into plugins 2022-08-25 19:28:21 +02:00
405def199f PodServer: Feed updates 2022-08-23 18:19:40 +02:00
ea9fe8386d PodServer: Public pages updates 2022-08-22 17:38:18 +02:00
5dd8b3701f Merge branch 'master' into plugins 2022-08-22 12:09:19 +02:00
e6b4b83bc0 Merge branch 'master' into plugins 2022-08-19 19:17:21 +02:00
253cfda4b4 PodServer: Add value recipient form 2022-08-19 19:17:05 +02:00
f03063e390 PodServer: Add slug to podcast and episode 2022-08-19 17:41:43 +02:00
5e295bab89 PodServer: Various updates 2022-08-18 17:25:32 +02:00
92cf26dc3b LNbank: Minor style updates 2022-08-18 12:03:25 +02:00
a3ed557244 PodServer: Add README 2022-08-17 10:18:19 +02:00
475f16b280 LNbank: Fix typos in README 2022-08-17 10:18:09 +02:00
b3b54ccaf7 Merge branch 'master' into plugins 2022-08-17 09:41:58 +02:00
07cba914bb LNbank: Add README with some documentation 2022-08-16 22:10:20 +02:00
7e569c044f LNbank: Update access keys; add test 2022-08-16 13:21:43 +02:00
7bb32e3fdf LNbank: Update LNURL controller 2022-08-15 19:25:45 +02:00
8aea701f6c Merge branch 'lnbank-lnurl' into plugins 2022-08-15 13:31:28 +02:00
177628d05b LNbank: Handle payment API errors 2022-08-15 13:31:17 +02:00
fe644bb979 LNbank: Send to LNURL 2022-08-15 10:10:28 +02:00
70168f73ce Merge branch 'master' into plugins 2022-08-15 10:10:18 +02:00
5a27155984 Merge branch 'master' into plugins 2022-08-10 17:26:12 +02:00
b38c05f33f Merge branch 'master' into plugins 2022-08-09 22:59:07 +02:00
2ff1c80661 Merge branch 'master' into plugins 2022-08-08 17:07:04 +02:00
6f7bce216c LNbank: Custom policies refactoring 2022-08-02 17:57:57 +02:00
f7981b2b12 Merge branch 'master' into plugins 2022-08-02 12:29:22 +02:00
2b81ea5040 LNbank: Add custom authorization policies 2022-08-02 09:32:34 +02:00
733dc7561a Merge branch 'master' into plugins 2022-08-02 09:08:37 +02:00
c323ef51fa LNbank: Owner stays admin, even without access key 2022-07-22 12:30:50 +02:00
4d97de8208 Merge branch 'master' into plugins 2022-07-22 12:19:04 +02:00
dd99fec838 LNbank: AccessKeys refactoring 2022-07-17 22:36:58 +02:00
539352f598 Merge branch 'master' into plugins 2022-07-15 09:40:43 +02:00
a63288fce8 LNbank: Split WalletService and WalletRepository 2022-07-14 18:23:04 +02:00
0f57c09aff LNbank: Manage access keys for wallet access 2022-07-13 20:35:52 +02:00
15068c1398 Move ConfirmModel to Abstractions
To make it available to plugins.
2022-07-12 14:44:07 +02:00
6ef4822ad2 LNbank: Update on model creating calls 2022-07-11 12:34:55 +02:00
f2f3e472b0 LNbank: Extend access keys; add migration runner 2022-07-11 12:34:26 +02:00
57e42df769 LNbank v1.2.3 2022-07-08 16:12:13 +02:00
0918c79305 Update release scripts 2022-07-08 16:10:49 +02:00
ef0f61dafa LNbank: Update BTCPay version requirement 2022-07-08 16:02:49 +02:00
4e1c841615 LNbank: Watcher should not invalidate on null 2022-07-08 16:02:09 +02:00
713e0bf5e5 Merge branch 'master' into plugins 2022-07-08 16:00:25 +02:00
aa6f41bace Improve send error handling 2022-07-07 15:55:31 +02:00
d44c06320e Merge branch 'master' into plugins 2022-07-06 16:39:02 +02:00
0d2603dd5a Merge branch 'master' into plugins 2022-07-06 13:12:48 +02:00
c87a4511cd LNbank: Send improvements 2022-07-04 19:00:59 +02:00
3b61be1b82 Merge branch 'master' into plugins 2022-07-04 16:37:34 +02:00
28d05edd96 Merge branch 'master' into plugins 2022-06-28 10:45:01 +02:00
21d159d5f9 Merge branch 'master' into plugins 2022-06-21 12:05:14 +02:00
d2d02e18e7 LNbank: Add API endpoint for balance check 2022-06-20 18:06:29 +02:00
47208db8db Merge branch 'master' into plugins 2022-06-20 07:06:26 +02:00
ea6ad0995f LNbank: Update BTCPay version requirement 2022-06-16 15:06:24 +02:00
0c1b474c35 Merge branch 'master' into plugins 2022-06-16 15:05:16 +02:00
0e90f1219c Merge branch 'master' into plugins 2022-06-07 12:58:17 +02:00
abb21c68cb Merge branch 'master' into plugins 2022-06-02 17:27:33 +02:00
9c1404c261 LNbank: Update logging 2022-05-31 15:18:09 +02:00
ceb722c71f LNbank: Validate invoice expiry 2022-05-31 13:39:51 +02:00
d863dfa0c8 Greenfield: Fix GetDepositAddress return type
The local clients GetFromActionResult cannot handle the JValue return type, because it gets invoked with GetFromActionResult<string>.
2022-05-31 10:58:29 +02:00
0cc849be91 LNbank: Map explicit LightningInvoiceStatuses 2022-05-31 10:27:48 +02:00
110524391b LNbank: Allow creation of zero amount invoices 2022-05-31 10:27:38 +02:00
d452c1e578 LNbank: Improve API responses 2022-05-31 10:27:25 +02:00
86fead2809 Update release scripts 2022-05-30 13:31:56 +02:00
6c80ca1d18 Merge branch 'master' into plugins 2022-05-30 12:55:03 +02:00
aa742b3079 LNbank v1.2.2 2022-05-30 11:28:12 +02:00
01eb93bc43 Merge branch 'master' into plugins 2022-05-30 09:01:17 +02:00
e6d29afed6 LNbank: Update changelog and version requirement 2022-05-30 09:00:21 +02:00
3f88ea1e15 LNbank: Improve API response 2022-05-26 22:04:44 +02:00
409b6df8a2 Merge branch 'master' into plugins 2022-05-25 14:03:48 +02:00
8304afd36c LNbank: Fix routing fee accessor 2022-05-25 13:50:10 +02:00
56400dcc27 Merge branch 'master' into plugins 2022-05-25 13:46:18 +02:00
d59ab2ef01 Merge branch 'master' into plugins 2022-05-24 15:45:39 +02:00
458f8979fb Merge branch 'master' into plugins 2022-05-19 17:04:42 +02:00
ca7b306dea LNbank: Update changelog 2022-05-19 16:33:57 +02:00
f509b937fd LNbank: Improve test 2022-05-19 16:33:41 +02:00
19c75f51bd LNbank: Distinguish original invoice amount and actual amount settled 2022-05-19 16:04:18 +02:00
ca821a643e LNbank: Update hold invoice handling 2022-05-19 15:59:00 +02:00
d8d3172b17 LNbank: Zero amount invoice handling fixes 2022-05-19 14:48:37 +02:00
14ce5dd41f LNbank: Use info log instead of debug in invoice watcher 2022-05-19 11:59:27 +02:00
505b1d8310 Update project settings 2022-05-19 11:49:18 +02:00
c60a95a6fe Merge branch 'master' into plugins 2022-05-18 15:49:52 +02:00
63119c63f1 LNbank: Require BTCPay Server v1.5.3 2022-05-18 08:38:26 +02:00
c5f4db4df6 Merge branch 'plugins-expl-amt' into plugins 2022-05-18 08:21:09 +02:00
77932f6e20 LNbank: Allow explicit amount for zero amount invoices
Fixes 
2022-05-18 08:20:49 +02:00
2d67e0e75c Merge branch 'master' into plugins 2022-05-18 08:12:00 +02:00
db8b7a4621 PodServer: Public pages 2022-05-12 09:18:01 +02:00
a6ac322246 LNbank: Public wallet LNURL page for sharing 2022-05-11 16:47:39 +02:00
8b5ea9fd56 Merge branch 'master' into plugins 2022-05-11 14:15:13 +02:00
d9aa8f7dbc Merge branch 'master' into plugins 2022-05-04 14:57:23 +02:00
5341b4eb08 LNbank v1.2.1 2022-04-30 20:52:51 +02:00
935577de71 LNbank: Update changelog 2022-04-30 20:51:13 +02:00
3d15ff31f5 LNbank: Fix test 2022-04-30 20:48:33 +02:00
e68942056d LNbank: More hold invoice updates 2022-04-30 20:27:54 +02:00
eff872148c LNbank: Nullify properties when cancelling transactions 2022-04-30 20:27:38 +02:00
3692602b15 LNbank: Trim payment requests 2022-04-30 20:26:44 +02:00
3de38298b6 LNbank: Autofocus input fields 2022-04-30 20:06:26 +02:00
eb7d8694f5 Merge branch 'master' into plugins 2022-04-30 19:00:50 +02:00
8e53b3c9f0 Merge branch 'master' into plugins 2022-04-29 16:05:49 +02:00
6a714e6d9c LNbank: Hold invoice handling improvements 2022-04-28 22:13:17 +02:00
f227b9aec5 Merge branch 'master' into plugins 2022-04-28 22:13:03 +02:00
a741ad446c LNbank: Refresh transactions list on update 2022-04-27 12:57:14 +02:00
e4d45798f1 PodServer: View Refactoring 2022-04-27 12:56:55 +02:00
e5b4f2a399 Merge branch 'master' into plugins 2022-04-26 21:00:03 +02:00
55fb4ec233 LNbank: Handle hold invoices 2022-04-26 20:58:56 +02:00
c5b4b1b9e6 Merge branch 'plugins-hodlinvoice' into plugins 2022-04-26 14:25:13 +02:00
ad6effa3bb PodServer: Introduce Editor 2022-04-26 14:24:05 +02:00
c5e48eb975 Add and pass cancellation tokens 2022-04-26 11:04:59 +02:00
675bb63d14 Set PodServer version 2022-04-26 11:04:35 +02:00
5c64a4ac40 Use Safe.Json to output values 2022-04-26 11:03:37 +02:00
0adb2cf233 Merge branch 'master' into plugins 2022-04-26 06:59:22 +02:00
316ed2a9bb PodServer: Add medium 2022-04-25 17:38:12 +02:00
cea2a3f513 PodServer: Add player basics 2022-04-25 17:13:31 +02:00
5d01e07e2d Separate plugin views strictly 2022-04-25 16:53:54 +02:00
ee0547448e Merge branch 'master' into plugins 2022-04-25 10:44:11 +02:00
08580eb244 Update changelog 2022-04-22 17:08:05 +02:00
e127478c2e Handle cancelled invoices in watcher 2022-04-22 17:04:29 +02:00
9b7bd57cb1 Merge branch 'master' into plugins 2022-04-22 14:08:33 +02:00
f771b6dd70 LNbank: Add methods to get payment info 2022-04-21 10:24:45 +02:00
f4ed031f99 Allow for empty description when creating invoices 2022-04-20 22:12:06 +02:00
b7d8077467 LNbank: Log send and receive exceptions 2022-04-20 22:04:58 +02:00
8197375845 Merge branch 'master' into plugins 2022-04-20 08:38:29 +02:00
74a3c6739c PodServer: UI updates 2022-04-19 17:34:04 +02:00
392023219b Merge branch 'master' into plugins 2022-04-19 15:21:21 +02:00
97420dfd3e Add Buchhalter plugin basics 2022-04-16 21:53:10 +02:00
30d1bcfa4d Merge branch 'master' into plugins 2022-04-15 20:20:34 +02:00
36b5a13d1c Merge branch 'master' into plugins 2022-04-08 14:39:47 +02:00
0dadac914e LNbank v1.2.0 2022-03-31 20:46:47 +02:00
3a76bebcbc Prepare v1.2.0 2022-03-31 20:45:04 +02:00
53f46d4b8d Simplify Swagger provider 2022-03-31 20:44:38 +02:00
5911b998b5 Export for accounting
Closes .
2022-03-31 20:41:06 +02:00
45885c5137 Display LNURL on Receive page 2022-03-31 17:34:30 +02:00
1b5a7ed1e1 Use existing AddFile method 2022-03-31 14:19:47 +02:00
9be0810fb7 Merge branch 'master' into plugins 2022-03-31 14:17:17 +02:00
a3efae4cc5 Add LNbank Greenfield API docs 2022-03-31 09:36:50 +02:00
2141eafa37 Importer 2022-03-30 18:28:37 +02:00
b403f6f1e8 Merge branch 'master' into plugins 2022-03-30 11:27:45 +02:00
0055746be6 Import service 2022-03-24 17:44:29 +01:00
2885335780 Form updates 2022-03-21 21:16:54 +01:00
d1524a5e5d Add import jobs 2022-03-21 21:02:23 +01:00
775101a31f Merge branch 'master' into plugins 2022-03-21 14:02:00 +01:00
48a0ed7b98 Update wallets list 2022-03-19 07:46:20 +01:00
7843b6173c Update plugin navigations 2022-03-19 07:45:59 +01:00
4446d506e9 Update LNbank nav icon 2022-03-18 20:16:15 +01:00
7b824d5ec0 LNbank: Support LNURL Pay for wallets
Closes .
2022-03-18 17:00:36 +01:00
967c91f711 Merge branch 'master' into plugins 2022-03-18 15:41:52 +01:00
f7271f6ffb Merge branch 'podserver' into plugins 2022-03-18 15:41:28 +01:00
0b83d2b859 Add feed importer 2022-03-18 15:41:06 +01:00
fc4a7a26fe Add people and seasons management 2022-03-18 12:56:18 +01:00
f6e3ca32fb Improve syntax in wallet model 2022-03-17 14:48:13 +01:00
0a3b4f5d61 Add enclosures 2022-03-14 22:57:39 +01:00
60d8dd81a4 Add richtext editor tofor episode description 2022-03-14 15:05:08 +01:00
0e2a7b5efd LNbank: API for accessing, updating and deleting wallets 2022-03-14 13:46:26 +01:00
a664937526 Merge branch 'master' into plugins 2022-03-14 10:22:10 +01:00
00354de289 Merge branch 'master' into plugins 2022-03-11 10:23:16 +01:00
c70f58fd70 LNbank v1.1.1 2022-03-09 15:52:49 +01:00
21c08acfd5 Update changelog and release script 2022-03-09 15:51:57 +01:00
f38432e15e Handle paid but unsettled payments 2022-03-09 15:44:53 +01:00
58309dd7aa Fix redirects
Fixes 
2022-03-09 15:44:53 +01:00
59017e77eb Update Lightning test settings 2022-03-09 13:15:53 +01:00
be323ba147 Merge branch 'master' into plugins 2022-03-08 13:30:06 +01:00
f3350bcdbf Merge branch 'master' into plugins 2022-03-08 09:28:02 +01:00
79df9a027a Remove old wallet deletion code 2022-03-07 18:13:46 +01:00
3fe981d6f5 Handle errors when checking pending transactions 2022-03-07 18:13:12 +01:00
80265b56b8 API updates 2022-03-07 10:58:56 +01:00
90ce5acf99 Fix transaction hub IDs 2022-03-04 08:43:12 +01:00
eef2780e70 Fix redirect path after creating an invoice 2022-03-03 17:40:15 +01:00
cedb04cc42 Use store invoice expiry time 2022-03-03 17:39:23 +01:00
1fd8f48bda PodServer: File upload 2022-03-03 15:17:36 +01:00
4f63ca4af4 Refactoring: Extract extensions
Refactoring: Extract HttpRequest extensions

Refactoring: Extract ITempDataDictionary extensions


Fixes


Try test fix
2022-03-03 15:17:36 +01:00
6d72d7f6de Update issue template 2022-03-02 15:44:19 +01:00
78e4cb868d Merge branch 'master' into plugins 2022-03-02 12:44:08 +01:00
560117fe59 Merge branch 'master' into plugins 2022-03-01 18:32:27 +01:00
ee0e199add LNbank wording and message updates 2022-02-28 18:32:07 +01:00
fb48b9fa52 PodServer UI basics 2022-02-28 18:31:50 +01:00
c888a845ab Clean up project files 2022-02-25 11:56:15 +01:00
c3e43cb5b3 Move PodServer and add data model 2022-02-25 11:44:53 +01:00
9a855deca4 Add LNbank API for creating wallets
Closes 
2022-02-24 09:02:00 +01:00
41bdbd784d Refactoring: Allow GreenfieldExtensions to be used by plugins 2022-02-24 09:00:44 +01:00
e2e87652e1 Soft delete wallets 2022-02-23 18:35:51 +01:00
c29b1be070 Add PodServer basics 2022-02-21 23:03:57 +01:00
82fc59cc76 LNbank refactorings 2022-02-21 23:03:38 +01:00
1f1f2ca819 Improve release script 2022-02-21 18:49:59 +01:00
d43119e4ac Prepare release 2022-02-21 18:41:03 +01:00
2b4c3e68b1 Prevent deletion of wallet with balance; refactor messages.
Closes 
2022-02-21 18:15:34 +01:00
a35e8fa69a Merge branch 'master' into plugins 2022-02-21 16:41:11 +01:00
e855441455 Common wallet header 2022-02-21 16:32:15 +01:00
62f426fea8 Cleanups 2022-02-21 14:20:34 +01:00
094db3dab9 Remove transaction details page 2022-02-21 14:20:19 +01:00
14770d7a6b Separate wallets list and wallet detail views 2022-02-21 13:12:33 +01:00
ac0722246b Merge branch 'master' into plugins 2022-02-21 10:32:44 +01:00
14785278ec Improve E2E test 2022-02-18 21:59:24 +01:00
359247542e Proper redirects on homepage 2022-02-18 21:04:06 +01:00
e028f5d0c1 Toggle for attaching description to pay request when receiving 2022-02-18 20:44:13 +01:00
66c90ae5f7 Customize description when sending 2022-02-18 20:07:24 +01:00
72c876b62b Lightning payment info and fee handling 2022-02-18 19:46:53 +01:00
89204f2256 Merge branch 'master' into plugins 2022-02-18 12:23:43 +01:00
0580e5bf9b Add helper for sats values 2022-02-15 13:21:44 +01:00
6bfcb02a8c Merge branch 'master' into plugins 2022-02-14 18:22:50 +01:00
b9d73415a4 Prevent paying requests multiple times 2022-02-11 15:36:00 +01:00
beaac40222 Improve wallet headings and title 2022-02-11 10:13:19 +01:00
17bd4a3d9c Allow for empty description
Closes 
2022-02-11 10:11:59 +01:00
ce61a07111 Update CHANGELOG and release script 2022-02-10 16:19:51 +01:00
cadd197dd9 LNbank v1.0.4 2022-02-10 15:54:50 +01:00
f97275ae16 Add CHANGELOG and update release script 2022-02-10 15:51:05 +01:00
6845c511c6 Merge branch 'master' into lnbank 2022-02-10 12:26:41 +01:00
34c482a991 Lowercase page paths 2022-02-09 16:27:20 +01:00
250762c758 UI updates
Remove button icons, improve wallet view
2022-02-09 16:20:26 +01:00
06df50c5d0 Add support for private route hints
Will be enabled if the connected store has the required setting or if the toggle on the receive page is activated.

Closes 
2022-02-09 16:08:44 +01:00
240cb72d24 Improve Lightning test scripts 2022-02-09 15:24:17 +01:00
22678d2b50 Share page fixes 2022-02-09 15:23:52 +01:00
1f593d0710 Merge branch 'master' into lnbank 2022-02-07 11:21:38 +01:00
ec41e806dd Add release script 2022-02-01 15:15:33 +01:00
bac5ad4bbc LNbank v1.0.3 2022-02-01 12:47:05 +01:00
28ae60faf3 Add Selenium test 2022-02-01 12:42:36 +01:00
c9544b22d1 Improve form validation 2022-02-01 12:02:25 +01:00
01d7ff2525 Omit userId when using the local API client
Otherwise non-admins cannot send and receive when using the internal node. Thanks @kukks.

Fixes .
Fixes .
Fixes .
2022-02-01 10:15:53 +01:00
4f67a443c5 Improve create wallet for LN node connection case 2022-01-31 13:51:57 +01:00
1f333058c9 Merge branch 'master' into lnbank 2022-01-31 12:10:15 +01:00
6ef90503df Merge branch 'master' into lnbank 2022-01-27 15:57:12 +01:00
cece43a921 LNbank v1.0.2 2022-01-27 11:01:46 +01:00
8aae72f63e Improve layout 2022-01-27 10:47:14 +01:00
d3df78e71b Update headlines 2022-01-27 09:58:50 +01:00
4f6bd1a523 Remove connection string display 2022-01-27 09:34:00 +01:00
4b2d70e3fa Add policy (and store context); use file-scoped namespaces 2022-01-27 09:31:14 +01:00
ab729b0f7c LNbank v1.0.1
Integrate existing LNbank app


Fix db column types

Refactor copy to clipboard

General updates


Set plugin category active

More updates


Remove custom QR controller, reuse BTCPay one


Make transaction hub work


Cleanup WalletService


Configure network properly


Hide connection string info for now


Show first wallet by default


Add LNbank icon sprite


Fix payment request code styles


Remove BOLT11 string and fix QR code display


Upgrade SignalR client


Allow unauthenticated connection to hub (for share page)


Re-add initial API version


Remove custom authorization

Leftover from standalone app

add packer script

Support descriptionhash for lnurl

Refactor Auth

fix conn string display

add cancel invoice endpoint

deep integration

Improve date/time display


UI improvements


Improve LN setup integration


Add bash version of pack script


Add plugin description


Declare BTCPayServer version dependency


Updates for new layout


Update project info


Update active page function


Fix log warning


Upgrade LNbank to net6.0
2022-01-27 08:48:57 +01:00
138 changed files with 4353 additions and 2445 deletions
.github/ISSUE_TEMPLATE
.gitmodules
BTCPayServer.Abstractions
BTCPayServer.Client
BTCPayServer.Data
BTCPayServer.Rating
BTCPayServer.Tests
BTCPayServer
BTCPayServer.csproj
Components
Configuration
Controllers
Data
DerivationSchemeSettings.cs
Forms
HostedServices
Hosting
Models/PaymentRequestViewModels
PaymentRequest
PayoutProcessors
Plugins/PointOfSale/Controllers
Properties
Services
Storage/Services/Providers/FileSystemStorage
Views
wwwroot
Build
Changelog.md
Plugins
README.mdbtcpayserver.sln

58
.github/ISSUE_TEMPLATE/bug-report.md vendored Normal file

@ -0,0 +1,58 @@
---
name: "\U0001F41B Bug report"
about: Report a bug or a technical issue
---
<!--
Thank you for reporting a technical issue with one of my BTCPay Server plugins, like LNbank or PodServer.
For general issues with BTCPay Server please visit https://github.com/btcpayserver/btcpayserver/issues
General support is available on our community chat chat.btcpayserver.org
Please fill in as much of the template below as you're able.
-->
**Plugin**
Name and version of the plugin. <!--[available on the Server Settings > Plugins page] -->
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce the bug**
Steps to reproduce the reported bug:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
<!--
A clear and concise description of what you expected to happen.
-->
**Screenshots**
<!--
If applicable, add screenshots to help explain your problem.
-->
**Your BTCPay Environment (please complete the following information):**
- BTCPay Server Version: <!--[available in the right bottom corner of footer] -->
- Lightning implementation <!--[e.g. LND, Core Lightning] -->
- Deployment Method: <!--[e.g. Docker, Manual, Third-Party-host]-->
- Browser: <!--[e.g. Chrome, Safari]-->
**Logs (if applicable)**
<!--
If you are using the Docker setup, please post the output of the following command:
docker logs generated_btcpayserver_1
Otherwise, basic logs can be found in Server Settings > Logs.
More logs https://docs.btcpayserver.org/Troubleshooting/#2-looking-through-the-logs
-->

@ -1,68 +0,0 @@
name: 🐛 Bug Report
description: File a bug report
title: "[Bug]: "
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report! Please provide as much information as you can. It helps us better understand the problem and fix it faster.
- type: textarea
id: version
attributes:
label: What is your BTCPay version?
description: You can see the version in the footer's bottom right corner
placeholder: I'm running BTCPay v1.X.X.X
validations:
required: true
- type: textarea
id: deployment
attributes:
label: How did you deploy BTCPay Server?
description: Docker, manual, third-party host? Read more on deployment methods [here](https://docs.btcpayserver.org/Deployment/)
placeholder: I'm running BTCPay Server on a...
validations:
required: true
- type: textarea
id: what-happened
attributes:
label: What happened?
description: A clear and concise description of what the bug is.
placeholder: Tell us what you see!
validations:
required: true
- type: textarea
id: reproduce
attributes:
label: How did you encounter this bug?
description: Step by step describe how did you encounter the bug?
placeholder: 1. I clicked X 2. Then I clicked Y 3. See error
validations:
required: true
- type: textarea
id: logoutput
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. Logs can be found in Server Settings > Logs. Here's how you can [troubleshoot an issue](https://docs.btcpayserver.org/Troubleshooting/)
render: shell
- type: textarea
id: browser
attributes:
label: What browser do you use?
description: Provide your browser and it's version. If you replicated issues on multiple browsers, let us know which ones.
placeholder: For example Safari 15.00, Chrome 10.0, Tor, Edge, etc
validations:
required: false
- type: textarea
id: additonal
attributes:
label: Additional information
description: Feel free to provide additional information. Screenshots are always helpful.
- type: checkboxes
id: terms
attributes:
label: Are you sure this is a bug report?
description: By submitting this report, you agree that this is not a support or a feature request. For general questions please read our [documentation](https://docs.btcpayserver.org). You can ask questions in [discussions](https://github.com/btcpayserver/btcpayserver/discussions) and [on our community chat](https://chat.btcpayserver.org)
options:
- label: I confirm this is a bug report
required: true

6
.gitmodules vendored Normal file

@ -0,0 +1,6 @@
[submodule "LNbank"]
path = Plugins/BTCPayServer.Plugins.LNbank
url = git@github.com:dennisreimann/btcpayserver-plugin-lnbank.git
[submodule "PodServer"]
path = Plugins/BTCPayServer.Plugins.PodServer
url = git@github.com:dennisreimann/btcpayserver-plugin-podserver.git

@ -0,0 +1,18 @@
using Newtonsoft.Json;
namespace BTCPayServer.Abstractions
{
public class CamelCaseSerializerSettings
{
static CamelCaseSerializerSettings()
{
Settings = new JsonSerializerSettings()
{
ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver()
};
Serializer = JsonSerializer.Create(Settings);
}
public static readonly JsonSerializerSettings Settings;
public static readonly JsonSerializer Serializer;
}
}

@ -2,6 +2,7 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
@ -9,16 +10,50 @@ namespace BTCPayServer.Abstractions.Form;
public class Field
{
public static Field Create(string label, string name, string value, bool required, string helpText, string type = "text")
{
return new Field()
{
Label = label,
Name = name,
Value = value,
OriginalValue = value,
Required = required,
HelpText = helpText,
Type = type
};
}
// The name of the HTML5 node. Should be used as the key for the posted data.
public string Name;
public bool Hidden;
// 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;
public static Field CreateFieldset()
{
return new Field() { Type = "fieldset" };
}
// 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;
public bool Required;
// 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;
[JsonExtensionData] public IDictionary<string, JToken> AdditionalData { get; set; }
public List<Field> Fields { get; set; } = new();
// The field is considered "valid" if there are no validation errors
public List<string> ValidationErrors = new List<string>();
@ -26,9 +61,4 @@ public class Field
{
return ValidationErrors.Count == 0 && Fields.All(field => field.IsValid());
}
[JsonExtensionData] public IDictionary<string, JToken> AdditionalData { get; set; }
public List<Field> Fields { get; set; } = new();
}

@ -1,12 +0,0 @@
namespace BTCPayServer.Abstractions.Form;
public class Fieldset : Field
{
public bool Hidden { get; set; }
public string Label { get; set; }
public Fieldset()
{
Type = "fieldset";
}
}

@ -2,12 +2,24 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Abstractions.Form;
public class Form
{
#nullable enable
public static Form Parse(string str)
{
ArgumentNullException.ThrowIfNull(str);
return JObject.Parse(str).ToObject<Form>(CamelCaseSerializerSettings.Serializer) ?? throw new InvalidOperationException("Impossible to deserialize Form");
}
public override string ToString()
{
return JObject.FromObject(this, CamelCaseSerializerSettings.Serializer).ToString(Newtonsoft.Json.Formatting.Indented);
}
#nullable restore
// 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();
@ -17,7 +29,7 @@ public class Form
// Are all the fields valid in the form?
public bool IsValid()
{
return Fields.All(field => field.IsValid());
return Fields.Select(f => f.IsValid()).All(o => o);
}
public Field GetFieldByName(string name)
@ -52,6 +64,7 @@ public class Form
}
return null;
}
public List<string> GetAllNames()
{
return GetAllNames(Fields);

@ -1,27 +0,0 @@
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.
}

@ -1,5 +1,12 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
@ -8,7 +15,7 @@ namespace BTCPayServer.Abstractions.TagHelpers;
// Make sure that <svg><use href=/ are correctly working if rootpath is present
[HtmlTargetElement("use", Attributes = "href")]
public class SVGUse : UrlResolutionTagHelper
public class SVGUse : UrlResolutionTagHelper2
{
private readonly IFileVersionProvider _fileVersionProvider;
@ -21,5 +28,6 @@ public class SVGUse : UrlResolutionTagHelper
var attr = output.Attributes["href"].Value.ToString();
attr = _fileVersionProvider.AddFileVersionToPath(ViewContext.HttpContext.Request.PathBase, attr);
output.Attributes.SetAttribute("href", attr);
}
base.Process(context, output);
}
}

@ -0,0 +1,314 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace BTCPayServer.Abstractions.TagHelpers
{
// A copy of https://github.com/dotnet/aspnetcore/blob/39f0e0b8f40b4754418f81aef0de58a9204a1fe5/src/Mvc/Mvc.Razor/src/TagHelpers/UrlResolutionTagHelper.cs
// slightly modified to also work on use tag.
public class UrlResolutionTagHelper2 : TagHelper
{
// Valid whitespace characters defined by the HTML5 spec.
private static readonly char[] ValidAttributeWhitespaceChars =
new[] { '\t', '\n', '\u000C', '\r', ' ' };
private static readonly Dictionary<string, string[]> ElementAttributeLookups =
new(StringComparer.OrdinalIgnoreCase)
{
{ "use", new[] { "href" } },
{ "a", new[] { "href" } },
{ "applet", new[] { "archive" } },
{ "area", new[] { "href" } },
{ "audio", new[] { "src" } },
{ "base", new[] { "href" } },
{ "blockquote", new[] { "cite" } },
{ "button", new[] { "formaction" } },
{ "del", new[] { "cite" } },
{ "embed", new[] { "src" } },
{ "form", new[] { "action" } },
{ "html", new[] { "manifest" } },
{ "iframe", new[] { "src" } },
{ "img", new[] { "src", "srcset" } },
{ "input", new[] { "src", "formaction" } },
{ "ins", new[] { "cite" } },
{ "link", new[] { "href" } },
{ "menuitem", new[] { "icon" } },
{ "object", new[] { "archive", "data" } },
{ "q", new[] { "cite" } },
{ "script", new[] { "src" } },
{ "source", new[] { "src", "srcset" } },
{ "track", new[] { "src" } },
{ "video", new[] { "poster", "src" } },
};
/// <summary>
/// Creates a new <see cref="UrlResolutionTagHelper"/>.
/// </summary>
/// <param name="urlHelperFactory">The <see cref="IUrlHelperFactory"/>.</param>
/// <param name="htmlEncoder">The <see cref="HtmlEncoder"/>.</param>
public UrlResolutionTagHelper2(IUrlHelperFactory urlHelperFactory, HtmlEncoder htmlEncoder)
{
UrlHelperFactory = urlHelperFactory;
HtmlEncoder = htmlEncoder;
}
/// <inheritdoc />
public override int Order => -1000 - 999;
/// <summary>
/// The <see cref="IUrlHelperFactory"/>.
/// </summary>
protected IUrlHelperFactory UrlHelperFactory { get; }
/// <summary>
/// The <see cref="HtmlEncoder"/>.
/// </summary>
protected HtmlEncoder HtmlEncoder { get; }
/// <summary>
/// The <see cref="ViewContext"/>.
/// </summary>
[HtmlAttributeNotBound]
[ViewContext]
public ViewContext ViewContext { get; set; } = default!;
/// <inheritdoc />
public override void Process(TagHelperContext context, TagHelperOutput output)
{
ArgumentNullException.ThrowIfNull(context);
ArgumentNullException.ThrowIfNull(output);
if (output.TagName == null)
{
return;
}
if (ElementAttributeLookups.TryGetValue(output.TagName, out var attributeNames))
{
for (var i = 0; i < attributeNames.Length; i++)
{
ProcessUrlAttribute(attributeNames[i], output);
}
}
// itemid can be present on any HTML element.
ProcessUrlAttribute("itemid", output);
}
/// <summary>
/// Resolves and updates URL values starting with '~/' (relative to the application's 'webroot' setting) for
/// <paramref name="output"/>'s <see cref="TagHelperOutput.Attributes"/> whose
/// <see cref="TagHelperAttribute.Name"/> is <paramref name="attributeName"/>.
/// </summary>
/// <param name="attributeName">The attribute name used to lookup values to resolve.</param>
/// <param name="output">The <see cref="TagHelperOutput"/>.</param>
protected void ProcessUrlAttribute(string attributeName, TagHelperOutput output)
{
ArgumentNullException.ThrowIfNull(attributeName);
ArgumentNullException.ThrowIfNull(output);
var attributes = output.Attributes;
// Read interface .Count once rather than per iteration
var attributesCount = attributes.Count;
for (var i = 0; i < attributesCount; i++)
{
var attribute = attributes[i];
if (!string.Equals(attribute.Name, attributeName, StringComparison.OrdinalIgnoreCase))
{
continue;
}
if (attribute.Value is string stringValue)
{
if (TryResolveUrl(stringValue, resolvedUrl: out string? resolvedUrl))
{
attributes[i] = new TagHelperAttribute(
attribute.Name,
resolvedUrl,
attribute.ValueStyle);
}
}
else
{
if (attribute.Value is IHtmlContent htmlContent)
{
var htmlString = htmlContent as HtmlString;
if (htmlString != null)
{
// No need for a StringWriter in this case.
stringValue = htmlString.ToString();
}
else
{
using var writer = new StringWriter();
htmlContent.WriteTo(writer, HtmlEncoder);
stringValue = writer.ToString();
}
if (TryResolveUrl(stringValue, resolvedUrl: out IHtmlContent? resolvedUrl))
{
attributes[i] = new TagHelperAttribute(
attribute.Name,
resolvedUrl,
attribute.ValueStyle);
}
else if (htmlString == null)
{
// Not a ~/ URL. Just avoid re-encoding the attribute value later.
attributes[i] = new TagHelperAttribute(
attribute.Name,
new HtmlString(stringValue),
attribute.ValueStyle);
}
}
}
}
}
/// <summary>
/// Tries to resolve the given <paramref name="url"/> value relative to the application's 'webroot' setting.
/// </summary>
/// <param name="url">The URL to resolve.</param>
/// <param name="resolvedUrl">Absolute URL beginning with the application's virtual root. <c>null</c> if
/// <paramref name="url"/> could not be resolved.</param>
/// <returns><c>true</c> if the <paramref name="url"/> could be resolved; <c>false</c> otherwise.</returns>
protected bool TryResolveUrl(string url, out string? resolvedUrl)
{
resolvedUrl = null;
var start = FindRelativeStart(url);
if (start == -1)
{
return false;
}
var trimmedUrl = CreateTrimmedString(url, start);
var urlHelper = UrlHelperFactory.GetUrlHelper(ViewContext);
resolvedUrl = urlHelper.Content(trimmedUrl);
return true;
}
/// <summary>
/// Tries to resolve the given <paramref name="url"/> value relative to the application's 'webroot' setting.
/// </summary>
/// <param name="url">The URL to resolve.</param>
/// <param name="resolvedUrl">
/// Absolute URL beginning with the application's virtual root. <c>null</c> if <paramref name="url"/> could
/// not be resolved.
/// </param>
/// <returns><c>true</c> if the <paramref name="url"/> could be resolved; <c>false</c> otherwise.</returns>
protected bool TryResolveUrl(string url, [NotNullWhen(true)] out IHtmlContent? resolvedUrl)
{
resolvedUrl = null;
var start = FindRelativeStart(url);
if (start == -1)
{
return false;
}
var trimmedUrl = CreateTrimmedString(url, start);
var urlHelper = UrlHelperFactory.GetUrlHelper(ViewContext);
var appRelativeUrl = urlHelper.Content(trimmedUrl);
var postTildeSlashUrlValue = trimmedUrl.Substring(2);
if (!appRelativeUrl.EndsWith(postTildeSlashUrlValue, StringComparison.Ordinal))
{
throw new InvalidOperationException();
}
resolvedUrl = new EncodeFirstSegmentContent(
appRelativeUrl,
appRelativeUrl.Length - postTildeSlashUrlValue.Length,
postTildeSlashUrlValue);
return true;
}
private static int FindRelativeStart(string url)
{
if (url == null || url.Length < 2)
{
return -1;
}
var maxTestLength = url.Length - 2;
var start = 0;
for (; start < url.Length; start++)
{
if (start > maxTestLength)
{
return -1;
}
if (!IsCharWhitespace(url[start]))
{
break;
}
}
// Before doing more work, ensure that the URL we're looking at is app-relative.
if (url[start] != '~' || url[start + 1] != '/')
{
return -1;
}
return start;
}
private static string CreateTrimmedString(string input, int start)
{
var end = input.Length - 1;
for (; end >= start; end--)
{
if (!IsCharWhitespace(input[end]))
{
break;
}
}
var len = end - start + 1;
// Substring returns same string if start == 0 && len == Length
return input.Substring(start, len);
}
private static bool IsCharWhitespace(char ch)
{
return ValidAttributeWhitespaceChars.AsSpan().IndexOf(ch) != -1;
}
private sealed class EncodeFirstSegmentContent : IHtmlContent
{
private readonly string _firstSegment;
private readonly int _firstSegmentLength;
private readonly string _secondSegment;
public EncodeFirstSegmentContent(string firstSegment, int firstSegmentLength, string secondSegment)
{
_firstSegment = firstSegment;
_firstSegmentLength = firstSegmentLength;
_secondSegment = secondSegment;
}
public void WriteTo(TextWriter writer, HtmlEncoder encoder)
{
encoder.Encode(writer, _firstSegment, 0, _firstSegmentLength);
writer.Write(_secondSegment);
}
}
}
}

@ -14,7 +14,7 @@
<RepositoryType>git</RepositoryType>
</PropertyGroup>
<PropertyGroup>
<Version Condition=" '$(Version)' == '' ">1.7.0</Version>
<Version Condition=" '$(Version)' == '' ">1.7.1</Version>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<PublishRepositoryUrl>true</PublishRepositoryUrl>
@ -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.15" />
<PackageReference Include="NBitcoin" Version="7.0.14" />
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.3.16" />
<PackageReference Include="NBitcoin" Version="7.0.20" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>
<ItemGroup>

@ -128,5 +128,18 @@ namespace BTCPayServer.Client
method: HttpMethod.Post), token);
await HandleResponse(response);
}
public virtual async Task<PullPaymentData> RefundInvoice(
string storeId,
string invoiceId,
RefundInvoiceRequest request,
CancellationToken token = default
)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/invoices/{invoiceId}/refund", bodyPayload: request,
method: HttpMethod.Post), token);
return await HandleResponse<PullPaymentData>(response);
}
}
}

@ -27,6 +27,6 @@ namespace BTCPayServer.Client.Models
public string FormId { get; set; }
public string FormResponse { get; set; }
public JObject FormResponse { get; set; }
}
}

@ -0,0 +1,27 @@
#nullable enable
using BTCPayServer.JsonConverters;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace BTCPayServer.Client.Models
{
public enum RefundVariant
{
RateThen,
CurrentRate,
Fiat,
Custom
}
public class RefundInvoiceRequest
{
public string? Name { get; set; } = null;
public string? PaymentMethod { get; set; }
public string? Description { get; set; } = null;
[JsonConverter(typeof(StringEnumConverter))]
public RefundVariant? RefundVariant { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal? CustomAmount { get; set; }
public string? CustomCurrency { get; set; }
}
}

@ -1,8 +1,5 @@
using System;
using System.Collections.Generic;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client.Models
{
@ -19,6 +16,7 @@ namespace BTCPayServer.Client.Models
[JsonProperty(Order = 1)] public string StoreId { get; set; }
[JsonProperty(Order = 2)] public string InvoiceId { get; set; }
[JsonProperty(Order = 3)] public JObject Metadata { get; set; }
}
public class WebhookInvoiceSettledEvent : WebhookInvoiceEvent

@ -105,10 +105,10 @@ namespace BTCPayServer.Data
//PlannedTransaction.OnModelCreating(builder);
PullPaymentData.OnModelCreating(builder);
RefundData.OnModelCreating(builder);
//SettingData.OnModelCreating(builder);
SettingData.OnModelCreating(builder, Database);
StoreSettingData.OnModelCreating(builder, Database);
StoreWebhookData.OnModelCreating(builder);
//StoreData.OnModelCreating(builder);
StoreData.OnModelCreating(builder, Database);
U2FDevice.OnModelCreating(builder);
Fido2Credential.OnModelCreating(builder);
BTCPayServer.Data.UserStore.OnModelCreating(builder);

@ -1,3 +1,6 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
namespace BTCPayServer.Data
{
public class SettingData
@ -5,5 +8,15 @@ namespace BTCPayServer.Data
public string Id { get; set; }
public string Value { get; set; }
public static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
{
if (databaseFacade.IsNpgsql())
{
builder.Entity<SettingData>()
.Property(o => o.Value)
.HasColumnType("JSONB");
}
}
}
}

@ -3,6 +3,8 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using BTCPayServer.Client.Models;
using BTCPayServer.Data.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using PayoutProcessorData = BTCPayServer.Data.Data.PayoutProcessorData;
namespace BTCPayServer.Data
@ -36,7 +38,7 @@ namespace BTCPayServer.Data
[NotMapped] public string Role { get; set; }
public byte[] StoreBlob { get; set; }
public string StoreBlob { get; set; }
[Obsolete("Use GetDefaultPaymentId instead")]
public string DefaultCrypto { get; set; }
@ -48,5 +50,15 @@ namespace BTCPayServer.Data
public IEnumerable<PayoutData> Payouts { get; set; }
public IEnumerable<CustodianAccountData> CustodianAccounts { get; set; }
public IEnumerable<StoreSettingData> Settings { get; set; }
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
{
if (databaseFacade.IsNpgsql())
{
builder.Entity<StoreData>()
.Property(o => o.StoreBlob)
.HasColumnType("JSONB");
}
}
}
}

@ -21,42 +21,44 @@ namespace BTCPayServer.Data
public const string PayjoinExposed = "pj-exposed";
public const string Payout = "payout";
public const string PullPayment = "pull-payment";
public const string Script = "script";
public const string Utxo = "utxo";
}
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 List<WalletObjectLinkData> Bs { get; set; }
public List<WalletObjectLinkData> As { get; set; }
public IEnumerable<(string type, string id, string linkdata, string objectdata)> GetLinks()
{
if (ChildLinks is not null)
foreach (var c in ChildLinks)
if (Bs is not null)
foreach (var c in Bs)
{
yield return (c.ChildType, c.ChildId, c.Data, c.Child?.Data);
yield return (c.BType, c.BId, c.Data, c.B?.Data);
}
if (ParentLinks is not null)
foreach (var c in ParentLinks)
if (As is not null)
foreach (var c in As)
{
yield return (c.ParentType, c.ParentId, c.Data, c.Parent?.Data);
yield return (c.AType, c.AId, c.Data, c.A?.Data);
}
}
public IEnumerable<WalletObjectData> GetNeighbours()
{
if (ChildLinks != null)
foreach (var c in ChildLinks)
if (Bs != null)
foreach (var c in Bs)
{
if (c.Child != null)
yield return c.Child;
if (c.B != null)
yield return c.B;
}
if (ParentLinks != null)
foreach (var c in ParentLinks)
if (As != null)
foreach (var c in As)
{
if (c.Parent != null)
yield return c.Parent;
if (c.A != null)
yield return c.A;
}
}

@ -11,14 +11,14 @@ 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 AType { get; set; }
public string AId { get; set; }
public string BType { get; set; }
public string BId { get; set; }
public string Data { get; set; }
public WalletObjectData Parent { get; set; }
public WalletObjectData Child { get; set; }
public WalletObjectData A { get; set; }
public WalletObjectData B { get; set; }
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
{
@ -26,28 +26,28 @@ namespace BTCPayServer.Data
new
{
o.WalletId,
o.ParentType,
o.ParentId,
o.ChildType,
o.ChildId,
o.AType,
o.AId,
o.BType,
o.BId,
});
builder.Entity<WalletObjectLinkData>().HasIndex(o => new
{
o.WalletId,
o.ChildType,
o.ChildId,
o.BType,
o.BId,
});
builder.Entity<WalletObjectLinkData>()
.HasOne(o => o.Parent)
.WithMany(o => o.ChildLinks)
.HasForeignKey(o => new { o.WalletId, o.ParentType, o.ParentId })
.HasOne(o => o.A)
.WithMany(o => o.Bs)
.HasForeignKey(o => new { o.WalletId, o.AType, o.AId })
.OnDelete(DeleteBehavior.Cascade);
builder.Entity<WalletObjectLinkData>()
.HasOne(o => o.Child)
.WithMany(o => o.ParentLinks)
.HasForeignKey(o => new { o.WalletId, o.ChildType, o.ChildId })
.HasOne(o => o.B)
.WithMany(o => o.As)
.HasForeignKey(o => new { o.WalletId, o.BType, o.BId })
.OnDelete(DeleteBehavior.Cascade);
if (databaseFacade.IsNpgsql())

@ -40,33 +40,33 @@ namespace BTCPayServer.Migrations
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),
AType = table.Column<string>(type: "TEXT", nullable: false),
AId = table.Column<string>(type: "TEXT", nullable: false),
BType = table.Column<string>(type: "TEXT", nullable: false),
BId = 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.PrimaryKey("PK_WalletObjectLinks", x => new { x.WalletId, x.AType, x.AId, x.BType, x.BId });
table.ForeignKey(
name: "FK_WalletObjectLinks_WalletObjects_WalletId_ChildType_ChildId",
columns: x => new { x.WalletId, x.ChildType, x.ChildId },
name: "FK_WalletObjectLinks_WalletObjects_WalletId_BType_BId",
columns: x => new { x.WalletId, x.BType, x.BId },
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 },
name: "FK_WalletObjectLinks_WalletObjects_WalletId_AType_AId",
columns: x => new { x.WalletId, x.AType, x.AId },
principalTable: "WalletObjects",
principalColumns: new[] { "WalletId", "Type", "Id" },
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_WalletObjectLinks_WalletId_ChildType_ChildId",
name: "IX_WalletObjectLinks_WalletId_BType_BId",
table: "WalletObjectLinks",
columns: new[] { "WalletId", "ChildType", "ChildId" });
columns: new[] { "WalletId", "BType", "BId" });
}
protected override void Down(MigrationBuilder migrationBuilder)

@ -0,0 +1,31 @@
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("20221128062447_jsonb")]
public partial class jsonb : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
if (migrationBuilder.IsNpgsql())
{
migrationBuilder.Sql("ALTER TABLE \"Settings\" ALTER COLUMN \"Value\" TYPE JSONB USING \"Value\"::JSONB");
migrationBuilder.Sql("ALTER TABLE \"Stores\" ALTER COLUMN \"StoreBlob\" TYPE JSONB USING regexp_replace(convert_from(\"StoreBlob\",'UTF8'), '\\\\u0000', '', 'g')::JSONB");
}
}
protected override void Down(MigrationBuilder migrationBuilder)
{
// Not supported
}
}
}

@ -872,24 +872,24 @@ namespace BTCPayServer.Migrations
b.Property<string>("WalletId")
.HasColumnType("TEXT");
b.Property<string>("ParentType")
b.Property<string>("AType")
.HasColumnType("TEXT");
b.Property<string>("ParentId")
b.Property<string>("AId")
.HasColumnType("TEXT");
b.Property<string>("ChildType")
b.Property<string>("BType")
.HasColumnType("TEXT");
b.Property<string>("ChildId")
b.Property<string>("BId")
.HasColumnType("TEXT");
b.Property<string>("Data")
.HasColumnType("TEXT");
b.HasKey("WalletId", "ParentType", "ParentId", "ChildType", "ChildId");
b.HasKey("WalletId", "AType", "AId", "BType", "BId");
b.HasIndex("WalletId", "ChildType", "ChildId");
b.HasIndex("WalletId", "BType", "BId");
b.ToTable("WalletObjectLinks");
});
@ -1384,21 +1384,21 @@ namespace BTCPayServer.Migrations
modelBuilder.Entity("BTCPayServer.Data.WalletObjectLinkData", b =>
{
b.HasOne("BTCPayServer.Data.WalletObjectData", "Child")
.WithMany("ParentLinks")
.HasForeignKey("WalletId", "ChildType", "ChildId")
b.HasOne("BTCPayServer.Data.WalletObjectData", "A")
.WithMany("Bs")
.HasForeignKey("WalletId", "AType", "AId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("BTCPayServer.Data.WalletObjectData", "Parent")
.WithMany("ChildLinks")
.HasForeignKey("WalletId", "ParentType", "ParentId")
b.HasOne("BTCPayServer.Data.WalletObjectData", "B")
.WithMany("As")
.HasForeignKey("WalletId", "BType", "BId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Child");
b.Navigation("A");
b.Navigation("Parent");
b.Navigation("B");
});
modelBuilder.Entity("BTCPayServer.Data.WalletTransactionData", b =>
@ -1545,9 +1545,9 @@ namespace BTCPayServer.Migrations
modelBuilder.Entity("BTCPayServer.Data.WalletObjectData", b =>
{
b.Navigation("ChildLinks");
b.Navigation("As");
b.Navigation("ParentLinks");
b.Navigation("Bs");
});
modelBuilder.Entity("BTCPayServer.Data.WebhookData", b =>

@ -6,7 +6,7 @@
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<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.14" />
<PackageReference Include="NBitcoin" Version="7.0.20" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="1.0.2" />
</ItemGroup>

@ -1,6 +1,6 @@
[
{
"name":"Afghani",
"name":"Afghan Afghani",
"code":"AFN",
"divisibility":2,
"symbol":null,
@ -21,7 +21,7 @@
"crypto":false
},
{
"name":"Lek",
"name":"Albanian Lek",
"code":"ALL",
"divisibility":2,
"symbol":null,
@ -42,7 +42,7 @@
"crypto":false
},
{
"name":"Kwanza",
"name":"Angolan Kwanza",
"code":"AOA",
"divisibility":2,
"symbol":null,
@ -84,7 +84,7 @@
"crypto":false
},
{
"name":"Azerbaijanian Manat",
"name":"Azerbaijani Manat",
"code":"AZN",
"divisibility":2,
"symbol":null,
@ -105,14 +105,14 @@
"crypto":false
},
{
"name":"Taka",
"name":"Bangladeshi Taka",
"code":"BDT",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Barbados Dollar",
"name":"Barbadian Dollar",
"code":"BBD",
"divisibility":2,
"symbol":null,
@ -161,21 +161,21 @@
"crypto":false
},
{
"name":"Ngultrum",
"name":"Bhutanese Ngultrum",
"code":"BTN",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Boliviano",
"name":"Bolivian Boliviano",
"code":"BOB",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Mvdol",
"name":"Bolivian Mvdol",
"code":"BOV",
"divisibility":2,
"symbol":null,
@ -189,7 +189,7 @@
"crypto":false
},
{
"name":"Pula",
"name":"Botswana Pula",
"code":"BWP",
"divisibility":2,
"symbol":null,
@ -224,21 +224,21 @@
"crypto":false
},
{
"name":"Burundi Franc",
"name":"Burundian Franc",
"code":"BIF",
"divisibility":0,
"symbol":null,
"crypto":false
},
{
"name":"Cabo Verde Escudo",
"name":"Cape Verdean Escudo",
"code":"CVE",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Riel",
"name":"Cambodian Riel",
"code":"KHR",
"divisibility":2,
"symbol":null,
@ -301,7 +301,7 @@
"crypto":false
},
{
"name":"Comoro Franc",
"name":"Comorian Franc",
"code":"KMF",
"divisibility":0,
"symbol":null,
@ -329,7 +329,7 @@
"crypto":false
},
{
"name":"Kuna",
"name":"Croatian Kuna",
"code":"HRK",
"divisibility":2,
"symbol":null,
@ -371,7 +371,7 @@
"crypto":false
},
{
"name":"Djibouti Franc",
"name":"Djiboutian Franc",
"code":"DJF",
"divisibility":0,
"symbol":null,
@ -392,14 +392,14 @@
"crypto":false
},
{
"name":"El Salvador Colon",
"name":"Salvadoran Colon",
"code":"SVC",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Nakfa",
"name":"Eritrean Nakfa",
"code":"ERN",
"divisibility":2,
"symbol":null,
@ -420,7 +420,7 @@
"crypto":false
},
{
"name":"Fiji Dollar",
"name":"Fijian Dollar",
"code":"FJD",
"divisibility":2,
"symbol":null,
@ -434,21 +434,21 @@
"crypto":false
},
{
"name":"Dalasi",
"name":"Gambian Dalasi",
"code":"GMD",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Lari",
"name":"Georgian Lari",
"code":"GEL",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Ghana Cedi",
"name":"Ghanaian Cedi",
"code":"GHS",
"divisibility":2,
"symbol":null,
@ -462,7 +462,7 @@
"crypto":false
},
{
"name":"Quetzal",
"name":"Guatemalan Quetzal",
"code":"GTQ",
"divisibility":2,
"symbol":null,
@ -476,28 +476,28 @@
"crypto":false
},
{
"name":"Guinea Franc",
"name":"Guinean Franc",
"code":"GNF",
"divisibility":0,
"symbol":null,
"crypto":false
},
{
"name":"Guyana Dollar",
"name":"Guyanese Dollar",
"code":"GYD",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Gourde",
"name":"Haitian Gourde",
"code":"HTG",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Lempira",
"name":"Honduran Lempira",
"code":"HNL",
"divisibility":2,
"symbol":null,
@ -511,21 +511,21 @@
"crypto":false
},
{
"name":"Forint",
"name":"Hungarian Forint",
"code":"HUF",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Iceland Krona",
"name":"Icelandic Krona",
"code":"ISK",
"divisibility":0,
"symbol":null,
"crypto":false
},
{
"name":"Rupiah",
"name":"Indonesian Rupiah",
"code":"IDR",
"divisibility":2,
"symbol":null,
@ -546,7 +546,7 @@
"crypto":false
},
{
"name":"New Israeli Sheqel",
"name":"New Israeli Shekel",
"code":"ILS",
"divisibility":2,
"symbol":null,
@ -560,7 +560,7 @@
"crypto":false
},
{
"name":"Yen",
"name":"Japanese Yen",
"code":"JPY",
"divisibility":0,
"symbol":"¥",
@ -574,7 +574,7 @@
"crypto":false
},
{
"name":"Tenge",
"name":"Kazakhstani Tenge",
"code":"KZT",
"divisibility":2,
"symbol":null,
@ -595,7 +595,7 @@
"crypto":false
},
{
"name":"Won",
"name":"South Korean Won",
"code":"KRW",
"divisibility":0,
"symbol":"₩",
@ -609,14 +609,14 @@
"crypto":false
},
{
"name":"Som",
"name":"Kyrgyzstani Som",
"code":"KGS",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Kip",
"name":"Lao Kip",
"code":"LAK",
"divisibility":2,
"symbol":null,
@ -630,14 +630,14 @@
"crypto":false
},
{
"name":"Loti",
"name":"Lesotho Loti",
"code":"LSL",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Rand",
"name":"South African Rand",
"code":"ZAR",
"divisibility":2,
"symbol":null,
@ -665,14 +665,14 @@
"crypto":false
},
{
"name":"Pataca",
"name":"Macanese Pataca",
"code":"MOP",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Denar",
"name":"Macedonian Denar",
"code":"MKD",
"divisibility":2,
"symbol":null,
@ -686,7 +686,7 @@
"crypto":false
},
{
"name":"Malawi Kwacha",
"name":"Malawian Kwacha",
"code":"MWK",
"divisibility":2,
"symbol":null,
@ -700,21 +700,21 @@
"crypto":false
},
{
"name":"Rufiyaa",
"name":"Maldivian Rufiyaa",
"code":"MVR",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Ouguiya",
"name":"Mauritanian Ouguiya",
"code":"MRO",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Mauritius Rupee",
"name":"Mauritian Rupee",
"code":"MUR",
"divisibility":2,
"symbol":null,
@ -742,7 +742,7 @@
"crypto":false
},
{
"name":"Tugrik",
"name":"Mongolian Tugrik",
"code":"MNT",
"divisibility":2,
"symbol":null,
@ -756,21 +756,21 @@
"crypto":false
},
{
"name":"Mozambique Metical",
"name":"Mozambican Metical",
"code":"MZN",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Kyat",
"name":"Myanmar Kyat",
"code":"MMK",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Namibia Dollar",
"name":"Namibian dollar",
"code":"NAD",
"divisibility":2,
"symbol":null,
@ -784,56 +784,56 @@
"crypto":false
},
{
"name":"Cordoba Oro",
"name":"Nicaraguan Cordoba",
"code":"NIO",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Naira",
"name":"Nigerian Naira",
"code":"NGN",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Rial Omani",
"name":"Omani Rial",
"code":"OMR",
"divisibility":3,
"symbol":null,
"crypto":false
},
{
"name":"Pakistan Rupee",
"name":"Pakistani Rupee",
"code":"PKR",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Balboa",
"name":"Panamanian Balboa",
"code":"PAB",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Kina",
"name":"Papua New Guinean Kina",
"code":"PGK",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Guarani",
"name":"Paraguayan Guarani",
"code":"PYG",
"divisibility":0,
"symbol":null,
"crypto":false
},
{
"name":"Sol",
"name":"Peruvian Sol",
"code":"PEN",
"divisibility":2,
"symbol":null,
@ -847,7 +847,7 @@
"crypto":false
},
{
"name":"Zloty",
"name":"Polish Zloty",
"code":"PLN",
"divisibility":2,
"symbol":null,
@ -875,7 +875,7 @@
"crypto":false
},
{
"name":"Rwanda Franc",
"name":"Rwandan Franc",
"code":"RWF",
"divisibility":0,
"symbol":null,
@ -889,14 +889,14 @@
"crypto":false
},
{
"name":"Tala",
"name":"Samoan Tala",
"code":"WST",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Dobra",
"name":"São Tomé and Príncipe sDobra",
"code":"STD",
"divisibility":2,
"symbol":null,
@ -917,14 +917,14 @@
"crypto":false
},
{
"name":"Seychelles Rupee",
"name":"Seychellois Rupee",
"code":"SCR",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Leone",
"name":"Sierra Leonean Leone",
"code":"SLL",
"divisibility":2,
"symbol":null,
@ -959,7 +959,7 @@
"crypto":false
},
{
"name":"Sri Lanka Rupee",
"name":"Sri Lankan Rupee",
"code":"LKR",
"divisibility":2,
"symbol":null,
@ -973,14 +973,14 @@
"crypto":false
},
{
"name":"Surinam Dollar",
"name":"Surinamese Dollar",
"code":"SRD",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Lilangeni",
"name":"Swazi Lilangeni",
"code":"SZL",
"divisibility":2,
"symbol":null,
@ -1022,7 +1022,7 @@
"crypto":false
},
{
"name":"Somoni",
"name":"Tajikistani Somoni",
"code":"TJS",
"divisibility":2,
"symbol":null,
@ -1036,14 +1036,14 @@
"crypto":false
},
{
"name":"Baht",
"name":"Thai Baht",
"code":"THB",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Paanga",
"name":"Tongan paʻanga",
"code":"TOP",
"divisibility":2,
"symbol":null,
@ -1071,21 +1071,21 @@
"crypto":false
},
{
"name":"Turkmenistan New Manat",
"name":"Turkmenistani Manat",
"code":"TMT",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Uganda Shilling",
"name":"Ugandan Shilling",
"code":"UGX",
"divisibility":0,
"symbol":null,
"crypto":false
},
{
"name":"Hryvnia",
"name":"Ukrainian Hryvnia",
"code":"UAH",
"divisibility":2,
"symbol":null,
@ -1106,7 +1106,7 @@
"crypto":false
},
{
"name":"Peso Uruguayo",
"name":"Uruguayan Peso",
"code":"UYU",
"divisibility":2,
"symbol":null,
@ -1120,28 +1120,28 @@
"crypto":false
},
{
"name":"Uzbekistan Sum",
"name":"Uzbekistani Sum",
"code":"UZS",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Vatu",
"name":"Vanuatu Vatu",
"code":"VUV",
"divisibility":0,
"symbol":null,
"crypto":false
},
{
"name":"Bolívar",
"name":"Venezuelan Bolívar",
"code":"VEF",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Dong",
"name":"Vietnamese Dong",
"code":"VND",
"divisibility":0,
"symbol":null,
@ -1162,7 +1162,7 @@
"crypto":false
},
{
"name":"Zimbabwe Dollar",
"name":"Zimbabwean Dollar",
"code":"ZWL",
"divisibility":2,
"symbol":null,

@ -171,8 +171,9 @@ namespace BTCPayServer.Tests
#pragma warning disable CS0618 // Type or member is obsolete
.OfType<DerivationSchemeSettings>().First(o => o.PaymentId.IsBTCOnChain);
#pragma warning restore CS0618 // Type or member is obsolete
DerivationSchemeSettings.TryParseFromWalletFile(content, onchainBTC.Network, out var expected);
DerivationSchemeSettings.TryParseFromWalletFile(content, onchainBTC.Network, out var expected, out var error);
Assert.Equal(expected.ToJson(), onchainBTC.ToJson());
Assert.Null(error);
// Let's check that the root hdkey and account key path are taken into account when making a PSBT
invoice = await user.BitPay.CreateInvoiceAsync(

@ -151,6 +151,15 @@ namespace BTCPayServer.Tests
Assert.StartsWith("bitcoin:", payUrl);
Assert.Contains("&LIGHTNING=", payUrl);
// BIP21 with LN as default payment method
s.GoToHome();
invoiceId = s.CreateInvoice(defaultPaymentMethod: "BTC_LightningLike");
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);

@ -656,7 +656,8 @@ namespace BTCPayServer.Tests
// ColdCard
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
"{\"keystore\": {\"ckcc_xpub\": \"xpub661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw\", \"xpub\": \"ypub6WWc2gWwHbdnAAyJDnR4SPL1phRh7REqrPBfZeizaQ1EmTshieRXJC3Z5YoU4wkcdKHEjQGkh6AYEzCQC1Kz3DNaWSwdc1pc8416hAjzqyD\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/0'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}",
mainnet, out var settings));
mainnet, out var settings, out var error));
Assert.Null(error);
Assert.Equal(root.GetPublicKey().GetHDFingerPrint(), settings.AccountKeySettings[0].RootFingerprint);
Assert.Equal(settings.AccountKeySettings[0].RootFingerprint,
HDFingerprint.TryParse("8bafd160", out var hd) ? hd : default);
@ -672,30 +673,41 @@ namespace BTCPayServer.Tests
// Should be legacy
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
"{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"tpubDDWYqT3P24znfsaGX7kZcQhNc5LAjnQiKQvUCHF2jS6dsgJBRtymopEU5uGpMaR5YChjuiExZG1X2aTbqXkp82KqH5qnqwWHp6EWis9ZvKr\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/44'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}",
testnet, out settings));
testnet, out settings, out error));
Assert.True(settings.AccountDerivation is DirectDerivationStrategy s && !s.Segwit);
Assert.Null(error);
// Should be segwit p2sh
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
"{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"upub5DSddA9NoRUyJrQ4p86nsCiTSY7kLHrSxx3joEJXjHd4HPARhdXUATuk585FdWPVC2GdjsMePHb6BMDmf7c6KG4K4RPX6LVqBLtDcWpQJmh\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}",
testnet, out settings));
testnet, out settings, out error));
Assert.True(settings.AccountDerivation is P2SHDerivationStrategy p &&
p.Inner is DirectDerivationStrategy s2 && s2.Segwit);
Assert.Null(error);
// Should be segwit
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
"{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"vpub5YjYxTemJ39tFRnuAhwduyxG2tKGjoEpmvqVQRPqdYrqa6YGoeSzBtHXaJUYB19zDbXs3JjbEcVWERjQBPf9bEfUUMZNMv1QnMyHV8JPqyf\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/84'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}",
testnet, out settings));
testnet, out settings, out error));
Assert.True(settings.AccountDerivation is DirectDerivationStrategy s3 && s3.Segwit);
Assert.Null(error);
// Specter
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
"{\"label\": \"Specter\", \"blockheight\": 123456, \"descriptor\": \"wpkh([8bafd160/49h/0h/0h]xpub661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw/0/*)#9x4vkw48\"}",
mainnet, out var specter));
mainnet, out var specter, out error));
Assert.Equal(root.GetPublicKey().GetHDFingerPrint(), specter.AccountKeySettings[0].RootFingerprint);
Assert.Equal(specter.AccountKeySettings[0].RootFingerprint, hd);
Assert.Equal("49'/0'/0'", specter.AccountKeySettings[0].AccountKeyPath.ToString());
Assert.Equal("Specter", specter.Label);
Assert.Null(error);
// Failure case
Assert.False(DerivationSchemeSettings.TryParseFromWalletFile(
"{\"keystore\": {\"ckcc_xpub\": \"tpubFailure\", \"xpub\": \"tpubFailure\", \"label\": \"Failure\"}, \"wallet_type\": \"standard\"}",
testnet, out settings, out error));
Assert.Null(settings);
Assert.NotNull(error);
}
[Fact]
@ -1749,8 +1761,7 @@ namespace BTCPayServer.Tests
PaymentMethod = new PaymentMethodId("BTC", PaymentTypes.BTCLike)
}
};
var newBlob = Encoding.UTF8.GetBytes(
new Serializer(null).ToString(blob).Replace( "paymentMethod\":\"BTC\"","paymentMethod\":\"ETH_ZYC\""));
var newBlob = new Serializer(null).ToString(blob).Replace( "paymentMethod\":\"BTC\"","paymentMethod\":\"ETH_ZYC\"");
Assert.Empty(StoreDataExtensions.GetStoreBlob(new StoreData() {StoreBlob = newBlob}).PaymentMethodCriteria);
}
}

@ -1561,6 +1561,127 @@ namespace BTCPayServer.Tests
});
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanRefundInvoice()
{
using var tester = CreateServerTester();
await tester.StartAsync();
var user = tester.NewAccount();
await user.RegisterDerivationSchemeAsync("BTC");
var client = await user.CreateClient();
var invoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest() { Amount = 5000.0m, Currency = "USD" });
var methods = await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id);
var method = methods.First();
var amount = method.Amount;
Assert.Equal(amount, method.Due);
await tester.WaitForEvent<NewOnChainTransactionEvent>(async () =>
{
await tester.ExplorerNode.SendToAddressAsync(
BitcoinAddress.Create(method.Destination, tester.NetworkProvider.BTC.NBitcoinNetwork),
Money.Coins(method.Due)
);
});
// test validation that the invoice exists
await AssertHttpError(404, async () =>
{
await client.RefundInvoice(user.StoreId, "lol fake invoice id", new RefundInvoiceRequest() {
PaymentMethod = method.PaymentMethod,
RefundVariant = RefundVariant.RateThen
});
});
// test validation error for when invoice is not yet in the state in which it can be refunded
var apiError = await AssertAPIError("non-refundable", () => client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest() {
PaymentMethod = method.PaymentMethod,
RefundVariant = RefundVariant.RateThen
}));
Assert.Equal("Cannot refund this invoice", apiError.Message);
await TestUtils.EventuallyAsync(async () =>
{
invoice = await client.GetInvoice(user.StoreId, invoice.Id);
Assert.True(invoice.Status == InvoiceStatus.Processing);
});
// need to set the status to the one in which we can actually refund the invoice
await client.MarkInvoiceStatus(user.StoreId, invoice.Id, new MarkInvoiceStatusRequest() {
Status = InvoiceStatus.Settled
});
// test validation for the payment method
var validationError = await AssertValidationError(new[] { "PaymentMethod" }, async () =>
{
await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest() {
PaymentMethod = "fake payment method",
RefundVariant = RefundVariant.RateThen
});
});
Assert.Contains("PaymentMethod: Please select one of the payment methods which were available for the original invoice", validationError.Message);
// test RefundVariant.RateThen
var pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest() {
PaymentMethod = method.PaymentMethod,
RefundVariant = RefundVariant.RateThen
});
Assert.Equal("BTC", pp.Currency);
Assert.True(pp.AutoApproveClaims);
Assert.Equal(1, pp.Amount);
Assert.Equal(pp.Name, $"Refund {invoice.Id}");
// test RefundVariant.CurrentRate
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest() {
PaymentMethod = method.PaymentMethod,
RefundVariant = RefundVariant.CurrentRate
});
Assert.Equal("BTC", pp.Currency);
Assert.True(pp.AutoApproveClaims);
Assert.Equal(1, pp.Amount);
// test RefundVariant.Fiat
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest() {
PaymentMethod = method.PaymentMethod,
RefundVariant = RefundVariant.Fiat,
Name = "my test name"
});
Assert.Equal("USD", pp.Currency);
Assert.False(pp.AutoApproveClaims);
Assert.Equal(5000, pp.Amount);
Assert.Equal("my test name", pp.Name);
// test RefundVariant.Custom
validationError = await AssertValidationError(new[] { "CustomAmount", "CustomCurrency" }, async () =>
{
await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest() {
PaymentMethod = method.PaymentMethod,
RefundVariant = RefundVariant.Custom,
});
});
Assert.Contains("CustomAmount: Amount must be greater than 0", validationError.Message);
Assert.Contains("CustomCurrency: Invalid currency", validationError.Message);
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest() {
PaymentMethod = method.PaymentMethod,
RefundVariant = RefundVariant.Custom,
CustomAmount = 69420,
CustomCurrency = "JPY"
});
Assert.Equal("JPY", pp.Currency);
Assert.False(pp.AutoApproveClaims);
Assert.Equal(69420, pp.Amount);
// should auto-approve if currencies match
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest() {
PaymentMethod = method.PaymentMethod,
RefundVariant = RefundVariant.Custom,
CustomAmount = 0.00069420m,
CustomCurrency = "BTC"
});
Assert.True(pp.AutoApproveClaims);
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task InvoiceTests()
@ -3073,7 +3194,7 @@ namespace BTCPayServer.Tests
// 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 allObjects = await repo.GetWalletObjects(new(wid) { 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 }));

@ -0,0 +1,250 @@
using System;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Lightning;
using BTCPayServer.Plugins.LNbank.Data.Models;
using NBitcoin;
using OpenQA.Selenium;
using OpenQA.Selenium.Support.UI;
using Xunit;
using Xunit.Abstractions;
namespace BTCPayServer.Tests
{
[Trait("Selenium", "Selenium")]
[Collection(nameof(NonParallelizableCollectionDefinition))]
public class LNbankTests : UnitTestBase
{
private const int TestTimeout = TestUtils.TestTimeout;
public LNbankTests(ITestOutputHelper helper) : base(helper)
{
}
[Fact(Timeout = TestTimeout)]
[Trait("Lightning", "Lightning")]
public async Task CanUseLNbank()
{
var implementations = new []
{
LightningConnectionType.CLightning,
LightningConnectionType.LndREST
};
foreach (var nodeType in implementations)
{
using var s = CreateSeleniumTester();
s.Server.ActivateLightning(nodeType);
await s.StartAsync();
s.RegisterNewUser(true);
// Setup store LN node with LNbank
s.CreateNewStore();
s.Driver.FindElement(By.Id("StoreNav-LightningBTC")).Click();
s.Driver.FindElement(By.CssSelector("label[for=\"LightningNodeType-LNbank\"]")).Click();
s.Driver.WaitForElement(By.Id("LNbank-CreateWallet"));
Assert.Equal("", s.Driver.FindElement(By.Id("LNbankWallet")).GetAttribute("value"));
// Create new wallet, which is pre-selected afterwards
s.Driver.FindElement(By.Id("LNbank-CreateWallet")).Click();
var walletName = "Wallet" + RandomUtils.GetUInt64();
s.Driver.FindElement(By.Id("Wallet_Name")).SendKeys(walletName);
s.Driver.FindElement(By.Id("LNbank-Create")).Click();
s.Driver.WaitForElement(By.Id("LNbankWallet"));
var walletSelect = new SelectElement(s.Driver.FindElement(By.Id("LNbankWallet")));
Assert.Equal(walletName, walletSelect.SelectedOption.Text);
// Finish and validate setup
s.Driver.FindElement(By.Id("save")).Click();
Assert.Contains("LNbank", s.Driver.FindElement(By.Id("CustomNodeInfo")).Text);
// LNbank wallets
s.Driver.FindElement(By.Id("Nav-LNbank")).Click();
Assert.Contains(walletName, s.Driver.FindElement(By.Id("LNbank-Wallets")).Text);
Assert.Single(s.Driver.FindElements(By.CssSelector("#LNbank-Wallets a")));
s.Driver.FindElement(By.CssSelector("#LNbank-Wallets a")).Click();
// Wallet
Assert.Contains("0 sats", s.Driver.FindElement(By.Id("LNbank-WalletBalance")).Text);
Assert.Contains("There are no transactions yet.", s.Driver.FindElement(By.Id("LNbank-WalletTransactions")).Text);
s.Driver.FindElement(By.Id("LNbank-WalletSettings")).Click();
Assert.Contains(walletName, s.Driver.FindElement(By.Id("LNbank-WalletName")).Text);
// Receive
var description = "First invoice";
s.Driver.FindElement(By.Id("LNbank-WalletReceive")).Click();
s.Driver.FindElement(By.Id("Description")).SendKeys(description);
s.Driver.FindElement(By.Id("Amount")).Clear();
s.Driver.FindElement(By.Id("Amount")).SendKeys("21");
s.Driver.FindElement(By.Id("LNbank-CreateInvoice")).Click();
// Details
Assert.Contains(description, s.Driver.FindElement(By.Id("LNbank-TransactionDescription")).Text);
Assert.Contains("21 sats unpaid", s.Driver.FindElement(By.Id("LNbank-TransactionAmount")).Text);
var bolt11 = s.Driver.FindElement(By.Id("LNbank-CopyPaymentRequest")).GetAttribute("data-clipboard");
var shareUrl = s.Driver.FindElement(By.Id("LNbank-CopyShareUrl")).GetAttribute("data-clipboard");
Assert.StartsWith("ln", bolt11);
// List
s.Driver.FindElement(By.Id("LNbank-WalletOverview")).Click();
var listUrl = s.Driver.Url;
Assert.Single(s.Driver.FindElements(By.CssSelector("#LNbank-WalletTransactions tr")));
Assert.Contains("21 sats", s.Driver.FindElement(By.CssSelector("#LNbank-WalletTransactions tr .transaction-amount")).Text);
Assert.Contains(description, s.Driver.FindElement(By.CssSelector("#LNbank-WalletTransactions tr .transaction-description")).Text);
Assert.Contains("unpaid", s.Driver.FindElement(By.CssSelector("#LNbank-WalletTransactions tr .transaction-status")).Text);
Assert.Contains("0 sats", s.Driver.FindElement(By.Id("LNbank-WalletBalance")).Text);
// Share
s.GoToUrl(shareUrl);
Assert.Contains(description, s.Driver.FindElement(By.Id("LNbank-TransactionDescription")).Text);
Assert.Contains("21 sats unpaid", s.Driver.FindElement(By.Id("LNbank-TransactionAmount")).Text);
// Pay invoice
var resp = await s.Server.CustomerLightningD.Pay(bolt11);
Assert.Equal(PayResult.Ok, resp.Result);
TestUtils.Eventually(() =>
{
s.Driver.Navigate().Refresh();
Assert.Contains("21 sats settled", s.Driver.FindElement(By.Id("LNbank-TransactionSettled")).Text);
});
// List
s.GoToUrl(listUrl);
Assert.Single(s.Driver.FindElements(By.CssSelector("#LNbank-WalletTransactions tr")));
Assert.Contains("21 sats", s.Driver.FindElement(By.CssSelector("#LNbank-WalletTransactions tr .transaction-settled")).Text);
Assert.Contains("21 sats", s.Driver.FindElement(By.Id("LNbank-WalletBalance")).Text);
// Send
var memo = "Donation";
var amount = LightMoney.Satoshis(5);
var invoice = await s.Server.CustomerLightningD.CreateInvoice(amount, memo, TimeSpan.FromHours(1));
s.Driver.FindElement(By.Id("LNbank-WalletSend")).Click();
s.Driver.FindElement(By.Id("Destination")).SendKeys(invoice.BOLT11);
s.Driver.FindElement(By.Id("LNbank-Decode")).Click();
// Confirm
Assert.Contains(memo, s.Driver.FindElement(By.Id("Description")).GetAttribute("value"));
Assert.Contains("5 sats", s.Driver.FindElement(By.Id("LNbank-Amount")).Text);
s.Driver.FindElement(By.Id("Description")).Clear();
s.Driver.FindElement(By.Id("Description")).SendKeys("For Uncle Jim");
s.Driver.FindElement(By.Id("LNbank-Send")).Click();
Assert.Contains("Payment successfully sent and settled.", s.FindAlertMessage().Text);
// List
s.Driver.FindElement(By.Id("LNbank-WalletOverview")).Click();
var amountEl = s.Driver.FindElement(By.CssSelector("#LNbank-WalletTransactions tr .transaction-amount"));
var settledEl = s.Driver.FindElement(By.CssSelector("#LNbank-WalletTransactions tr .transaction-settled"));
var amountMoney = LightMoney.MilliSatoshis(long.Parse(amountEl.GetAttribute("data-amount")));
var amountSettledMoney = LightMoney.MilliSatoshis(long.Parse(settledEl.GetAttribute("data-amount-settled")));
var feeMoney = LightMoney.MilliSatoshis(long.Parse(settledEl.GetAttribute("data-transaction-fee")));
var amountSettled = (amountMoney + feeMoney) * -1;
var balance = LightMoney.Satoshis(21) + amountSettled;
Assert.Equal(2, s.Driver.FindElements(By.CssSelector("#LNbank-WalletTransactions tr")).Count);
Assert.Equal(amount, amountMoney);
Assert.Equal(amountSettled, amountSettledMoney);
Assert.Contains("For Uncle Jim", s.Driver.FindElement(By.CssSelector("#LNbank-WalletTransactions tr .transaction-description")).Text);
Assert.Contains($"{amount.ToUnit(LightMoneyUnit.Satoshi)} sats", amountEl.Text);
Assert.Contains($"{amountSettled.ToUnit(LightMoneyUnit.Satoshi)} sats", settledEl.Text);
Assert.Contains($"{balance.ToUnit(LightMoneyUnit.Satoshi)} sats", s.Driver.FindElement(By.Id("LNbank-WalletBalance")).Text);
}
}
[Fact(Timeout = TestTimeout)]
[Trait("Lightning", "Lightning")]
public async Task CanUseLNbankAccessKeys()
{
using var s = CreateSeleniumTester();
s.Server.ActivateLightning(LightningConnectionType.CLightning);
await s.StartAsync();
s.GoToRegister();
var user = s.RegisterNewUser();
s.GoToRegister();
var admin = s.RegisterNewUser(true);
// Create new wallet
s.Driver.FindElement(By.Id("Nav-LNbank")).Click();
var walletName = "AccessKeys" + RandomUtils.GetUInt64();
s.Driver.FindElement(By.Id("Wallet_Name")).SendKeys(walletName);
s.Driver.FindElement(By.Id("LNbank-Create")).Click();
Assert.Contains("Wallet successfully created.", s.FindAlertMessage().Text);
s.Driver.FindElement(By.Id("LNbank-WalletSettings")).Click();
var walletId = s.Driver.FindElement(By.Id("LNbank-WalletId")).Text;
var walletNavId = $"Nav-LNbank-Wallet-{walletId}";
// Check if the user sees it
s.Logout();
s.LogIn(user);
s.Driver.AssertElementNotFound(By.Id(walletNavId));
void SetAccessLevel(AccessLevel level)
{
s.Logout();
s.LogIn(admin);
s.Driver.FindElement(By.Id(walletNavId)).Click();
s.Driver.FindElement(By.Id("LNbank-WalletSettings")).Click();
s.Driver.FindElement(By.Id("SectionNav-WalletAccessKeys")).Click();
s.Driver.FindElement(By.Id("AccessKey_Email")).SendKeys(user);
var levelSelect = new SelectElement(s.Driver.FindElement(By.Id("AccessKey_Level")));
levelSelect.SelectByValue(level.ToString());
s.Driver.FindElement(By.Id("LNbank-CreateAccessKey")).Click();
Assert.Contains("Access key added successfully.", s.FindAlertMessage().Text);
// Switch user
s.Logout();
s.LogIn(user);
s.Driver.FindElement(By.Id(walletNavId)).Click();
}
// Add read-only access key for user
SetAccessLevel(AccessLevel.ReadOnly);
s.Driver.AssertElementNotFound(By.Id("LNbank-WalletSend"));
s.Driver.AssertElementNotFound(By.Id("LNbank-WalletReceive"));
s.Driver.AssertElementNotFound(By.Id("LNbank-WalletSettings"));
// Update access key for user: Invoice
SetAccessLevel(AccessLevel.Invoice);
s.Driver.AssertElementNotFound(By.Id("LNbank-WalletSend"));
s.Driver.AssertElementNotFound(By.Id("LNbank-WalletSettings"));
// Receive is allowed now
var description = "My invoice";
s.Driver.FindElement(By.Id("LNbank-WalletReceive")).Click();
s.Driver.FindElement(By.Id("Description")).SendKeys(description);
s.Driver.SetCheckbox(By.Id("AttachDescription"), true);
s.Driver.FindElement(By.Id("Amount")).Clear();
s.Driver.FindElement(By.Id("Amount")).SendKeys("21");
s.Driver.FindElement(By.Id("LNbank-CreateInvoice")).Click();
Assert.Contains(description, s.Driver.FindElement(By.Id("LNbank-TransactionDescription")).Text);
Assert.Contains("21 sats unpaid", s.Driver.FindElement(By.Id("LNbank-TransactionAmount")).Text);
var bolt11 = s.Driver.FindElement(By.Id("LNbank-CopyPaymentRequest")).GetAttribute("data-clipboard");
Assert.StartsWith("ln", bolt11);
// Update access key for user: Send
SetAccessLevel(AccessLevel.Send);
s.Driver.AssertElementNotFound(By.Id("LNbank-WalletSettings"));
// Send is allowed now
s.Driver.FindElement(By.Id("LNbank-WalletSend")).Click();
s.Driver.FindElement(By.Id("Destination")).SendKeys(bolt11);
s.Driver.FindElement(By.Id("LNbank-Decode")).Click();
Assert.Contains(description, s.Driver.FindElement(By.Id("Description")).GetAttribute("value"));
Assert.Contains("21 sats", s.Driver.FindElement(By.Id("LNbank-Amount")).Text);
s.Driver.FindElement(By.Id("LNbank-Send")).Click();
Assert.Contains("Insufficient balance: 0 sats — tried to send 21 sats.", s.FindAlertMessage(StatusMessageModel.StatusSeverity.Error).Text);
// Update access key for user: Send
SetAccessLevel(AccessLevel.Admin);
s.Driver.FindElement(By.Id("LNbank-WalletSettings")).Click();
}
}
}

@ -71,7 +71,7 @@ services:
- "sshd_datadir:/root/.ssh"
devlnd:
image: btcpayserver/bitcoin:23.0-1
image: btcpayserver/bitcoin:24.0
environment:
BITCOIN_NETWORK: regtest
BITCOIN_WALLETDIR: "/data/wallets"
@ -126,7 +126,7 @@ services:
bitcoind:
restart: unless-stopped
image: btcpayserver/bitcoin:23.0-1
image: btcpayserver/bitcoin:24.0
environment:
BITCOIN_NETWORK: regtest
BITCOIN_WALLETDIR: "/data/wallets"

@ -68,7 +68,7 @@ services:
- "sshd_datadir:/root/.ssh"
devlnd:
image: btcpayserver/bitcoin:23.0-1
image: btcpayserver/bitcoin:24.0
environment:
BITCOIN_NETWORK: regtest
BITCOIN_WALLETDIR: "/data/wallets"
@ -113,7 +113,7 @@ services:
bitcoind:
restart: unless-stopped
image: btcpayserver/bitcoin:23.0-1
image: btcpayserver/bitcoin:24.0
environment:
BITCOIN_NETWORK: regtest
BITCOIN_WALLETDIR: "/data/wallets"

@ -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 --memo "hodl invoice $@" $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"

@ -97,8 +97,17 @@ connect $C_LN $m_ln_uri "Customer (LND) to Merchant (LND)"
# Channels
printf "\n\rEstablishing channels\n\r----------------------\n\r"
create_channel $M_LN $c_ln_id "Merchant (LND) to Customer (LND)"
create_channel $C_LN $c_cl_id "Customer (LND) to Customer (c-lightning)"
create_channel $C_CL $m_cl_id "Customer (c-lightning) to Merchant (c-lightning)"
create_channel $C_CL $m_cl_id "Customer (c-lightning) to Merchant (c-lightning)"
create_channel $C_CL $m_ln_id "Customer (c-lightning) to Merchant (LND)"
create_channel $C_LN $c_cl_id "Customer (LND) to Customer (c-lightning)"
create_channel $C_LN $m_cl_id "Customer (LND) to Merchant (c-lightning)"
create_channel $M_CL $m_ln_id "Merchant (c-lightning) to Merchant (LND)" "announce=false"
create_channel $M_CL $c_ln_id "Merchant (c-lightning) to Customer (LND)" "announce=false"
create_channel $M_CL $c_cl_id "Merchant (c-lightning) to Customer (c-lightning)" "announce=false"
create_channel $M_LN $c_ln_id "Merchant (LND) to Customer (LND)"
create_channel $M_LN $c_cl_id "Merchant (LND) to Customer (c-lightning)"
create_channel $C_LN $m_ln_id "Customer (LND) to Merchant (LND)" --private

@ -1,4 +1,4 @@
 <Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<Import Project="../Build/Version.csproj" Condition="Exists('../Build/Version.csproj')" />
<Import Project="../Build/Common.csproj" />
@ -47,7 +47,7 @@
<ItemGroup>
<PackageReference Include="BIP78.Sender" Version="0.2.2" />
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.2" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.4.8" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.4.10" />
<PackageReference Include="CsvHelper" Version="15.0.5" />
<PackageReference Include="Dapper" Version="2.0.123" />
<PackageReference Include="Fido2" Version="2.0.2" />
@ -133,6 +133,8 @@
<ProjectReference Include="..\BTCPayServer.Data\BTCPayServer.Data.csproj" />
<ProjectReference Include="..\BTCPayServer.Rating\BTCPayServer.Rating.csproj" />
<ProjectReference Include="..\BTCPayServer.Common\BTCPayServer.Common.csproj" />
<ProjectReference Include="..\Plugins\BTCPayServer.Plugins.LNbank\BTCPayServer.Plugins.LNbank.csproj" />
<ProjectReference Include="..\Plugins\BTCPayServer.Plugins.PodServer\BTCPayServer.Plugins.PodServer.csproj" />
<ProjectReference Include="..\Plugins\BTCPayServer.Plugins.Custodians.FakeCustodian\BTCPayServer.Plugins.Custodians.FakeCustodian.csproj" />
</ItemGroup>

@ -1,5 +1,5 @@
@model BTCPayServer.Components.Icon.IconViewModel
<svg role="img" class="icon icon-@Model.Symbol">
<use href="/img/icon-sprite.svg#@Model.Symbol"></use>
<use href="~/img/icon-sprite.svg#@Model.Symbol"></use>
</svg>

@ -14,7 +14,7 @@
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"/>
<use href="~/img/logo.svg#small" class="main-logo-btcpay--small"/>
<use href="~/img/logo.svg#large" class="main-logo-btcpay--large"/>
</svg>
}

@ -41,7 +41,7 @@ else
@if (Model.Options.Count > 0)
{
<div id="StoreSelectorDropdown" class="dropdown only-for-js">
<button id="StoreSelectorToggle" class="btn btn-secondary dropdown-toggle rounded-pill px-3 @(Model.CurrentStoreId == null ? "text-secondary" : "")" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<button id="StoreSelectorToggle" class="btn btn-secondary dropdown-toggle rounded-pill px-3 @(Model.CurrentStoreId == null ? "empty-state" : "")" type="button" data-bs-toggle="dropdown" aria-expanded="false">
@if (!string.IsNullOrEmpty(Model.CurrentStoreLogoFileId))
{
<img class="logo" src="@(await FileService.GetFileUrl(Context.Request.GetAbsoluteRootUri(), Model.CurrentStoreLogoFileId))" alt="@Model.CurrentDisplayName" />

@ -65,6 +65,10 @@ namespace BTCPayServer.Configuration
if (conf.GetOrDefault<bool>("launchsettings", false) && NetworkType != ChainName.Regtest)
throw new ConfigException($"You need to run BTCPayServer with the run.sh or run.ps1 script");
if (conf.GetOrDefault<string>("SQLITEFILE", null) != null)
Logs.Configuration.LogWarning("SQLITE backend support is deprecated and will be soon out of support");
if (conf.GetOrDefault<string>("MYSQL", null) != null)
Logs.Configuration.LogWarning("MYSQL backend support is deprecated and will be soon out of support");
DockerDeployment = conf.GetOrDefault<bool>("dockerdeployment", true);
TorrcFile = conf.GetOrDefault<string>("torrcfile", null);
TorServices = conf.GetOrDefault<string>("torservices", null)

@ -27,9 +27,9 @@ namespace BTCPayServer.Configuration
app.Option("--signet | -signet", $"Use signet (deprecated, use --network instead)", CommandOptionType.BoolValue);
app.Option("--chains | -c", $"Chains to support as a comma separated (default: btc; available: {chains})", CommandOptionType.SingleValue);
app.Option("--postgres", $"Connection string to a PostgreSQL database", CommandOptionType.SingleValue);
app.Option("--mysql", $"Connection string to a MySQL database", CommandOptionType.SingleValue);
app.Option("--mysql", $"DEPRECATED: Connection string to a MySQL database", CommandOptionType.SingleValue);
app.Option("--nocsp", $"Disable CSP (default false)", CommandOptionType.BoolValue);
app.Option("--sqlitefile", $"File name to an SQLite database file inside the data directory", CommandOptionType.SingleValue);
app.Option("--sqlitefile", $"DEPRECATED: File name to an SQLite database file inside the data directory", CommandOptionType.SingleValue);
app.Option("--externalservices", $"Links added to external services inside Server Settings / Services under the format service1:path2;service2:path2.(default: empty)", CommandOptionType.SingleValue);
app.Option("--rootpath", "The root path in the URL to access BTCPay (default: /)", CommandOptionType.SingleValue);
app.Option("--sshconnection", "SSH server to manage BTCPay under the form user@server:port (default: root@externalhost or empty)", CommandOptionType.SingleValue);

@ -2,14 +2,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.Payments;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Rating;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Http;
@ -32,12 +37,19 @@ namespace BTCPayServer.Controllers.Greenfield
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly EventAggregator _eventAggregator;
private readonly PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary;
private readonly CurrencyNameTable _currencyNameTable;
private readonly BTCPayNetworkProvider _networkProvider;
private readonly PullPaymentHostedService _pullPaymentService;
private readonly RateFetcher _rateProvider;
private readonly ApplicationDbContextFactory _dbContextFactory;
public LanguageService LanguageService { get; }
public GreenfieldInvoiceController(UIInvoiceController invoiceController, InvoiceRepository invoiceRepository,
LinkGenerator linkGenerator, LanguageService languageService, BTCPayNetworkProvider btcPayNetworkProvider,
EventAggregator eventAggregator, PaymentMethodHandlerDictionary paymentMethodHandlerDictionary)
EventAggregator eventAggregator, PaymentMethodHandlerDictionary paymentMethodHandlerDictionary,
CurrencyNameTable currencyNameTable, BTCPayNetworkProvider networkProvider, RateFetcher rateProvider,
PullPaymentHostedService pullPaymentService, ApplicationDbContextFactory dbContextFactory)
{
_invoiceController = invoiceController;
_invoiceRepository = invoiceRepository;
@ -45,6 +57,11 @@ namespace BTCPayServer.Controllers.Greenfield
_btcPayNetworkProvider = btcPayNetworkProvider;
_eventAggregator = eventAggregator;
_paymentMethodHandlerDictionary = paymentMethodHandlerDictionary;
_currencyNameTable = currencyNameTable;
_networkProvider = networkProvider;
_rateProvider = rateProvider;
_pullPaymentService = pullPaymentService;
_dbContextFactory = dbContextFactory;
LanguageService = languageService;
}
@ -333,6 +350,175 @@ namespace BTCPayServer.Controllers.Greenfield
return this.CreateValidationError(ModelState);
}
[Authorize(Policy = Policies.CanModifyStoreSettings,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/stores/{storeId}/invoices/{invoiceId}/refund")]
public async Task<IActionResult> RefundInvoice(
string storeId,
string invoiceId,
RefundInvoiceRequest request,
CancellationToken cancellationToken = default
)
{
var store = HttpContext.GetStoreData();
if (store == null)
{
return StoreNotFound();
}
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
if (invoice == null)
{
return InvoiceNotFound();
}
if (invoice.StoreId != store.Id)
{
return InvoiceNotFound();
}
if (!invoice.GetInvoiceState().CanRefund())
{
return this.CreateAPIError("non-refundable", "Cannot refund this invoice");
}
PaymentMethod? invoicePaymentMethod = null;
PaymentMethodId? paymentMethodId = null;
if (request.PaymentMethod is not null && PaymentMethodId.TryParse(request.PaymentMethod, out paymentMethodId))
{
invoicePaymentMethod = invoice.GetPaymentMethods().SingleOrDefault(method => method.GetId() == paymentMethodId);
}
if (invoicePaymentMethod is null)
{
this.ModelState.AddModelError(nameof(request.PaymentMethod), "Please select one of the payment methods which were available for the original invoice");
}
if (request.RefundVariant is null)
this.ModelState.AddModelError(nameof(request.RefundVariant), "`refundVariant` is mandatory");
if (!ModelState.IsValid || invoicePaymentMethod is null || paymentMethodId is null)
return this.CreateValidationError(ModelState);
var cryptoPaid = invoicePaymentMethod.Calculate().Paid.ToDecimal(MoneyUnit.BTC);
var cdCurrency = _currencyNameTable.GetCurrencyData(invoice.Currency, true);
var paidCurrency = Math.Round(cryptoPaid * invoicePaymentMethod.Rate, cdCurrency.Divisibility);
var rateResult = await _rateProvider.FetchRate(
new CurrencyPair(paymentMethodId.CryptoCode, invoice.Currency),
store.GetStoreBlob().GetRateRules(_networkProvider),
cancellationToken
);
var paymentMethodDivisibility = _currencyNameTable.GetCurrencyData(paymentMethodId.CryptoCode, false)?.Divisibility ?? 8;
var createPullPayment = new HostedServices.CreatePullPayment()
{
BOLT11Expiration = store.GetStoreBlob().RefundBOLT11Expiration,
Name = request.Name ?? $"Refund {invoice.Id}",
Description = request.Description,
StoreId = storeId,
PaymentMethodIds = new[] { paymentMethodId },
};
if (request.RefundVariant != RefundVariant.Custom)
{
if (request.CustomAmount is not null)
this.ModelState.AddModelError(nameof(request.CustomAmount), "CustomAmount should only be set if the refundVariant is Custom");
if (request.CustomCurrency is not null)
this.ModelState.AddModelError(nameof(request.CustomCurrency), "CustomCurrency should only be set if the refundVariant is Custom");
if (!ModelState.IsValid)
return this.CreateValidationError(ModelState);
}
switch (request.RefundVariant)
{
case RefundVariant.RateThen:
createPullPayment.Currency = invoicePaymentMethod.GetId().CryptoCode;
createPullPayment.Amount = cryptoPaid.RoundToSignificant(paymentMethodDivisibility);
createPullPayment.AutoApproveClaims = true;
break;
case RefundVariant.CurrentRate:
createPullPayment.Currency = invoicePaymentMethod.GetId().CryptoCode;
createPullPayment.Amount = Math.Round(paidCurrency / rateResult.BidAsk.Bid, paymentMethodDivisibility);
createPullPayment.AutoApproveClaims = true;
break;
case RefundVariant.Fiat:
createPullPayment.Currency = invoice.Currency;
createPullPayment.Amount = paidCurrency;
createPullPayment.AutoApproveClaims = false;
break;
case RefundVariant.Custom:
if (request.CustomAmount is null || (request.CustomAmount is decimal v && v <= 0)) {
this.ModelState.AddModelError(nameof(request.CustomAmount), "Amount must be greater than 0");
}
if (
string.IsNullOrEmpty(request.CustomCurrency) ||
_currencyNameTable.GetCurrencyData(request.CustomCurrency, false) == null
)
{
ModelState.AddModelError(nameof(request.CustomCurrency), "Invalid currency");
}
if (rateResult.BidAsk is null)
{
ModelState.AddModelError(nameof(request.RefundVariant),
$"Impossible to fetch rate: {rateResult.EvaluatedRule}");
}
if (!ModelState.IsValid || request.CustomAmount is null)
{
return this.CreateValidationError(ModelState);
}
createPullPayment.Currency = request.CustomCurrency;
createPullPayment.Amount = request.CustomAmount.Value;
createPullPayment.AutoApproveClaims = paymentMethodId.CryptoCode == request.CustomCurrency;
break;
default:
ModelState.AddModelError(nameof(request.RefundVariant), "Please select a valid refund option");
return this.CreateValidationError(ModelState);
}
var ppId = await _pullPaymentService.CreatePullPayment(createPullPayment);
await using var ctx = _dbContextFactory.CreateContext();
(await ctx.Invoices.FindAsync(new[] { invoice.Id }, cancellationToken))!.CurrentRefundId = ppId;
ctx.Refunds.Add(new RefundData
{
InvoiceDataId = invoice.Id,
PullPaymentDataId = ppId
});
await ctx.SaveChangesAsync(cancellationToken);
var pp = await _pullPaymentService.GetPullPayment(ppId, false);
return this.Ok(CreatePullPaymentData(pp));
}
private Client.Models.PullPaymentData CreatePullPaymentData(Data.PullPaymentData pp)
{
var ppBlob = pp.GetBlob();
return new BTCPayServer.Client.Models.PullPaymentData()
{
Id = pp.Id,
StartsAt = pp.StartDate,
ExpiresAt = pp.EndDate,
Amount = ppBlob.Limit,
Name = ppBlob.Name,
Description = ppBlob.Description,
Currency = ppBlob.Currency,
Period = ppBlob.Period,
Archived = pp.Archived,
AutoApproveClaims = ppBlob.AutoApproveClaims,
BOLT11Expiration = ppBlob.BOLT11Expiration,
ViewLink = _linkGenerator.GetUriByAction(
nameof(UIPullPaymentController.ViewPullPayment),
"UIPullPayment",
new { pullPaymentId = pp.Id },
Request.Scheme,
Request.Host,
Request.PathBase)
};
}
private IActionResult InvoiceNotFound()
{
return this.CreateAPIError(404, "invoice-not-found", "The invoice was not found");

@ -460,12 +460,12 @@ namespace BTCPayServer.Controllers.Greenfield
ModelState.AddModelError(nameof(approvePayoutRequest.RateRule), "Invalid RateRule");
return this.CreateValidationError(ModelState);
}
var result = await _pullPaymentService.Approve(new PullPaymentHostedService.PayoutApproval()
var result = (await _pullPaymentService.Approve(new PullPaymentHostedService.PayoutApproval()
{
PayoutId = payoutId,
Revision = revision!.Value,
Rate = rateResult.BidAsk.Ask
});
})).Result;
var errorMessage = PullPaymentHostedService.PayoutApproval.GetErrorMessage(result);
switch (result)
{

@ -189,7 +189,7 @@ namespace BTCPayServer.Controllers.Greenfield
var wallet = _btcPayWalletProvider.GetWallet(network);
var walletId = new WalletId(storeId, cryptoCode);
var walletTransactionsInfoAsync = await _walletRepository.GetWalletTransactionsInfo(walletId);
var walletTransactionsInfoAsync = await _walletRepository.GetWalletTransactionsInfo(walletId, (string[] ) null);
var preFiltering = true;
if (statusFilter?.Any() is true || !string.IsNullOrWhiteSpace(labelFilter))

@ -24,7 +24,6 @@ using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Invoices.Export;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
@ -134,7 +133,7 @@ namespace BTCPayServer.Controllers
Events = invoice.Events,
PosData = PosDataParser.ParsePosData(invoice.Metadata.PosData),
Archived = invoice.Archived,
CanRefund = CanRefund(invoiceState),
CanRefund = invoiceState.CanRefund(),
Refunds = invoice.Refunds,
ShowCheckout = invoice.Status == InvoiceStatusLegacy.New,
ShowReceipt = invoice.Status.ToModernStatus() == InvoiceStatus.Settled && (invoice.ReceiptOptions?.Enabled ?? receipt.Enabled is true),
@ -179,11 +178,6 @@ namespace BTCPayServer.Controllers
}
JToken? receiptData = null;
i.Metadata?.AdditionalData?.TryGetValue("receiptData", out receiptData);
string? formResponse = null;
if (i.Metadata?.AdditionalData?.TryGetValue("formResponse", out var formResponseRaw)is true)
{
formResponseRaw.Value<string>();
}
var payments = i.GetPayments(true)
.Select(paymentEntity =>
@ -240,16 +234,6 @@ namespace BTCPayServer.Controllers
var network = _NetworkProvider.GetNetwork(paymentMethodId.CryptoCode);
return network == null ? null : paymentMethodId.PaymentType.GetTransactionLink(network, txId);
}
bool CanRefund(InvoiceState invoiceState)
{
return invoiceState.Status == InvoiceStatusLegacy.Confirmed ||
invoiceState.Status == InvoiceStatusLegacy.Complete ||
(invoiceState.Status == InvoiceStatusLegacy.Expired &&
(invoiceState.ExceptionStatus == InvoiceExceptionStatus.PaidLate ||
invoiceState.ExceptionStatus == InvoiceExceptionStatus.PaidOver ||
invoiceState.ExceptionStatus == InvoiceExceptionStatus.PaidPartial)) ||
invoiceState.Status == InvoiceStatusLegacy.Invalid;
}
[HttpGet("invoices/{invoiceId}/refund")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
@ -268,7 +252,7 @@ namespace BTCPayServer.Controllers
return NotFound();
if (invoice.CurrentRefund?.PullPaymentDataId is null && GetUserId() is null)
return NotFound();
if (!CanRefund(invoice.GetInvoiceState()))
if (!invoice.GetInvoiceState().CanRefund())
return NotFound();
if (invoice.CurrentRefund?.PullPaymentDataId is string ppId && !invoice.CurrentRefund.PullPaymentData.Archived)
{
@ -324,7 +308,7 @@ namespace BTCPayServer.Controllers
if (invoice == null)
return NotFound();
if (!CanRefund(invoice.GetInvoiceState()))
if (!invoice.GetInvoiceState().CanRefund())
return NotFound();
var store = GetCurrentStore();
@ -659,9 +643,23 @@ namespace BTCPayServer.Controllers
return null;
bool isDefaultPaymentId = false;
var storeBlob = store.GetStoreBlob();
var btcId = PaymentMethodId.Parse("BTC");
var lnId = PaymentMethodId.Parse("BTC_LightningLike");
if (paymentMethodId is null)
{
var enabledPaymentIds = store.GetEnabledPaymentIds(_NetworkProvider);
var enabledPaymentIds = store.GetEnabledPaymentIds(_NetworkProvider)
// Exclude LNURL for Checkout v2
.Where(pmId => storeBlob.CheckoutType == CheckoutType.V1 || pmId.PaymentType is not LNURLPayPaymentType)
.ToArray();
// Exclude Lightning if OnChainWithLnInvoiceFallback is active and we have both payment methods
if (storeBlob.CheckoutType == CheckoutType.V2 && storeBlob.OnChainWithLnInvoiceFallback &&
enabledPaymentIds.Contains(btcId) && enabledPaymentIds.Contains(lnId))
{
enabledPaymentIds = enabledPaymentIds.Where(pmId => pmId != lnId).ToArray();
}
PaymentMethodId? invoicePaymentId = invoice.GetDefaultPaymentMethod();
PaymentMethodId? storePaymentId = store.GetDefaultPaymentId();
if (invoicePaymentId is not null)
@ -692,6 +690,7 @@ namespace BTCPayServer.Controllers
}
if (paymentMethodId is null)
return null;
BTCPayNetworkBase network = _NetworkProvider.GetNetwork<BTCPayNetworkBase>(paymentMethodId.CryptoCode);
if (network is null || !invoice.Support(paymentMethodId))
{
@ -718,12 +717,10 @@ namespace BTCPayServer.Controllers
return await GetInvoiceModel(invoiceId, paymentMethodId, lang);
}
}
var dto = invoice.EntityToDTO();
var storeBlob = store.GetStoreBlob();
var accounting = paymentMethod.Calculate();
var paymentMethodHandler = _paymentMethodHandlerDictionary[paymentMethodId];
var divisibility = _CurrencyNameTable.GetNumberFormatInfo(paymentMethod.GetId().CryptoCode, false)?.CurrencyDecimalDigits;
switch (lang?.ToLowerInvariant())
@ -826,16 +823,18 @@ namespace BTCPayServer.Controllers
.OrderByDescending(a => a.CryptoCode == _NetworkProvider.DefaultNetwork.CryptoCode).ThenBy(a => a.PaymentMethodName).ThenBy(a => a.IsLightning ? 1 : 0)
.ToList()
};
// Exclude Lightning if OnChainWithLnInvoiceFallback is active and we have both payment methods
if (storeBlob.CheckoutType == CheckoutType.V2 && storeBlob.OnChainWithLnInvoiceFallback)
{
var onchainPM = model.AvailableCryptos.Find(c => c.PaymentMethodId == "BTC");
var lightningPM = model.AvailableCryptos.Find(c => c.PaymentMethodId == "BTC_LightningLike");
var onchainPM = model.AvailableCryptos.Find(c => c.PaymentMethodId == btcId.ToString());
var lightningPM = model.AvailableCryptos.Find(c => c.PaymentMethodId == lnId.ToString());
if (onchainPM != null && lightningPM != null)
{
model.AvailableCryptos.Remove(lightningPM);
}
}
paymentMethodHandler.PreparePaymentModel(model, dto, storeBlob, paymentMethod);
model.UISettings = paymentMethodHandler.GetCheckoutUISettings();
model.PaymentMethodId = paymentMethodId.ToString();

@ -193,7 +193,8 @@ namespace BTCPayServer.Controllers
Metadata = invoiceMetadata.ToJObject(),
Currency = pr.Currency,
Amount = amount,
Checkout = { RedirectURL = redirectUrl }
Checkout = { RedirectURL = redirectUrl },
Receipt = new InvoiceDataBase.ReceiptOptions { Enabled = false }
};
var additionalTags = new List<string> { PaymentRequestRepository.GetInternalTag(pr.Id) };

@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
@ -20,6 +21,7 @@ using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Plugins.PointOfSale.Models;
using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
@ -51,6 +53,7 @@ namespace BTCPayServer
private readonly LightningAddressService _lightningAddressService;
private readonly LightningLikePayoutHandler _lightningLikePayoutHandler;
private readonly PullPaymentHostedService _pullPaymentHostedService;
private readonly BTCPayNetworkJsonSerializerSettings _btcPayNetworkJsonSerializerSettings;
public UILNURLController(InvoiceRepository invoiceRepository,
EventAggregator eventAggregator,
@ -62,7 +65,8 @@ namespace BTCPayServer
LinkGenerator linkGenerator,
LightningAddressService lightningAddressService,
LightningLikePayoutHandler lightningLikePayoutHandler,
PullPaymentHostedService pullPaymentHostedService)
PullPaymentHostedService pullPaymentHostedService,
BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings)
{
_invoiceRepository = invoiceRepository;
_eventAggregator = eventAggregator;
@ -75,11 +79,12 @@ namespace BTCPayServer
_lightningAddressService = lightningAddressService;
_lightningLikePayoutHandler = lightningLikePayoutHandler;
_pullPaymentHostedService = pullPaymentHostedService;
_btcPayNetworkJsonSerializerSettings = btcPayNetworkJsonSerializerSettings;
}
[HttpGet("withdraw/pp/{pullPaymentId}")]
public async Task<IActionResult> GetLNURLForPullPayment(string cryptoCode, string pullPaymentId, string pr)
public async Task<IActionResult> GetLNURLForPullPayment(string cryptoCode, string pullPaymentId, string pr, CancellationToken cancellationToken)
{
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
@ -155,25 +160,28 @@ namespace BTCPayServer
{
var client =
_lightningLikePaymentHandler.CreateLightningClient(pm, network);
PayResponse payResult;
try
{
payResult = await client.Pay(pr);
}
catch (Exception e)
{
payResult = new PayResponse(PayResult.Error, e.Message);
}
var payResult = await UILightningLikePayoutController.TrypayBolt(client,
claimResponse.PayoutData.GetBlob(_btcPayNetworkJsonSerializerSettings),
claimResponse.PayoutData, result, pmi, cancellationToken);
switch (payResult.Result)
{
case PayResult.Ok:
case PayResult.Unknown:
await _pullPaymentHostedService.MarkPaid(new MarkPayoutRequest()
{
PayoutId = claimResponse.PayoutData.Id, State = PayoutState.Completed
PayoutId = claimResponse.PayoutData.Id,
State = claimResponse.PayoutData.State,
Proof = claimResponse.PayoutData.GetProofBlobJson()
});
return Ok(new LNUrlStatusResponse {Status = "OK"});
return Ok(new LNUrlStatusResponse
{
Status = "OK",
Reason = payResult.Message
});
case PayResult.CouldNotFindRoute:
case PayResult.Error:
default:
await _pullPaymentHostedService.Cancel(
new PullPaymentHostedService.CancelRequest(new string[]
@ -184,7 +192,7 @@ namespace BTCPayServer
return Ok(new LNUrlStatusResponse
{
Status = "ERROR",
Reason = $"Pr could not be paid because {payResult.ErrorDetail}"
Reason = payResult.Message
});
}
}

@ -1,14 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Form;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Filters;
using BTCPayServer.Forms;
using BTCPayServer.Models;
using BTCPayServer.Models.PaymentRequestViewModels;
using BTCPayServer.PaymentRequest;
@ -17,10 +18,8 @@ using BTCPayServer.Services.PaymentRequests;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Newtonsoft.Json.Linq;
using PaymentRequestData = BTCPayServer.Data.PaymentRequestData;
using StoreData = BTCPayServer.Data.StoreData;
@ -40,6 +39,8 @@ namespace BTCPayServer.Controllers
private readonly InvoiceRepository _InvoiceRepository;
private readonly StoreRepository _storeRepository;
private FormComponentProviders FormProviders { get; }
public UIPaymentRequestController(
UIInvoiceController invoiceController,
UserManager<ApplicationUser> userManager,
@ -48,7 +49,8 @@ namespace BTCPayServer.Controllers
EventAggregator eventAggregator,
CurrencyNameTable currencies,
StoreRepository storeRepository,
InvoiceRepository invoiceRepository)
InvoiceRepository invoiceRepository,
FormComponentProviders formProviders)
{
_InvoiceController = invoiceController;
_UserManager = userManager;
@ -58,6 +60,7 @@ namespace BTCPayServer.Controllers
_Currencies = currencies;
_storeRepository = storeRepository;
_InvoiceRepository = invoiceRepository;
FormProviders = formProviders;
}
[BitpayAPIConstraint(false)]
@ -181,7 +184,7 @@ namespace BTCPayServer.Controllers
[HttpGet("{payReqId}/form")]
[HttpPost("{payReqId}/form")]
[AllowAnonymous]
public async Task<IActionResult> ViewPaymentRequestForm(string payReqId, [FromForm] string formId, [FromForm] string formData)
public async Task<IActionResult> ViewPaymentRequestForm(string payReqId)
{
var result = await _PaymentRequestRepository.FindPaymentRequest(payReqId, GetUserId());
if (result == null)
@ -191,36 +194,41 @@ namespace BTCPayServer.Controllers
var prBlob = result.GetBlob();
var prFormId = prBlob.FormId;
switch (prFormId)
var formConfig = prFormId is null ? null : Forms.UIFormsController.GetFormData(prFormId)?.Config;
switch (formConfig)
{
case null:
case { } when string.IsNullOrEmpty(prFormId):
case { } when !this.Request.HasFormContentType && prBlob.FormResponse is not null:
return RedirectToAction("ViewPaymentRequest", new { payReqId });
case { } when !this.Request.HasFormContentType && prBlob.FormResponse is null:
break;
default:
// POST case: Handle form submit
if (!string.IsNullOrEmpty(formData) && formId == prFormId)
var formData = Form.Parse(formConfig);
formData.ApplyValuesFromForm(Request.Form);
if (FormProviders.Validate(formData, ModelState))
{
prBlob.FormResponse = formData;
prBlob.FormResponse = JObject.FromObject(formData.GetValues());
result.SetBlob(prBlob);
await _PaymentRequestRepository.CreateOrUpdatePaymentRequest(result);
return RedirectToAction("PayPaymentRequest", new { payReqId });
}
// GET or empty form data case: Redirect to form
return View("PostRedirect", new PostRedirectViewModel
{
AspController = "UIForms",
AspAction = "ViewPublicForm",
FormParameters =
{
{ "formId", prFormId },
{ "redirectUrl", Request.GetCurrentUrl() }
}
});
break;
}
return RedirectToAction("ViewPaymentRequest", new { payReqId });
return View("PostRedirect", new PostRedirectViewModel
{
AspController = "UIForms",
AspAction = "ViewPublicForm",
RouteParameters =
{
{ "formId", prFormId }
},
FormParameters =
{
{ "redirectUrl", Request.GetCurrentUrl() }
}
});
}
[HttpGet("{payReqId}/pay")]

@ -338,11 +338,11 @@ namespace BTCPayServer.Controllers
Revision = payout.GetBlob(_jsonSerializerSettings).Revision,
Rate = rateResult.BidAsk.Ask
});
if (approveResult != PullPaymentHostedService.PayoutApproval.Result.Ok)
if (approveResult.Result != PullPaymentHostedService.PayoutApproval.Result.Ok)
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Message = PullPaymentHostedService.PayoutApproval.GetErrorMessage(approveResult),
Message = PullPaymentHostedService.PayoutApproval.GetErrorMessage(approveResult.Result),
Severity = StatusMessageModel.StatusSeverity.Error
});
failed = true;

@ -89,17 +89,17 @@ namespace BTCPayServer.Controllers
if (vm.WalletFile != null)
{
if (!DerivationSchemeSettings.TryParseFromWalletFile(await ReadAllText(vm.WalletFile), network, out strategy))
if (!DerivationSchemeSettings.TryParseFromWalletFile(await ReadAllText(vm.WalletFile), network, out strategy, out var error))
{
ModelState.AddModelError(nameof(vm.WalletFile), "Wallet file was not in the correct format");
ModelState.AddModelError(nameof(vm.WalletFile), $"Importing wallet failed: {error}");
return View(vm.ViewName, vm);
}
}
else if (!string.IsNullOrEmpty(vm.WalletFileContent))
{
if (!DerivationSchemeSettings.TryParseFromWalletFile(vm.WalletFileContent, network, out strategy))
if (!DerivationSchemeSettings.TryParseFromWalletFile(vm.WalletFileContent, network, out strategy, out var error))
{
ModelState.AddModelError(nameof(vm.WalletFileContent), "QR import was not in the correct format");
ModelState.AddModelError(nameof(vm.WalletFileContent), $"QR import failed: {error}");
return View(vm.ViewName, vm);
}
}

@ -578,17 +578,24 @@ namespace BTCPayServer.Controllers
var utxos = await _walletProvider.GetWallet(network)
.GetUnspentCoins(schemeSettings.AccountDerivation, false, cancellation);
var walletTransactionsInfoAsync = await this.WalletRepository.GetWalletTransactionsInfo(walletId, utxos.Select(u => u.OutPoint.Hash.ToString()).Distinct().ToArray());
var walletTransactionsInfoAsync = await this.WalletRepository.GetWalletTransactionsInfo(walletId,
utxos.SelectMany(u => GetWalletObjectsQuery.Get(u)).Distinct().ToArray());
vm.InputsAvailable = utxos.Select(coin =>
{
walletTransactionsInfoAsync.TryGetValue(coin.OutPoint.Hash.ToString(), out var info);
var labels = CreateTransactionTagModels(info).ToList();
walletTransactionsInfoAsync.TryGetValue(coin.ScriptPubKey.ToHex(), out var info2);
if (info is not null && info2 is not null)
{
info.Merge(info2);
}
info ??= info2;
return new WalletSendModel.InputSelectionOption()
{
Outpoint = coin.OutPoint.ToString(),
Amount = coin.Value.GetValue(network),
Comment = info?.Comment,
Labels = labels,
Labels = CreateTransactionTagModels(info),
Link = string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink,
coin.OutPoint.Hash.ToString()),
Confirmations = coin.Confirmations
@ -1291,7 +1298,7 @@ namespace BTCPayServer.Controllers
return NotFound();
var wallet = _walletProvider.GetWallet(paymentMethod.Network);
var walletTransactionsInfoAsync = WalletRepository.GetWalletTransactionsInfo(walletId);
var walletTransactionsInfoAsync = WalletRepository.GetWalletTransactionsInfo(walletId, (string[] ) null);
var input = await wallet.FetchTransactionHistory(paymentMethod.AccountDerivation, null, null);
var walletTransactionsInfo = await walletTransactionsInfoAsync;
var export = new TransactionsExport(wallet, walletTransactionsInfo);

@ -257,9 +257,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
});
}
}
public static readonly TimeSpan SendTimeout = TimeSpan.FromSeconds(20);
public static async Task<ResultVM> TrypayBolt(
ILightningClient lightningClient, PayoutBlob payoutBlob, PayoutData payoutData, BOLT11PaymentRequest bolt11PaymentRequest,
PaymentMethodId pmi, CancellationToken cancellationToken)
@ -281,17 +279,13 @@ namespace BTCPayServer.Data.Payouts.LightningLike
var proofBlob = new PayoutLightningBlob() { PaymentHash = bolt11PaymentRequest.PaymentHash.ToString() };
try
{
// TODO: Incorporate the changes from this PR here:
// https://github.com/btcpayserver/BTCPayServer.Lightning/pull/106
using var timeout = new CancellationTokenSource(SendTimeout);
using var c = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, cancellationToken);
var result = await lightningClient.Pay(bolt11PaymentRequest.ToString(),
new PayInvoiceParams()
{
Amount = bolt11PaymentRequest.MinimumAmount == LightMoney.Zero
? new LightMoney((decimal)payoutBlob.CryptoAmount, LightMoneyUnit.BTC)
: null
}, c.Token);
}, cancellationToken);
string message = null;
if (result.Result == PayResult.Ok)
{
@ -309,6 +303,11 @@ namespace BTCPayServer.Data.Payouts.LightningLike
// ignored
}
}
else if(result.Result == PayResult.Unknown)
{
payoutData.State = PayoutState.InProgress;
message = "The payment has been initiated but is still in-flight.";
}
payoutData.SetProofBlob(proofBlob, null);
return new ResultVM

@ -47,7 +47,7 @@ namespace BTCPayServer.Data
public static StoreBlob GetStoreBlob(this StoreData storeData)
{
var result = storeData.StoreBlob == null ? new StoreBlob() : new Serializer(null).ToObject<StoreBlob>(Encoding.UTF8.GetString(storeData.StoreBlob));
var result = storeData.StoreBlob == null ? new StoreBlob() : new Serializer(null).ToObject<StoreBlob>(storeData.StoreBlob);
if (result.PreferredExchange == null)
result.PreferredExchange = CoinGeckoRateProvider.CoinGeckoName;
if (result.PaymentMethodCriteria is null)
@ -62,7 +62,7 @@ namespace BTCPayServer.Data
var newBlob = new Serializer(null).ToString(storeBlob);
if (original == newBlob)
return false;
storeData.StoreBlob = Encoding.UTF8.GetBytes(newBlob);
storeData.StoreBlob = newBlob;
return true;
}

@ -7,7 +7,6 @@ using BTCPayServer.Services;
using BTCPayServer.Services.Labels;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
namespace BTCPayServer.Data
{
@ -83,5 +82,23 @@ namespace BTCPayServer.Data
}
}
public string Type { get; set; }
public void Merge(WalletTransactionInfo? value)
{
if (value is null)
return;
foreach (var valueLabelColor in value.LabelColors)
{
LabelColors.TryAdd(valueLabelColor.Key, valueLabelColor.Value);
}
foreach (var valueAttachment in value.Attachments.Where(valueAttachment => !Attachments.Any(attachment =>
attachment.Id == valueAttachment.Id && attachment.Type == valueAttachment.Type)))
{
Attachments.Add(valueAttachment);
}
}
}
}

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using BTCPayServer.Payments;
using NBitcoin;
using NBitcoin.DataEncoders;
@ -16,17 +17,18 @@ namespace BTCPayServer
{
public static DerivationSchemeSettings Parse(string derivationStrategy, BTCPayNetwork network)
{
string error = null;
ArgumentNullException.ThrowIfNull(network);
ArgumentNullException.ThrowIfNull(derivationStrategy);
var result = new DerivationSchemeSettings();
result.Network = network;
var parser = new DerivationSchemeParser(network);
if (TryParseXpub(derivationStrategy, parser, ref result, false) || TryParseXpub(derivationStrategy, parser, ref result, true))
if (TryParseXpub(derivationStrategy, parser, ref result, ref error, false) || TryParseXpub(derivationStrategy, parser, ref result, ref error, true))
{
return result;
}
throw new FormatException("Invalid Derivation Scheme");
throw new FormatException($"Invalid Derivation Scheme: {error}");
}
public static bool TryParseFromJson(string config, BTCPayNetwork network, out DerivationSchemeSettings strategy)
@ -47,10 +49,11 @@ namespace BTCPayServer
{
return AccountDerivation is null ? null : DBUtils.nbxv1_get_wallet_id(Network.CryptoCode, AccountDerivation.ToString());
}
private static bool TryParseXpub(string xpub, DerivationSchemeParser derivationSchemeParser, ref DerivationSchemeSettings derivationSchemeSettings, bool electrum = true)
private static bool TryParseXpub(string xpub, DerivationSchemeParser derivationSchemeParser, ref DerivationSchemeSettings derivationSchemeSettings, ref string error, bool electrum = true)
{
if (!electrum)
{
var isOD = Regex.Match(xpub, @"\(.*?\)").Success;
try
{
var result = derivationSchemeParser.ParseOutputDescriptor(xpub);
@ -64,9 +67,13 @@ namespace BTCPayServer
}).ToArray();
return true;
}
catch (Exception)
catch (Exception exception)
{
// ignored
error = exception.Message;
if (isOD)
{
return false;
} // otherwise continue and try to parse input as xpub
}
}
try
@ -82,20 +89,22 @@ namespace BTCPayServer
derivationSchemeSettings.AccountOriginal = null; // Saving this would be confusing for user, as xpub of electrum is legacy derivation, but for btcpay, it is segwit derivation
return true;
}
catch (Exception)
catch (Exception exception)
{
error = exception.Message;
return false;
}
}
public static bool TryParseFromWalletFile(string fileContents, BTCPayNetwork network, out DerivationSchemeSettings settings)
public static bool TryParseFromWalletFile(string fileContents, BTCPayNetwork network, out DerivationSchemeSettings settings, out string error)
{
settings = null;
error = null;
ArgumentNullException.ThrowIfNull(fileContents);
ArgumentNullException.ThrowIfNull(network);
var result = new DerivationSchemeSettings();
var derivationSchemeParser = new DerivationSchemeParser(network);
JObject jobj = null;
JObject jobj;
try
{
if (HexEncoder.IsWellFormed(fileContents))
@ -107,8 +116,8 @@ namespace BTCPayServer
catch
{
result.Source = "GenericFile";
if (TryParseXpub(fileContents, derivationSchemeParser, ref result) ||
TryParseXpub(fileContents, derivationSchemeParser, ref result, false))
if (TryParseXpub(fileContents, derivationSchemeParser, ref result, ref error) ||
TryParseXpub(fileContents, derivationSchemeParser, ref result, ref error, false))
{
settings = result;
settings.Network = network;
@ -125,7 +134,7 @@ namespace BTCPayServer
jobj = (JObject)jobj["keystore"];
if (!jobj.ContainsKey("xpub") ||
!TryParseXpub(jobj["xpub"].Value<string>(), derivationSchemeParser, ref result))
!TryParseXpub(jobj["xpub"].Value<string>(), derivationSchemeParser, ref result, ref error))
{
return false;
}
@ -162,7 +171,7 @@ namespace BTCPayServer
{
result.Source = "SpecterFile";
if (!TryParseXpub(jobj["descriptor"].Value<string>(), derivationSchemeParser, ref result, false))
if (!TryParseXpub(jobj["descriptor"].Value<string>(), derivationSchemeParser, ref result, ref error, false))
{
return false;
}
@ -181,7 +190,7 @@ namespace BTCPayServer
{
result.Source = "WasabiFile";
if (!jobj.ContainsKey("ExtPubKey") ||
!TryParseXpub(jobj["ExtPubKey"].Value<string>(), derivationSchemeParser, ref result, false))
!TryParseXpub(jobj["ExtPubKey"].Value<string>(), derivationSchemeParser, ref result, ref error, false))
{
return false;
}

@ -1,20 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using BTCPayServer.Abstractions.Form;
namespace BTCPayServer.Forms;
public class FormComponentProvider : IFormComponentProvider
{
private readonly IEnumerable<IFormComponentProvider> _formComponentProviders;
public FormComponentProvider(IEnumerable<IFormComponentProvider> formComponentProviders)
{
_formComponentProviders = formComponentProviders;
}
public string CanHandle(Field field)
{
return _formComponentProviders.Select(formComponentProvider => formComponentProvider.CanHandle(field)).FirstOrDefault(result => !string.IsNullOrEmpty(result));
}
}

@ -0,0 +1,34 @@
using System.Collections.Generic;
using System.Linq;
using BTCPayServer.Abstractions.Form;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace BTCPayServer.Forms;
public class FormComponentProviders
{
private readonly IEnumerable<IFormComponentProvider> _formComponentProviders;
public Dictionary<string, IFormComponentProvider> TypeToComponentProvider = new Dictionary<string, IFormComponentProvider>();
public FormComponentProviders(IEnumerable<IFormComponentProvider> formComponentProviders)
{
_formComponentProviders = formComponentProviders;
foreach (var prov in _formComponentProviders)
prov.Register(TypeToComponentProvider);
}
public bool Validate(Form form, ModelStateDictionary modelState)
{
foreach (var field in form.Fields)
{
if (TypeToComponentProvider.TryGetValue(field.Type, out var provider))
{
provider.Validate(form, field);
foreach (var err in field.ValidationErrors)
modelState.TryAddModelError(field.Name, err);
}
}
return modelState.IsValid;
}
}

@ -10,7 +10,7 @@ public static class FormDataExtensions
public static void AddForms(this IServiceCollection serviceCollection)
{
serviceCollection.AddSingleton<FormDataService>();
serviceCollection.AddSingleton<FormComponentProvider>();
serviceCollection.AddSingleton<FormComponentProviders>();
serviceCollection.AddSingleton<IFormComponentProvider, HtmlInputFormProvider>();
serviceCollection.AddSingleton<IFormComponentProvider, HtmlFieldsetFormProvider>();
}

@ -15,21 +15,21 @@ public class FormDataService
public static readonly Form StaticFormEmail = new()
{
Fields = new List<Field>() {new HtmlInputField("Enter your email", "buyerEmail", null, true, null)}
Fields = new List<Field>() {Field.Create("Enter your email", "buyerEmail", null, true, null, "email")}
};
public static readonly Form StaticFormAddress = new()
{
Fields = new List<Field>()
{
new HtmlInputField("Enter your email", "buyerEmail", null, true, null, "email"),
new HtmlInputField("Name", "buyerName", null, true, null),
new HtmlInputField("Address Line 1", "buyerAddress1", null, true, null),
new HtmlInputField("Address Line 2", "buyerAddress2", null, false, null),
new HtmlInputField("City", "buyerCity", null, true, null),
new HtmlInputField("Postcode", "buyerZip", null, false, null),
new HtmlInputField("State", "buyerState", null, false, null),
new HtmlInputField("Country", "buyerCountry", null, true, null)
Field.Create("Enter your email", "buyerEmail", null, true, null, "email"),
Field.Create("Name", "buyerName", null, true, null),
Field.Create("Address Line 1", "buyerAddress1", null, true, null),
Field.Create("Address Line 2", "buyerAddress2", null, false, null),
Field.Create("City", "buyerCity", null, true, null),
Field.Create("Postcode", "buyerZip", null, false, null),
Field.Create("State", "buyerState", null, false, null),
Field.Create("Country", "buyerCountry", null, true, null)
}
};
}

@ -1,12 +1,23 @@
using System.Linq;
using System.Collections.Generic;
using System.Linq;
using BTCPayServer.Abstractions.Form;
namespace BTCPayServer.Forms;
public class HtmlFieldsetFormProvider: IFormComponentProvider
{
public string CanHandle(Field field)
public string View => "Forms/FieldSetElement";
public void Register(Dictionary<string, IFormComponentProvider> typeToComponentProvider)
{
return new[] { "fieldset"}.Contains(field.Type) ? "Forms/FieldSetElement" : null;
typeToComponentProvider.Add("fieldset", this);
}
}
public void Validate(Field field)
{
}
public void Validate(Form form, Field field)
{
}
}

@ -1,13 +1,16 @@
using System.Linq;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using BTCPayServer.Abstractions.Form;
using BTCPayServer.Validation;
namespace BTCPayServer.Forms;
public class HtmlInputFormProvider: IFormComponentProvider
public class HtmlInputFormProvider: FormComponentProviderBase
{
public string CanHandle(Field field)
public override void Register(Dictionary<string, IFormComponentProvider> typeToComponentProvider)
{
return new[] {
foreach (var t in new[] {
"text",
"radio",
"checkbox",
@ -29,6 +32,20 @@ public class HtmlInputFormProvider: IFormComponentProvider
"search",
"url",
"tel",
"reset"}.Contains(field.Type) ? "Forms/InputElement" : null;
"reset"})
typeToComponentProvider.Add(t, this);
}
}
public override string View => "Forms/InputElement";
public override void Validate(Form form, Field field)
{
if (field.Required)
{
ValidateField<RequiredAttribute>(field);
}
if (field.Type == "email")
{
ValidateField<MailboxAddressAttribute>(field);
}
}
}

@ -1,8 +1,26 @@
using BTCPayServer.Abstractions.Form;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using BTCPayServer.Abstractions.Form;
namespace BTCPayServer.Forms;
public interface IFormComponentProvider
{
public string CanHandle(Field field);
string View { get; }
void Validate(Form form, Field field);
void Register(Dictionary<string, IFormComponentProvider> typeToComponentProvider);
}
public abstract class FormComponentProviderBase : IFormComponentProvider
{
public abstract string View { get; }
public abstract void Register(Dictionary<string, IFormComponentProvider> typeToComponentProvider);
public abstract void Validate(Form form, Field field);
public void ValidateField<T>(Field field) where T : ValidationAttribute, new()
{
var result = new T().GetValidationResult(field.Value, new ValidationContext(field) { DisplayName = field.Label, MemberName = field.Name });
if (result != null)
field.ValidationErrors.Add(result.ErrorMessage);
}
}

@ -8,5 +8,6 @@ public class FormViewModel
{
public string RedirectUrl { get; set; }
public FormData FormData { get; set; }
public Form Form { get => JObject.Parse(FormData.Config).ToObject<Form>(); }
Form _Form;
public Form Form { get => _Form ??= Form.Parse(FormData.Config); }
}

@ -1,36 +1,36 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Form;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Data.Data;
using BTCPayServer.Forms.Models;
using BTCPayServer.Models;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Forms;
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public class UIFormsController : Controller
{
private FormComponentProviders FormProviders { get; }
public UIFormsController(FormComponentProviders formProviders)
{
FormProviders = formProviders;
}
[AllowAnonymous]
[HttpGet("~/forms/{formId}")]
[HttpPost("~/forms")]
public IActionResult ViewPublicForm(string? formId, string? redirectUrl)
{
if (!IsValidRedirectUri(redirectUrl))
return BadRequest();
FormData? formData = string.IsNullOrEmpty(formId) ? null : GetFormData(formId);
if (formData == null)
{
@ -39,25 +39,35 @@ public class UIFormsController : Controller
: Redirect(redirectUrl);
}
return View("View", new FormViewModel { FormData = formData, RedirectUrl = redirectUrl });
return GetFormView(formData, redirectUrl);
}
ViewResult GetFormView(FormData formData, string? redirectUrl)
{
return View("View", new FormViewModel { FormData = formData, RedirectUrl = redirectUrl });
}
[AllowAnonymous]
[HttpPost("~/forms/{formId}")]
public IActionResult SubmitForm(
string formId, string? redirectUrl,
[FromServices] StoreRepository storeRepository,
[FromServices] UIInvoiceController invoiceController)
public IActionResult SubmitForm(string formId, string? redirectUrl, string? command)
{
if (!IsValidRedirectUri(redirectUrl))
return BadRequest();
var formData = GetFormData(formId);
if (formData is null)
{
if (formData?.Config is null)
return NotFound();
}
if (!Request.HasFormContentType)
return GetFormView(formData, redirectUrl);
var dbForm = JObject.Parse(formData.Config!).ToObject<Form>()!;
dbForm.ApplyValuesFromForm(Request.Form);
Dictionary<string, object> data = dbForm.GetValues();
var conf = Form.Parse(formData.Config);
conf.ApplyValuesFromForm(Request.Form);
if (!FormProviders.Validate(conf, ModelState))
return GetFormView(formData, redirectUrl);
var form = new MultiValueDictionary<string, string>();
foreach (var kv in Request.Form)
form.Add(kv.Key, kv.Value);
// With redirect, the form comes from another entity that we need to send the data back to
if (!string.IsNullOrEmpty(redirectUrl))
@ -65,30 +75,26 @@ public class UIFormsController : Controller
return View("PostRedirect", new PostRedirectViewModel
{
FormUrl = redirectUrl,
FormParameters =
{
{ "formId", formData.Id },
{ "formData", JsonConvert.SerializeObject(data) }
}
FormParameters = form
});
}
return NotFound();
}
private FormData? GetFormData(string id)
internal static FormData? GetFormData(string id)
{
FormData? form = id switch
{
{ } formId when formId == GenericFormOption.Address.ToString() => new FormData
{
Config = JObject.FromObject(FormDataService.StaticFormAddress).ToString(),
Config = FormDataService.StaticFormAddress.ToString(),
Id = GenericFormOption.Address.ToString(),
Name = "Provide your address",
},
{ } formId when formId == GenericFormOption.Email.ToString() => new FormData
{
Config = JObject.FromObject(FormDataService.StaticFormEmail).ToString(),
Config = FormDataService.StaticFormEmail.ToString(),
Id = GenericFormOption.Email.ToString(),
Name = "Provide your email address",
},
@ -96,4 +102,8 @@ public class UIFormsController : Controller
};
return form;
}
private bool IsValidRedirectUri(string? redirectUrl) =>
!string.IsNullOrEmpty(redirectUrl) && Uri.TryCreate(redirectUrl, UriKind.RelativeOrAbsolute, out var uri) &&
(Url.IsLocalUrl(redirectUrl) || uri.Host.Equals(Request.Host.Host));
}

@ -173,10 +173,10 @@ next:
db.WalletObjectLinks.Add(new WalletObjectLinkData()
{
WalletId = tx.WalletDataId,
ChildType = Data.WalletObjectData.Types.Tx,
ChildId = tx.TransactionId,
ParentType = Data.WalletObjectData.Types.Label,
ParentId = labelId
BType = Data.WalletObjectData.Types.Tx,
BId = tx.TransactionId,
AType = Data.WalletObjectData.Types.Label,
AId = labelId
});
if (label.Value is ReferenceLabel reflabel)
@ -195,10 +195,10 @@ next:
db.WalletObjectLinks.Add(new WalletObjectLinkData()
{
WalletId = tx.WalletDataId,
ChildType = Data.WalletObjectData.Types.Tx,
ChildId = tx.TransactionId,
ParentType = reflabel.Type,
ParentId = reflabel.Reference ?? String.Empty
BType = Data.WalletObjectData.Types.Tx,
BId = tx.TransactionId,
AType = reflabel.Type,
AId = reflabel.Reference ?? String.Empty
});
}
}
@ -224,10 +224,10 @@ next:
db.WalletObjectLinks.Add(new WalletObjectLinkData()
{
WalletId = tx.WalletDataId,
ChildType = Data.WalletObjectData.Types.Tx,
ChildId = tx.TransactionId,
ParentType = "payout",
ParentId = payout
BType = Data.WalletObjectData.Types.Tx,
BId = tx.TransactionId,
AType = "payout",
AId = payout
});
}
}

@ -79,10 +79,12 @@ namespace BTCPayServer.HostedServices
OldRevision
}
public record ApprovalResult(Result Result, decimal? CryptoAmount);
public string PayoutId { get; set; }
public int Revision { get; set; }
public decimal Rate { get; set; }
internal TaskCompletionSource<Result> Completion { get; set; }
internal TaskCompletionSource<ApprovalResult> Completion { get; set; }
public static string GetErrorMessage(Result result)
{
@ -333,10 +335,10 @@ namespace BTCPayServer.HostedServices
return _rateFetcher.FetchRate(rule, cancellationToken);
}
public Task<PayoutApproval.Result> Approve(PayoutApproval approval)
public Task<PayoutApproval.ApprovalResult> Approve(PayoutApproval approval)
{
approval.Completion =
new TaskCompletionSource<PayoutApproval.Result>(TaskCreationOptions.RunContinuationsAsynchronously);
new TaskCompletionSource<PayoutApproval.ApprovalResult>(TaskCreationOptions.RunContinuationsAsynchronously);
if (!_Channel.Writer.TryWrite(approval))
throw new ObjectDisposedException(nameof(PullPaymentHostedService));
return approval.Completion.Task;
@ -351,26 +353,26 @@ namespace BTCPayServer.HostedServices
.FirstOrDefaultAsync();
if (payout is null)
{
req.Completion.SetResult(PayoutApproval.Result.NotFound);
req.Completion.SetResult(new PayoutApproval.ApprovalResult(PayoutApproval.Result.NotFound, null));
return;
}
if (payout.State != PayoutState.AwaitingApproval)
{
req.Completion.SetResult(PayoutApproval.Result.InvalidState);
req.Completion.SetResult(new PayoutApproval.ApprovalResult(PayoutApproval.Result.InvalidState, null));
return;
}
var payoutBlob = payout.GetBlob(this._jsonSerializerSettings);
if (payoutBlob.Revision != req.Revision)
{
req.Completion.SetResult(PayoutApproval.Result.OldRevision);
req.Completion.SetResult(new PayoutApproval.ApprovalResult(PayoutApproval.Result.OldRevision, null));
return;
}
if (!PaymentMethodId.TryParse(payout.PaymentMethodId, out var paymentMethod))
{
req.Completion.SetResult(PayoutApproval.Result.NotFound);
req.Completion.SetResult(new PayoutApproval.ApprovalResult(PayoutApproval.Result.NotFound, null));
return;
}
@ -388,7 +390,7 @@ namespace BTCPayServer.HostedServices
await payoutHandler.GetMinimumPayoutAmount(paymentMethod, dest.destination);
if (cryptoAmount < minimumCryptoAmount)
{
req.Completion.TrySetResult(PayoutApproval.Result.TooLowAmount);
req.Completion.TrySetResult(new PayoutApproval.ApprovalResult(PayoutApproval.Result.TooLowAmount, null));
return;
}
@ -397,7 +399,7 @@ namespace BTCPayServer.HostedServices
payout.SetBlob(payoutBlob, _jsonSerializerSettings);
await ctx.SaveChangesAsync();
req.Completion.SetResult(PayoutApproval.Result.Ok);
req.Completion.SetResult(new PayoutApproval.ApprovalResult(PayoutApproval.Result.Ok, payoutBlob.CryptoAmount));
}
catch (Exception ex)
{
@ -566,18 +568,20 @@ namespace BTCPayServer.HostedServices
var rateResult = await GetRate(payout, null, CancellationToken.None);
if (rateResult.BidAsk != null)
{
var approveResult = new TaskCompletionSource<PayoutApproval.Result>();
var approveResultTask = new TaskCompletionSource<PayoutApproval.ApprovalResult>();
await HandleApproval(new PayoutApproval()
{
PayoutId = payout.Id,
Revision = payoutBlob.Revision,
Rate = rateResult.BidAsk.Ask,
Completion = approveResult
Completion = approveResultTask
});
if ((await approveResult.Task) == PayoutApproval.Result.Ok)
var approveResult = await approveResultTask.Task;
if (approveResult.Result == PayoutApproval.Result.Ok)
{
payout.State = PayoutState.AwaitingPayment;
payoutBlob.CryptoAmount = approveResult.CryptoAmount;
payout.SetBlob(payoutBlob, _jsonSerializerSettings);
}
}
}

@ -5,6 +5,7 @@ using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Logging;
@ -22,42 +23,79 @@ namespace BTCPayServer.HostedServices
{
public class TransactionLabelMarkerHostedService : EventHostedServiceBase
{
private readonly EventAggregator _eventAggregator;
private readonly WalletRepository _walletRepository;
public TransactionLabelMarkerHostedService(EventAggregator eventAggregator, WalletRepository walletRepository, Logs logs) :
base(eventAggregator, logs)
{
_eventAggregator = eventAggregator;
_walletRepository = walletRepository;
}
protected override void SubscribeToEvents()
{
Subscribe<InvoiceEvent>();
Subscribe<NewOnChainTransactionEvent>();
}
protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken)
{
if (evt is InvoiceEvent invoiceEvent && invoiceEvent.Name == InvoiceEvent.ReceivedPayment &&
invoiceEvent.Payment.GetPaymentMethodId()?.PaymentType == BitcoinPaymentType.Instance &&
invoiceEvent.Payment.GetCryptoPaymentData() is BitcoinLikePaymentData bitcoinLikePaymentData)
switch (evt)
{
var walletId = new WalletId(invoiceEvent.Invoice.StoreId, invoiceEvent.Payment.GetCryptoCode());
var transactionId = bitcoinLikePaymentData.Outpoint.Hash;
var labels = new List<Attachment>
// For each new transaction that we detect, we check if we can find
// any utxo or script object matching it.
// If we find, then we create a link between them and the tx object.
case NewOnChainTransactionEvent transactionEvent:
{
Attachment.Invoice(invoiceEvent.Invoice.Id)
};
foreach (var paymentId in PaymentRequestRepository.GetPaymentIdsFromInternalTags(invoiceEvent.Invoice))
{
labels.Add(Attachment.PaymentRequest(paymentId));
}
foreach (var appId in AppService.GetAppInternalTags(invoiceEvent.Invoice))
{
labels.Add(Attachment.App(appId));
}
var txHash = transactionEvent.NewTransactionEvent.TransactionData.TransactionHash.ToString();
// find all wallet objects that fit this transaction
// that means see if there are any utxo objects that match in/outs and scripts/addresses that match outs
var matchedObjects = transactionEvent.NewTransactionEvent.TransactionData.Transaction.Inputs
.Select(txIn => new ObjectTypeId(WalletObjectData.Types.Utxo, txIn.PrevOut.ToString()))
.Concat(transactionEvent.NewTransactionEvent.TransactionData.Transaction.Outputs.AsIndexedOutputs().SelectMany(txOut =>
new[]{
new ObjectTypeId(WalletObjectData.Types.Script,txOut.TxOut.ScriptPubKey.ToHex()),
new ObjectTypeId(WalletObjectData.Types.Utxo,txOut.ToCoin().Outpoint.ToString())
} )).Distinct().ToArray();
await _walletRepository.AddWalletTransactionAttachment(walletId, transactionId, labels);
var objs = await _walletRepository.GetWalletObjects(new GetWalletObjectsQuery(){TypesIds = matchedObjects});
foreach (var walletObjectDatas in objs.GroupBy(data => data.Key.WalletId))
{
var txWalletObject = new WalletObjectId(walletObjectDatas.Key,
WalletObjectData.Types.Tx, txHash);
await _walletRepository.EnsureWalletObject(txWalletObject);
foreach (var walletObjectData in walletObjectDatas)
{
await _walletRepository.EnsureWalletObjectLink(txWalletObject, walletObjectData.Key);
}
}
break;
}
case InvoiceEvent {Name: InvoiceEvent.ReceivedPayment} invoiceEvent when
invoiceEvent.Payment.GetPaymentMethodId()?.PaymentType == BitcoinPaymentType.Instance &&
invoiceEvent.Payment.GetCryptoPaymentData() is BitcoinLikePaymentData bitcoinLikePaymentData:
{
var walletId = new WalletId(invoiceEvent.Invoice.StoreId, invoiceEvent.Payment.GetCryptoCode());
var transactionId = bitcoinLikePaymentData.Outpoint.Hash;
var labels = new List<Attachment>
{
Attachment.Invoice(invoiceEvent.Invoice.Id)
};
foreach (var paymentId in PaymentRequestRepository.GetPaymentIdsFromInternalTags(invoiceEvent.Invoice))
{
labels.Add(Attachment.PaymentRequest(paymentId));
}
foreach (var appId in AppService.GetAppInternalTags(invoiceEvent.Invoice))
{
labels.Add(Attachment.App(appId));
}
await _walletRepository.AddWalletTransactionAttachment(walletId, transactionId, labels);
break;
}
}
}
}

@ -157,6 +157,7 @@ namespace BTCPayServer.HostedServices
webhookEvent.DeliveryId = delivery.Id;
webhookEvent.WebhookId = webhook.Id;
webhookEvent.OriginalDeliveryId = delivery.Id;
webhookEvent.Metadata = invoiceEvent.Invoice.Metadata.ToJObject();
webhookEvent.IsRedelivery = false;
webhookEvent.Timestamp = delivery.Timestamp;
var context = new WebhookDeliveryRequest(webhook.Id, webhookEvent, delivery, webhookBlob);

@ -88,7 +88,7 @@ namespace BTCPayServer.Hosting
if (settings is null)
{
// If it is null, then it's the first run: let's skip all the migrations by migration flags to true
settings = new MigrationSettings() { MigratedInvoiceTextSearchPages = int.MaxValue };
settings = new MigrationSettings() { MigratedInvoiceTextSearchPages = int.MaxValue, MigratedTransactionLabels = int.MaxValue };
foreach (var prop in settings.GetType().GetProperties().Where(p => p.CanWrite && p.PropertyType == typeof(bool)))
{
prop.SetValue(settings, true);

@ -46,9 +46,9 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
CustomCSSLink = blob.CustomCSSLink;
EmbeddedCSS = blob.EmbeddedCSS;
AllowCustomPaymentAmounts = blob.AllowCustomPaymentAmounts;
FormResponse = string.IsNullOrEmpty(blob.FormResponse)
FormResponse = blob.FormResponse is null
? null
: JObject.Parse(blob.FormResponse).ToObject<Dictionary<string, object>>();
: blob.FormResponse.ToObject<Dictionary<string, object>>();
}
[Display(Name = "Request customer data on checkout")]

@ -98,7 +98,7 @@ namespace BTCPayServer.PaymentRequest
CurrencyData = _currencies.GetCurrencyData(blob.Currency, true),
LastUpdated = DateTime.UtcNow,
FormId = blob.FormId,
FormSubmitted = !string.IsNullOrEmpty(blob.FormResponse),
FormSubmitted = blob.FormResponse is not null,
AnyPendingInvoice = pendingInvoice != null,
PendingInvoiceHasPayments = pendingInvoice != null &&
pendingInvoice.ExceptionStatus != InvoiceExceptionStatus.None,

@ -14,7 +14,6 @@ public static class PayoutProcessorsExtensions
serviceCollection.AddSingleton<IPayoutProcessorFactory>(provider => provider.GetRequiredService<OnChainAutomatedPayoutSenderFactory>());
serviceCollection.AddSingleton<LightningAutomatedPayoutSenderFactory>();
serviceCollection.AddSingleton<IPayoutProcessorFactory>(provider => provider.GetRequiredService<LightningAutomatedPayoutSenderFactory>());
serviceCollection.AddHostedService<PayoutProcessorService>();
serviceCollection.AddSingleton<PayoutProcessorService>();
serviceCollection.AddHostedService(s=> s.GetRequiredService<PayoutProcessorService>());
}

@ -9,11 +9,13 @@ using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Form;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Filters;
using BTCPayServer.Forms;
using BTCPayServer.ModelBinders;
using BTCPayServer.Models;
using BTCPayServer.Plugins.PointOfSale.Models;
@ -39,19 +41,23 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
AppService appService,
CurrencyNameTable currencies,
StoreRepository storeRepository,
UIInvoiceController invoiceController)
UIInvoiceController invoiceController,
FormComponentProviders formProviders)
{
_currencies = currencies;
_appService = appService;
_storeRepository = storeRepository;
_invoiceController = invoiceController;
FormProviders = formProviders;
}
private readonly CurrencyNameTable _currencies;
private readonly StoreRepository _storeRepository;
private readonly AppService _appService;
private readonly UIInvoiceController _invoiceController;
public FormComponentProviders FormProviders { get; }
[HttpGet("/")]
[HttpGet("/apps/{appId}/pos/{viewType?}")]
[XFrameOptions(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
@ -118,8 +124,6 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
string notificationUrl,
string redirectUrl,
string choiceKey,
string formId = null,
string formData = null,
string posData = null,
RequiresRefundEmail requiresRefundEmail = RequiresRefundEmail.InheritFromStore,
CancellationToken cancellationToken = default)
@ -221,18 +225,21 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
var store = await _appService.GetStore(app);
var posFormId = settings.FormId;
var formConfig = posFormId is null ? null : Forms.UIFormsController.GetFormData(posFormId)?.Config;
JObject formResponse = null;
switch (posFormId)
switch (formConfig)
{
case null:
case { } when string.IsNullOrEmpty(posFormId):
case { } when !this.Request.HasFormContentType:
break;
default:
// POST case: Handle form submit
if (!string.IsNullOrEmpty(formData) && formId == posFormId)
var formData = Form.Parse(formConfig);
formData.ApplyValuesFromForm(this.Request.Form);
if (FormProviders.Validate(formData, ModelState))
{
formResponse = JObject.Parse(formData);
formResponse = JObject.FromObject(formData.GetValues());
break;
}
@ -247,9 +254,12 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
{
AspController = "UIForms",
AspAction = "ViewPublicForm",
RouteParameters =
{
{ "formId", posFormId }
},
FormParameters =
{
{ "formId", posFormId },
{ "redirectUrl", Request.GetCurrentUrl() + query }
}
});

@ -7,6 +7,7 @@
"BTCPAY_NETWORK": "regtest",
"BTCPAY_LAUNCHSETTINGS": "true",
"BTCPAY_BTCLIGHTNING": "type=clightning;server=tcp://127.0.0.1:30993",
"BTCPAY_BTCEXTERNALCLIGHTNING": "type=clightning;server=tcp://127.0.0.1:30993",
"BTCPAY_BTCEXTERNALLNDREST": "type=lnd-rest;server=http://lnd:lnd@127.0.0.1:35531/;allowinsecure=true",
"BTCPAY_BTCEXTERNALLNDSEEDBACKUP": "../BTCPayServer.Tests/TestData/LndSeedBackup/walletunlock.json",
"BTCPAY_BTCEXTERNALSPARK": "server=/spark/btc/;cookiefile=fake",
@ -31,7 +32,7 @@
"BTCPAY_CHEATMODE": "true",
"BTCPAY_EXPLORERPOSTGRES": "User ID=postgres;Include Error Detail=true;Host=127.0.0.1;Port=39372;Database=nbxplorer"
},
"applicationUrl": "http://localhost:14142/"
"applicationUrl": "http://0.0.0.0:14142/"
},
"Bitcoin-HTTPS": {
"commandName": "Project",
@ -42,7 +43,8 @@
"BTCPAY_PORT": "14142",
"BTCPAY_HttpsUseDefaultCertificate": "true",
"BTCPAY_VERBOSE": "true",
"BTCPAY_BTCLIGHTNING": "type=clightning;server=tcp://127.0.0.1:30993",
"BTCPAY_BTCLIGHTNING": "type=lnd-rest;server=http://lnd:lnd@127.0.0.1:35531/;allowinsecure=true",
"BTCPAY_BTCEXTERNALCLIGHTNING": "type=clightning;server=tcp://127.0.0.1:30993",
"BTCPAY_BTCEXTERNALLNDREST": "type=lnd-rest;server=http://lnd:lnd@127.0.0.1:35531/;allowinsecure=true",
"BTCPAY_BTCEXTERNALLNDSEEDBACKUP": "../BTCPayServer.Tests/TestData/LndSeedBackup/walletunlock.json",
"BTCPAY_BTCEXTERNALSPARK": "server=/spark/btc/;cookiefile=fake",
@ -68,7 +70,7 @@
"BTCPAY_CHEATMODE": "true",
"BTCPAY_EXPLORERPOSTGRES": "User ID=postgres;Include Error Detail=true;Host=127.0.0.1;Port=39372;Database=nbxplorer"
},
"applicationUrl": "https://localhost:14142/"
"applicationUrl": "https://0.0.0.0:14142/"
},
"Altcoins-HTTPS": {
"commandName": "Project",
@ -107,7 +109,7 @@
"BTCPAY_CHEATMODE": "true",
"BTCPAY_EXPLORERPOSTGRES": "User ID=postgres;Include Error Detail=true;Host=127.0.0.1;Port=39372;Database=nbxplorer"
},
"applicationUrl": "https://localhost:14142/"
"applicationUrl": "https://0.0.0.0:14142/"
}
}
}

@ -835,6 +835,17 @@ namespace BTCPayServer.Services.Invoices
(Status != InvoiceStatusLegacy.Invalid && ExceptionStatus == InvoiceExceptionStatus.Marked);
}
public bool CanRefund()
{
return Status == InvoiceStatusLegacy.Confirmed ||
Status == InvoiceStatusLegacy.Complete ||
(Status == InvoiceStatusLegacy.Expired &&
(ExceptionStatus == InvoiceExceptionStatus.PaidLate ||
ExceptionStatus == InvoiceExceptionStatus.PaidOver ||
ExceptionStatus == InvoiceExceptionStatus.PaidPartial)) ||
Status == InvoiceStatusLegacy.Invalid;
}
public override int GetHashCode()
{
return HashCode.Combine(Status, ExceptionStatus);

@ -36,5 +36,5 @@ public static class CheckoutFormSelectList
typeof(GenericFormOption).DisplayName(opt.ToString());
private static SelectListItem GenericOptionItem(GenericFormOption opt) =>
new() { Text = DisplayName(opt), Value = opt.ToString() };
new() { Text = DisplayName(opt), Value = opt == GenericFormOption.None ? null : opt.ToString() };
}

@ -5,6 +5,7 @@ using System.Threading.Tasks;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Services.Wallets;
using Microsoft.EntityFrameworkCore;
using NBitcoin;
using Newtonsoft.Json;
@ -37,8 +38,9 @@ namespace BTCPayServer.Services
Type = type;
Ids = ids;
}
public GetWalletObjectsQuery(ObjectTypeId[]? typesIds)
public GetWalletObjectsQuery(WalletId? walletId,ObjectTypeId[]? typesIds)
{
WalletId = walletId;
TypesIds = typesIds;
}
@ -50,6 +52,18 @@ namespace BTCPayServer.Services
public string[]? Ids { get; set; }
public bool IncludeNeighbours { get; set; } = true;
public bool UseInefficientPath { get; set; }
public static ObjectTypeId Get(Script script)
{
return new ObjectTypeId(WalletObjectData.Types.Script, script.ToHex());
}
public static IEnumerable<ObjectTypeId> Get(ReceivedCoin coin)
{
yield return new ObjectTypeId(WalletObjectData.Types.Tx, coin.OutPoint.Hash.ToString());
yield return Get(coin.ScriptPubKey);
yield return new ObjectTypeId(WalletObjectData.Types.Utxo, coin.OutPoint.ToString());
}
}
#nullable restore
@ -78,7 +92,7 @@ namespace BTCPayServer.Services
using var ctx = _ContextFactory.CreateContext();
// If we are using postgres, the `transactionIds.Contains(w.ChildId)` result in a long query like `ANY(@txId1, @txId2, @txId3, @txId4)`
// If we are using postgres, the `transactionIds.Contains(w.BId)` result in a long query like `ANY(@txId1, @txId2, @txId3, @txId4)`
// Such request isn't well optimized by postgres, and create different requests clogging up
// pg_stat_statements output, making it impossible to analyze the performance impact of this query.
// On top of this, the entity version is doing 2 left join to satisfy the Include queries, resulting in n*m row returned for each transaction.
@ -106,9 +120,9 @@ namespace BTCPayServer.Services
var query =
$"SELECT wos.\"WalletId\", wos.\"Id\", wos.\"Type\", wos.\"Data\", wol.\"LinkData\", wol.\"Type2\", wol.\"Id2\"{includeNeighbourSelect} FROM ({selectWalletObjects}) wos " +
$"LEFT JOIN LATERAL ( " +
"SELECT \"ParentType\" AS \"Type2\", \"ParentId\" AS \"Id2\", \"Data\" AS \"LinkData\" FROM \"WalletObjectLinks\" WHERE \"WalletId\"=wos.\"WalletId\" AND \"ChildType\"=wos.\"Type\" AND \"ChildId\"=wos.\"Id\" " +
"SELECT \"AType\" AS \"Type2\", \"AId\" AS \"Id2\", \"Data\" AS \"LinkData\" FROM \"WalletObjectLinks\" WHERE \"WalletId\"=wos.\"WalletId\" AND \"BType\"=wos.\"Type\" AND \"BId\"=wos.\"Id\" " +
"UNION " +
"SELECT \"ChildType\" AS \"Type2\", \"ChildId\" AS \"Id2\", \"Data\" AS \"LinkData\" FROM \"WalletObjectLinks\" WHERE \"WalletId\"=wos.\"WalletId\" AND \"ParentType\"=wos.\"Type\" AND \"ParentId\"=wos.\"Id\"" +
"SELECT \"BType\" AS \"Type2\", \"BId\" AS \"Id2\", \"Data\" AS \"LinkData\" FROM \"WalletObjectLinks\" WHERE \"WalletId\"=wos.\"WalletId\" AND \"AType\"=wos.\"Type\" AND \"AId\"=wos.\"Id\"" +
$" ) wol ON true " + includeNeighbourJoin;
cmd.CommandText = query;
if (queryObject.WalletId is not null)
@ -177,21 +191,21 @@ namespace BTCPayServer.Services
else
{
wosById.Add(id, wo);
wo.ChildLinks = new List<WalletObjectLinkData>();
wo.Bs = new List<WalletObjectLinkData>();
}
if (reader["Type2"] is not DBNull)
{
var l = new WalletObjectLinkData()
{
ChildType = (string)reader["Type2"],
ChildId = (string)reader["Id2"],
BType = (string)reader["Type2"],
BId = (string)reader["Id2"],
Data = reader["LinkData"] is DBNull ? null : (string)reader["LinkData"]
};
wo.ChildLinks.Add(l);
l.Child = new WalletObjectData()
wo.Bs.Add(l);
l.B = new WalletObjectData()
{
Type = l.ChildType,
Id = l.ChildId,
Type = l.BType,
Id = l.BId,
Data = (!queryObject.IncludeNeighbours || reader["Data2"] is DBNull) ? null : (string)reader["Data2"]
};
}
@ -215,8 +229,8 @@ namespace BTCPayServer.Services
}
if (queryObject.IncludeNeighbours)
{
q = q.Include(o => o.ChildLinks).ThenInclude(o => o.Child)
.Include(o => o.ParentLinks).ThenInclude(o => o.Parent);
q = q.Include(o => o.Bs).ThenInclude(o => o.B)
.Include(o => o.As).ThenInclude(o => o.A);
}
q = q.AsNoTracking();
@ -230,9 +244,28 @@ namespace BTCPayServer.Services
}
}
#nullable restore
public async Task<Dictionary<string, WalletTransactionInfo>> GetWalletTransactionsInfo(WalletId walletId, string[] transactionIds = null)
public async Task<Dictionary<string, WalletTransactionInfo>> GetWalletTransactionsInfo(WalletId walletId,
string[] transactionIds = null)
{
var wos = await GetWalletObjects((GetWalletObjectsQuery)(new(walletId, WalletObjectData.Types.Tx, transactionIds)));
var wos = await GetWalletObjects(
new GetWalletObjectsQuery(walletId, WalletObjectData.Types.Tx, transactionIds));
return await GetWalletTransactionsInfoCore(walletId, wos);
}
public async Task<Dictionary<string, WalletTransactionInfo>> GetWalletTransactionsInfo(WalletId walletId,
ObjectTypeId[] transactionIds = null)
{
var wos = await GetWalletObjects(
new GetWalletObjectsQuery(walletId, transactionIds));
return await GetWalletTransactionsInfoCore(walletId, wos);
}
private async Task<Dictionary<string, WalletTransactionInfo>> GetWalletTransactionsInfoCore(WalletId walletId,
Dictionary<WalletObjectId, WalletObjectData> wos)
{
var result = new Dictionary<string, WalletTransactionInfo>(wos.Count);
foreach (var obj in wos.Values)
{
@ -299,10 +332,10 @@ namespace BTCPayServer.Services
var l = new WalletObjectLinkData()
{
WalletId = a.WalletId.ToString(),
ParentType = a.Type,
ParentId = a.Id,
ChildType = b.Type,
ChildId = b.Id,
AType = a.Type,
AId = a.Id,
BType = b.Type,
BId = b.Id,
Data = data?.ToString(Formatting.None)
};
ctx.WalletObjectLinks.Add(l);
@ -345,10 +378,10 @@ namespace BTCPayServer.Services
var l = new WalletObjectLinkData()
{
WalletId = a.WalletId.ToString(),
ParentType = a.Type,
ParentId = a.Id,
ChildType = b.Type,
ChildId = b.Id,
AType = a.Type,
AId = a.Id,
BType = b.Type,
BId = b.Id,
Data = data?.ToString(Formatting.None)
};
var e = ctx.WalletObjectLinks.Add(l);
@ -421,13 +454,20 @@ namespace BTCPayServer.Services
}
public Task AddWalletTransactionAttachment(WalletId walletId, uint256 txId, Attachment attachment)
{
return AddWalletTransactionAttachment(walletId, txId, new[] { attachment });
return AddWalletTransactionAttachment(walletId, txId.ToString(), new []{attachment}, WalletObjectData.Types.Tx);
}
public async Task AddWalletTransactionAttachment(WalletId walletId, uint256 txId, IEnumerable<Attachment> attachments)
public Task AddWalletTransactionAttachment(WalletId walletId, uint256 txId,
IEnumerable<Attachment> attachments)
{
return AddWalletTransactionAttachment(walletId, txId.ToString(), attachments, WalletObjectData.Types.Tx);
}
public async Task AddWalletTransactionAttachment(WalletId walletId, string txId, IEnumerable<Attachment> attachments, string type)
{
ArgumentNullException.ThrowIfNull(walletId);
ArgumentNullException.ThrowIfNull(txId);
var txObjId = new WalletObjectId(walletId, WalletObjectData.Types.Tx, txId.ToString());
var txObjId = new WalletObjectId(walletId, type, txId.ToString());
await EnsureWalletObject(txObjId);
foreach (var attachment in attachments)
{
@ -453,10 +493,10 @@ namespace BTCPayServer.Services
ctx.WalletObjectLinks.Remove(new WalletObjectLinkData()
{
WalletId = a.WalletId.ToString(),
ParentId = a.Id,
ParentType = a.Type,
ChildId = b.Id,
ChildType = b.Type
AId = a.Id,
AType = a.Type,
BId = b.Id,
BType = b.Type
});
try
{

@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Services.Stores;
using Microsoft.Extensions.Hosting;
@ -22,19 +23,21 @@ namespace BTCPayServer.Services.Wallets
private readonly BTCPayWalletProvider _btcPayWalletProvider;
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly StoreRepository _storeRepository;
private readonly WalletRepository _walletRepository;
private readonly ConcurrentDictionary<WalletId, KeyPathInformation> _walletReceiveState =
new ConcurrentDictionary<WalletId, KeyPathInformation>();
public WalletReceiveService(EventAggregator eventAggregator, ExplorerClientProvider explorerClientProvider,
BTCPayWalletProvider btcPayWalletProvider, BTCPayNetworkProvider btcPayNetworkProvider,
StoreRepository storeRepository)
StoreRepository storeRepository, WalletRepository walletRepository )
{
_eventAggregator = eventAggregator;
_explorerClientProvider = explorerClientProvider;
_btcPayWalletProvider = btcPayWalletProvider;
_btcPayNetworkProvider = btcPayNetworkProvider;
_storeRepository = storeRepository;
_walletRepository = walletRepository;
}
public async Task<string> UnReserveAddress(WalletId walletId)
@ -73,6 +76,8 @@ namespace BTCPayServer.Services.Wallets
}
var reserve = (await wallet.ReserveAddressAsync(derivationScheme.AccountDerivation));
await _walletRepository.AddWalletTransactionAttachment(walletId, reserve.ScriptPubKey.ToString(), new []{new Attachment("receive")},
WalletObjectData.Types.Script);
Set(walletId, reserve);
return reserve;
}

@ -40,8 +40,11 @@ namespace BTCPayServer.Storage.Services.Providers.FileSystemStorage
// Set the relative URL to the directory name if the root path is default, otherwise add root path before the directory name
var relativeUrl = baseUri.AbsolutePath == "/" ? LocalStorageDirectoryName : $"{baseUri.AbsolutePath}/{LocalStorageDirectoryName}";
var url = new Uri(baseUri, relativeUrl);
return baseResult.Replace(new DirectoryInfo(_datadirs.Value.StorageDir).FullName, url.AbsoluteUri,
var r = baseResult.Replace(new DirectoryInfo(_datadirs.Value.StorageDir).FullName, url.AbsoluteUri,
StringComparison.InvariantCultureIgnoreCase);
if (Path.DirectorySeparatorChar == '\\')
r = r.Replace(Path.DirectorySeparatorChar, '/');
return r;
}
public override async Task<string> GetTemporaryFileUrl(Uri baseUri, StoredFile storedFile,

@ -16,7 +16,7 @@
@if (!string.IsNullOrEmpty(Model.CustomCSSLink))
{
<link href="@Model.CustomCSSLink" rel="stylesheet"/>
<link href="@Model.CustomCSSLink" rel="stylesheet" asp-append-version="true"/>
}
@if (!string.IsNullOrEmpty(Model.EmbeddedCSS))
{

@ -92,7 +92,7 @@
<div class="form-group">
<label asp-for="TargetCurrency" class="form-label"></label>
<input asp-for="TargetCurrency" class="form-control w-auto" currency-selection />
<small class="d-inline-block form-text text-muted">Uses the store's default currency (@Model.StoreDefaultCurrency) if empty.</small>
<div class="form-text">Uses the store's default currency (@Model.StoreDefaultCurrency) if empty.</div>
<span asp-validation-for="TargetCurrency" class="text-danger"></span>
</div>
</div>

@ -47,9 +47,7 @@
<div class="form-group">
<label asp-for="Settings.Login" class="form-label"></label>
<input asp-for="Settings.Login" class="form-control"/>
<small class="form-text text-muted">
For many email providers (like Gmail) your login is your email address.
</small>
<div class="form-text">For many email providers (like Gmail) your login is your email address.</div>
<span asp-validation-for="Settings.Login" class="text-danger"></span>
</div>
<div class="form-group">

@ -1,27 +1,19 @@
@using BTCPayServer.Abstractions.Form
@using BTCPayServer.Abstractions.Form
@using BTCPayServer.Forms
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using Newtonsoft.Json.Linq
@inject FormComponentProvider FormComponentProvider
@inject FormComponentProviders FormComponentProviders
@model BTCPayServer.Abstractions.Form.Field
@{
if (Model is not Fieldset fieldset)
{
fieldset = JObject.FromObject(Model).ToObject<Fieldset>();
}
}
@if (!fieldset.Hidden)
@if (!Model.Hidden)
{
<fieldset>
<legend class="h3 mt-4 mb-3">@fieldset.Label</legend>
@foreach (var field in fieldset.Fields)
<legend class="h3 mt-4 mb-3">@Model.Label</legend>
@foreach (var field in Model.Fields)
{
var partial = FormComponentProvider.CanHandle(field);
if (string.IsNullOrEmpty(partial))
{
continue;
}
<partial name="@partial" for="@field"></partial>
if (FormComponentProviders.TypeToComponentProvider.TryGetValue(field.Type, out var partial))
{
<partial name="@partial.View" for="@field"></partial>
}
}
</fieldset>
}

@ -1,32 +1,33 @@
@using BTCPayServer.Abstractions.Form
@using BTCPayServer.Abstractions.Form
@using Newtonsoft.Json.Linq
@model BTCPayServer.Abstractions.Form.Field
@{
if (Model is not HtmlInputField field)
{
field = JObject.FromObject(Model).ToObject<HtmlInputField>();
}
var isInvalid = this.ViewContext.ModelState[Model.Name]?.ValidationState is Microsoft.AspNetCore.Mvc.ModelBinding.ModelValidationState.Invalid;
var error = isInvalid ? this.ViewContext.ModelState[Model.Name].Errors[0].ErrorMessage : null;
}
<div class="form-group">
@if (field.Required)
@if (Model.Required)
{
<label class="form-label" for="@field.Name" data-required>
@field.Label
<label class="form-label" for="@Model.Name" data-required>
@Model.Label
</label>
}
else
{
<label class="form-label" for="@field.Name">
@field.Label
<label class="form-label" for="@Model.Name">
@Model.Label
</label>
}
<input class="form-control @(field.IsValid() ? "" : "is-invalid")" id="@field.Name" type="@field.Type" required="@field.Required" name="@field.Name" value="@field.Value" aria-describedby="@("HelpText" + field.Name)"/>
@if (!string.IsNullOrEmpty(field.HelpText))
<input class="form-control @(Model.IsValid() ? "" : "is-invalid")" id="@Model.Name" type="@Model.Type" required="@Model.Required" name="@Model.Name" value="@Model.Value" aria-describedby="@("HelpText" + Model.Name)"/>
@if(isInvalid)
{
<span class="text-danger">@error</span>
}
@if (!string.IsNullOrEmpty(Model.HelpText))
{
<small id="@("HelpText" + field.Name)" class="form-text text-muted">
@field.HelpText
</small>
<div id="@("HelpText" + Model.Name)" class="form-text">@Model.HelpText</div>
}

@ -305,9 +305,7 @@
v-model="srvModel.serverIpn" v-on:change="inputChanges"
v-validate="'url'" :class="{'is-invalid': errors.has('serverIpn') }">
<small class="text-danger">{{ errors.first('serverIpn') }}</small>
<p class="form-text text-muted">
The URL to post purchase data.
</p>
<div class="form-text">The URL to post purchase data.</div>
</div>
<div class="form-group" v-if="!srvModel.appIdEndpoint">
<label class="form-label" for="email-notifications">Email Notifications</label>
@ -316,9 +314,7 @@
v-model="srvModel.notifyEmail" v-on:change="inputChanges"
v-validate="'email'" :class="{'is-invalid': errors.has('notifyEmail') }">
<small class="text-danger">{{ errors.first('notifyEmail') }}</small>
<p class="form-text text-muted">
Receive email notification updates.
</p>
<div class="form-text">Receive email notification updates.</div>
</div>
<div class="form-group">
<label class="form-label" for="browser-redirect">Browser Redirect</label>
@ -326,9 +322,7 @@
v-model="srvModel.browserRedirect" v-on:change="inputChanges"
v-validate="'url'" :class="{'is-invalid': errors.has('browserRedirect') }">
<small class="text-danger">{{ errors.first('browserRedirect') }}</small>
<p class="form-text text-muted">
Where to redirect the customer after payment is complete.
</p>
<div class="form-text">Where to redirect the customer after payment is complete</div>
</div>
</div>
</div>

@ -1,7 +1,7 @@
<div class="container p-0 l-pos-wrapper">
<div class="l-pos-header bg-primary py-3 px-3">
@if (!string.IsNullOrEmpty(Model.CustomLogoLink)) {
<img src="@Model.CustomLogoLink" height="40">
<img src="@Model.CustomLogoLink" height="40" asp-append-version="true">
} else {
<h1 class="mb-0">@Model.Title</h1>
}

@ -36,7 +36,6 @@
{
<form method="post" asp-action="ViewPointOfSale" asp-route-appId="@Model.AppId" asp-antiforgery="false" data-buy>
<input type="hidden" name="choicekey" value="@item.Id"/>
<input type="hidden" name="requiresRefundEmail" value="@Model.RequiresRefundEmail.ToString()" />
@{PayFormInputContent(item.BuyButtonText ?? Model.CustomButtonText, item.Price.Type, item.Price.Value, item.Price.Value);}
</form>
}

@ -4,7 +4,7 @@
<div class="l-pos-header bg-primary py-3 px-3">
@if (!string.IsNullOrEmpty(Model.CustomLogoLink))
{
<img src="@Model.CustomLogoLink" height="40"/>
<img src="@Model.CustomLogoLink" height="40" asp-append-version="true" />
}
else
{

@ -25,7 +25,12 @@
var jObject = JObject.Parse(await reader.ReadToEndAsync());
jObject["short_name"] = title;
jObject["name"] = $"BTCPay Server: {title}";
return $"data:application/manifest+json, {jObject.ToString(Formatting.None)}";
foreach (var jToken in jObject["icons"]!)
{
var icon = (JObject)jToken;
icon["src"] = $"{Context.Request.GetAbsoluteRoot()}/{icon["src"]}";
}
return $"data:application/manifest+json,{Safe.Json(jObject)}";
}
}
@ -45,11 +50,10 @@
<link href="~/main/fonts/OpenSans.css" asp-append-version="true" rel="stylesheet" />
<link href="~/main/layout.css" asp-append-version="true" rel="stylesheet" />
<link href="~/main/site.css" asp-append-version="true" rel="stylesheet" />
<link href="@Context.Request.GetRelativePathOrAbsolute(Theme.CssUri)" rel="stylesheet" asp-append-version="true"/>
@if (Model.CustomCSSLink != null)
{
<link href="@Model.CustomCSSLink" rel="stylesheet" />
<link href="@Model.CustomCSSLink" rel="stylesheet" asp-append-version="true" />
}
<link href="~/vendor/font-awesome/css/font-awesome.min.css" rel="stylesheet" asp-append-version="true" />

@ -40,7 +40,7 @@
<div class="form-group">
<label asp-for="Currency" class="form-label"></label>
<input asp-for="Currency" class="form-control w-auto" currency-selection />
<small class="d-inline-block form-text text-muted">Uses the store's default currency (@Model.StoreDefaultCurrency) if empty.</small>
<div class="form-text">Uses the store's default currency (@Model.StoreDefaultCurrency) if empty.</div>
<span asp-validation-for="Currency" class="text-danger"></span>
</div>
</div>
@ -75,7 +75,7 @@
<label asp-for="DefaultView" class="form-label" data-required></label>
<select asp-for="DefaultView" asp-items="@Html.GetEnumSelectList<PosViewType>()" class="form-select" required></select>
<span asp-validation-for="DefaultView" class="text-danger"></span>
<p class="form-text text-muted">Choose the point of sale style for your customers.</p>
<div class="form-text">Choose the point of sale style for your customers.</div>
</div>
<div class="form-group" id="button-price-text">
<label asp-for="ButtonText" class="form-label" data-required></label>

@ -93,16 +93,12 @@
<div class="form-group">
<label class="form-label">Inventory</label>
<input type="number" inputmode="numeric" min="0" step="1" class="form-control mb-2" v-model="editingItem.inventory" ref="txtInventory" />
<p class="form-text text-muted">
Leave blank to not use this feature.
</p>
<div class="form-text">Leave blank to not use this feature.</div>
</div>
<div class="form-group">
<label class="form-label">ID</label>
<input type="text" required pattern="[^\*#]+" class="form-control mb-2" v-model="editingItem.id" ref="txtId" />
<p class="form-text text-muted">
Leave blank to generate ID from title.
</p>
<div class="form-text">Leave blank to generate ID from title.</div>
</div>
<div class="form-group">
<label class="form-label">Buy Button Text</label>

@ -1,14 +1,12 @@
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using BTCPayServer.Forms
@model BTCPayServer.Abstractions.Form.Form
@inject FormComponentProvider FormComponentProvider
@inject FormComponentProviders FormComponentProviders
@foreach (var field in Model.Fields)
{
var partial = FormComponentProvider.CanHandle(field);
if (string.IsNullOrEmpty(partial))
if (FormComponentProviders.TypeToComponentProvider.TryGetValue(field.Type, out var partial))
{
continue;
<partial name="@partial.View" for="@field"></partial>
}
<partial name="@partial" for="@field"></partial>
}

@ -364,7 +364,7 @@
</tbody>
</table>
-->
<small class="form-text text-muted">Final results may vary due to trading fees and slippage.</small>
<div class="form-text">Final results may vary due to trading fees and slippage.</div>
</div>
<div v-if="trade.results !== null">
<p class="alert alert-success">Successfully traded {{ trade.results.fromAsset}} into {{ trade.results.toAsset}}.</p>

@ -33,7 +33,7 @@
<input type="hidden" asp-for="RedirectUrl" value="@Model.RedirectUrl"/>
}
<partial name="_Form" model="@Model.Form"/>
<input type="submit" class="btn btn-primary" value="Submit"/>
<input type="submit" class="btn btn-primary" name="command" value="Submit"/>
</form>
</div>
</div>

@ -45,7 +45,7 @@
@if (!string.IsNullOrEmpty(Model.CustomCSSLink))
{
<link href="@Model.CustomCSSLink" rel="stylesheet" />
<link href="@Model.CustomCSSLink" rel="stylesheet" asp-append-version="true"/>
}
@if (Model.IsModal)

@ -134,9 +134,9 @@
<payment-details :srv-model="srvModel" :is-active="isActive" class="mb-5"></payment-details>
</div>
<div class="buttons">
<a v-if="srvModel.receiptLink" class="btn btn-primary" :href="srvModel.receiptLink" :target="isModal ? '_top' : null" v-t="'view_receipt'" id="ReceiptLink"></a>
<a v-if="storeLink" class="btn btn-secondary" :href="storeLink" :target="isModal ? '_top' : null" v-t="{ path: 'return_to_store', args: { storeName: srvModel.storeName }}" id="StoreLink"></a>
<button v-else-if="isModal" class="btn btn-secondary" v-on:click="close" v-t="'Close'"></button>
<a v-if="srvModel.receiptLink" class="btn btn-primary rounded-pill w-100" :href="srvModel.receiptLink" :target="isModal ? '_top' : null" v-t="'view_receipt'" id="ReceiptLink"></a>
<a v-if="storeLink" class="btn btn-secondary rounded-pill w-100" :href="storeLink" :target="isModal ? '_top' : null" v-t="{ path: 'return_to_store', args: { storeName: srvModel.storeName }}" id="StoreLink"></a>
<button v-else-if="isModal" class="btn btn-secondary rounded-pill w-100" v-on:click="close" v-t="'Close'"></button>
</div>
</div>
<div id="expired" v-if="isUnpayable">
@ -165,8 +165,8 @@
<p class="text-center mt-3" v-html="replaceNewlines($t('invoice_expired_body', { storeName: srvModel.storeName, minutes: @Model.MaxTimeMinutes }))"></p>
</div>
<div class="buttons">
<a v-if="storeLink" class="btn btn-primary" :href="storeLink" :target="isModal ? '_top' : null" v-t="{ path: 'return_to_store', args: { storeName: srvModel.storeName }}" id="StoreLink"></a>
<button v-else-if="isModal" class="btn btn-primary" v-on:click="close" v-t="'Close'"></button>
<a v-if="storeLink" class="btn btn-primary rounded-pill w-100" :href="storeLink" :target="isModal ? '_top' : null" v-t="{ path: 'return_to_store', args: { storeName: srvModel.storeName }}" id="StoreLink"></a>
<button v-else-if="isModal" class="btn btn-primary rounded-pill w-100" v-on:click="close" v-t="'Close'"></button>
</div>
</div>
</section>

@ -135,9 +135,7 @@
<label asp-for="NotificationEmail" class="form-label"></label>
<input asp-for="NotificationEmail" class="form-control" />
<span asp-validation-for="NotificationEmail" class="text-danger"></span>
<p id="InvoiceEmailHelpBlock" class="form-text text-muted">
Receive updates for this invoice.
</p>
<div id="InvoiceEmailHelpBlock" class="form-text">Receive updates for this invoice.</div>
</div>
</div>
</div>

@ -186,15 +186,15 @@
<div class="dropdown-menu" aria-labelledby="markStatusDropdownMenuButton">
@if (Model.CanMarkInvalid)
{
<a class="dropdown-item changeInvoiceState" href="#" data-id="@Model.Id" data-status="invalid" data-change-invoice-status-button>
Mark as invalid <span class="fa fa-times"></span>
</a>
<button type="button" class="dropdown-item lh-base changeInvoiceState" data-id="@Model.Id" data-status="invalid" data-change-invoice-status-button>
Mark as invalid
</button>
}
@if (Model.CanMarkSettled)
{
<a class="dropdown-item changeInvoiceState" href="#" data-id="@Model.Id" data-status="settled" data-change-invoice-status-button>
Mark as settled <span class="fa fa-check-circle"></span>
</a>
<button type="button" class="dropdown-item lh-base changeInvoiceState" href="#" data-id="@Model.Id" data-status="settled" data-change-invoice-status-button>
Mark as settled
</button>
}
</div>
</div>
@ -416,7 +416,7 @@
<h3 class="mb-3 mt-4">Webhooks</h3>
<div class="table-responsive-xl">
<table class="table table-hover table-responsive-md mb-5">
<thead class="thead-inverse">
<thead>
<tr>
<th>Status</th>
<th>ID</th>
@ -491,7 +491,7 @@
<h3 class="mb-3 mt-4">Refunds</h3>
<div class="table-responsive-xl">
<table class="table table-hover table-responsive-md mb-5">
<thead class="thead-inverse">
<thead>
<tr>
<th>Pull Payment</th>
<th>Amount</th>
@ -526,9 +526,9 @@
</table>
</div>
}
<h3 class="mb-0">Events</h3>
<table class="table table-hover">
<thead class="thead-inverse">
<h3 class="mb-0 mt-5">Events</h3>
<table class="table table-hover mt-3 mb-4">
<thead>
<tr>
<th>Date</th>
<th>Message</th>

@ -41,7 +41,7 @@
pavpill.replaceWith(statusHtml);
})
.fail(function (data) {
pavpill.html(originalHtml.replace("dropdown-menu pull-right show", "dropdown-menu pull-right"));
pavpill.html(originalHtml.replace("dropdown-menu show", "dropdown-menu"));
alert("Invoice state update failed");
});
})
@ -325,16 +325,16 @@
<span class="dropdown-toggle changeInvoiceStateToggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
@invoice.Status.ToString()
</span>
<div class="dropdown-menu pull-right">
<div class="dropdown-menu">
@if (invoice.CanMarkInvalid)
{
<button class="dropdown-item cursorPointer changeInvoiceState" data-invoice-id="@invoice.InvoiceId" data-new-state="invalid">
<button type="button" class="dropdown-item lh-base changeInvoiceState" data-invoice-id="@invoice.InvoiceId" data-new-state="invalid">
Mark as invalid
</button>
}
@if (invoice.CanMarkSettled)
{
<button class="dropdown-item cursorPointer changeInvoiceState" data-invoice-id="@invoice.InvoiceId" data-new-state="settled">
<button type="button" class="dropdown-item lh-base changeInvoiceState" data-invoice-id="@invoice.InvoiceId" data-new-state="settled">
Mark as settled
</button>
}

@ -50,21 +50,21 @@
<div class="form-check">
<input id="RateThenOption" asp-for="SelectedRefundOption" type="radio" value="RateThen" class="form-check-input"/>
<label for="RateThenOption" class="form-check-label">@Model.RateThenText</label>
<div class="form-text text-muted">The crypto currency price, at the rate the invoice got paid.</div>
<div class="form-text">The crypto currency price, at the rate the invoice got paid.</div>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input id="CurrentRateOption" asp-for="SelectedRefundOption" type="radio" value="CurrentRate" class="form-check-input"/>
<label for="CurrentRateOption" class="form-check-label">@Model.CurrentRateText</label>
<div class="form-text text-muted">The crypto currency price, at the current rate.</div>
<div class="form-text">The crypto currency price, at the current rate.</div>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input id="FiatOption" asp-for="SelectedRefundOption" type="radio" value="Fiat" class="form-check-input"/>
<label for="FiatOption" class="form-check-label">@Model.FiatText</label>
<div class="form-text text-muted">The invoice currency, at the rate when the refund will be sent.</div>
<div class="form-text">The invoice currency, at the rate when the refund will be sent.</div>
</div>
</div>
@ -72,7 +72,7 @@
<div class="form-check">
<input id="CustomOption" asp-for="SelectedRefundOption" type="radio" value="Custom" class="form-check-input"/>
<label for="CustomOption" class="form-check-label">Custom amount</label>
<div class="form-text text-muted">The specified amount with the specified currency, at the rate when the refund will be sent.</div>
<div class="form-text">The specified amount with the specified currency, at the rate when the refund will be sent.</div>
<div class="form-group pt-2">
<label asp-for="CustomAmount" class="form-label"></label>
<div class="input-group">

@ -128,7 +128,7 @@
<div>
<label for="@Model.PermissionValues[i].Permission" class="form-check-label">@Model.PermissionValues[i].Title</label>
</div>
<div class="form-text text-muted">@Model.PermissionValues[i].Description</div>
<div class="form-text">@Model.PermissionValues[i].Description</div>
<span asp-validation-for="PermissionValues[i].Value" class="text-danger"></span>
@if (Model.PermissionValues[i].Forbidden)
{

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