Compare commits

...

261 Commits

Author SHA1 Message Date
3ef5bfb6eb bump 2019-02-22 17:30:54 +09:00
4016ded584 Affect orderId to crowdfund app invoices 2019-02-22 17:29:54 +09:00
5b0b4adb1c Fix service link for RTL 2019-02-22 15:46:43 +09:00
f1ec3b0c75 bump 2019-02-22 15:08:45 +09:00
b5d55a2066 Add RTL support 2019-02-22 15:06:52 +09:00
0d2c9fe377 Fix https://github.com/btcpayserver/btcpayserver/issues/585 2019-02-22 13:52:35 +09:00
2c7cc9a796 Fix: invoice Price was not being rounded if no taxIncluded present 2019-02-21 21:58:49 +09:00
2e1d623755 fix https://github.com/btcpayserver/btcpayserver/issues/596 2019-02-21 21:30:30 +09:00
52fee8f842 Make sure no nullreferenceexception is thrown if invalid invoice 2019-02-21 19:36:05 +09:00
6ba17e8e30 Can filter supported payment methods for an invoice 2019-02-21 19:34:11 +09:00
ac3432920a Fix build 2019-02-21 18:42:12 +09:00
63c88be533 Use CreateInvoiceRequest instead of NBitpay Invoice type 2019-02-21 18:40:27 +09:00
3cb577e6ba Add link back to official website 2019-02-21 14:04:03 +09:00
1e0d64c548 Improve homepage, document mattermost and point on the official website. 2019-02-21 13:50:46 +09:00
bc1b9ff59c update translations 2019-02-20 23:16:13 +09:00
7d73bed3be bump 2019-02-20 23:06:52 +09:00
126fbdfd60 Fix null reference exception if the NotificationUrl is not set 2019-02-20 23:03:04 +09:00
15094436fd bump lnd 2019-02-20 21:29:16 +09:00
010c653995 Create EventHostedServiceBase and make AppHubStreamer use this 2019-02-20 12:27:10 +09:00
119f82fd4e Properly aggregate contributions amount 2019-02-19 16:15:14 +09:00
3bbf4de5d2 Fix live update of crowdfunding, add tests, consider payments as confirmed if invoice is confirmed 2019-02-19 16:01:28 +09:00
0807f3b87b Remote internal tags at store level 2019-02-19 13:24:04 +09:00
4e9b3b40aa Fix crowdfunding-admin js file not being included 2019-02-19 13:20:06 +09:00
cc444811db Rename CrowdfundHubStream to AppHubSteamer 2019-02-19 13:18:30 +09:00
50c8525012 Moving CrowdfundSettings in its own file 2019-02-19 13:07:10 +09:00
aedad497e8 Rename AppsHelper to AppService 2019-02-19 13:04:58 +09:00
b1b231e645 Add tests on tagging 2019-02-19 12:59:12 +09:00
dc46fd225a Migrate old crowdfund deployment to the new tagging system 2019-02-19 12:53:24 +09:00
6226de7cff Refactor Crowdfund to use the tagging system 2019-02-19 12:48:48 +09:00
37327ec674 Apps can tag invoices 2019-02-19 12:48:08 +09:00
c071c81403 Pass the whole Entity object to internal InvoiceEvent 2019-02-19 12:08:07 +09:00
85d75a013a The invoices link of crowdfund show all invoices of the store if it is set to use all store's invoice 2019-02-19 11:45:04 +09:00
3816b36131 Add internal tags to invoice 2019-02-19 11:14:21 +09:00
dc7965267b Use GetRelativePathOrAbsolute in ViewCrowdfund and ViewPointOfSale 2019-02-19 00:28:44 +09:00
ce9a6bced7 Use GetRelativePathOrAbsolute in ShowLightningNodeInfo 2019-02-18 12:25:14 +09:00
85325dc710 Update translations 2019-02-18 12:24:55 +09:00
ac4050df70 Improve the UI of lightning node info 2019-02-17 19:40:39 +09:00
a16a53167b Can put lightning node info inside an XFrame 2019-02-17 19:30:16 +09:00
afab3cf847 Better Datetime picker picker in crowdfund page 2019-02-17 19:25:18 +09:00
8fdaeb7bac Fix race condition on calculation of contributions, refactor the methods to AppHelper 2019-02-17 19:17:59 +09:00
7e0f9f6e0d Inject HtmlSanitizer in AddBTCPayServer, remove AppHelpers deps when possible 2019-02-17 18:47:25 +09:00
5b1bf6cd88 add email to export (#583) 2019-02-17 18:33:40 +09:00
b1584c352b Free some memory 2019-02-17 16:13:16 +09:00
b06b83503c Better status message 2019-02-15 10:05:29 -06:00
b03d89c190 Different message for admin deletion, check not to delete last admin
Ref: #549, #550
2019-02-15 10:05:29 -06:00
f53548d10f Showing warning when user tries to delete last admin 2019-02-15 10:05:29 -06:00
5ec2f54d7f Merge pull request #593 from BenSanex/bugfix/591_FixValidationMessage
Custom validation message for Crowdfund form primary currency
2019-02-15 10:03:05 -06:00
db588ff961 I've added asterisk. Isn't that impressive? 2019-02-15 10:02:17 -06:00
2b7006a14c add asterisk, revert primary currency error message, remove the 2019-02-11 21:53:45 -06:00
8f5f07882f Custom validation message for Crowdfund form primary currency 2019-02-07 20:27:26 -06:00
0eee8e7464 Returns Access-Control-Allow-Origin * on all Bitpay GET and post requests. 2019-02-02 16:12:51 +09:00
3725a5b644 Correctly set Access-Control-Allow-Headers 2019-02-02 15:51:38 +09:00
c84c0ac64d set CORS headers 2019-02-02 15:22:00 +09:00
098e07988c Bypass MVC for replying to CORS requests if Bitpay API 2019-02-02 15:19:22 +09:00
66bb702aca Fix CORS for bitpay API again 2019-02-02 13:58:32 +09:00
03ff2fedf0 Update Translator grammar (#579) 2019-02-01 17:35:49 +09:00
c707f47b11 bump 2019-01-31 22:03:46 +09:00
585efa3ff5 Fix: Default payment method should not return a disabled one 2019-01-31 22:03:28 +09:00
07d0b98a23 Update language 2019-01-31 19:33:07 +09:00
c7c0f01010 bump 2019-01-31 19:24:36 +09:00
cf6b17250a Can set lightning network as default payment method (close #290) 2019-01-31 19:07:38 +09:00
90503a490c Add dots to make derivation examples clearer (#561) 2019-01-31 17:00:15 +09:00
ebdd53b99b fix unfairly long dropdown in ledger account selection (#574)
Closes #570
2019-01-31 16:56:39 +09:00
51a5d2e812 Refactor XFrames Attribute & simplify pos settings page (#576)
* Enable better error when invoice cannot be created on crowdfund

Closes #572

* Allow all public apps in iframe

* cleanup pos page dev info
2019-01-31 16:56:21 +09:00
2ad509d56a Update Readme.md (#577)
* Update readme

* Update README.md

* add apps link

* fix broken link
2019-01-31 16:55:27 +09:00
1a98bfba36 Fix formatting of currencies in Invoice detail page 2019-01-30 19:18:44 +09:00
d05bb6c60e Properly format currencies in Invoice list 2019-01-30 19:01:18 +09:00
ed81b6a6aa bump 2019-01-30 15:52:31 +09:00
264914588f fix bitpay API not having CORS 2019-01-30 14:57:10 +09:00
05df43b426 fix bitpay API not having CORS 2019-01-30 14:36:26 +09:00
0334a4e176 bump 2019-01-30 13:46:55 +09:00
38dca425da Fix repetitive IPN for lightning network payments (https://github.com/btcpayserver/btcpayserver/issues/564) 2019-01-30 13:40:08 +09:00
82d4a79dd4 Fix potential crash if the current host is an IP instead of DNS name, might fix https://github.com/btcpayserver/btcpayserver/issues/543 2019-01-30 12:52:34 +09:00
6725be8145 Remove warnings 2019-01-29 18:35:27 +09:00
f5b693f01b Disable quadricagx tests because exchange is down 2019-01-29 18:34:30 +09:00
f09f23e570 Enable better error when invoice cannot be created on crowdfund (#575)
Closes #572
2019-01-29 18:32:44 +09:00
4f4d05b8cd Make sure CORS is enabled on Bitpay's API 2019-01-29 18:20:53 +09:00
0c5b5ff49c Add link to no stores error (#558)
* add link

* safer status message

* refactor

* small view cleanup
2019-01-29 16:44:46 +09:00
a815fad3f1 Put back the list of ledger accounts to 5. 2019-01-29 13:06:43 +09:00
d8b1c7c10a Fix broken lightning payments on Checkout page 2019-01-28 18:50:26 +09:00
02e1aea80c add warning for third parties (#562)
* add warning for third parties

* Update UpdateCoinSwitchSettings.cshtml

* Update UpdateChangellySettings.cshtml

* Update UpdateChangellySettings.cshtml

* Update UpdateCoinSwitchSettings.cshtml
2019-01-28 17:40:23 +09:00
1892f7e0f4 rename field 2019-01-28 17:10:51 +09:00
b7b50349a7 Convert Ledger account list to dropdown and add more accounts to list (#560) 2019-01-28 17:07:01 +09:00
02d227ee02 Fix connection to checkout backend (bad links) 2019-01-28 16:24:11 +09:00
47f8938b89 Catch websocket connection issues 2019-01-28 15:12:40 +09:00
4945a640a7 Use PaymentHash of a lightning payment as PaymentId 2019-01-27 13:06:55 +09:00
0136977359 update translations 2019-01-26 21:23:41 +09:00
0acd3e20b0 bump 2019-01-26 20:58:15 +09:00
30bdfeee37 Enhance PosData Viewer & add cart to posdata in POS app (#559) 2019-01-26 13:26:49 +09:00
7ea665d884 Merge pull request #557 from Kukks/master
Fix close invoice button for modal invoices #555
2019-01-25 20:48:45 +09:00
073edcfb12 Merge remote-tracking branch 'btcpayserver/master' 2019-01-25 12:41:20 +01:00
a645366a25 Fix close invoice button for modal invoices #555 2019-01-25 12:41:15 +01:00
12aa0b7abd Merge pull request #556 from ChekaZ/master
Support Bitcoinplus
2019-01-25 16:09:39 +09:00
3f98a50410 Support Bitcoinplus 2019-01-25 01:03:04 +01:00
24c8c076d5 Add taxIncluded field in invoice 2019-01-24 20:53:29 +09:00
37e6931d33 Improve help 2019-01-23 17:44:03 +09:00
86493568e9 Fix external services parsing 2019-01-23 13:31:00 +09:00
bb51436ae3 Accept absolute url for external services 2019-01-23 13:17:36 +09:00
854a55ac1a Merge branch 'store-level-email' 2019-01-22 21:39:55 +09:00
cfb4b080d3 Emails on store level 2019-01-22 21:38:39 +09:00
00aa2e4e17 Merge pull request #546 from britttttk/fix/message
Fix delete user message
2019-01-21 17:11:24 +09:00
69c67d99f6 Fix message for delete user 2019-01-20 21:19:01 -07:00
65596ec8c1 fix delete user message 2019-01-20 21:12:20 -07:00
49643cb00e Make CanScheduleBackgroundTasks more robust 2019-01-19 21:19:15 +09:00
35b0faee57 Merge pull request #541 from Horndev/patch-2
Improve exception messages in server configuration parsing.
2019-01-19 20:49:08 +09:00
88ef4d69b2 Improve help and exception messages.
Improve the messages passed to users and in exceptions when parsing the BTCPayServerOptions configuration.
2019-01-18 10:23:47 -04:00
575b6ca222 Improve error messages when the store has no payment method configured 2019-01-18 19:15:31 +09:00
b5a0e844d2 Cann GetInvoicesTotal in parallel 2019-01-17 23:40:47 +09:00
2642e11ce2 Fix amount format in wallet send 2019-01-17 23:37:39 +09:00
b4fe655efe Merge pull request #534 from sipsorcery/fixpaging
Small improvement to the paging buttons on the list invoices page
2019-01-17 11:14:31 +09:00
ffb761909a Merge pull request #535 from dalijolijo/master
Change default exchange for Bitcore
2019-01-17 11:10:55 +09:00
b443e1ac6e Change default exchange for Bitcore 2019-01-16 22:04:24 +00:00
a4792f54a7 Added bootstrap paging buttons to the invoice list page and fixed paging buttons. 2019-01-16 21:33:04 +01:00
686ae029e0 bump 2019-01-16 23:49:40 +09:00
f3fd2e7d0f Update translations 2019-01-16 23:08:48 +09:00
7efd9ba0a5 Fix ledger on firefox 2019-01-16 23:07:22 +09:00
1c2a6bb8a1 Delete unused code 2019-01-16 19:35:29 +09:00
7bcf1cbdd5 Remove references to hangfire 2019-01-16 19:30:03 +09:00
2aaa2544bd Do not send mail synchronously in InvoiceNotificationManager 2019-01-16 19:21:02 +09:00
d85f03ba20 Remove HangFire dependency 2019-01-16 19:15:09 +09:00
cfb51a6be4 Merge pull request #531 from Kukks/patch-2
fix merge bug
2019-01-16 15:58:29 +09:00
c9d778c94b Bump nbitcoin and nbxplorer 2019-01-16 15:16:41 +09:00
fd62f882de fix merge bug 2019-01-15 18:18:41 +01:00
adc050f190 Trim destination address 2019-01-16 01:19:37 +09:00
2d551b9fc5 bump 2019-01-16 00:13:03 +09:00
884acdde32 Disabled POLIS and Bitcoin because default exchange (cryptopedia) is down 2019-01-16 00:08:17 +09:00
8f896de794 Merge pull request #516 from Kukks/feature/crowdfund
Crowdfund Bug fixes
2019-01-15 23:57:29 +09:00
5e4e26d2fd Merge pull request #529 from Kukks/bugfix/app-proper-redirect
fix app redirect to app instead of root url
2019-01-15 23:54:54 +09:00
ae688e6615 Merge pull request #530 from Horndev/patch-1
Grammatical corrections in exception messages
2019-01-15 23:54:20 +09:00
c4c812bdf6 Remove cryptopia from directly queried exchanges 2019-01-15 23:53:32 +09:00
e620fc0283 Add expert mode to BTCPay with No Change UTXO option 2019-01-15 23:50:45 +09:00
c333902468 Round up invoice price 2019-01-15 22:12:29 +09:00
4c83ecd06a Remove unused code 2019-01-15 21:56:33 +09:00
b28a547dc4 Grammatical corrections in exception messages
Fixed a few grammatical errors in LightningLikePaymentHandler.
2019-01-15 12:21:31 +00:00
6bc17e05bd add ids for better styling possibilities 2019-01-15 13:12:19 +01:00
0903350d30 add more log 2019-01-15 12:12:17 +01:00
6c0f19b457 Merge branch 'master' into feature/crowdfund 2019-01-15 09:48:16 +01:00
e119dc823f fix app redirect to app instead of root url 2019-01-15 09:46:07 +01:00
43295c9c57 Merge pull request #528 from bolatovumar/master
Update .NET Core SDK version in documentation
2019-01-15 16:42:40 +09:00
ded8b54042 Merge pull request #524 from Kukks/escapedstorename
Fix Store name character escaping on paid invoice #522
2019-01-15 16:41:51 +09:00
50a3178d51 Update .NET Core SDK version in documentation
Address #523
2019-01-14 16:49:12 -08:00
393c226032 fix escaped store name in return 2019-01-14 09:32:22 +01:00
f2630df387 dispose streamer properly 2019-01-14 08:21:27 +01:00
abcd2c1750 add padding when disqus enabled 2019-01-14 08:01:07 +01:00
cc95f3b5b5 fix exponent numbers in contribution amounts 2019-01-14 07:53:03 +01:00
a08ee93b43 fix issue with perk ordering 2019-01-14 07:45:21 +01:00
4b90f873d5 Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2019-01-11 10:52:31 +01:00
419ab8e0b1 add loader and fix perk badge zindex 2019-01-11 10:52:21 +01:00
c95ef27998 bump 2019-01-11 00:17:58 +09:00
63dfd93834 Merge pull request #511 from Kukks/feature/crowdfund
crowdfund: change text
2019-01-11 00:17:37 +09:00
57610881de change text 2019-01-10 16:12:00 +01:00
7469faf296 Merge pull request #510 from Kukks/feature/crowdfund
fix notif number
2019-01-10 23:58:57 +09:00
55a884a559 fix js number 2019-01-10 15:57:30 +01:00
ee2b3c3d10 bump 2019-01-10 23:41:08 +09:00
e5819a260b Merge pull request #509 from Kukks/feature/crowdfund
missed commit crowdfund
2019-01-10 23:40:52 +09:00
a3ecf48702 fix pos update too 2019-01-10 15:37:50 +01:00
1c0b904cd2 Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2019-01-10 15:35:19 +01:00
072d8a1728 fix exponents in js product editor 2019-01-10 15:35:03 +01:00
964e541c32 Update translations 2019-01-10 23:33:12 +09:00
78fec4ed22 bump 2019-01-10 23:31:35 +09:00
ef111d36c9 Merge pull request #484 from Kukks/feature/crowdfund
New App: Crowdfunding 🎉 🎉
2019-01-10 23:30:31 +09:00
4f64193e85 add rank badge to minimal and fix css in minimal 2019-01-10 14:54:41 +01:00
89bb6d1268 add validation for ranking 2019-01-10 14:43:47 +01:00
9f4226bf0f remove inline styles and fix checkbox setting text 2019-01-10 14:19:06 +01:00
a87c2a3374 Merge remote-tracking branch 'origin/master' into feature/crowdfund 2019-01-10 09:50:22 +01:00
d7294ba5a0 fix product item template 2019-01-10 09:28:51 +01:00
82d286dc6f Fix test 2019-01-10 14:00:26 +09:00
1fa18ab997 Merge pull request #507 from hubiktomas/patch-1
Typo fix
2019-01-10 13:49:52 +09:00
afc90f32c9 Fix tests 2019-01-10 13:47:21 +09:00
e9cfb7c21e Update link to accounting doc 2019-01-10 13:08:25 +09:00
1af8ea3769 Typo fix 2019-01-09 17:35:32 +01:00
9f7af190f1 fix ranking style 2019-01-09 15:44:16 +01:00
9c703fe94d fix number issue 2019-01-09 12:55:02 +01:00
a7a11a4f13 Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2019-01-09 12:22:42 +01:00
c32c3bb62b add contribution ranking 2019-01-09 12:22:36 +01:00
e29d1480a6 Add link to doc for export 2019-01-09 17:28:30 +09:00
8f299d7791 Fix build 2019-01-09 17:25:46 +09:00
65fb2e992e Round InvoiceDue and PaidCurrency in export 2019-01-09 17:18:01 +09:00
41f5d677d5 Merge pull request #491 from bitcoinshirt/bitcoinshirt-patch-ny
Update 2019 license
2019-01-09 13:33:56 +09:00
a2b78b8cd9 Merge pull request #506 from britttttk/fix/PasswordLength
Fix registration password length
2019-01-09 13:33:41 +09:00
c93f217033 Fix minimum registration password length 2019-01-08 18:32:07 -07:00
82c47b6e9a fix margin on crowdfund 2019-01-08 21:42:11 +01:00
94fb738c67 fix choice key and currency data 2019-01-08 15:49:07 +01:00
89071e40fc oops 2019-01-08 15:14:06 +01:00
95a90c410e Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2019-01-08 15:10:13 +01:00
59fc371cd5 perk count + img fixer 2019-01-08 15:10:05 +01:00
9b404e330d bump 2019-01-08 23:03:53 +09:00
1667f9b2ef Merge pull request #504 from Kukks/bugfix/general
Fix Coinswitch Issues, Fix LN Node Info Clipboard, Fix Vue-Cloak Styles
2019-01-08 23:02:34 +09:00
caadfc8641 use bolt icon in view 2019-01-08 13:52:44 +01:00
bffc2e70c1 use summernote instead 2019-01-08 13:52:30 +01:00
8b686f0b12 fix coin switch issues 2019-01-08 11:27:37 +01:00
def8d1e0cb fix ln node clipboard 2019-01-08 10:54:02 +01:00
ca28c34be0 fix ln payment calculator 2019-01-08 10:32:10 +01:00
196bc3ea00 Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2019-01-08 09:50:50 +01:00
b15267be4d Merge pull request #453 from 2pac1/master
Update Anyone can enable invoices text so its much more clear
2019-01-08 12:56:04 +09:00
04cba61888 add bundle helper 2019-01-07 14:40:51 +01:00
a41e2e1ceb Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2019-01-07 14:39:08 +01:00
d1d03c98ba pr changes 2019-01-07 14:39:04 +01:00
3e48a54ab5 Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2019-01-07 14:25:41 +01:00
f6e389ff62 fix issues 2019-01-07 14:25:35 +01:00
561ec57cc8 Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2019-01-07 11:13:02 +01:00
c63feb488c Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2019-01-07 10:58:52 +01:00
f2ccc4d963 Add sanity check in loading crowdfun 2019-01-06 14:44:51 +01:00
a92d48efdd move button below help text 2019-01-06 14:37:40 +01:00
b633206b45 add helpful texts 2019-01-06 14:28:53 +01:00
b6f3d2af5e Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2019-01-06 14:12:25 +01:00
5ca4494eed reorder options in update crowdfund 2019-01-06 13:51:40 +01:00
de7e419ef4 fix overflow of descriptions 2019-01-06 13:50:30 +01:00
20a6b3fc33 Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2019-01-06 10:25:24 +01:00
e6357d2ac8 fix build 2019-01-06 09:29:21 +01:00
1eecd85ceb Merge branch 'master' into feature/crowdfund 2019-01-06 09:26:58 +01:00
d63176da19 Update BTCPayServer.csproj 2019-01-05 22:38:44 +01:00
887da5aa9a new year 2019-01-05 22:22:19 +01:00
6e7f1151bc bug fixes and optimizations 2019-01-05 19:47:39 +01:00
fb6d852827 switc back to regtest 2019-01-05 10:18:01 +01:00
ba17612461 Link to associated invoices 2019-01-05 10:17:52 +01:00
a15c7a0213 change crowdfund app prefix to not break invoice searcher 2019-01-05 09:53:57 +01:00
7e321d4016 expand list invoices search 2019-01-05 09:49:06 +01:00
2ccf007b9a fix permissions 2019-01-05 09:38:27 +01:00
895b8c2c80 ux fixes 2019-01-05 09:18:15 +01:00
493466683c start adding UTs 2019-01-04 16:42:35 +01:00
761c342c51 add validation 2019-01-04 13:47:06 +01:00
5341da28d9 add date time picker 2019-01-04 12:58:29 +01:00
7768f41849 add reset every x amount of time feature 2019-01-04 11:42:37 +01:00
c52a49f747 add minimal crowdfund version 2019-01-02 14:08:30 +01:00
e4b9895ba7 add rich text and options 2019-01-02 12:47:06 +01:00
92a2bb4d32 fixes to computed goal result 2019-01-02 12:04:35 +01:00
bfec722312 protect contrib endpoint when needed 2019-01-02 11:29:47 +01:00
5a3f7b5b70 add in more info and simplify backend model 2019-01-02 10:41:54 +01:00
2aa097be46 fix cache expiration time 2019-01-02 09:45:04 +01:00
8a646d85c6 fix bundles 2019-01-02 09:03:20 +01:00
890b3eaa00 remove api key for disqus 2019-01-02 08:10:42 +01:00
cda28ebf15 ui fixes + toggle sound options 2018-12-31 13:20:00 +01:00
2245027ca3 ux fixes 2018-12-31 12:34:27 +01:00
3dc250f801 Add Disqus & fix ux 2018-12-31 11:38:05 +01:00
66e786a1b0 styles and perks 2018-12-30 20:28:36 +01:00
8bd7ea5bbc start ux for perks 2018-12-29 20:21:23 +01:00
6eb36abe2e start contrib perks 2018-12-29 11:52:07 +01:00
6fced3fab2 better tooltips and icons 2018-12-29 10:19:50 +01:00
774e456e54 cleanup 2018-12-28 23:57:39 +01:00
519859e1c5 ux fixwes 2018-12-28 23:31:41 +01:00
26f0c488e5 hook up proper payments to events and super crazy ux shit 2018-12-28 23:12:16 +01:00
1b0b53fbd0 small ux fixes 2018-12-28 18:23:32 +01:00
35f4ea29f9 more integration 2018-12-28 17:38:20 +01:00
d9426d301d Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2018-12-28 12:08:32 +01:00
8bcf7109a3 integrate invoice popup 2018-12-28 12:07:15 +01:00
b11f8acba1 wip 2018-12-28 00:10:03 +01:00
ef9a633aa4 fixes for hub 2018-12-27 20:55:46 +01:00
c7e2f979dd Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2018-12-27 20:19:29 +01:00
e97bb9c933 work on vue and signalr fro crowdfund 2018-12-27 20:19:21 +01:00
2b84791391 fix raw html 2018-12-22 21:03:43 +01:00
9a488c60f2 fix some styling 2018-12-22 17:30:54 +01:00
9cb50446f4 update bootstrap to 4.2.1 2018-12-22 16:55:24 +01:00
8c00a2359e better layout 2018-12-22 15:43:40 +01:00
d1ff34d16d add minimal crowdfund system and UI 2018-12-22 15:02:16 +01:00
8e8615dab8 Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2018-12-21 11:51:13 +01:00
27bde55f54 work on building the viewmodel for crowdfund 2018-12-18 16:27:03 +01:00
b5d360594a Merge remote-tracking branch 'origin/master' into feature/crowdfund 2018-12-18 13:29:22 +01:00
e3833914b3 Update AddDerivationScheme.cshtml 2018-12-13 16:18:01 +01:00
49cdec6961 Update PayButtonEnable.cshtml 2018-12-13 12:26:30 +01:00
7fa1b65af0 initial commit 2018-12-11 16:36:25 +01:00
172 changed files with 13946 additions and 1095 deletions

View File

@ -0,0 +1,303 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Models;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments.Changelly;
using BTCPayServer.Payments.Changelly.Models;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Tests.Logging;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using NBitcoin;
using NBitpayClient;
using Xunit;
using Xunit.Abstractions;
using static BTCPayServer.Tests.UnitTest1;
namespace BTCPayServer.Tests
{
public class CrowdfundTests
{
public CrowdfundTests(ITestOutputHelper helper)
{
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
Logs.LogProvider = new XUnitLogProvider(helper);
}
[Fact]
[Trait("Integration", "Integration")]
public void CanCreateAndDeleteCrowdfundApp()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
var user2 = tester.NewAccount();
user2.GrantAccess();
var apps = user.GetController<AppsController>();
var apps2 = user2.GetController<AppsController>();
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model);
Assert.NotNull(vm.SelectedAppType);
Assert.Null(vm.Name);
vm.Name = "test";
vm.SelectedAppType = AppType.Crowdfund.ToString();
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.CreateApp(vm).Result);
Assert.Equal(nameof(apps.UpdateCrowdfund), redirectToAction.ActionName);
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model);
var appList2 =
Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps2.ListApps().Result).Model);
Assert.Single(appList.Apps);
Assert.Empty(appList2.Apps);
Assert.Equal("test", appList.Apps[0].AppName);
Assert.Equal(apps.CreatedAppId, appList.Apps[0].Id);
Assert.True(appList.Apps[0].IsOwner);
Assert.Equal(user.StoreId, appList.Apps[0].StoreId);
Assert.IsType<NotFoundResult>(apps2.DeleteApp(appList.Apps[0].Id).Result);
Assert.IsType<ViewResult>(apps.DeleteApp(appList.Apps[0].Id).Result);
redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(appList.Apps[0].Id).Result);
Assert.Equal(nameof(apps.ListApps), redirectToAction.ActionName);
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model);
Assert.Empty(appList.Apps);
}
}
[Fact]
[Trait("Integration", "Integration")]
public async Task CanContributeOnlyWhenAllowed()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
var apps = user.GetController<AppsController>();
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model);
vm.Name = "test";
vm.SelectedAppType = AppType.Crowdfund.ToString();
Assert.IsType<RedirectToActionResult>(apps.CreateApp(vm).Result);
var appId = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model)
.Apps[0].Id;
//Scenario 1: Not Enabled - Not Allowed
var crowdfundViewModel = Assert.IsType<UpdateCrowdfundViewModel>(Assert
.IsType<ViewResult>(apps.UpdateCrowdfund(appId).Result).Model);
crowdfundViewModel.TargetCurrency = "BTC";
crowdfundViewModel.Enabled = false;
crowdfundViewModel.EndDate = null;
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel).Result);
var anonAppPubsController = tester.PayTester.GetController<AppsPublicController>();
var publicApps = user.GetController<AppsPublicController>();
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
{
Amount = new decimal(0.01)
}));
Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(appId, string.Empty));
//Scenario 2: Not Enabled But Admin - Allowed
Assert.IsType<OkObjectResult>(await publicApps.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
{
RedirectToCheckout = false,
Amount = new decimal(0.01)
}));
Assert.IsType<ViewResult>(await publicApps.ViewCrowdfund(appId, string.Empty));
Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(appId, string.Empty));
//Scenario 3: Enabled But Start Date > Now - Not Allowed
crowdfundViewModel.StartDate= DateTime.Today.AddDays(2);
crowdfundViewModel.Enabled = true;
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel).Result);
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
{
Amount = new decimal(0.01)
}));
//Scenario 4: Enabled But End Date < Now - Not Allowed
crowdfundViewModel.StartDate= DateTime.Today.AddDays(-2);
crowdfundViewModel.EndDate= DateTime.Today.AddDays(-1);
crowdfundViewModel.Enabled = true;
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel).Result);
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
{
Amount = new decimal(0.01)
}));
//Scenario 5: Enabled and within correct timeframe, however target is enforced and Amount is Over - Not Allowed
crowdfundViewModel.StartDate= DateTime.Today.AddDays(-2);
crowdfundViewModel.EndDate= DateTime.Today.AddDays(2);
crowdfundViewModel.Enabled = true;
crowdfundViewModel.TargetAmount = 1;
crowdfundViewModel.TargetCurrency = "BTC";
crowdfundViewModel.EnforceTargetAmount = true;
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel).Result);
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
{
Amount = new decimal(1.01)
}));
//Scenario 6: Allowed
Assert.IsType<OkObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
{
Amount = new decimal(0.05)
}));
}
}
[Fact]
[Trait("Integration", "Integration")]
public void CanComputeCrowdfundModel()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
var apps = user.GetController<AppsController>();
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model);
vm.Name = "test";
vm.SelectedAppType = AppType.Crowdfund.ToString();
Assert.IsType<RedirectToActionResult>(apps.CreateApp(vm).Result);
var appId = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model)
.Apps[0].Id;
Logs.Tester.LogInformation("We create an invoice with a hardcap");
var crowdfundViewModel = Assert.IsType<UpdateCrowdfundViewModel>(Assert
.IsType<ViewResult>(apps.UpdateCrowdfund(appId).Result).Model);
crowdfundViewModel.Enabled = true;
crowdfundViewModel.EndDate = null;
crowdfundViewModel.TargetAmount = 100;
crowdfundViewModel.TargetCurrency = "BTC";
crowdfundViewModel.UseAllStoreInvoices = true;
crowdfundViewModel.EnforceTargetAmount = true;
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel).Result);
var anonAppPubsController = tester.PayTester.GetController<AppsPublicController>();
var publicApps = user.GetController<AppsPublicController>();
var model = Assert.IsType<ViewCrowdfundViewModel>(Assert
.IsType<ViewResult>(publicApps.ViewCrowdfund(appId, String.Empty).Result).Model);
Assert.Equal(crowdfundViewModel.TargetAmount, model.TargetAmount );
Assert.Equal(crowdfundViewModel.EndDate, model.EndDate );
Assert.Equal(crowdfundViewModel.StartDate, model.StartDate );
Assert.Equal(crowdfundViewModel.TargetCurrency, model.TargetCurrency );
Assert.Equal(0m, model.Info.CurrentAmount );
Assert.Equal(0m, model.Info.CurrentPendingAmount);
Assert.Equal(0m, model.Info.ProgressPercentage);
Logs.Tester.LogInformation("Unpaid invoices should show as pending contribution because it is hardcap");
Logs.Tester.LogInformation("Because UseAllStoreInvoices is true, we can manually create an invoice and it should show as contribution");
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Buyer = new Buyer() { email = "test@fwf.com" },
Price = 1m,
Currency = "BTC",
PosData = "posData",
ItemDesc = "Some description",
TransactionSpeed = "high",
FullNotifications = true
}, Facade.Merchant);
model = Assert.IsType<ViewCrowdfundViewModel>(Assert
.IsType<ViewResult>(publicApps.ViewCrowdfund(appId, String.Empty).Result).Model);
Assert.Equal(0m ,model.Info.CurrentAmount);
Assert.Equal(1m, model.Info.CurrentPendingAmount);
Assert.Equal(0m, model.Info.ProgressPercentage);
Assert.Equal(1m, model.Info.PendingProgressPercentage);
Logs.Tester.LogInformation("Let's check current amount change once payment is confirmed");
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, tester.ExplorerNode.Network);
tester.ExplorerNode.SendToAddress(invoiceAddress, invoice.BtcDue);
tester.ExplorerNode.Generate(1); // By default invoice confirmed at 1 block
TestUtils.Eventually(() =>
{
model = Assert.IsType<ViewCrowdfundViewModel>(Assert
.IsType<ViewResult>(publicApps.ViewCrowdfund(appId, String.Empty).Result).Model);
Assert.Equal(1m, model.Info.CurrentAmount);
Assert.Equal(0m, model.Info.CurrentPendingAmount);
});
Logs.Tester.LogInformation("Because UseAllStoreInvoices is true, let's make sure the invoice is tagged");
var invoiceEntity = tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id).GetAwaiter().GetResult();
Assert.True(invoiceEntity.Version >= InvoiceEntity.InternalTagSupport_Version);
Assert.Contains(AppService.GetAppInternalTag(appId), invoiceEntity.InternalTags);
crowdfundViewModel.UseAllStoreInvoices = false;
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel).Result);
Logs.Tester.LogInformation("Because UseAllStoreInvoices is false, let's make sure the invoice is not tagged");
invoice = user.BitPay.CreateInvoice(new Invoice()
{
Buyer = new Buyer() { email = "test@fwf.com" },
Price = 1m,
Currency = "BTC",
PosData = "posData",
ItemDesc = "Some description",
TransactionSpeed = "high",
FullNotifications = true
}, Facade.Merchant);
invoiceEntity = tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id).GetAwaiter().GetResult();
Assert.DoesNotContain(AppService.GetAppInternalTag(appId), invoiceEntity.InternalTags);
Logs.Tester.LogInformation("After turning setting a softcap, let's check that only actual payments are counted");
crowdfundViewModel.EnforceTargetAmount = false;
crowdfundViewModel.UseAllStoreInvoices = true;
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel).Result);
invoice = user.BitPay.CreateInvoice(new Invoice()
{
Buyer = new Buyer() { email = "test@fwf.com" },
Price = 1m,
Currency = "BTC",
PosData = "posData",
ItemDesc = "Some description",
TransactionSpeed = "high",
FullNotifications = true
}, Facade.Merchant);
Assert.Equal(0m, model.Info.CurrentPendingAmount);
invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, tester.ExplorerNode.Network);
tester.ExplorerNode.SendToAddress(invoiceAddress, Money.Coins(0.5m));
tester.ExplorerNode.SendToAddress(invoiceAddress, Money.Coins(0.2m));
TestUtils.Eventually(() =>
{
model = Assert.IsType<ViewCrowdfundViewModel>(Assert
.IsType<ViewResult>(publicApps.ViewCrowdfund(appId, String.Empty).Result).Model);
Assert.Equal(0.7m, model.Info.CurrentPendingAmount);
});
}
}
}
}

View File

@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace BTCPayServer.Tests
{
public class MockDelay : IDelay
{
class WaitObj
{
public DateTimeOffset Expiration;
public TaskCompletionSource<bool> CTS;
}
List<WaitObj> waits = new List<WaitObj>();
DateTimeOffset _Now = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero);
public async Task Wait(TimeSpan delay, CancellationToken cancellation)
{
WaitObj w = new WaitObj();
w.Expiration = _Now + delay;
w.CTS = new TaskCompletionSource<bool>();
using (cancellation.Register(() =>
{
w.CTS.TrySetCanceled();
}))
{
lock (waits)
{
waits.Add(w);
}
await w.CTS.Task;
}
}
public void Advance(TimeSpan time)
{
_Now += time;
lock (waits)
{
foreach (var wait in waits.ToArray())
{
if (_Now >= wait.Expiration)
{
wait.CTS.TrySetResult(true);
waits.Remove(wait);
}
}
}
}
public void AdvanceMilliseconds(long milli)
{
Advance(TimeSpan.FromMilliseconds(milli));
}
public override string ToString()
{
return _Now.Millisecond.ToString(CultureInfo.InvariantCulture);
}
}
}

View File

@ -329,7 +329,7 @@ namespace BTCPayServer.Tests
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, tester.ExplorerNode.Network);
tester.ExplorerNode.SendToAddress(invoiceAddress, Money.Satoshis((decimal)invoice.BtcDue.Satoshi * 0.75m));
Eventually(() =>
TestUtils.Eventually(() =>
{
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("paid", localInvoice.Status);
@ -451,7 +451,7 @@ namespace BTCPayServer.Tests
});
await Task.Delay(TimeSpan.FromMilliseconds(1000)); // Give time to listen the new invoices
await tester.SendLightningPaymentAsync(invoice);
await EventuallyAsync(async () =>
await TestUtils.EventuallyAsync(async () =>
{
var localInvoice = await user.BitPay.GetInvoiceAsync(invoice.Id);
Assert.Equal("complete", localInvoice.Status);
@ -685,7 +685,7 @@ namespace BTCPayServer.Tests
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
var firstPayment = invoice.CryptoInfo[0].TotalDue - Money.Satoshis(10);
cashCow.SendToAddress(invoiceAddress, firstPayment);
Eventually(() =>
TestUtils.Eventually(() =>
{
invoice = acc.BitPay.GetInvoice(invoice.Id);
Assert.Equal(firstPayment, invoice.CryptoInfo[0].Paid);
@ -782,7 +782,7 @@ namespace BTCPayServer.Tests
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, user.SupportedNetwork.NBitcoinNetwork);
Logs.Tester.LogInformation($"The invoice should be paidOver");
Eventually(() =>
TestUtils.Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal(payment1, invoice.BtcPaid);
@ -811,7 +811,7 @@ namespace BTCPayServer.Tests
Assert.Equal(tx2, ((NewTransactionEvent)listener.NextEvent(cts.Token)).TransactionData.TransactionHash);
}
Logs.Tester.LogInformation($"The invoice should now not be paidOver anymore");
Eventually(() =>
TestUtils.Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal(payment2, invoice.BtcPaid);
@ -859,6 +859,44 @@ namespace BTCPayServer.Tests
Assert.Equal(f1.ToString(), f2.ToString());
}
[Fact]
[Trait("Integration", "Integration")]
public async void CheckCORSSetOnBitpayAPI()
{
using (var tester = ServerTester.Create())
{
tester.Start();
foreach(var req in new[]
{
"invoices/",
"invoices",
"rates",
"tokens"
}.Select(async path =>
{
using (HttpClient client = new HttpClient())
{
HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Options, tester.PayTester.ServerUri.AbsoluteUri + path);
message.Headers.Add("Access-Control-Request-Headers", "test");
var response = await client.SendAsync(message);
response.EnsureSuccessStatusCode();
Assert.True(response.Headers.TryGetValues("Access-Control-Allow-Origin", out var val));
Assert.Equal("*", val.FirstOrDefault());
Assert.True(response.Headers.TryGetValues("Access-Control-Allow-Headers", out val));
Assert.Equal("test", val.FirstOrDefault());
}
}).ToList())
{
await req;
}
HttpClient client2 = new HttpClient();
HttpRequestMessage message2 = new HttpRequestMessage(HttpMethod.Options, tester.PayTester.ServerUri.AbsoluteUri + "rates");
var response2 = await client2.SendAsync(message2);
Assert.True(response2.Headers.TryGetValues("Access-Control-Allow-Origin", out var val2));
Assert.Equal("*", val2.FirstOrDefault());
}
}
[Fact]
[Trait("Integration", "Integration")]
public void TestAccessBitpayAPI()
@ -1058,7 +1096,7 @@ namespace BTCPayServer.Tests
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
var firstPayment = Money.Coins(0.1m);
cashCow.SendToAddress(invoiceAddress, firstPayment);
Eventually(() =>
TestUtils.Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal(firstPayment, invoice.CryptoInfo[0].Paid);
@ -1079,7 +1117,7 @@ namespace BTCPayServer.Tests
Assert.NotEqual(invoice.BtcDue, invoice.CryptoInfo[0].Due); // Should be BTC rate
cashCow.SendToAddress(invoiceAddress, invoice.CryptoInfo[0].Due);
Eventually(() =>
TestUtils.Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal("paid", invoice.Status);
@ -1177,7 +1215,7 @@ namespace BTCPayServer.Tests
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
var firstPayment = Money.Coins(0.04m);
cashCow.SendToAddress(invoiceAddress, firstPayment);
Eventually(() =>
TestUtils.Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.True(invoice.BtcPaid == firstPayment);
@ -1219,7 +1257,7 @@ namespace BTCPayServer.Tests
firstPayment = Money.Coins(0.04m);
cashCow.SendToAddress(invoiceAddress, firstPayment);
Logs.Tester.LogInformation("First payment sent to " + invoiceAddress);
Eventually(() =>
TestUtils.Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.True(invoice.BtcPaid == firstPayment);
@ -1233,7 +1271,7 @@ namespace BTCPayServer.Tests
cashCow.Generate(2); // LTC is not worth a lot, so just to make sure we have money...
cashCow.SendToAddress(invoiceAddress, secondPayment);
Logs.Tester.LogInformation("Second payment sent to " + invoiceAddress);
Eventually(() =>
TestUtils.Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal(Money.Zero, invoice.BtcDue);
@ -1260,6 +1298,25 @@ namespace BTCPayServer.Tests
Assert.True(invoice.SupportedTransactionCurrencies["LTC"].Enabled);
Assert.True(invoice.PaymentSubtotals.ContainsKey("LTC"));
Assert.True(invoice.PaymentTotals.ContainsKey("LTC"));
// Check if we can disable LTC
invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5000.0m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true,
SupportedTransactionCurrencies = new Dictionary<string, InvoiceSupportedTransactionCurrency>()
{
{ "BTC", new InvoiceSupportedTransactionCurrency() { Enabled = true } }
}
}, Facade.Merchant);
Assert.Single(invoice.CryptoInfo.Where(c => c.CryptoCode == "BTC"));
Assert.Empty(invoice.CryptoInfo.Where(c => c.CryptoCode == "LTC"));
}
}
@ -1549,23 +1606,96 @@ donation:
}
}
[Fact]
[Trait("Fast", "Fast")]
public void CanScheduleBackgroundTasks()
{
BackgroundJobClient client = new BackgroundJobClient();
MockDelay mockDelay = new MockDelay();
client.Delay = mockDelay;
bool[] jobs = new bool[4];
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
Logs.Tester.LogInformation("Start Job[0] in 5 sec");
client.Schedule(async () => { Logs.Tester.LogInformation("Job[0]"); jobs[0] = true; }, TimeSpan.FromSeconds(5.0));
Logs.Tester.LogInformation("Start Job[1] in 2 sec");
client.Schedule(async () => { Logs.Tester.LogInformation("Job[1]"); jobs[1] = true; }, TimeSpan.FromSeconds(2.0));
Logs.Tester.LogInformation("Start Job[2] fails in 6 sec");
client.Schedule(async () => { jobs[2] = true; throw new Exception("Job[2]"); }, TimeSpan.FromSeconds(6.0));
Logs.Tester.LogInformation("Start Job[3] starts in in 7 sec");
client.Schedule(async () => { Logs.Tester.LogInformation("Job[3]"); jobs[3] = true; }, TimeSpan.FromSeconds(7.0));
Assert.True(new[] { false, false, false, false }.SequenceEqual(jobs));
CancellationTokenSource cts = new CancellationTokenSource();
var processing = client.ProcessJobs(cts.Token);
Assert.Equal(4, client.GetExecutingCount());
var waitJobsFinish = client.WaitAllRunning(default);
mockDelay.Advance(TimeSpan.FromSeconds(2.0));
Assert.True(new[] { false, true, false, false }.SequenceEqual(jobs));
mockDelay.Advance(TimeSpan.FromSeconds(3.0));
Assert.True(new[] { true, true, false, false }.SequenceEqual(jobs));
mockDelay.Advance(TimeSpan.FromSeconds(1.0));
Assert.True(new[] { true, true, true, false }.SequenceEqual(jobs));
Assert.Equal(1, client.GetExecutingCount());
Assert.False(waitJobsFinish.Wait(100));
Assert.False(waitJobsFinish.IsCompletedSuccessfully);
mockDelay.Advance(TimeSpan.FromSeconds(1.0));
Assert.True(new[] { true, true, true, true }.SequenceEqual(jobs));
Assert.True(waitJobsFinish.Wait(100));
Assert.True(waitJobsFinish.IsCompletedSuccessfully);
Assert.True(!waitJobsFinish.IsFaulted);
Assert.Equal(0, client.GetExecutingCount());
bool jobExecuted = false;
Logs.Tester.LogInformation("This job will be cancelled");
client.Schedule(async () => { jobExecuted = true; }, TimeSpan.FromSeconds(1.0));
mockDelay.Advance(TimeSpan.FromSeconds(0.5));
Assert.False(jobExecuted);
Thread.Sleep(100);
Assert.Equal(1, client.GetExecutingCount());
waitJobsFinish = client.WaitAllRunning(default);
Assert.False(waitJobsFinish.Wait(100));
cts.Cancel();
Assert.True(waitJobsFinish.Wait(1000));
Assert.True(waitJobsFinish.IsCompletedSuccessfully);
Assert.True(!waitJobsFinish.IsFaulted);
Assert.False(jobExecuted);
mockDelay.Advance(TimeSpan.FromSeconds(1.0));
Thread.Sleep(100); // Make sure it get cancelled
Assert.False(jobExecuted);
Assert.Equal(0, client.GetExecutingCount());
Assert.True(processing.IsCanceled);
Assert.True(client.WaitAllRunning(default).Wait(100));
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
}
[Fact]
[Trait("Fast", "Fast")]
public void PosDataParser_ParsesCorrectly()
{
var testCases =
new List<(string input, Dictionary<string, string> expectedOutput)>()
new List<(string input, Dictionary<string, object> expectedOutput)>()
{
{ (null, new Dictionary<string, string>())},
{("", new Dictionary<string, string>())},
{("{}", new Dictionary<string, string>())},
{("non-json-content", new Dictionary<string, string>(){ {string.Empty, "non-json-content"}})},
{("[1,2,3]", new Dictionary<string, string>(){ {string.Empty, "[1,2,3]"}})},
{("{ \"key\": \"value\"}", new Dictionary<string, string>(){ {"key", "value"}})},
{("{ \"key\": true}", new Dictionary<string, string>(){ {"key", "True"}})},
{("{ \"key\": \"value\", \"key2\": [\"value\", \"value2\"]}",
new Dictionary<string, string>(){ {"key", "value"}, {"key2", "value,value2"}})},
{("{ invalidjson file here}", new Dictionary<string, string>(){ {String.Empty, "{ invalidjson file here}"}})}
{ (null, new Dictionary<string, object>())},
{("", new Dictionary<string, object>())},
{("{}", new Dictionary<string, object>())},
{("non-json-content", new Dictionary<string, object>(){ {string.Empty, "non-json-content"}})},
{("[1,2,3]", new Dictionary<string, object>(){ {string.Empty, "[1,2,3]"}})},
{("{ \"key\": \"value\"}", new Dictionary<string, object>(){ {"key", "value"}})},
{("{ \"key\": true}", new Dictionary<string, object>(){ {"key", "True"}})},
{("{ invalidjson file here}", new Dictionary<string, object>(){ {String.Empty, "{ invalidjson file here}"}})}
};
testCases.ForEach(tuple =>
@ -1588,18 +1718,16 @@ donation:
var controller = tester.PayTester.GetController<InvoiceController>(null);
var testCases =
new List<(string input, Dictionary<string, string> expectedOutput)>()
new List<(string input, Dictionary<string, object> expectedOutput)>()
{
{ (null, new Dictionary<string, string>())},
{("", new Dictionary<string, string>())},
{("{}", new Dictionary<string, string>())},
{("non-json-content", new Dictionary<string, string>(){ {string.Empty, "non-json-content"}})},
{("[1,2,3]", new Dictionary<string, string>(){ {string.Empty, "[1,2,3]"}})},
{("{ \"key\": \"value\"}", new Dictionary<string, string>(){ {"key", "value"}})},
{("{ \"key\": true}", new Dictionary<string, string>(){ {"key", "True"}})},
{("{ \"key\": \"value\", \"key2\": [\"value\", \"value2\"]}",
new Dictionary<string, string>(){ {"key", "value"}, {"key2", "value,value2"}})},
{("{ invalidjson file here}", new Dictionary<string, string>(){ {String.Empty, "{ invalidjson file here}"}})}
{ (null, new Dictionary<string, object>())},
{("", new Dictionary<string, object>())},
{("{}", new Dictionary<string, object>())},
{("non-json-content", new Dictionary<string, object>(){ {string.Empty, "non-json-content"}})},
{("[1,2,3]", new Dictionary<string, object>(){ {string.Empty, "[1,2,3]"}})},
{("{ \"key\": \"value\"}", new Dictionary<string, object>(){ {"key", "value"}})},
{("{ \"key\": true}", new Dictionary<string, object>(){ {"key", "True"}})},
{("{ invalidjson file here}", new Dictionary<string, object>(){ {String.Empty, "{ invalidjson file here}"}})}
};
var tasks = new List<Task>();
@ -1672,7 +1800,7 @@ donation:
cashCow.SendToAddress(invoiceAddress, 4 * networkFee);
Thread.Sleep(1000);
Eventually(() =>
TestUtils.Eventually(() =>
{
var jsonResultPaid = user.GetController<InvoiceController>().Export("json").GetAwaiter().GetResult();
var paidresult = Assert.IsType<ContentResult>(jsonResultPaid);
@ -1744,7 +1872,7 @@ donation:
var firstPayment = productPartDue - missingMoney;
cashCow.SendToAddress(invoiceAddress, Money.Coins(firstPayment));
Eventually(() =>
TestUtils.Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
// Check that for the second payment, network fee are included
@ -1769,7 +1897,7 @@ donation:
}
});
cashCow.SendToAddress(invoiceAddress, due);
Eventually(() =>
TestUtils.Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal("paid", invoice.Status);
@ -1801,17 +1929,18 @@ donation:
var cashCow = tester.ExplorerNode;
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
var firstPayment = invoice.CryptoInfo[0].TotalDue - Money.Satoshis(10);
var firstPayment = invoice.CryptoInfo[0].TotalDue - Money.Coins(0.001m);
cashCow.SendToAddress(invoiceAddress, firstPayment);
Eventually(() =>
TestUtils.Eventually(() =>
{
var exportResultPaid = user.GetController<InvoiceController>().Export("csv").GetAwaiter().GetResult();
var paidresult = Assert.IsType<ContentResult>(exportResultPaid);
Assert.Equal("application/csv", paidresult.ContentType);
Assert.Contains($",\"orderId\",\"{invoice.Id}\",", paidresult.Content);
Assert.Contains($",\"OnChain\",\"BTC\",\"0.1000999\",\"0.0001\",\"5000.0\"", paidresult.Content);
Assert.Contains($",\"USD\",\"0.00050000\",\"500.0\",\"\",\"Some ``, description\",\"new (paidPartial)\"", paidresult.Content);
Assert.Contains($",\"OnChain\",\"BTC\",\"0.0991\",\"0.0001\",\"5000.0\"", paidresult.Content);
Assert.Contains($",\"USD\",\"5.00", paidresult.Content); // Seems hacky but some plateform does not render this decimal the same
Assert.Contains($"0\",\"500.0\",\"\",\"Some ``, description\",\"new (paidPartial)\"", paidresult.Content);
});
}
}
@ -1855,6 +1984,50 @@ donation:
}
}
[Fact]
[Trait("Integration", "Integration")]
public void CanCreateStrangeInvoice()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
var invoice1 = user.BitPay.CreateInvoice(new Invoice()
{
Price = 0.000000012m,
Currency = "BTC",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
var invoice2 = user.BitPay.CreateInvoice(new Invoice()
{
Price = 0.000000019m,
Currency = "BTC",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
Assert.Equal(0.00000001m, invoice1.Price);
Assert.Equal(0.00000002m, invoice2.Price);
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = -0.1m,
Currency = "BTC",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
Assert.Equal(0.0m, invoice.Price);
}
}
[Fact]
[Trait("Integration", "Integration")]
public void InvoiceFlowThroughDifferentStatesCorrectly()
@ -1868,6 +2041,7 @@ donation:
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5000.0m,
TaxIncluded = 1000.0m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
@ -1879,7 +2053,7 @@ donation:
Assert.Equal(0, invoice.CryptoInfo[0].TxCount);
Assert.True(invoice.MinerFees.ContainsKey("BTC"));
Assert.Contains(invoice.MinerFees["BTC"].SatoshiPerBytes, new[] { 100.0m, 20.0m });
Eventually(() =>
TestUtils.Eventually(() =>
{
var textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery()
{
@ -1897,6 +2071,8 @@ donation:
});
invoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal(1000.0m, invoice.TaxIncluded);
Assert.Equal(5000.0m, invoice.Price);
Assert.Equal(Money.Coins(0), invoice.BtcPaid);
Assert.Equal("new", invoice.Status);
Assert.False((bool)((JValue)invoice.ExceptionStatus).Value);
@ -1925,7 +2101,7 @@ donation:
Money secondPayment = Money.Zero;
Eventually(() =>
TestUtils.Eventually(() =>
{
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("new", localInvoice.Status);
@ -1948,7 +2124,7 @@ donation:
cashCow.SendToAddress(invoiceAddress, secondPayment);
Eventually(() =>
TestUtils.Eventually(() =>
{
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("paid", localInvoice.Status);
@ -1962,7 +2138,7 @@ donation:
cashCow.Generate(1); //The user has medium speed settings, so 1 conf is enough to be confirmed
Eventually(() =>
TestUtils.Eventually(() =>
{
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("confirmed", localInvoice.Status);
@ -1970,7 +2146,7 @@ donation:
cashCow.Generate(5); //Now should be complete
Eventually(() =>
TestUtils.Eventually(() =>
{
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("complete", localInvoice.Status);
@ -1992,7 +2168,7 @@ donation:
var txId = cashCow.SendToAddress(invoiceAddress, invoice.BtcDue + Money.Coins(1));
Eventually(() =>
TestUtils.Eventually(() =>
{
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("paid", localInvoice.Status);
@ -2009,7 +2185,7 @@ donation:
cashCow.Generate(1);
Eventually(() =>
TestUtils.Eventually(() =>
{
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("confirmed", localInvoice.Status);
@ -2019,19 +2195,20 @@ donation:
}
}
[Fact]
[Trait("Integration", "Integration")]
public void CheckQuadrigacxRateProvider()
{
var quadri = new QuadrigacxRateProvider();
var rates = quadri.GetRatesAsync().GetAwaiter().GetResult();
Assert.NotEmpty(rates);
Assert.NotEqual(0.0m, rates.First().BidAsk.Bid);
Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("BTC_CAD")).Bid);
Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("BTC_USD")).Bid);
Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("LTC_CAD")).Bid);
Assert.Null(rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("LTC_USD")));
}
//[Fact]
//[Trait("Integration", "Integration")]
// 29 january, the exchange is down
//public void CheckQuadrigacxRateProvider()
//{
// var quadri = new QuadrigacxRateProvider();
// var rates = quadri.GetRatesAsync().GetAwaiter().GetResult();
// Assert.NotEmpty(rates);
// Assert.NotEqual(0.0m, rates.First().BidAsk.Bid);
// Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("BTC_CAD")).Bid);
// Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("BTC_USD")).Bid);
// Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("LTC_CAD")).Bid);
// Assert.Null(rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("LTC_USD")));
//}
[Fact]
[Trait("Integration", "Integration")]
@ -2045,6 +2222,9 @@ donation:
.Select(p => (ExpectedName: p.Key, ResultAsync: p.Value.GetRatesAsync(), Fetcher: (BackgroundFetcherRateProvider)p.Value))
.ToList())
{
Logs.Tester.LogInformation($"Testing {result.ExpectedName}");
if (result.ExpectedName == "quadrigacx")
continue; // 29 january, the exchange is down
result.Fetcher.InvalidateCache();
var exchangeRates = result.ResultAsync.Result;
result.Fetcher.InvalidateCache();
@ -2193,42 +2373,81 @@ donation:
Assert.Throws<InvalidOperationException>(() => fetch.GetRatesAsync().GetAwaiter().GetResult());
}
[Fact]
[Trait("Fast", "Fast")]
public void CheckParseStatusMessageModel()
{
var legacyStatus = "Error: some bad shit happened";
var parsed = new StatusMessageModel(legacyStatus);
Assert.Equal(legacyStatus, parsed.Message);
Assert.Equal(StatusMessageModel.StatusSeverity.Error, parsed.Severity);
var legacyStatus2 = "Some normal shit happened";
parsed = new StatusMessageModel(legacyStatus2);
Assert.Equal(legacyStatus2, parsed.Message);
Assert.Equal(StatusMessageModel.StatusSeverity.Success, parsed.Severity);
var newStatus = new StatusMessageModel()
{
Html = "<a href='xxx'>something new</a>",
Severity = StatusMessageModel.StatusSeverity.Info
};
parsed = new StatusMessageModel(newStatus.ToString());
Assert.Null(parsed.Message);
Assert.Equal(newStatus.Html, parsed.Html);
Assert.Equal(StatusMessageModel.StatusSeverity.Info, parsed.Severity);
var newStatus2 = new StatusMessageModel()
{
Message = "something new",
Severity = StatusMessageModel.StatusSeverity.Success
};
parsed = new StatusMessageModel(newStatus2.ToString());
Assert.Null(parsed.Html);
Assert.Equal(newStatus2.Message, parsed.Message);
Assert.Equal(StatusMessageModel.StatusSeverity.Success, parsed.Severity);
}
private static bool IsMapped(Invoice invoice, ApplicationDbContext ctx)
{
var h = BitcoinAddress.Create(invoice.BitcoinAddress, Network.RegTest).ScriptPubKey.Hash.ToString();
return ctx.AddressInvoices.FirstOrDefault(i => i.InvoiceDataId == invoice.Id && i.GetAddress() == h) != null;
}
private void Eventually(Action act)
public static class TestUtils
{
CancellationTokenSource cts = new CancellationTokenSource(20000);
while (true)
public static void Eventually(Action act)
{
try
CancellationTokenSource cts = new CancellationTokenSource(20000);
while (true)
{
act();
break;
}
catch (XunitException) when (!cts.Token.IsCancellationRequested)
{
cts.Token.WaitHandle.WaitOne(500);
try
{
act();
break;
}
catch (XunitException) when (!cts.Token.IsCancellationRequested)
{
cts.Token.WaitHandle.WaitOne(500);
}
}
}
}
private async Task EventuallyAsync(Func<Task> act)
{
CancellationTokenSource cts = new CancellationTokenSource(20000);
while (true)
public static async Task EventuallyAsync(Func<Task> act)
{
try
CancellationTokenSource cts = new CancellationTokenSource(20000);
while (true)
{
await act();
break;
}
catch (XunitException) when (!cts.Token.IsCancellationRequested)
{
await Task.Delay(500);
try
{
await act();
break;
}
catch (XunitException) when (!cts.Token.IsCancellationRequested)
{
await Task.Delay(500);
}
}
}
}

View File

@ -2,7 +2,7 @@ version: "3"
# Run `docker-compose up dev` for bootstrapping your development environment
# Doing so will expose NBXplorer, Bitcoind RPC and postgres port to the host so that tests can Run,
# The Visual Studio launch setting `Docker-Regtest` is configured to use this environment.
# The Visual Studio launch setting `Docker-regtest` is configured to use this environment.
services:
tests:
@ -69,7 +69,7 @@ services:
nbxplorer:
image: nicolasdorier/nbxplorer:2.0.0.2
image: nicolasdorier/nbxplorer:2.0.0.8
restart: unless-stopped
ports:
- "32838:32838"
@ -222,7 +222,7 @@ services:
- MYSQL_ALLOW_EMPTY_PASSWORD=yes
merchant_lnd:
image: btcpayserver/lnd:v0.5.1-beta
image: btcpayserver/lnd:v0.5.2-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -252,7 +252,7 @@ services:
- bitcoind
customer_lnd:
image: btcpayserver/lnd:v0.5.1-beta
image: btcpayserver/lnd:v0.5.2-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"

View File

@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
using NBitcoin;
using NBXplorer;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
public void InitBitcoinplus()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("XBC");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Bitcoinplus",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://chainz.cryptoid.info/xbc/tx.dws?{0}" : "https://chainz.cryptoid.info/xbc/tx.dws?{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "bitcoinplus",
DefaultRateRules = new[]
{
"XBC_X = XBC_BTC * BTC_X",
"XBC_BTC = cryptopia(XBC_BTC)"
},
CryptoImagePath = "imlegacy/bitcoinplus.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("65'") : new KeyPath("1'")
});
}
}
}

View File

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

View File

@ -52,10 +52,13 @@ namespace BTCPayServer
InitBitcoinGold();
InitMonacoin();
InitDash();
InitPolis();
InitFeathercoin();
InitGroestlcoin();
InitViacoin();
// Disabled because of https://twitter.com/Cryptopia_NZ/status/1085084168852291586
//InitPolis();
//InitBitcoinplus();
//InitUfo();
}

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<Version>1.0.3.38</Version>
<Version>1.0.3.62</Version>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup>
<PropertyGroup>
@ -33,11 +33,10 @@
<EmbeddedResource Include="Currencies.txt" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.4" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.9" />
<PackageReference Include="BuildBundlerMinifier" Version="2.7.385" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.5.3" />
<PackageReference Include="Hangfire" Version="1.6.20" />
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.2" />
<PackageReference Include="HtmlSanitizer" Version="4.0.199" />
<PackageReference Include="LedgerWallet" Version="2.0.0.3" />
<PackageReference Include="Meziantou.AspNetCore.BundleTagHelpers" Version="2.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
@ -46,10 +45,10 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="NBitcoin" Version="4.1.1.73" />
<PackageReference Include="NBitpayClient" Version="1.0.0.30" />
<PackageReference Include="NBitcoin" Version="4.1.1.78" />
<PackageReference Include="NBitpayClient" Version="1.0.0.31" />
<PackageReference Include="DBreeze" Version="1.92.0" />
<PackageReference Include="NBXplorer.Client" Version="2.0.0.1" />
<PackageReference Include="NBXplorer.Client" Version="2.0.0.3" />
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.2" />
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.3" />
<PackageReference Include="NicolasDorier.RateLimits" Version="1.0.0.3" />
@ -126,6 +125,7 @@
<Folder Include="Build\" />
<Folder Include="wwwroot\vendor\clipboard.js\" />
<Folder Include="wwwroot\vendor\highlightjs\" />
<Folder Include="wwwroot\vendor\summernote" />
</ItemGroup>
<ItemGroup>
@ -139,7 +139,7 @@
<Content Update="Views\Server\LightningChargeServices.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\SparkServices.cshtml">
<Content Update="Views\Server\LightningWalletServices.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\SSHService.cshtml">

View File

@ -110,7 +110,7 @@ namespace BTCPayServer.Configuration
if (!LightningConnectionString.TryParse(lightning, true, out var connectionString, out var error))
{
throw new ConfigException($"Invalid setting {net.CryptoCode}.lightning, " + Environment.NewLine +
$"If you have a lightning server use: 'type=clightning;server=/root/.lightning/lightning-rpc', " + Environment.NewLine +
$"If you have a c-lightning server use: 'type=clightning;server=/root/.lightning/lightning-rpc', " + Environment.NewLine +
$"If you have a lightning charge server: 'type=charge;server=https://charge.example.com;api-token=yourapitoken'" + Environment.NewLine +
$"If you have a lnd server: 'type=lnd-rest;server=https://lnd:lnd@lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
$" lnd server: 'type=lnd-rest;server=https://lnd:lnd@lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
@ -118,7 +118,7 @@ namespace BTCPayServer.Configuration
}
if (connectionString.IsLegacy)
{
Logs.Configuration.LogWarning($"Setting {net.CryptoCode}.lightning will work but use an deprecated format, please replace it by '{connectionString.ToString()}'");
Logs.Configuration.LogWarning($"Setting {net.CryptoCode}.lightning is a deprecated format, it will work now, but please replace it for future versions with '{connectionString.ToString()}'");
}
InternalLightningByCryptoCode.Add(net.CryptoCode, connectionString);
}
@ -144,15 +144,30 @@ namespace BTCPayServer.Configuration
externalLnd<ExternalLndGrpc>($"{net.CryptoCode}.external.lnd.grpc", "lnd-grpc");
externalLnd<ExternalLndRest>($"{net.CryptoCode}.external.lnd.rest", "lnd-rest");
var spark = conf.GetOrDefault<string>($"{net.CryptoCode}.external.spark", string.Empty);
if (spark.Length != 0)
{
if (!SparkConnectionString.TryParse(spark, out var connectionString))
var spark = conf.GetOrDefault<string>($"{net.CryptoCode}.external.spark", string.Empty);
if (spark.Length != 0)
{
throw new ConfigException($"Invalid setting {net.CryptoCode}.external.spark, " + Environment.NewLine +
$"Valid example: 'server=https://btcpay.example.com/spark/btc/;cookiefile=/etc/clightning_bitcoin_spark/.cookie'");
if (!SparkConnectionString.TryParse(spark, out var connectionString))
{
throw new ConfigException($"Invalid setting {net.CryptoCode}.external.spark, " + Environment.NewLine +
$"Valid example: 'server=https://btcpay.example.com/spark/btc/;cookiefile=/etc/clightning_bitcoin_spark/.cookie'");
}
ExternalServicesByCryptoCode.Add(net.CryptoCode, new ExternalSpark(connectionString));
}
}
{
var rtl = conf.GetOrDefault<string>($"{net.CryptoCode}.external.rtl", string.Empty);
if (rtl.Length != 0)
{
if (!SparkConnectionString.TryParse(rtl, out var connectionString))
{
throw new ConfigException($"Invalid setting {net.CryptoCode}.external.rtl, " + Environment.NewLine +
$"Valid example: 'server=https://btcpay.example.com/rtl/btc/;cookiefile=/etc/clightning_bitcoin_rtl/.cookie'");
}
ExternalServicesByCryptoCode.Add(net.CryptoCode, new ExternalRTL(connectionString));
}
ExternalServicesByCryptoCode.Add(net.CryptoCode, new ExternalSpark(connectionString));
}
var charge = conf.GetOrDefault<string>($"{net.CryptoCode}.external.charge", string.Empty);
@ -177,10 +192,11 @@ namespace BTCPayServer.Configuration
var services = conf.GetOrDefault<string>("externalservices", null);
if (services != null)
{
foreach (var service in services.Split(new[] { ';', ',' })
.Select(p => p.Split(':'))
.Where(p => p.Length == 2)
.Select(p => (Name: p[0], Link: p[1])))
foreach (var service in services.Split(new[] { ';', ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(p => (p, SeparatorIndex: p.IndexOf(':', StringComparison.OrdinalIgnoreCase)))
.Where(p => p.SeparatorIndex != -1)
.Select(p => (Name: p.p.Substring(0, p.SeparatorIndex),
Link: p.p.Substring(p.SeparatorIndex + 1))))
{
ExternalServices.AddOrReplace(service.Name, service.Link);
}
@ -235,7 +251,7 @@ namespace BTCPayServer.Configuration
RootPath = "/" + RootPath;
var old = conf.GetOrDefault<Uri>("internallightningnode", null);
if (old != null)
throw new ConfigException($"internallightningnode should not be used anymore, use btclightning instead");
throw new ConfigException($"internallightningnode is deprecated and should not be used anymore, use btclightning instead");
LogFile = GetDebugLog(conf);
if (!string.IsNullOrEmpty(LogFile))

View File

@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Configuration.External
{
public class ExternalRTL : ExternalService, IAccessKeyService
{
public SparkConnectionString ConnectionString { get; }
public ExternalRTL(SparkConnectionString connectionString)
{
if (connectionString == null)
throw new ArgumentNullException(nameof(connectionString));
ConnectionString = connectionString;
}
public async Task<string> ExtractAccessKey()
{
if (ConnectionString?.CookeFile == null)
throw new FormatException("Invalid connection string");
return await System.IO.File.ReadAllTextAsync(ConnectionString.CookeFile);
}
}
}

View File

@ -5,7 +5,12 @@ using System.Threading.Tasks;
namespace BTCPayServer.Configuration.External
{
public class ExternalSpark : ExternalService
public interface IAccessKeyService
{
SparkConnectionString ConnectionString { get; }
Task<string> ExtractAccessKey();
}
public class ExternalSpark : ExternalService, IAccessKeyService
{
public SparkConnectionString ConnectionString { get; }
@ -15,5 +20,19 @@ namespace BTCPayServer.Configuration.External
throw new ArgumentNullException(nameof(connectionString));
ConnectionString = connectionString;
}
public async Task<string> ExtractAccessKey()
{
if (ConnectionString?.CookeFile == null)
throw new FormatException("Invalid connection string");
var cookie = (ConnectionString.CookeFile == "fake"
? "fake:fake:fake" // Hacks for testing
: await System.IO.File.ReadAllTextAsync(ConnectionString.CookeFile)).Split(':');
if (cookie.Length >= 3)
{
return cookie[2];
}
throw new FormatException("Invalid cookiefile format");
}
}
}

View File

@ -2,6 +2,7 @@
using BTCPayServer.Filters;
using BTCPayServer.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using NBitcoin.DataEncoders;
using NBitpayClient;
@ -13,7 +14,7 @@ using System.Threading.Tasks;
namespace BTCPayServer.Controllers
{
[Authorize(AuthenticationSchemes = Security.Policies.BitpayAuthentication)]
[BitpayAPIConstraint(true)]
[BitpayAPIConstraint()]
public class AccessTokenController : Controller
{
TokenRepository _TokenRepository;

View File

@ -28,7 +28,7 @@ namespace BTCPayServer.Controllers
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly IEmailSender _emailSender;
private readonly EmailSenderFactory _EmailSenderFactory;
StoreRepository storeRepository;
RoleManager<IdentityRole> _RoleManager;
SettingsRepository _SettingsRepository;
@ -40,14 +40,14 @@ namespace BTCPayServer.Controllers
RoleManager<IdentityRole> roleManager,
StoreRepository storeRepository,
SignInManager<ApplicationUser> signInManager,
IEmailSender emailSender,
EmailSenderFactory emailSenderFactory,
SettingsRepository settingsRepository,
Configuration.BTCPayServerOptions options)
{
this.storeRepository = storeRepository;
_userManager = userManager;
_signInManager = signInManager;
_emailSender = emailSender;
_EmailSenderFactory = emailSenderFactory;
_RoleManager = roleManager;
_SettingsRepository = settingsRepository;
_Options = options;
@ -286,7 +286,8 @@ namespace BTCPayServer.Controllers
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme);
RegisteredUserId = user.Id;
await _emailSender.SendEmailConfirmationAsync(model.Email, callbackUrl);
_EmailSenderFactory.GetEmailSender().SendEmailConfirmation(model.Email, callbackUrl);
if (!policies.RequiresConfirmedEmail)
{
if(logon)
@ -446,8 +447,9 @@ namespace BTCPayServer.Controllers
// visit https://go.microsoft.com/fwlink/?LinkID=532713
var code = await _userManager.GeneratePasswordResetTokenAsync(user);
var callbackUrl = Url.ResetPasswordCallbackLink(user.Id, code, Request.Scheme);
await _emailSender.SendEmailAsync(model.Email, "Reset Password",
$"Please reset your password by clicking here: <a href='{callbackUrl}'>link</a>");
_EmailSenderFactory.GetEmailSender().SendEmail(model.Email, "Reset Password",
$"Please reset your password by clicking here: <a href='{callbackUrl}'>link</a>");
return RedirectToAction(nameof(ForgotPasswordConfirmation));
}

View File

@ -0,0 +1,144 @@
using System;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Services.Apps;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers
{
public partial class AppsController
{
public class AppUpdated
{
public string AppId { get; set; }
public object Settings { get; set; }
public string StoreId { get; set; }
public override string ToString()
{
return String.Empty;
}
}
[HttpGet]
[Route("{appId}/settings/crowdfund")]
public async Task<IActionResult> UpdateCrowdfund(string appId)
{
var app = await GetOwnedApp(appId, AppType.Crowdfund);
if (app == null)
return NotFound();
var settings = app.GetSettings<CrowdfundSettings>();
var vm = new UpdateCrowdfundViewModel()
{
Title = settings.Title,
Enabled = settings.Enabled,
EnforceTargetAmount = settings.EnforceTargetAmount,
StartDate = settings.StartDate,
TargetCurrency = settings.TargetCurrency,
Description = settings.Description,
MainImageUrl = settings.MainImageUrl,
EmbeddedCSS = settings.EmbeddedCSS,
EndDate = settings.EndDate,
TargetAmount = settings.TargetAmount,
CustomCSSLink = settings.CustomCSSLink,
NotificationUrl = settings.NotificationUrl,
Tagline = settings.Tagline,
PerksTemplate = settings.PerksTemplate,
DisqusEnabled = settings.DisqusEnabled,
SoundsEnabled = settings.SoundsEnabled,
DisqusShortname = settings.DisqusShortname,
AnimationsEnabled = settings.AnimationsEnabled,
ResetEveryAmount = settings.ResetEveryAmount,
ResetEvery = Enum.GetName(typeof(CrowdfundResetEvery), settings.ResetEvery),
UseAllStoreInvoices = app.TagAllInvoices,
AppId = appId,
SearchTerm = app.TagAllInvoices ? $"storeid:{app.StoreDataId}" : $"orderid:{AppService.GetCrowdfundOrderId(appId)}",
DisplayPerksRanking = settings.DisplayPerksRanking,
SortPerksByPopularity = settings.SortPerksByPopularity
};
return View(vm);
}
[HttpPost]
[Route("{appId}/settings/crowdfund")]
public async Task<IActionResult> UpdateCrowdfund(string appId, UpdateCrowdfundViewModel vm)
{
if (!string.IsNullOrEmpty( vm.TargetCurrency) && _currencies.GetCurrencyData(vm.TargetCurrency, false) == null)
ModelState.AddModelError(nameof(vm.TargetCurrency), "Invalid currency");
try
{
_AppService.Parse(vm.PerksTemplate, vm.TargetCurrency).ToString();
}
catch
{
ModelState.AddModelError(nameof(vm.PerksTemplate), "Invalid template");
}
if (Enum.Parse<CrowdfundResetEvery>(vm.ResetEvery) != CrowdfundResetEvery.Never && !vm.StartDate.HasValue)
{
ModelState.AddModelError(nameof(vm.StartDate), "A start date is needed when the goal resets every X amount of time.");
}
if (Enum.Parse<CrowdfundResetEvery>(vm.ResetEvery) != CrowdfundResetEvery.Never && vm.ResetEveryAmount <= 0)
{
ModelState.AddModelError(nameof(vm.ResetEveryAmount), "You must reset the goal at a minimum of 1 ");
}
if (vm.DisplayPerksRanking && !vm.SortPerksByPopularity)
{
ModelState.AddModelError(nameof(vm.DisplayPerksRanking), "You must sort by popularity in order to display ranking.");
}
if (!ModelState.IsValid)
{
return View(vm);
}
var app = await GetOwnedApp(appId, AppType.Crowdfund);
if (app == null)
return NotFound();
var newSettings = new CrowdfundSettings()
{
Title = vm.Title,
Enabled = vm.Enabled,
EnforceTargetAmount = vm.EnforceTargetAmount,
StartDate = vm.StartDate,
TargetCurrency = vm.TargetCurrency,
Description = _htmlSanitizer.Sanitize( vm.Description),
EndDate = vm.EndDate,
TargetAmount = vm.TargetAmount,
CustomCSSLink = vm.CustomCSSLink,
MainImageUrl = vm.MainImageUrl,
EmbeddedCSS = vm.EmbeddedCSS,
NotificationUrl = vm.NotificationUrl,
Tagline = vm.Tagline,
PerksTemplate = vm.PerksTemplate,
DisqusEnabled = vm.DisqusEnabled,
SoundsEnabled = vm.SoundsEnabled,
DisqusShortname = vm.DisqusShortname,
AnimationsEnabled = vm.AnimationsEnabled,
ResetEveryAmount = vm.ResetEveryAmount,
ResetEvery = Enum.Parse<CrowdfundResetEvery>(vm.ResetEvery),
DisplayPerksRanking = vm.DisplayPerksRanking,
SortPerksByPopularity = vm.SortPerksByPopularity
};
app.TagAllInvoices = vm.UseAllStoreInvoices;
app.SetSettings(newSettings);
await UpdateAppSettings(app);
_EventAggregator.Publish(new AppUpdated()
{
AppId = appId,
StoreId = app.StoreDataId,
Settings = newSettings
});
StatusMessage = "App updated";
return RedirectToAction(nameof(UpdateCrowdfund), new {appId});
}
}
}

View File

@ -85,6 +85,7 @@ namespace BTCPayServer.Controllers
var settings = app.GetSettings<PointOfSaleSettings>();
var vm = new UpdatePointOfSaleViewModel()
{
Id = appId,
Title = settings.Title,
EnableShoppingCart = settings.EnableShoppingCart,
ShowCustomAmount = settings.ShowCustomAmount,
@ -115,7 +116,7 @@ namespace BTCPayServer.Controllers
}
try
{
var items = _AppsHelper.Parse(settings.Template, settings.Currency);
var items = _AppService.Parse(settings.Template, settings.Currency);
var builder = new StringBuilder();
builder.AppendLine($"<form method=\"POST\" action=\"{encoder.Encode(appUrl)}\">");
builder.AppendLine($" <input type=\"hidden\" name=\"email\" value=\"customer@example.com\" />");
@ -137,11 +138,11 @@ namespace BTCPayServer.Controllers
[Route("{appId}/settings/pos")]
public async Task<IActionResult> UpdatePointOfSale(string appId, UpdatePointOfSaleViewModel vm)
{
if (_AppsHelper.GetCurrencyData(vm.Currency, false) == null)
if (_currencies.GetCurrencyData(vm.Currency, false) == null)
ModelState.AddModelError(nameof(vm.Currency), "Invalid currency");
try
{
_AppsHelper.Parse(vm.Template, vm.Currency);
_AppService.Parse(vm.Template, vm.Currency);
}
catch
{
@ -179,6 +180,7 @@ namespace BTCPayServer.Controllers
ctx.Apps.Add(app);
ctx.Entry<AppData>(app).State = EntityState.Modified;
ctx.Entry<AppData>(app).Property(a => a.Settings).IsModified = true;
ctx.Entry<AppData>(app).Property(a => a.TagAllInvoices).IsModified = true;
await ctx.SaveChangesAsync();
}
}

View File

@ -7,6 +7,8 @@ using BTCPayServer.Models;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Security;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Rates;
using Ganss.XSS;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
@ -24,19 +26,28 @@ namespace BTCPayServer.Controllers
public AppsController(
UserManager<ApplicationUser> userManager,
ApplicationDbContextFactory contextFactory,
EventAggregator eventAggregator,
BTCPayNetworkProvider networkProvider,
AppsHelper appsHelper)
CurrencyNameTable currencies,
HtmlSanitizer htmlSanitizer,
AppService AppService)
{
_UserManager = userManager;
_ContextFactory = contextFactory;
_EventAggregator = eventAggregator;
_NetworkProvider = networkProvider;
_AppsHelper = appsHelper;
_currencies = currencies;
_htmlSanitizer = htmlSanitizer;
_AppService = AppService;
}
private UserManager<ApplicationUser> _UserManager;
private ApplicationDbContextFactory _ContextFactory;
private readonly EventAggregator _EventAggregator;
private BTCPayNetworkProvider _NetworkProvider;
private AppsHelper _AppsHelper;
private readonly CurrencyNameTable _currencies;
private readonly HtmlSanitizer _htmlSanitizer;
private AppService _AppService;
[TempData]
public string StatusMessage { get; set; }
@ -44,7 +55,7 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> ListApps()
{
var apps = await GetAllApps();
var apps = await _AppService.GetAllApps(GetUserId());
return View(new ListAppsViewModel()
{
Apps = apps
@ -58,7 +69,7 @@ namespace BTCPayServer.Controllers
var appData = await GetOwnedApp(appId);
if (appData == null)
return NotFound();
if (await DeleteApp(appData))
if (await _AppService.DeleteApp(appData))
StatusMessage = "App removed successfully";
return RedirectToAction(nameof(ListApps));
}
@ -67,10 +78,15 @@ namespace BTCPayServer.Controllers
[Route("create")]
public async Task<IActionResult> CreateApp()
{
var stores = await GetOwnedStores();
var stores = await _AppService.GetOwnedStores(GetUserId());
if (stores.Length == 0)
{
StatusMessage = "Error: You must have created at least one store";
StatusMessage = new StatusMessageModel()
{
Html =
$"Error: You must have created at least one store. <a href='{(Url.Action("CreateStore", "UserStores"))}'>Create store</a>",
Severity = StatusMessageModel.StatusSeverity.Error
}.ToString();
return RedirectToAction(nameof(ListApps));
}
var vm = new CreateAppViewModel();
@ -82,10 +98,15 @@ namespace BTCPayServer.Controllers
[Route("create")]
public async Task<IActionResult> CreateApp(CreateAppViewModel vm)
{
var stores = await GetOwnedStores();
var stores = await _AppService.GetOwnedStores(GetUserId());
if (stores.Length == 0)
{
StatusMessage = "Error: You must own at least one store";
StatusMessage = new StatusMessageModel()
{
Html =
$"Error: You must have created at least one store. <a href='{(Url.Action("CreateStore", "UserStores"))}'>Create store</a>",
Severity = StatusMessageModel.StatusSeverity.Error
}.ToString();
return RedirectToAction(nameof(ListApps));
}
var selectedStore = vm.SelectedStore;
@ -117,9 +138,17 @@ namespace BTCPayServer.Controllers
}
StatusMessage = "App successfully created";
CreatedAppId = id;
if (appType == AppType.PointOfSale)
return RedirectToAction(nameof(UpdatePointOfSale), new { appId = id });
return RedirectToAction(nameof(ListApps));
switch (appType)
{
case AppType.PointOfSale:
return RedirectToAction(nameof(UpdatePointOfSale), new { appId = id });
case AppType.Crowdfund:
return RedirectToAction(nameof(UpdateCrowdfund), new { appId = id });
default:
return RedirectToAction(nameof(ListApps));
}
}
[HttpGet]
@ -139,53 +168,10 @@ namespace BTCPayServer.Controllers
private Task<AppData> GetOwnedApp(string appId, AppType? type = null)
{
return _AppsHelper.GetAppDataIfOwner(GetUserId(), appId, type);
}
private async Task<StoreData[]> GetOwnedStores()
{
var userId = GetUserId();
using (var ctx = _ContextFactory.CreateContext())
{
return await ctx.UserStore
.Where(us => us.ApplicationUserId == userId && us.Role == StoreRoles.Owner)
.Select(u => u.StoreData)
.ToArrayAsync();
}
}
private async Task<bool> DeleteApp(AppData appData)
{
using (var ctx = _ContextFactory.CreateContext())
{
ctx.Apps.Add(appData);
ctx.Entry<AppData>(appData).State = EntityState.Deleted;
return await ctx.SaveChangesAsync() == 1;
}
}
private async Task<ListAppsViewModel.ListAppViewModel[]> GetAllApps()
{
var userId = GetUserId();
using (var ctx = _ContextFactory.CreateContext())
{
return await ctx.UserStore
.Where(us => us.ApplicationUserId == userId)
.Join(ctx.Apps, us => us.StoreDataId, app => app.StoreDataId,
(us, app) =>
new ListAppsViewModel.ListAppViewModel()
{
IsOwner = us.Role == StoreRoles.Owner,
StoreId = us.StoreDataId,
StoreName = us.StoreData.StoreName,
AppName = app.Name,
AppType = app.AppType,
Id = app.Id
})
.ToArrayAsync();
}
return _AppService.GetAppDataIfOwner(GetUserId(), appId, type);
}
private string GetUserId()
{
return _UserManager.GetUserId(User);

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
@ -7,14 +8,21 @@ using System.Security.Claims;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Filters;
using BTCPayServer.Models;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Rating;
using BTCPayServer.Security;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using Microsoft.AspNetCore.Authorization;
using Ganss.XSS;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using NBitpayClient;
using YamlDotNet.RepresentationModel;
using static BTCPayServer.Controllers.AppsController;
@ -22,26 +30,30 @@ namespace BTCPayServer.Controllers
{
public class AppsPublicController : Controller
{
public AppsPublicController(AppsHelper appsHelper, InvoiceController invoiceController)
public AppsPublicController(AppService AppService,
InvoiceController invoiceController,
UserManager<ApplicationUser> userManager)
{
_AppsHelper = appsHelper;
_AppService = AppService;
_InvoiceController = invoiceController;
_UserManager = userManager;
}
private AppsHelper _AppsHelper;
private AppService _AppService;
private InvoiceController _InvoiceController;
private readonly UserManager<ApplicationUser> _UserManager;
[HttpGet]
[Route("/apps/{appId}/pos")]
[XFrameOptionsAttribute(null)]
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
public async Task<IActionResult> ViewPointOfSale(string appId)
{
var app = await _AppsHelper.GetApp(appId, AppType.PointOfSale);
var app = await _AppService.GetApp(appId, AppType.PointOfSale);
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
var numberFormatInfo = _AppsHelper.Currencies.GetNumberFormatInfo(settings.Currency) ?? _AppsHelper.Currencies.GetNumberFormatInfo("USD");
var numberFormatInfo = _AppService.Currencies.GetNumberFormatInfo(settings.Currency) ?? _AppService.Currencies.GetNumberFormatInfo("USD");
double step = Math.Pow(10, -(numberFormatInfo.CurrencyDecimalDigits));
return View(new ViewPointOfSaleViewModel()
@ -61,7 +73,7 @@ namespace BTCPayServer.Controllers
Prefixed = new[] { 0, 2 }.Contains(numberFormatInfo.CurrencyPositivePattern),
SymbolSpace = new[] { 2, 3 }.Contains(numberFormatInfo.CurrencyPositivePattern)
},
Items = _AppsHelper.Parse(settings.Template, settings.Currency),
Items = _AppService.Parse(settings.Template, settings.Currency),
ButtonText = settings.ButtonText,
CustomButtonText = settings.CustomButtonText,
CustomTipText = settings.CustomTipText,
@ -70,9 +82,129 @@ namespace BTCPayServer.Controllers
AppId = appId
});
}
[HttpGet]
[Route("/apps/{appId}/crowdfund")]
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
public async Task<IActionResult> ViewCrowdfund(string appId, string statusMessage)
{
var app = await _AppService.GetApp(appId, AppType.Crowdfund, true);
if (app == null)
return NotFound();
var settings = app.GetSettings<CrowdfundSettings>();
var isAdmin = await _AppService.GetAppDataIfOwner(GetUserId(), appId, AppType.Crowdfund) != null;
var hasEnoughSettingsToLoad = !string.IsNullOrEmpty(settings.TargetCurrency );
if (!hasEnoughSettingsToLoad)
{
if(!isAdmin)
return NotFound();
return NotFound("A Target Currency must be set for this app in order to be loadable.");
}
if (settings.Enabled) return View(await _AppService.GetAppInfo(appId));
if(!isAdmin)
return NotFound();
return View(await _AppService.GetAppInfo(appId));
}
[HttpPost]
[Route("/apps/{appId}/crowdfund")]
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
[IgnoreAntiforgeryToken]
[EnableCors(CorsPolicies.All)]
public async Task<IActionResult> ContributeToCrowdfund(string appId, ContributeToCrowdfund request)
{
var app = await _AppService.GetApp(appId, AppType.Crowdfund, true);
if (app == null)
return NotFound();
var settings = app.GetSettings<CrowdfundSettings>();
var isAdmin = await _AppService.GetAppDataIfOwner(GetUserId(), appId, AppType.Crowdfund) != null;
if (!settings.Enabled)
{
if (!isAdmin)
return NotFound("Crowdfund is not currently active");
}
var info = (ViewCrowdfundViewModel)await _AppService.GetAppInfo(appId);
if (!isAdmin &&
((settings.StartDate.HasValue && DateTime.Now < settings.StartDate) ||
(settings.EndDate.HasValue && DateTime.Now > settings.EndDate) ||
(settings.EnforceTargetAmount &&
(info.Info.PendingProgressPercentage.GetValueOrDefault(0) +
info.Info.ProgressPercentage.GetValueOrDefault(0)) >= 100)))
{
return NotFound("Crowdfund is not currently active");
}
var store = await _AppService.GetStore(app);
var title = settings.Title;
var price = request.Amount;
ViewPointOfSaleViewModel.Item choice = null;
if (!string.IsNullOrEmpty(request.ChoiceKey))
{
var choices = _AppService.Parse(settings.PerksTemplate, settings.TargetCurrency);
choice = choices.FirstOrDefault(c => c.Id == request.ChoiceKey);
if (choice == null)
return NotFound("Incorrect option provided");
title = choice.Title;
price = choice.Price.Value;
if (request.Amount > price)
price = request.Amount;
}
if (!isAdmin && (settings.EnforceTargetAmount && info.TargetAmount.HasValue && price >
(info.TargetAmount - (info.Info.CurrentAmount + info.Info.CurrentPendingAmount))))
{
return NotFound("Contribution Amount is more than is currently allowed.");
}
store.AdditionalClaims.Add(new Claim(Policies.CanCreateInvoice.Key, store.Id));
try
{
var invoice = await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest()
{
OrderId = AppService.GetCrowdfundOrderId(appId),
Currency = settings.TargetCurrency,
ItemCode = request.ChoiceKey ?? string.Empty,
ItemDesc = title,
BuyerEmail = request.Email,
Price = price,
NotificationURL = settings.NotificationUrl,
FullNotifications = true,
ExtendedNotifications = true,
RedirectURL = request.RedirectUrl ?? Request.GetDisplayUrl()
}, store, HttpContext.Request.GetAbsoluteRoot(), new List<string> { AppService.GetAppInternalTag(appId) });
if (request.RedirectToCheckout)
{
return RedirectToAction(nameof(InvoiceController.Checkout), "Invoice",
new {invoiceId = invoice.Data.Id});
}
else
{
return Ok(invoice.Data.Id);
}
}
catch (BitpayHttpException e)
{
return BadRequest(e.Message);
}
}
[HttpPost]
[Route("/apps/{appId}/pos")]
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
[IgnoreAntiforgeryToken]
[EnableCors(CorsPolicies.All)]
public async Task<IActionResult> ViewPointOfSale(string appId,
@ -81,9 +213,10 @@ namespace BTCPayServer.Controllers
string orderId,
string notificationUrl,
string redirectUrl,
string choiceKey)
string choiceKey,
string posData = null)
{
var app = await _AppsHelper.GetApp(appId, AppType.PointOfSale);
var app = await _AppService.GetApp(appId, AppType.PointOfSale);
if (string.IsNullOrEmpty(choiceKey) && amount <= 0)
{
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
@ -97,10 +230,11 @@ namespace BTCPayServer.Controllers
}
string title = null;
var price = 0.0m;
ViewPointOfSaleViewModel.Item choice = null;
if (!string.IsNullOrEmpty(choiceKey))
{
var choices = _AppsHelper.Parse(settings.Template, settings.Currency);
var choice = choices.FirstOrDefault(c => c.Id == choiceKey);
var choices = _AppService.Parse(settings.Template, settings.Currency);
choice = choices.FirstOrDefault(c => c.Id == choiceKey);
if (choice == null)
return NotFound();
title = choice.Title;
@ -115,136 +249,28 @@ namespace BTCPayServer.Controllers
price = amount;
title = settings.Title;
}
var store = await _AppsHelper.GetStore(app);
var store = await _AppService.GetStore(app);
store.AdditionalClaims.Add(new Claim(Policies.CanCreateInvoice.Key, store.Id));
var invoice = await _InvoiceController.CreateInvoiceCore(new NBitpayClient.Invoice()
var invoice = await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest()
{
ItemCode = choiceKey ?? string.Empty,
ItemCode = choice?.Id,
ItemDesc = title,
Currency = settings.Currency,
Price = price,
BuyerEmail = email,
OrderId = orderId,
NotificationURL = notificationUrl,
RedirectURL = redirectUrl,
FullNotifications = true
RedirectURL = redirectUrl ?? Request.GetDisplayUrl(),
FullNotifications = true,
PosData = string.IsNullOrEmpty(posData) ? null : posData
}, store, HttpContext.Request.GetAbsoluteRoot());
return RedirectToAction(nameof(InvoiceController.Checkout), "Invoice", new { invoiceId = invoice.Data.Id });
}
}
public class AppsHelper
{
ApplicationDbContextFactory _ContextFactory;
CurrencyNameTable _Currencies;
public CurrencyNameTable Currencies => _Currencies;
public AppsHelper(ApplicationDbContextFactory contextFactory, CurrencyNameTable currencies)
private string GetUserId()
{
_ContextFactory = contextFactory;
_Currencies = currencies;
}
public async Task<AppData> GetApp(string appId, AppType appType)
{
using (var ctx = _ContextFactory.CreateContext())
{
return await ctx.Apps
.Where(us => us.Id == appId &&
us.AppType == appType.ToString())
.FirstOrDefaultAsync();
}
}
public async Task<StoreData> GetStore(AppData app)
{
using (var ctx = _ContextFactory.CreateContext())
{
return await ctx.Stores.FirstOrDefaultAsync(s => s.Id == app.StoreDataId);
}
}
public ViewPointOfSaleViewModel.Item[] Parse(string template, string currency)
{
if (string.IsNullOrWhiteSpace(template))
return Array.Empty<ViewPointOfSaleViewModel.Item>();
var input = new StringReader(template);
YamlStream stream = new YamlStream();
stream.Load(input);
var root = (YamlMappingNode)stream.Documents[0].RootNode;
return root
.Children
.Select(kv => new PosHolder { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlMappingNode })
.Where(kv => kv.Value != null)
.Select(c => new ViewPointOfSaleViewModel.Item()
{
Description = c.GetDetailString("description"),
Id = c.Key,
Image = c.GetDetailString("image"),
Title = c.GetDetailString("title") ?? c.Key,
Price = c.GetDetail("price")
.Select(cc => new ViewPointOfSaleViewModel.Item.ItemPrice()
{
Value = decimal.Parse(cc.Value.Value, CultureInfo.InvariantCulture),
Formatted = FormatCurrency(cc.Value.Value, currency)
}).Single(),
Custom = c.GetDetailString("custom") == "true"
})
.ToArray();
}
private class PosHolder
{
public string Key { get; set; }
public YamlMappingNode Value { get; set; }
public IEnumerable<PosScalar> GetDetail(string field)
{
var res = Value.Children
.Where(kv => kv.Value != null)
.Select(kv => new PosScalar { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlScalarNode })
.Where(cc => cc.Key == field);
return res;
}
public string GetDetailString(string field)
{
return GetDetail(field).FirstOrDefault()?.Value?.Value;
}
}
private class PosScalar
{
public string Key { get; set; }
public YamlScalarNode Value { get; set; }
}
public string FormatCurrency(string price, string currency)
{
return decimal.Parse(price, CultureInfo.InvariantCulture).ToString("C", _Currencies.GetCurrencyProvider(currency));
}
public CurrencyData GetCurrencyData(string currency, bool useFallback)
{
return _Currencies.GetCurrencyData(currency, useFallback);
}
public async Task<AppData> GetAppDataIfOwner(string userId, string appId, AppType? type = null)
{
if (userId == null || appId == null)
return null;
using (var ctx = _ContextFactory.CreateContext())
{
var app = await ctx.UserStore
.Where(us => us.ApplicationUserId == userId && us.Role == StoreRoles.Owner)
.SelectMany(us => us.StoreData.Apps.Where(a => a.Id == appId))
.FirstOrDefaultAsync();
if (app == null)
return null;
if (type != null && type.Value.ToString() != app.AppType)
return null;
return app;
}
return _UserManager.GetUserId(User);
}
}
}

View File

@ -12,7 +12,6 @@ using NBitpayClient;
namespace BTCPayServer.Controllers
{
[EnableCors("BitpayAPI")]
[BitpayAPIConstraint]
[Authorize(Policies.CanCreateInvoice.Key, AuthenticationSchemes = Policies.BitpayAuthentication)]
public class InvoiceControllerAPI : Controller
@ -33,8 +32,10 @@ namespace BTCPayServer.Controllers
[HttpPost]
[Route("invoices")]
[MediaTypeConstraint("application/json")]
public async Task<DataWrapper<InvoiceResponse>> CreateInvoice([FromBody] Invoice invoice)
public async Task<DataWrapper<InvoiceResponse>> CreateInvoice([FromBody] CreateInvoiceRequest invoice)
{
if (invoice == null)
throw new BitpayHttpException(400, "Invalid invoice");
return await _InvoiceController.CreateInvoiceCore(invoice, HttpContext.GetStoreData(), HttpContext.Request.GetAbsoluteRoot());
}
@ -66,7 +67,7 @@ namespace BTCPayServer.Controllers
{
if (dateEnd != null)
dateEnd = dateEnd.Value + TimeSpan.FromDays(1); //Should include the end day
var query = new InvoiceQuery()
{
Count = limit,

View File

@ -21,6 +21,7 @@ using BTCPayServer.Services.Invoices.Export;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore.Internal;
using NBitcoin;
using NBitpayClient;
using NBXplorer;
@ -64,6 +65,7 @@ namespace BTCPayServer.Controllers
OrderId = invoice.OrderId,
BuyerInformation = invoice.BuyerInformation,
Fiat = _CurrencyNameTable.DisplayFormatCurrency(dto.Price, dto.Currency),
TaxIncluded = _CurrencyNameTable.DisplayFormatCurrency(invoice.ProductInformation.TaxIncluded, dto.Currency),
NotificationEmail = invoice.NotificationEmail,
NotificationUrl = invoice.NotificationURL,
RedirectUrl = invoice.RedirectURL,
@ -80,9 +82,9 @@ namespace BTCPayServer.Controllers
var paymentMethodId = data.GetId();
var cryptoPayment = new InvoiceDetailsModel.CryptoPayment();
cryptoPayment.PaymentMethod = ToString(paymentMethodId);
cryptoPayment.Due = $"{accounting.Due} {paymentMethodId.CryptoCode}";
cryptoPayment.Paid = $"{accounting.CryptoPaid} {paymentMethodId.CryptoCode}";
cryptoPayment.Overpaid = $"{accounting.OverpaidHelper} {paymentMethodId.CryptoCode}";
cryptoPayment.Due = _CurrencyNameTable.DisplayFormatCurrency(accounting.Due.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode);
cryptoPayment.Paid = _CurrencyNameTable.DisplayFormatCurrency(accounting.CryptoPaid.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode);
cryptoPayment.Overpaid = _CurrencyNameTable.DisplayFormatCurrency(accounting.OverpaidHelper.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode);
var onchainMethod = data.GetPaymentMethodDetails() as Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod;
if (onchainMethod != null)
@ -188,7 +190,7 @@ namespace BTCPayServer.Controllers
id = invoiceId;
////
var model = await GetInvoiceModel(invoiceId, paymentMethodId);
var model = await GetInvoiceModel(invoiceId, paymentMethodId == null ? null : PaymentMethodId.Parse(paymentMethodId));
if (model == null)
return NotFound();
@ -211,31 +213,29 @@ namespace BTCPayServer.Controllers
return View(nameof(Checkout), model);
}
private async Task<PaymentModel> GetInvoiceModel(string invoiceId, string paymentMethodIdStr)
private async Task<PaymentModel> GetInvoiceModel(string invoiceId, PaymentMethodId paymentMethodId)
{
var invoice = await _InvoiceRepository.GetInvoice(invoiceId);
if (invoice == null)
return null;
var store = await _StoreRepository.FindStore(invoice.StoreId);
bool isDefaultCrypto = false;
if (paymentMethodIdStr == null)
bool isDefaultPaymentId = false;
if (paymentMethodId == null)
{
paymentMethodIdStr = store.GetDefaultCrypto(_NetworkProvider);
isDefaultCrypto = true;
paymentMethodId = store.GetDefaultPaymentId(_NetworkProvider);
isDefaultPaymentId = true;
}
var paymentMethodId = PaymentMethodId.Parse(paymentMethodIdStr);
var network = _NetworkProvider.GetNetwork(paymentMethodId.CryptoCode);
if (network == null && isDefaultCrypto)
if (network == null && isDefaultPaymentId)
{
network = _NetworkProvider.GetAll().FirstOrDefault();
paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
paymentMethodIdStr = paymentMethodId.ToString();
}
if (invoice == null || network == null)
return null;
if (!invoice.Support(paymentMethodId))
{
if (!isDefaultCrypto)
if (!isDefaultPaymentId)
return null;
var paymentMethodTemp = invoice.GetPaymentMethods(_NetworkProvider)
.Where(c => paymentMethodId.CryptoCode == c.GetId().CryptoCode)
@ -244,7 +244,6 @@ namespace BTCPayServer.Controllers
paymentMethodTemp = invoice.GetPaymentMethods(_NetworkProvider).First();
network = paymentMethodTemp.Network;
paymentMethodId = paymentMethodTemp.GetId();
paymentMethodIdStr = paymentMethodId.ToString();
}
var paymentMethod = invoice.GetPaymentMethod(paymentMethodId, _NetworkProvider);
@ -278,7 +277,6 @@ namespace BTCPayServer.Controllers
PaymentMethodName = GetDisplayName(paymentMethodId, network),
CryptoImage = GetImage(paymentMethodId, network),
IsLightning = paymentMethodId.PaymentType == PaymentTypes.LightningLike,
ServerUrl = HttpContext.Request.GetAbsoluteRoot(),
OrderId = invoice.OrderId,
InvoiceId = invoice.Id,
DefaultLang = storeBlob.DefaultLang ?? "en",
@ -369,9 +367,12 @@ namespace BTCPayServer.Controllers
[HttpGet]
[Route("i/{invoiceId}/status")]
[Route("i/{invoiceId}/{paymentMethodId}/status")]
[Route("invoice/{invoiceId}/status")]
[Route("invoice/{invoiceId}/{paymentMethodId}/status")]
[Route("invoice/status")]
public async Task<IActionResult> GetStatus(string invoiceId, string paymentMethodId = null)
{
var model = await GetInvoiceModel(invoiceId, paymentMethodId);
var model = await GetInvoiceModel(invoiceId, paymentMethodId == null ? null : PaymentMethodId.Parse(paymentMethodId));
if (model == null)
return NotFound();
return Json(model);
@ -379,6 +380,10 @@ namespace BTCPayServer.Controllers
[HttpGet]
[Route("i/{invoiceId}/status/ws")]
[Route("i/{invoiceId}/{paymentMethodId}/status/ws")]
[Route("invoice/{invoiceId}/status/ws")]
[Route("invoice/{invoiceId}/{paymentMethodId}/status")]
[Route("invoice/status/ws")]
public async Task<IActionResult> GetStatusWebSocket(string invoiceId)
{
if (!HttpContext.WebSockets.IsWebSocketRequest)
@ -424,6 +429,7 @@ namespace BTCPayServer.Controllers
[HttpPost]
[Route("i/{invoiceId}/UpdateCustomer")]
[Route("invoice/UpdateCustomer")]
public async Task<IActionResult> UpdateCustomer(string invoiceId, [FromBody]UpdateCustomerModel data)
{
if (!ModelState.IsValid)
@ -447,8 +453,12 @@ namespace BTCPayServer.Controllers
Count = count,
StatusMessage = StatusMessage
};
var list = await ListInvoicesProcess(searchTerm, skip, count);
InvoiceQuery invoiceQuery = GetInvoiceQuery(searchTerm);
var counting = _InvoiceRepository.GetInvoicesTotal(invoiceQuery);
invoiceQuery.Count = count;
invoiceQuery.Skip = skip;
var list = await _InvoiceRepository.GetInvoices(invoiceQuery);
foreach (var invoice in list)
{
var state = invoice.GetInvoiceState();
@ -460,32 +470,32 @@ namespace BTCPayServer.Controllers
InvoiceId = invoice.Id,
OrderId = invoice.OrderId ?? string.Empty,
RedirectUrl = invoice.RedirectURL ?? string.Empty,
AmountCurrency = $"{invoice.ProductInformation.Price.ToString(CultureInfo.InvariantCulture)} {invoice.ProductInformation.Currency}",
AmountCurrency = _CurrencyNameTable.DisplayFormatCurrency(invoice.ProductInformation.Price, invoice.ProductInformation.Currency),
CanMarkInvalid = state.CanMarkInvalid(),
CanMarkComplete = state.CanMarkComplete()
});
}
model.Total = await counting;
return View(model);
}
private async Task<InvoiceEntity[]> ListInvoicesProcess(string searchTerm = null, int skip = 0, int count = 50)
private InvoiceQuery GetInvoiceQuery(string searchTerm = null)
{
var filterString = new SearchString(searchTerm);
var list = await _InvoiceRepository.GetInvoices(new InvoiceQuery()
var invoiceQuery = new InvoiceQuery()
{
TextSearch = filterString.TextSearch,
Count = count,
Skip = skip,
UserId = GetUserId(),
Unusual = !filterString.Filters.ContainsKey("unusual") ? null
: !bool.TryParse(filterString.Filters["unusual"].First(), out var r) ? (bool?)null
: r,
Status = filterString.Filters.ContainsKey("status") ? filterString.Filters["status"].ToArray() : null,
ExceptionStatus = filterString.Filters.ContainsKey("exceptionstatus") ? filterString.Filters["exceptionstatus"].ToArray() : null,
StoreId = filterString.Filters.ContainsKey("storeid") ? filterString.Filters["storeid"].ToArray() : null
});
return list;
StoreId = filterString.Filters.ContainsKey("storeid") ? filterString.Filters["storeid"].ToArray() : null,
ItemCode = filterString.Filters.ContainsKey("itemcode") ? filterString.Filters["itemcode"].ToArray() : null,
OrderId = filterString.Filters.ContainsKey("orderid") ? filterString.Filters["orderid"].ToArray() : null
};
return invoiceQuery;
}
[HttpGet]
@ -493,9 +503,12 @@ namespace BTCPayServer.Controllers
[BitpayAPIConstraint(false)]
public async Task<IActionResult> Export(string format, string searchTerm = null)
{
var model = new InvoiceExport(_NetworkProvider);
var model = new InvoiceExport(_NetworkProvider, _CurrencyNameTable);
var invoices = await ListInvoicesProcess(searchTerm, 0, int.MaxValue);
InvoiceQuery invoiceQuery = GetInvoiceQuery(searchTerm);
invoiceQuery.Count = int.MaxValue;
invoiceQuery.Skip = 0;
var invoices = await _InvoiceRepository.GetInvoices(invoiceQuery);
var res = model.Process(invoices, format);
var cd = new ContentDisposition
@ -565,7 +578,7 @@ namespace BTCPayServer.Controllers
try
{
var result = await CreateInvoiceCore(new Invoice()
var result = await CreateInvoiceCore(new CreateInvoiceRequest()
{
Price = model.Amount.Value,
Currency = model.Currency,
@ -647,13 +660,13 @@ namespace BTCPayServer.Controllers
if (newState == "invalid")
{
await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoiceId);
_EventAggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1008, InvoiceEvent.MarkedInvalid));
_EventAggregator.Publish(new InvoiceEvent(invoice, 1008, InvoiceEvent.MarkedInvalid));
StatusMessage = "Invoice marked invalid";
}
else if(newState == "complete")
{
await _InvoiceRepository.UpdatePaidInvoiceToComplete(invoiceId);
_EventAggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 2008, InvoiceEvent.MarkedCompleted));
_EventAggregator.Publish(new InvoiceEvent(invoice, 2008, InvoiceEvent.MarkedCompleted));
StatusMessage = "Invoice marked complete";
}
return RedirectToAction(nameof(ListInvoices));
@ -673,9 +686,9 @@ namespace BTCPayServer.Controllers
public class PosDataParser
{
public static Dictionary<string, string> ParsePosData(string posData)
public static Dictionary<string, object> ParsePosData(string posData)
{
var result = new Dictionary<string,string>();
var result = new Dictionary<string,object>();
if (string.IsNullOrEmpty(posData))
{
return result;
@ -683,7 +696,6 @@ namespace BTCPayServer.Controllers
try
{
var jObject =JObject.Parse(posData);
foreach (var item in jObject)
{
@ -691,7 +703,14 @@ namespace BTCPayServer.Controllers
switch (item.Value.Type)
{
case JTokenType.Array:
result.Add(item.Key, string.Join(',', item.Value.AsEnumerable()));
var items = item.Value.AsEnumerable().ToList();
for (var i = 0; i < items.Count(); i++)
{
result.Add($"{item.Key}[{i}]", ParsePosData(items[i].ToString()));
}
break;
case JTokenType.Object:
result.Add(item.Key, ParsePosData(item.Value.ToString()));
break;
default:
result.Add(item.Key, item.Value.ToString());

View File

@ -11,6 +11,7 @@ using BTCPayServer.Models;
using BTCPayServer.Payments;
using BTCPayServer.Rating;
using BTCPayServer.Security;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
@ -61,7 +62,7 @@ namespace BTCPayServer.Controllers
}
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl)
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(CreateInvoiceRequest invoice, StoreData store, string serverUrl, List<string> additionalTags = null)
{
if (!store.HasClaim(Policies.CanCreateInvoice.Key))
throw new UnauthorizedAccessException();
@ -72,10 +73,8 @@ namespace BTCPayServer.Controllers
InvoiceTime = DateTimeOffset.UtcNow
};
var getAppsTaggingStore = _InvoiceRepository.GetAppsTaggingStore(store.Id);
var storeBlob = store.GetStoreBlob();
Uri notificationUri = Uri.IsWellFormedUriString(invoice.NotificationURL, UriKind.Absolute) ? new Uri(invoice.NotificationURL, UriKind.Absolute) : null;
if (notificationUri == null || (notificationUri.Scheme != "http" && notificationUri.Scheme != "https")) //TODO: Filer non routable addresses ?
notificationUri = null;
EmailAddressAttribute emailValidator = new EmailAddressAttribute();
entity.ExpirationTime = entity.InvoiceTime.AddMinutes(storeBlob.InvoiceExpiration);
entity.MonitoringExpiration = entity.ExpirationTime + TimeSpan.FromMinutes(storeBlob.MonitoringExpiration);
@ -83,10 +82,17 @@ namespace BTCPayServer.Controllers
entity.ServerUrl = serverUrl;
entity.FullNotifications = invoice.FullNotifications || invoice.ExtendedNotifications;
entity.ExtendedNotifications = invoice.ExtendedNotifications;
entity.NotificationURL = notificationUri?.AbsoluteUri;
if (invoice.NotificationURL != null &&
Uri.TryCreate(invoice.NotificationURL, UriKind.Absolute, out var notificationUri) &&
(notificationUri.Scheme == "http" || notificationUri.Scheme == "https"))
{
entity.NotificationURL = notificationUri.AbsoluteUri;
}
entity.NotificationEmail = invoice.NotificationEmail;
entity.BuyerInformation = Map<Invoice, BuyerInformation>(invoice);
entity.BuyerInformation = Map<CreateInvoiceRequest, BuyerInformation>(invoice);
entity.PaymentTolerance = storeBlob.PaymentTolerance;
if (additionalTags != null)
entity.InternalTags.AddRange(additionalTags);
//Another way of passing buyer info to support
FillBuyerInfo(invoice.Buyer, entity.BuyerInformation);
if (entity?.BuyerInformation?.BuyerEmail != null)
@ -95,7 +101,22 @@ namespace BTCPayServer.Controllers
throw new BitpayHttpException(400, "Invalid email");
entity.RefundMail = entity.BuyerInformation.BuyerEmail;
}
entity.ProductInformation = Map<Invoice, ProductInformation>(invoice);
var taxIncluded = invoice.TaxIncluded.HasValue ? invoice.TaxIncluded.Value : 0m;
var currencyInfo = _CurrencyNameTable.GetNumberFormatInfo(invoice.Currency, false);
if (currencyInfo != null)
{
invoice.Price = Math.Round(invoice.Price, currencyInfo.CurrencyDecimalDigits);
invoice.TaxIncluded = Math.Round(taxIncluded, currencyInfo.CurrencyDecimalDigits);
}
invoice.Price = Math.Max(0.0m, invoice.Price);
invoice.TaxIncluded = Math.Max(0.0m, taxIncluded);
invoice.TaxIncluded = Math.Min(taxIncluded, invoice.Price);
entity.ProductInformation = Map<CreateInvoiceRequest, ProductInformation>(invoice);
entity.RedirectURL = invoice.RedirectURL ?? store.StoreWebsite;
if (!Uri.IsWellFormedUriString(entity.RedirectURL, UriKind.Absolute))
entity.RedirectURL = null;
@ -106,6 +127,17 @@ namespace BTCPayServer.Controllers
HashSet<CurrencyPair> currencyPairsToFetch = new HashSet<CurrencyPair>();
var rules = storeBlob.GetRateRules(_NetworkProvider);
var excludeFilter = storeBlob.GetExcludedPaymentMethods(); // Here we can compose filters from other origin with PaymentFilter.Any()
if (invoice.SupportedTransactionCurrencies != null && invoice.SupportedTransactionCurrencies.Count != 0)
{
var supportedTransactionCurrencies = invoice.SupportedTransactionCurrencies
.Where(c => c.Value.Enabled)
.Select(c => PaymentMethodId.TryParse(c.Key, out var p) ? p : null)
.ToHashSet();
excludeFilter = PaymentFilter.Or(excludeFilter,
PaymentFilter.Where(p => !supportedTransactionCurrencies.Contains(p)));
}
foreach (var network in store.GetSupportedPaymentMethods(_NetworkProvider)
.Where(s => !excludeFilter.Match(s.PaymentId))
.Select(c => _NetworkProvider.GetNetwork(c.PaymentId.CryptoCode))
@ -147,7 +179,7 @@ namespace BTCPayServer.Controllers
if (supported.Count == 0)
{
StringBuilder errors = new StringBuilder();
errors.AppendLine("No payment method available for this store");
errors.AppendLine("Warning: No wallet has been linked to your BTCPay Store. See the following link for more information on how to connect your store and wallet. (https://docs.btcpayserver.org/btcpay-basics/gettingstarted#connecting-btcpay-store-to-your-wallet)");
foreach (var error in logs.ToList())
{
errors.AppendLine(error.ToString());
@ -158,9 +190,15 @@ namespace BTCPayServer.Controllers
entity.SetSupportedPaymentMethods(supported);
entity.SetPaymentMethods(paymentMethods);
entity.PosData = invoice.PosData;
foreach (var app in await getAppsTaggingStore)
{
entity.InternalTags.Add(AppService.GetAppInternalTag(app.Id));
}
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, logs, _NetworkProvider);
await fetchingAll;
_EventAggregator.Publish(new Events.InvoiceEvent(entity.EntityToDTO(_NetworkProvider), 1001, InvoiceEvent.Created));
_EventAggregator.Publish(new Events.InvoiceEvent(entity, 1001, InvoiceEvent.Created));
var resp = entity.EntityToDTO(_NetworkProvider);
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
}

View File

@ -31,7 +31,7 @@ namespace BTCPayServer.Controllers
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly IEmailSender _emailSender;
private readonly EmailSenderFactory _EmailSenderFactory;
private readonly ILogger _logger;
private readonly UrlEncoder _urlEncoder;
TokenRepository _TokenRepository;
@ -44,7 +44,7 @@ namespace BTCPayServer.Controllers
public ManageController(
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
IEmailSender emailSender,
EmailSenderFactory emailSenderFactory,
ILogger<ManageController> logger,
UrlEncoder urlEncoder,
TokenRepository tokenRepository,
@ -54,7 +54,7 @@ namespace BTCPayServer.Controllers
{
_userManager = userManager;
_signInManager = signInManager;
_emailSender = emailSender;
_EmailSenderFactory = emailSenderFactory;
_logger = logger;
_urlEncoder = urlEncoder;
_TokenRepository = tokenRepository;
@ -156,8 +156,7 @@ namespace BTCPayServer.Controllers
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme);
var email = user.Email;
await _emailSender.SendEmailConfirmationAsync(email, callbackUrl);
_EmailSenderFactory.GetEmailSender().SendEmailConfirmation(email, callbackUrl);
StatusMessage = "Verification email sent. Please check your email.";
return RedirectToAction(nameof(Index));
}

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Filters;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Cors;
@ -45,7 +46,7 @@ namespace BTCPayServer.Controllers
if (!ModelState.IsValid)
return View();
var invoice = await _InvoiceController.CreateInvoiceCore(new NBitpayClient.Invoice()
var invoice = await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest()
{
Price = model.Price,
Currency = model.Currency,

View File

@ -2,6 +2,7 @@
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Filters;
using BTCPayServer.Lightning;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
@ -30,6 +31,7 @@ namespace BTCPayServer.Controllers
}
[HttpGet]
[XFrameOptions(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
public async Task<IActionResult> ShowLightningNodeInfo(string storeId, string cryptoCode)
{
var store = await _StoreRepository.FindStore(storeId);

View File

@ -12,11 +12,13 @@ using BTCPayServer.Rating;
using Newtonsoft.Json;
using Microsoft.AspNetCore.Authorization;
using BTCPayServer.Authentication;
using Microsoft.AspNetCore.Cors;
namespace BTCPayServer.Controllers
{
[Authorize(AuthenticationSchemes = Security.Policies.BitpayAuthentication)]
[AllowAnonymous]
[EnableCors(CorsPolicies.All)]
public class RateController : Controller
{
RateFetcher _RateProviderFactory;
@ -139,9 +141,9 @@ namespace BTCPayServer.Controllers
{
var supportedMethods = store.GetSupportedPaymentMethods(_NetworkProvider);
var currencyCodes = supportedMethods.Select(method => method.PaymentId.CryptoCode).Distinct();
var defaultCrypto = store.GetDefaultCrypto(_NetworkProvider);
var defaultPaymentId = store.GetDefaultPaymentId(_NetworkProvider);
currencyPairs = BuildCurrencyPairs(currencyCodes, defaultCrypto);
currencyPairs = BuildCurrencyPairs(currencyCodes, defaultPaymentId.CryptoCode);
if (string.IsNullOrEmpty(currencyPairs))
{

View File

@ -27,6 +27,7 @@ using Renci.SshNet;
using BTCPayServer.Logging;
using BTCPayServer.Lightning;
using BTCPayServer.Configuration.External;
using System.Runtime.CompilerServices;
namespace BTCPayServer.Controllers
{
@ -170,7 +171,7 @@ namespace BTCPayServer.Controllers
vm.DNSDomain = null;
return View(vm);
}
[Route("server/maintenance")]
[HttpPost]
public async Task<IActionResult> Maintenance(MaintenanceViewModel vm, string command)
@ -208,8 +209,8 @@ namespace BTCPayServer.Controllers
{
builder.Scheme = this.Request.Scheme;
builder.Host = vm.DNSDomain;
var addresses1 = Dns.GetHostAddressesAsync(this.Request.Host.Host);
var addresses2 = Dns.GetHostAddressesAsync(vm.DNSDomain);
var addresses1 = GetAddressAsync(this.Request.Host.Host);
var addresses2 = GetAddressAsync(vm.DNSDomain);
await Task.WhenAll(addresses1, addresses2);
var addressesSet = addresses1.GetAwaiter().GetResult().Select(c => c.ToString()).ToHashSet();
@ -253,6 +254,13 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(Maintenance));
}
private Task<IPAddress[]> GetAddressAsync(string domainOrIP)
{
if (IPAddress.TryParse(domainOrIP, out var ip))
return Task.FromResult(new[] { ip });
return Dns.GetHostAddressesAsync(domainOrIP);
}
public static string RunId = Encoders.Hex.EncodeData(NBitcoin.RandomUtils.GetBytes(32));
[HttpGet]
[Route("runid")]
@ -345,22 +353,27 @@ namespace BTCPayServer.Controllers
var user = await _UserManager.FindByIdAsync(userId);
if (user == null)
return NotFound();
var roles = await _UserManager.GetRolesAsync(user);
var isAdmin = IsAdmin(roles);
bool updated = false;
if (isAdmin != viewModel.IsAdmin)
viewModel.StatusMessage = "";
var admins = await _UserManager.GetUsersInRoleAsync(Roles.ServerAdmin);
if (!viewModel.IsAdmin && admins.Count == 1)
{
viewModel.StatusMessage = "This is the only Admin, so their role can't be removed until another Admin is added.";
return View(viewModel); // return
}
var roles = await _UserManager.GetRolesAsync(user);
if (viewModel.IsAdmin != IsAdmin(roles))
{
if (viewModel.IsAdmin)
await _UserManager.AddToRoleAsync(user, Roles.ServerAdmin);
else
await _UserManager.RemoveFromRoleAsync(user, Roles.ServerAdmin);
updated = true;
}
if (updated)
{
viewModel.StatusMessage = "User successfully updated";
}
return View(viewModel);
}
@ -371,12 +384,29 @@ namespace BTCPayServer.Controllers
var user = userId == null ? null : await _UserManager.FindByIdAsync(userId);
if (user == null)
return NotFound();
return View("Confirm", new ConfirmModel()
var admins = await _UserManager.GetUsersInRoleAsync(Roles.ServerAdmin);
if (admins.Count == 1)
{
Title = "Delete user " + user.Email,
Description = "This user will be permanently deleted",
Action = "Delete"
});
// return
return View("Confirm", new ConfirmModel("Unable to Delete Last Admin",
"This is the last Admin, so it can't be removed"));
}
var roles = await _UserManager.GetRolesAsync(user);
if (IsAdmin(roles))
{
return View("Confirm", new ConfirmModel("Delete Admin " + user.Email,
"Are you sure you want to delete this Admin and delete all accounts, users and data associated with the server account?",
"Delete"));
}
else
{
return View("Confirm", new ConfirmModel("Delete user " + user.Email,
"This user will be permanently deleted",
"Delete"));
}
}
[Route("server/users/{userId}/delete")]
@ -445,7 +475,17 @@ namespace BTCPayServer.Controllers
{
Crypto = cryptoCode,
Type = "Spark server",
Action = nameof(SparkServices),
Action = nameof(SparkService),
Index = i++,
});
}
foreach (var rtlService in _Options.ExternalServicesByCryptoCode.GetServices<ExternalRTL>(cryptoCode))
{
result.LNDServices.Add(new ServicesViewModel.LNDServiceViewModel()
{
Crypto = cryptoCode,
Type = "Ride the Lightning server (RTL)",
Action = nameof(RTLService),
Index = i++,
});
}
@ -460,15 +500,15 @@ namespace BTCPayServer.Controllers
});
}
}
foreach(var externalService in _Options.ExternalServices)
foreach (var externalService in _Options.ExternalServices)
{
result.ExternalServices.Add(new ServicesViewModel.ExternalService()
{
Name = externalService.Key,
Link = this.Request.GetRelativePath(externalService.Value)
Link = this.Request.GetRelativePathOrAbsolute(externalService.Value)
});
}
if(_Options.SSHSettings != null)
if (_Options.SSHSettings != null)
{
result.ExternalServices.Add(new ServicesViewModel.ExternalService()
{
@ -519,37 +559,41 @@ namespace BTCPayServer.Controllers
}
[Route("server/services/spark/{cryptoCode}/{index}")]
public async Task<IActionResult> SparkServices(string cryptoCode, int index, bool showQR = false)
public async Task<IActionResult> SparkService(string cryptoCode, int index, bool showQR = false)
{
return await LightningWalletServicesCore<ExternalSpark>(cryptoCode, showQR, "Spark Wallet");
}
[Route("server/services/rtl/{cryptoCode}/{index}")]
public async Task<IActionResult> RTLService(string cryptoCode, int index, bool showQR = false)
{
return await LightningWalletServicesCore<ExternalRTL>(cryptoCode, showQR, "Ride the Lightning Wallet");
}
private async Task<IActionResult> LightningWalletServicesCore<T>(string cryptoCode, bool showQR, string walletName) where T : ExternalService, IAccessKeyService
{
if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
{
StatusMessage = $"Error: {cryptoCode} is not fully synched";
return RedirectToAction(nameof(Services));
}
var spark = _Options.ExternalServicesByCryptoCode.GetServices<ExternalSpark>(cryptoCode).Select(c => c.ConnectionString).FirstOrDefault();
if(spark == null)
var external = _Options.ExternalServicesByCryptoCode.GetServices<T>(cryptoCode).Where(c => c?.ConnectionString?.Server != null).FirstOrDefault();
if (external == null)
{
return NotFound();
}
SparkServicesViewModel vm = new SparkServicesViewModel();
LightningWalletServices vm = new LightningWalletServices();
vm.ShowQR = showQR;
vm.WalletName = walletName;
try
{
var cookie = (spark.CookeFile == "fake"
? "fake:fake:fake" // If we are testing, it should not crash
: await System.IO.File.ReadAllTextAsync(spark.CookeFile)).Split(':');
if (cookie.Length >= 3)
{
vm.SparkLink = $"{spark.Server.AbsoluteUri}?access-key={cookie[2]}";
}
vm.ServiceLink = $"{external.ConnectionString.Server.AbsoluteUri}?access-key={await external.ExtractAccessKey()}";
}
catch(Exception ex)
catch (Exception ex)
{
StatusMessage = $"Error: {ex.Message}";
return RedirectToAction(nameof(Services));
}
return View(vm);
return View("LightningWalletServices", vm);
}
[Route("server/services/lnd/{cryptoCode}/{index}")]
@ -571,7 +615,7 @@ namespace BTCPayServer.Controllers
model.ConnectionType = "GRPC";
model.GRPCSSLCipherSuites = "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256";
}
else if(external.ConnectionType == LightningConnectionType.LndREST)
else if (external.ConnectionType == LightningConnectionType.LndREST)
{
model.Uri = external.BaseUri.AbsoluteUri;
model.ConnectionType = "REST";
@ -785,7 +829,8 @@ namespace BTCPayServer.Controllers
.ToList();
vm.LogFileOffset = offset;
if (string.IsNullOrEmpty(file)) return View("Logs", vm);
if (string.IsNullOrEmpty(file))
return View("Logs", vm);
vm.Log = "";
var path = Path.Combine(di.FullName, file);
try

View File

@ -0,0 +1,65 @@
using System;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Models.ServerViewModels;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments.Changelly;
using BTCPayServer.Services.Mails;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers
{
public partial class StoresController
{
[Route("{storeId}/emails")]
public IActionResult Emails()
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
var data = store.GetStoreBlob().EmailSettings ?? new EmailSettings();
return View(new EmailsViewModel() { Settings = data });
}
[Route("{storeId}/emails")]
[HttpPost]
public async Task<IActionResult> Emails(string storeId, EmailsViewModel model, string command)
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
if (command == "Test")
{
try
{
if (!model.Settings.IsComplete())
{
model.StatusMessage = "Error: Required fields missing";
return View(model);
}
var client = model.Settings.CreateSmtpClient();
await client.SendMailAsync(model.Settings.From, model.TestEmail, "BTCPay test", "BTCPay test");
model.StatusMessage = "Email sent to " + model.TestEmail + ", please, verify you received it";
}
catch (Exception ex)
{
model.StatusMessage = "Error: " + ex.Message;
}
return View(model);
}
else // if(command == "Save")
{
var storeBlob = store.GetStoreBlob();
storeBlob.EmailSettings = model.Settings;
store.SetStoreBlob(storeBlob);
await _Repo.UpdateStore(store);
StatusMessage = "Email settings modified";
return RedirectToAction(nameof(UpdateStore), new {
storeId});
}
}
}
}

View File

@ -96,6 +96,11 @@ namespace BTCPayServer.Controllers
{
get; set;
}
[TempData]
public bool StoreNotConfigured
{
get; set;
}
[HttpGet]
[Route("{storeId}/users")]
@ -167,7 +172,7 @@ namespace BTCPayServer.Controllers
return View("Confirm", new ConfirmModel()
{
Title = $"Remove store user",
Description = $"Are you sure to remove access to remove access to {user.Email}?",
Description = $"Are you sure you want to remove store access for {user.Email}?",
Action = "Delete"
});
}
@ -326,7 +331,7 @@ namespace BTCPayServer.Controllers
{
var storeBlob = StoreData.GetStoreBlob();
var vm = new CheckoutExperienceViewModel();
vm.SetCryptoCurrencies(_ExplorerProvider, StoreData.GetDefaultCrypto(_NetworkProvider));
SetCryptoCurrencies(vm, StoreData);
vm.SetLanguages(_LangService, storeBlob.DefaultLang);
vm.LightningMaxValue = storeBlob.LightningMaxValue?.ToString() ?? "";
vm.OnChainMinValue = storeBlob.OnChainMinValue?.ToString() ?? "";
@ -336,6 +341,23 @@ namespace BTCPayServer.Controllers
vm.HtmlTitle = storeBlob.HtmlTitle;
return View(vm);
}
void SetCryptoCurrencies(CheckoutExperienceViewModel vm, Data.StoreData storeData)
{
var choices = storeData.GetEnabledPaymentIds(_NetworkProvider)
.Select(o => new CheckoutExperienceViewModel.Format() { Name = GetDisplayName(o), Value = o.ToString(), PaymentId = o }).ToArray();
var defaultPaymentId = storeData.GetDefaultPaymentId(_NetworkProvider);
var chosen = choices.FirstOrDefault(c => c.PaymentId == defaultPaymentId);
vm.CryptoCurrencies = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen?.Value);
vm.DefaultPaymentMethod = chosen?.Value;
}
private string GetDisplayName(PaymentMethodId paymentMethodId)
{
var display = _NetworkProvider.GetNetwork(paymentMethodId.CryptoCode)?.DisplayName ?? paymentMethodId.CryptoCode;
return paymentMethodId.PaymentType == PaymentTypes.BTCLike ?
display : $"{display} (Lightning)";
}
[HttpPost]
[Route("{storeId}/checkout")]
@ -360,12 +382,13 @@ namespace BTCPayServer.Controllers
}
bool needUpdate = false;
var blob = StoreData.GetStoreBlob();
if (StoreData.GetDefaultCrypto(_NetworkProvider) != model.DefaultCryptoCurrency)
var defaultPaymentMethodId = model.DefaultPaymentMethod == null ? null : PaymentMethodId.Parse(model.DefaultPaymentMethod);
if (StoreData.GetDefaultPaymentId(_NetworkProvider) != defaultPaymentMethodId)
{
needUpdate = true;
StoreData.SetDefaultCrypto(model.DefaultCryptoCurrency);
StoreData.SetDefaultPaymentId(defaultPaymentMethodId);
}
model.SetCryptoCurrencies(_ExplorerProvider, model.DefaultCryptoCurrency);
SetCryptoCurrencies(model, StoreData);
model.SetLanguages(_LangService, model.DefaultLang);
if (!ModelState.IsValid)
@ -567,6 +590,7 @@ namespace BTCPayServer.Controllers
var model = new TokensViewModel();
var tokens = await _TokenRepository.GetTokensByStoreIdAsync(StoreData.Id);
model.StatusMessage = StatusMessage;
model.StoreNotConfigured = StoreNotConfigured;
model.Tokens = tokens.Select(t => new TokenViewModel()
{
Facade = t.Facade,
@ -794,6 +818,10 @@ namespace BTCPayServer.Controllers
var pairingResult = await _TokenRepository.PairWithStoreAsync(pairingCode, store.Id);
if (pairingResult == PairingResult.Complete || pairingResult == PairingResult.Partial)
{
var excludeFilter = store.GetStoreBlob().GetExcludedPaymentMethods();
StoreNotConfigured = store.GetSupportedPaymentMethods(_NetworkProvider)
.Where(p => !excludeFilter.Match(p.PaymentId))
.Count() == 0;
StatusMessage = "Pairing is successful";
if (pairingResult == PairingResult.Partial)
StatusMessage = "Server initiated pairing code: " + pairingCode;

View File

@ -146,7 +146,7 @@ namespace BTCPayServer.Controllers
[Route("{walletId}/send")]
public async Task<IActionResult> WalletSend(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, string defaultDestination = null, string defaultAmount = null)
WalletId walletId, string defaultDestination = null, string defaultAmount = null, bool advancedMode = false)
{
if (walletId?.StoreId == null)
return NotFound();
@ -195,6 +195,7 @@ namespace BTCPayServer.Controllers
}
catch (Exception ex) { model.RateError = ex.Message; }
}
model.AdvancedMode = advancedMode;
return View(model);
}
@ -202,7 +203,7 @@ namespace BTCPayServer.Controllers
[Route("{walletId}/send")]
public async Task<IActionResult> WalletSend(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, WalletSendModel vm)
WalletId walletId, WalletSendModel vm, string command = null)
{
if (walletId?.StoreId == null)
return NotFound();
@ -212,6 +213,14 @@ namespace BTCPayServer.Controllers
var network = this.NetworkProvider.GetNetwork(walletId?.CryptoCode);
if (network == null)
return NotFound();
if (command == "noob" || command == "expert")
{
ModelState.Clear();
vm.AdvancedMode = command == "expert";
return View(vm);
}
var destination = ParseDestination(vm.Destination, network.NBitcoinNetwork);
if (destination == null)
ModelState.AddModelError(nameof(vm.Destination), "Invalid address");
@ -231,7 +240,8 @@ namespace BTCPayServer.Controllers
Destination = vm.Destination,
Amount = vm.Amount.Value,
SubstractFees = vm.SubstractFees,
FeeSatoshiPerByte = vm.FeeSatoshiPerByte
FeeSatoshiPerByte = vm.FeeSatoshiPerByte,
NoChange = vm.NoChange
});
}
@ -403,6 +413,7 @@ namespace BTCPayServer.Controllers
// getxpub
int account = 0,
// sendtoaddress
bool noChange = false,
string destination = null, string amount = null, string feeRate = null, string substractFees = null
)
{
@ -436,7 +447,7 @@ namespace BTCPayServer.Controllers
{
try
{
destinationAddress = BitcoinAddress.Create(destination, network.NBitcoinNetwork);
destinationAddress = BitcoinAddress.Create(destination.Trim(), network.NBitcoinNetwork);
}
catch { }
if (destinationAddress == null)
@ -487,24 +498,16 @@ namespace BTCPayServer.Controllers
var strategy = GetDirectDerivationStrategy(derivationScheme);
var wallet = _walletProvider.GetWallet(network);
var change = wallet.GetChangeAddressAsync(derivationScheme);
var unspentCoins = await wallet.GetUnspentCoins(derivationScheme);
var changeAddress = await change;
var send = new[] { (
destination: destinationAddress as IDestination,
amount: amountBTC,
substractFees: subsctractFeesValue) };
foreach (var element in send)
var keypaths = new Dictionary<Script, KeyPath>();
List<Coin> availableCoins = new List<Coin>();
foreach (var c in await wallet.GetUnspentCoins(derivationScheme))
{
if (element.destination == null)
throw new ArgumentNullException(nameof(element.destination));
if (element.amount == null)
throw new ArgumentNullException(nameof(element.amount));
if (element.amount <= Money.Zero)
throw new ArgumentOutOfRangeException(nameof(element.amount), "The amount should be above zero");
keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath);
availableCoins.Add(c.Coin);
}
var changeAddress = await change;
var storeBlob = storeData.GetStoreBlob();
var paymentId = new Payments.PaymentMethodId(cryptoCode, Payments.PaymentTypes.BTCLike);
var foundKeyPath = storeBlob.GetWalletKeyPathRoot(paymentId);
@ -520,10 +523,25 @@ namespace BTCPayServer.Controllers
storeData.SetStoreBlob(storeBlob);
await Repository.UpdateStore(storeData);
}
retry:
var send = new[] { (
destination: destinationAddress as IDestination,
amount: amountBTC,
substractFees: subsctractFeesValue) };
foreach (var element in send)
{
if (element.destination == null)
throw new ArgumentNullException(nameof(element.destination));
if (element.amount == null)
throw new ArgumentNullException(nameof(element.amount));
if (element.amount <= Money.Zero)
throw new ArgumentOutOfRangeException(nameof(element.amount), "The amount should be above zero");
}
TransactionBuilder builder = network.NBitcoinNetwork.CreateTransactionBuilder();
builder.StandardTransactionPolicy.MinRelayTxFee = summary.Status.BitcoinStatus.MinRelayTxFee;
builder.AddCoins(unspentCoins.Select(c => c.Coin).ToArray());
builder.AddCoins(availableCoins);
foreach (var element in send)
{
@ -531,6 +549,7 @@ namespace BTCPayServer.Controllers
if (element.substractFees)
builder.SubtractFees();
}
builder.SetChange(changeAddress.Item1);
if (network.MinFee == null)
@ -547,13 +566,15 @@ namespace BTCPayServer.Controllers
}
var unsigned = builder.BuildTransaction(false);
var keypaths = new Dictionary<Script, KeyPath>();
foreach (var c in unspentCoins)
var hasChange = unsigned.Outputs.Any(o => o.ScriptPubKey == changeAddress.Item1.ScriptPubKey);
if (noChange && hasChange)
{
keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath);
availableCoins = builder.FindSpentCoins(unsigned).Cast<Coin>().ToList();
amountBTC = builder.FindSpentCoins(unsigned).Select(c => c.TxOut.Value).Sum();
subsctractFeesValue = true;
goto retry;
}
var hasChange = unsigned.Outputs.Count == 2;
var usedCoins = builder.FindSpentCoins(unsigned);
Dictionary<uint256, Transaction> parentTransactions = new Dictionary<uint256, Transaction>();

View File

@ -23,6 +23,7 @@ namespace BTCPayServer.Data
{
get; set;
}
public bool TagAllInvoices { get; set; }
public string Settings { get; set; }
public T GetSettings<T>() where T : class, new()

View File

@ -3,8 +3,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Hangfire;
using Hangfire.MemoryStorage;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations;
using JetBrains.Annotations;
@ -98,15 +96,5 @@ namespace BTCPayServer.Data
else if (_Type == DatabaseType.MySQL)
builder.UseMySql(_ConnectionString);
}
public void ConfigureHangfireBuilder(IGlobalConfiguration builder)
{
builder.UseMemoryStorage();
//We always use memory storage because of incompatibilities with the latest postgres in 2.1
//if (_Type == DatabaseType.Sqlite)
// builder.UseMemoryStorage(); //Sqlite provider does not support multiple workers
//else if (_Type == DatabaseType.Postgres)
// builder.UsePostgreSqlStorage(_ConnectionString);
}
}
}

View File

@ -21,6 +21,7 @@ using BTCPayServer.Payments.Changelly;
using BTCPayServer.Payments.CoinSwitch;
using BTCPayServer.Security;
using BTCPayServer.Rating;
using BTCPayServer.Services.Mails;
namespace BTCPayServer.Data
{
@ -55,7 +56,6 @@ namespace BTCPayServer.Data
get;
set;
}
public IEnumerable<ISupportedPaymentMethod> GetSupportedPaymentMethods(BTCPayNetworkProvider networks)
{
#pragma warning disable CS0618
@ -194,7 +194,7 @@ namespace BTCPayServer.Data
get;
set;
}
[Obsolete("Use GetDefaultCrypto instead")]
[Obsolete("Use GetDefaultPaymentId instead")]
public string DefaultCrypto { get; set; }
public List<PairedSINData> PairedSINs { get; set; }
public IEnumerable<APIKeyData> APIKeys { get; set; }
@ -203,13 +203,32 @@ namespace BTCPayServer.Data
public List<Claim> AdditionalClaims { get; set; } = new List<Claim>();
#pragma warning disable CS0618
public string GetDefaultCrypto(BTCPayNetworkProvider networkProvider = null)
public PaymentMethodId GetDefaultPaymentId(BTCPayNetworkProvider networks)
{
return DefaultCrypto ?? (networkProvider == null ? "BTC" : GetSupportedPaymentMethods(networkProvider).Select(p => p.PaymentId.CryptoCode).FirstOrDefault() ?? "BTC");
PaymentMethodId[] paymentMethodIds = GetEnabledPaymentIds(networks);
var defaultPaymentId = string.IsNullOrEmpty(DefaultCrypto) ? null : PaymentMethodId.Parse(DefaultCrypto);
var chosen = paymentMethodIds.FirstOrDefault(f => f == defaultPaymentId) ??
paymentMethodIds.FirstOrDefault(f => f.CryptoCode == defaultPaymentId?.CryptoCode) ??
paymentMethodIds.FirstOrDefault();
return chosen;
}
public void SetDefaultCrypto(string defaultCryptoCurrency)
public PaymentMethodId[] GetEnabledPaymentIds(BTCPayNetworkProvider networks)
{
DefaultCrypto = defaultCryptoCurrency;
var excludeFilter = GetStoreBlob().GetExcludedPaymentMethods();
var paymentMethodIds = GetSupportedPaymentMethods(networks).Select(p => p.PaymentId)
.Where(a => !excludeFilter.Match(a))
.OrderByDescending(a => a.CryptoCode == "BTC")
.ThenBy(a => a.CryptoCode)
.ThenBy(a => a.PaymentType == PaymentTypes.LightningLike ? 1 : 0)
.ToArray();
return paymentMethodIds;
}
public void SetDefaultPaymentId(PaymentMethodId defaultPaymentId)
{
DefaultCrypto = defaultPaymentId.ToString();
}
#pragma warning restore CS0618
@ -403,6 +422,8 @@ namespace BTCPayServer.Data
[Obsolete("Use SetWalletKeyPathRoot/GetWalletKeyPathRoot instead")]
public Dictionary<string, string> WalletKeyPathRoots { get; set; } = new Dictionary<string, string>();
public EmailSettings EmailSettings { get; set; }
public IPaymentFilter GetExcludedPaymentMethods()
{
#pragma warning disable CS0618 // Type or member is obsolete

View File

@ -20,14 +20,14 @@ namespace BTCPayServer.Events
public const string Confirmed= "invoice_confirmed";
public const string Completed= "invoice_completed";
public InvoiceEvent(Models.InvoiceResponse invoice, int code, string name)
public InvoiceEvent(InvoiceEntity invoice, int code, string name)
{
Invoice = invoice;
EventCode = code;
Name = name;
}
public Models.InvoiceResponse Invoice { get; set; }
public InvoiceEntity Invoice { get; set; }
public int EventCode { get; set; }
public string Name { get; set; }

View File

@ -10,9 +10,9 @@ namespace BTCPayServer.Services
{
public static class EmailSenderExtensions
{
public static Task SendEmailConfirmationAsync(this IEmailSender emailSender, string email, string link)
public static void SendEmailConfirmation(this IEmailSender emailSender, string email, string link)
{
return emailSender.SendEmailAsync(email, "Confirm your email",
emailSender.SendEmail(email, "Confirm your email",
$"Please confirm your account by clicking this link: <a href='{HtmlEncoder.Default.Encode(link)}'>link</a>");
}
}

View File

@ -12,13 +12,32 @@ namespace BTCPayServer.Filters
{
Value = value;
}
public string Value
public XFrameOptionsAttribute(XFrameOptions type, string allowFrom = null)
{
get; set;
switch (type)
{
case XFrameOptions.Deny:
Value = "deny";
break;
case XFrameOptions.SameOrigin:
Value = "deny";
break;
case XFrameOptions.AllowFrom:
Value = $"allow-from {allowFrom}";
break;
case XFrameOptions.AllowAll:
Value = "allow-all";
break;
default:
throw new ArgumentOutOfRangeException(nameof(type), type, null);
}
}
public string Value { get; set; }
public void OnActionExecuted(ActionExecutedContext context)
{
}
public void OnActionExecuting(ActionExecutingContext context)
@ -28,5 +47,13 @@ namespace BTCPayServer.Filters
context.HttpContext.Response.SetHeaderOnStarting("X-Frame-Options", Value);
}
}
public enum XFrameOptions
{
Deny,
SameOrigin,
AllowFrom,
AllowAll
}
}
}

View File

@ -0,0 +1,133 @@
using System;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using BTCPayServer.Logging;
using BTCPayServer.Services;
using Microsoft.Extensions.Hosting;
using NicolasDorier.RateLimits;
namespace BTCPayServer.HostedServices
{
public class BackgroundJobSchedulerHostedService : IHostedService
{
public BackgroundJobSchedulerHostedService(IBackgroundJobClient backgroundJobClient)
{
BackgroundJobClient = (BackgroundJobClient)backgroundJobClient;
}
public BackgroundJobClient BackgroundJobClient { get; }
Task _Loop;
public Task StartAsync(CancellationToken cancellationToken)
{
_Stop = new CancellationTokenSource();
_Loop = BackgroundJobClient.ProcessJobs(_Stop.Token);
return Task.CompletedTask;
}
CancellationTokenSource _Stop;
public async Task StopAsync(CancellationToken cancellationToken)
{
_Stop.Cancel();
try
{
await _Loop;
}
catch (OperationCanceledException)
{
}
await BackgroundJobClient.WaitAllRunning(cancellationToken);
}
}
public class BackgroundJobClient : IBackgroundJobClient
{
class BackgroundJob
{
public Func<Task> Action;
public TimeSpan Delay;
public IDelay DelayImplementation;
public BackgroundJob(Func<Task> action, TimeSpan delay, IDelay delayImplementation)
{
this.Action = action;
this.Delay = delay;
this.DelayImplementation = delayImplementation;
}
public async Task Run(CancellationToken cancellationToken)
{
await DelayImplementation.Wait(Delay, cancellationToken);
await Action();
}
}
public IDelay Delay { get; set; } = TaskDelay.Instance;
public int GetExecutingCount()
{
lock (_Processing)
{
return _Processing.Count();
}
}
private Channel<BackgroundJob> _Jobs = Channel.CreateUnbounded<BackgroundJob>();
HashSet<Task> _Processing = new HashSet<Task>();
public void Schedule(Func<Task> action, TimeSpan delay)
{
_Jobs.Writer.TryWrite(new BackgroundJob(action, delay, Delay));
}
public async Task WaitAllRunning(CancellationToken cancellationToken)
{
Task[] processing = null;
lock (_Processing)
{
processing = _Processing.ToArray();
}
try
{
await Task.WhenAll(processing).WithCancellation(cancellationToken);
}
catch (Exception) when (cancellationToken.IsCancellationRequested)
{
throw;
}
}
public async Task ProcessJobs(CancellationToken cancellationToken)
{
while (await _Jobs.Reader.WaitToReadAsync(cancellationToken))
{
if (_Jobs.Reader.TryRead(out var job))
{
var processing = job.Run(cancellationToken);
lock (_Processing)
{
_Processing.Add(processing);
}
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
processing.ContinueWith(t =>
{
if (t.IsFaulted)
{
Logs.PayServer.LogWarning(t.Exception, "Unhandled exception while job running");
}
lock (_Processing)
{
_Processing.Remove(processing);
}
}, default, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
}
}
}
}
}

View File

@ -0,0 +1,85 @@
using System;
using System.Collections.Generic;
using Microsoft.Extensions.Logging;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using BTCPayServer.Logging;
using Microsoft.Extensions.Hosting;
namespace BTCPayServer.HostedServices
{
public class EventHostedServiceBase : IHostedService
{
private readonly EventAggregator _EventAggregator;
private List<IEventAggregatorSubscription> _Subscriptions;
private CancellationTokenSource _Cts;
public EventHostedServiceBase(EventAggregator eventAggregator)
{
_EventAggregator = eventAggregator;
}
Channel<object> _Events = Channel.CreateUnbounded<object>();
public async Task ProcessEvents(CancellationToken cancellationToken)
{
while (await _Events.Reader.WaitToReadAsync(cancellationToken))
{
if (_Events.Reader.TryRead(out var evt))
{
try
{
await ProcessEvent(evt, cancellationToken);
}
catch when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch (Exception ex)
{
Logs.PayServer.LogWarning(ex, $"Unhandled exception in {this.GetType().Name}");
}
}
}
}
protected virtual Task ProcessEvent(object evt, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
protected virtual void SubscibeToEvents()
{
}
protected void Subscribe<T>()
{
_Subscriptions.Add(_EventAggregator.Subscribe<T>(e => _Events.Writer.TryWrite(e)));
}
public Task StartAsync(CancellationToken cancellationToken)
{
_Subscriptions = new List<IEventAggregatorSubscription>();
SubscibeToEvents();
_Cts = new CancellationTokenSource();
_ProcessingEvents = ProcessEvents(_Cts.Token);
return Task.CompletedTask;
}
Task _ProcessingEvents = Task.CompletedTask;
public async Task StopAsync(CancellationToken cancellationToken)
{
_Subscriptions?.ForEach(subscription => subscription.Dispose());
_Cts?.Cancel();
try
{
await _ProcessingEvents;
}
catch (OperationCanceledException)
{ }
}
}
}

View File

@ -1,10 +1,7 @@
using Hangfire;
using Hangfire.Common;
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Hangfire.Annotations;
using System.Reflection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
@ -21,6 +18,7 @@ using NBXplorer;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Payments;
using BTCPayServer.Services.Mails;
using BTCPayServer.Services;
namespace BTCPayServer.HostedServices
{
@ -35,25 +33,17 @@ namespace BTCPayServer.HostedServices
get; set;
}
public InvoiceEntity Invoice
public InvoicePaymentNotificationEventWrapper Notification
{
get; set;
}
public int? EventCode { get; set; }
public string Message { get; set; }
}
public ILogger Logger
{
get; set;
}
IBackgroundJobClient _JobClient;
EventAggregator _EventAggregator;
InvoiceRepository _InvoiceRepository;
BTCPayNetworkProvider _NetworkProvider;
IEmailSender _EmailSender;
private readonly EmailSenderFactory _EmailSenderFactory;
public InvoiceNotificationManager(
IBackgroundJobClient jobClient,
@ -61,102 +51,114 @@ namespace BTCPayServer.HostedServices
InvoiceRepository invoiceRepository,
BTCPayNetworkProvider networkProvider,
ILogger<InvoiceNotificationManager> logger,
IEmailSender emailSender)
EmailSenderFactory emailSenderFactory)
{
Logger = logger as ILogger ?? NullLogger.Instance;
_JobClient = jobClient;
_EventAggregator = eventAggregator;
_InvoiceRepository = invoiceRepository;
_NetworkProvider = networkProvider;
_EmailSender = emailSender;
_EmailSenderFactory = emailSenderFactory;
}
async Task Notify(InvoiceEntity invoice, int? eventCode = null, string name = null)
void Notify(InvoiceEntity invoice, InvoiceEvent invoiceEvent, bool extendedNotification)
{
var dto = invoice.EntityToDTO(_NetworkProvider);
var notification = new InvoicePaymentNotificationEventWrapper()
{
Data = new InvoicePaymentNotification()
{
Id = dto.Id,
Currency = dto.Currency,
CurrentTime = dto.CurrentTime,
ExceptionStatus = dto.ExceptionStatus,
ExpirationTime = dto.ExpirationTime,
InvoiceTime = dto.InvoiceTime,
PosData = dto.PosData,
Price = dto.Price,
Status = dto.Status,
BuyerFields = invoice.RefundMail == null ? null : new Newtonsoft.Json.Linq.JObject() { new JProperty("buyerEmail", invoice.RefundMail) },
PaymentSubtotals = dto.PaymentSubtotals,
PaymentTotals = dto.PaymentTotals,
AmountPaid = dto.AmountPaid,
ExchangeRates = dto.ExchangeRates,
},
Event = new InvoicePaymentNotificationEvent()
{
Code = invoiceEvent.EventCode,
Name = invoiceEvent.Name
},
ExtendedNotification = extendedNotification,
NotificationURL = invoice.NotificationURL
};
// For lightning network payments, paid, confirmed and completed come all at once.
// So despite the event is "paid" or "confirmed" the Status of the invoice is technically complete
// This confuse loggers who think their endpoint get duplicated events
// So here, we just override the status expressed by the notification
if (invoiceEvent.Name == InvoiceEvent.Confirmed)
{
notification.Data.Status = InvoiceState.ToString(InvoiceStatus.Confirmed);
}
if (invoiceEvent.Name == InvoiceEvent.PaidInFull)
{
notification.Data.Status = InvoiceState.ToString(InvoiceStatus.Paid);
}
//////////////////
// We keep backward compatibility with bitpay by passing BTC info to the notification
// we don't pass other info, as it is a bad idea to use IPN data for logic processing (can be faked)
var btcCryptoInfo = dto.CryptoInfo.FirstOrDefault(c => c.GetpaymentMethodId() == new PaymentMethodId("BTC", Payments.PaymentTypes.BTCLike));
if (btcCryptoInfo != null)
{
#pragma warning disable CS0618
notification.Data.Rate = dto.Rate;
notification.Data.Url = dto.Url;
notification.Data.BTCDue = dto.BTCDue;
notification.Data.BTCPaid = dto.BTCPaid;
notification.Data.BTCPrice = dto.BTCPrice;
#pragma warning restore CS0618
}
CancellationTokenSource cts = new CancellationTokenSource(10000);
if (!String.IsNullOrEmpty(invoice.NotificationEmail))
{
// just extracting most important data for email body, merchant should query API back for full invoice based on Invoice.Id
var ipn = new
{
invoice.Id,
invoice.Status,
invoice.StoreId
};
// TODO: Consider adding info on ItemDesc and payment info (amount)
var emailBody = NBitcoin.JsonConverters.Serializer.ToString(notification);
_EmailSenderFactory.GetEmailSender(invoice.StoreId).SendEmail(
invoice.NotificationEmail,
$"BtcPayServer Invoice Notification - ${invoice.StoreId}",
emailBody);
var emailBody = NBitcoin.JsonConverters.Serializer.ToString(ipn);
await _EmailSender.SendEmailAsync(
invoice.NotificationEmail, $"BtcPayServer Invoice Notification - ${invoice.StoreId}", emailBody);
}
try
{
if (string.IsNullOrEmpty(invoice.NotificationURL))
return;
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id, eventCode, name));
var response = await SendNotification(invoice, eventCode, name, cts.Token);
response.EnsureSuccessStatusCode();
if (string.IsNullOrEmpty(invoice.NotificationURL) || !Uri.IsWellFormedUriString(invoice.NotificationURL, UriKind.Absolute))
return;
}
catch (OperationCanceledException) when (cts.IsCancellationRequested)
{
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id, eventCode, name)
{
Error = "Timeout"
});
}
catch (Exception ex) // It fails, it is OK because we try with hangfire after
{
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id, eventCode, name)
{
Error = ex.Message
});
}
var invoiceStr = NBitcoin.JsonConverters.Serializer.ToString(new ScheduledJob() { TryCount = 0, Invoice = invoice, EventCode = eventCode, Message = name });
var invoiceStr = NBitcoin.JsonConverters.Serializer.ToString(new ScheduledJob() { TryCount = 0, Notification = notification });
if (!string.IsNullOrEmpty(invoice.NotificationURL))
_JobClient.Schedule(() => NotifyHttp(invoiceStr), TimeSpan.Zero);
}
ConcurrentDictionary<string, string> _Executing = new ConcurrentDictionary<string, string>();
public async Task NotifyHttp(string invoiceData)
{
var job = NBitcoin.JsonConverters.Serializer.ToObject<ScheduledJob>(invoiceData);
var jobId = GetHttpJobId(job.Invoice);
if (!_Executing.TryAdd(jobId, jobId))
return; //For some reason, Hangfire fire the job several time
Logger.LogInformation("Running " + jobId);
bool reschedule = false;
var aggregatorEvent = new InvoiceIPNEvent(job.Notification.Data.Id, job.Notification.Event.Code, job.Notification.Event.Name);
CancellationTokenSource cts = new CancellationTokenSource(10000);
try
{
HttpResponseMessage response = await SendNotification(job.Invoice, job.EventCode, job.Message, cts.Token);
HttpResponseMessage response = await SendNotification(job.Notification, cts.Token);
reschedule = !response.IsSuccessStatusCode;
Logger.LogInformation("Job " + jobId + " returned " + response.StatusCode);
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
{
Error = reschedule ? $"Unexpected return code: {(int)response.StatusCode}" : null
});
aggregatorEvent.Error = reschedule ? $"Unexpected return code: {(int)response.StatusCode}" : null;
_EventAggregator.Publish<InvoiceIPNEvent>(aggregatorEvent);
}
catch (OperationCanceledException) when (cts.IsCancellationRequested)
{
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
{
Error = "Timeout"
});
aggregatorEvent.Error = "Timeout";
_EventAggregator.Publish<InvoiceIPNEvent>(aggregatorEvent);
reschedule = true;
Logger.LogInformation("Job " + jobId + " timed out");
}
catch (Exception ex) // It fails, it is OK because we try with hangfire after
catch (Exception ex)
{
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
{
Error = ex.Message
});
reschedule = true;
List<string> messages = new List<string>();
@ -166,21 +168,16 @@ namespace BTCPayServer.HostedServices
ex = ex.InnerException;
}
string message = String.Join(',', messages.ToArray());
Logger.LogInformation("Job " + jobId + " threw exception " + message);
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
{
Error = $"Unexpected error: {message}"
});
aggregatorEvent.Error = $"Unexpected error: {message}";
_EventAggregator.Publish<InvoiceIPNEvent>(aggregatorEvent);
}
finally { cts.Dispose(); _Executing.TryRemove(jobId, out jobId); }
finally { cts?.Dispose(); }
job.TryCount++;
if (job.TryCount < MaxTry && reschedule)
{
Logger.LogInformation("Rescheduling " + jobId + " in 10 minutes, remaining try " + (MaxTry - job.TryCount));
invoiceData = NBitcoin.JsonConverters.Serializer.ToString(job);
_JobClient.Schedule(() => NotifyHttp(invoiceData), TimeSpan.FromMinutes(10.0));
}
@ -199,64 +196,35 @@ namespace BTCPayServer.HostedServices
public InvoicePaymentNotificationEvent Event { get; set; }
[JsonProperty("data")]
public InvoicePaymentNotification Data { get; set; }
[JsonProperty("extendedNotification")]
public bool ExtendedNotification { get; set; }
[JsonProperty(PropertyName = "notificationURL")]
public string NotificationURL { get; set; }
}
Encoding UTF8 = new UTF8Encoding(false);
private async Task<HttpResponseMessage> SendNotification(InvoiceEntity invoice, int? eventCode, string name, CancellationToken cancellation)
private async Task<HttpResponseMessage> SendNotification(InvoicePaymentNotificationEventWrapper notification, CancellationToken cancellation)
{
var request = new HttpRequestMessage();
request.Method = HttpMethod.Post;
var dto = invoice.EntityToDTO(_NetworkProvider);
InvoicePaymentNotification notification = new InvoicePaymentNotification()
{
Id = dto.Id,
Currency = dto.Currency,
CurrentTime = dto.CurrentTime,
ExceptionStatus = dto.ExceptionStatus,
ExpirationTime = dto.ExpirationTime,
InvoiceTime = dto.InvoiceTime,
PosData = dto.PosData,
Price = dto.Price,
Status = dto.Status,
BuyerFields = invoice.RefundMail == null ? null : new Newtonsoft.Json.Linq.JObject() { new JProperty("buyerEmail", invoice.RefundMail) },
PaymentSubtotals = dto.PaymentSubtotals,
PaymentTotals = dto.PaymentTotals,
AmountPaid = dto.AmountPaid,
ExchangeRates = dto.ExchangeRates,
var notificationString = NBitcoin.JsonConverters.Serializer.ToString(notification);
var jobj = JObject.Parse(notificationString);
};
// We keep backward compatibility with bitpay by passing BTC info to the notification
// we don't pass other info, as it is a bad idea to use IPN data for logic processing (can be faked)
var btcCryptoInfo = dto.CryptoInfo.FirstOrDefault(c => c.GetpaymentMethodId() == new PaymentMethodId("BTC", Payments.PaymentTypes.BTCLike));
if (btcCryptoInfo != null)
if (notification.ExtendedNotification)
{
#pragma warning disable CS0618
notification.Rate = dto.Rate;
notification.Url = dto.Url;
notification.BTCDue = dto.BTCDue;
notification.BTCPaid = dto.BTCPaid;
notification.BTCPrice = dto.BTCPrice;
#pragma warning restore CS0618
}
string notificationString = null;
if (eventCode.HasValue)
{
var wrapper = new InvoicePaymentNotificationEventWrapper();
wrapper.Data = notification;
wrapper.Event = new InvoicePaymentNotificationEvent() { Code = eventCode.Value, Name = name };
notificationString = JsonConvert.SerializeObject(wrapper);
jobj.Remove("extendedNotification");
jobj.Remove("notificationURL");
notificationString = jobj.ToString();
}
else
{
notificationString = JsonConvert.SerializeObject(notification);
notificationString = jobj["data"].ToString();
}
request.RequestUri = new Uri(invoice.NotificationURL, UriKind.Absolute);
request.RequestUri = new Uri(notification.NotificationURL, UriKind.Absolute);
request.Content = new StringContent(notificationString, UTF8, "application/json");
var response = await Enqueue(invoice.Id, async () => await _Client.SendAsync(request, cancellation));
var response = await Enqueue(notification.Data.Id, async () => await _Client.SendAsync(request, cancellation));
return response;
}
@ -320,11 +288,6 @@ namespace BTCPayServer.HostedServices
int MaxTry = 6;
private static string GetHttpJobId(InvoiceEntity invoice)
{
return $"{invoice.Id}-{invoice.Status}-HTTP";
}
CompositeDisposable leases = new CompositeDisposable();
public Task StartAsync(CancellationToken cancellationToken)
{
@ -350,19 +313,18 @@ namespace BTCPayServer.HostedServices
e.Name == InvoiceEvent.Completed ||
e.Name == InvoiceEvent.ExpiredPaidPartial
)
tasks.Add(Notify(invoice));
Notify(invoice, e, false);
}
if (e.Name == "invoice_confirmed")
if (e.Name == InvoiceEvent.Confirmed)
{
tasks.Add(Notify(invoice));
Notify(invoice, e, false);
}
if (invoice.ExtendedNotifications)
{
tasks.Add(Notify(invoice, e.EventCode, e.Name));
Notify(invoice, e, true);
}
await Task.WhenAll(tasks.ToArray());
}));

View File

@ -11,7 +11,6 @@ using BTCPayServer.Logging;
using System.Threading;
using Microsoft.Extensions.Hosting;
using System.Collections.Concurrent;
using Hangfire;
using BTCPayServer.Services.Wallets;
using BTCPayServer.Controllers;
using BTCPayServer.Events;
@ -66,10 +65,10 @@ namespace BTCPayServer.HostedServices
context.MarkDirty();
await _InvoiceRepository.UnaffectAddress(invoice.Id);
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1004, InvoiceEvent.Expired));
invoice.Status = InvoiceStatus.Expired;
if(invoice.ExceptionStatus == InvoiceExceptionStatus.PaidPartial)
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 2000, InvoiceEvent.ExpiredPaidPartial));
context.Events.Add(new InvoiceEvent(invoice, 1004, InvoiceEvent.Expired));
if (invoice.ExceptionStatus == InvoiceExceptionStatus.PaidPartial)
context.Events.Add(new InvoiceEvent(invoice, 2000, InvoiceEvent.ExpiredPaidPartial));
}
var payments = invoice.GetPayments().Where(p => p.Accounted).ToArray();
@ -84,7 +83,7 @@ namespace BTCPayServer.HostedServices
{
if (invoice.Status == InvoiceStatus.New)
{
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1003, InvoiceEvent.PaidInFull));
context.Events.Add(new InvoiceEvent(invoice, 1003, InvoiceEvent.PaidInFull));
invoice.Status = InvoiceStatus.Paid;
invoice.ExceptionStatus = accounting.Paid > accounting.TotalDue ? InvoiceExceptionStatus.PaidOver : InvoiceExceptionStatus.None;
await _InvoiceRepository.UnaffectAddress(invoice.Id);
@ -93,7 +92,7 @@ namespace BTCPayServer.HostedServices
else if (invoice.Status == InvoiceStatus.Expired && invoice.ExceptionStatus != InvoiceExceptionStatus.PaidLate)
{
invoice.ExceptionStatus = InvoiceExceptionStatus.PaidLate;
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1009, InvoiceEvent.PaidAfterExpiration));
context.Events.Add(new InvoiceEvent(invoice, 1009, InvoiceEvent.PaidAfterExpiration));
context.MarkDirty();
}
}
@ -139,15 +138,15 @@ namespace BTCPayServer.HostedServices
(confirmedAccounting.Paid < accounting.MinimumTotalDue))
{
await _InvoiceRepository.UnaffectAddress(invoice.Id);
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1013, InvoiceEvent.FailedToConfirm));
context.Events.Add(new InvoiceEvent(invoice, 1013, InvoiceEvent.FailedToConfirm));
invoice.Status = InvoiceStatus.Invalid;
context.MarkDirty();
}
else if (confirmedAccounting.Paid >= accounting.MinimumTotalDue)
{
await _InvoiceRepository.UnaffectAddress(invoice.Id);
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1005, InvoiceEvent.Confirmed));
invoice.Status = InvoiceStatus.Confirmed;
context.Events.Add(new InvoiceEvent(invoice, 1005, InvoiceEvent.Confirmed));
context.MarkDirty();
}
}
@ -157,7 +156,7 @@ namespace BTCPayServer.HostedServices
var completedAccounting = paymentMethod.Calculate(p => p.GetCryptoPaymentData().PaymentCompleted(p, network));
if (completedAccounting.Paid >= accounting.MinimumTotalDue)
{
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1006, InvoiceEvent.Completed));
context.Events.Add(new InvoiceEvent(invoice, 1006, InvoiceEvent.Completed));
invoice.Status = InvoiceStatus.Complete;
context.MarkDirty();
}

View File

@ -65,6 +65,12 @@ namespace BTCPayServer.HostedServices
settings.ConvertNetworkFeeProperty = true;
await _Settings.UpdateSetting(settings);
}
if (!settings.ConvertCrowdfundOldSettings)
{
await ConvertCrowdfundOldSettings();
settings.ConvertCrowdfundOldSettings = true;
await _Settings.UpdateSetting(settings);
}
}
catch (Exception ex)
{
@ -73,6 +79,24 @@ namespace BTCPayServer.HostedServices
}
}
private async Task ConvertCrowdfundOldSettings()
{
using (var ctx = _DBContextFactory.CreateContext())
{
foreach (var app in ctx.Apps.Where(a => a.AppType == "Crowdfund"))
{
var settings = app.GetSettings<Services.Apps.CrowdfundSettings>();
#pragma warning disable CS0618 // Type or member is obsolete
if (settings.UseAllStoreInvoices)
#pragma warning restore CS0618 // Type or member is obsolete
{
app.TagAllInvoices = true;
}
}
await ctx.SaveChangesAsync();
}
}
private async Task ConvertNetworkFeeProperty()
{
using (var ctx = _DBContextFactory.CreateContext())

View File

@ -65,9 +65,8 @@ namespace BTCPayServer.HostedServices
async Task RefreshCoinAverageSupportedExchanges()
{
var tickers = await new CoinAverageRateProvider() { Authenticator = _coinAverageSettings }.GetExchangeTickersAsync();
var exchanges = new CoinAverageExchanges();
foreach (var item in tickers
foreach (var item in (await new CoinAverageRateProvider() { Authenticator = _coinAverageSettings }.GetExchangeTickersAsync())
.Exchanges
.Select(c => new CoinAverageExchange(c.Name, c.DisplayName)))
{

View File

@ -45,6 +45,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding;
using NBXplorer.DerivationStrategy;
using NicolasDorier.RateLimits;
using Npgsql;
using BTCPayServer.Services.Apps;
namespace BTCPayServer.Hosting
{
@ -104,7 +105,46 @@ namespace BTCPayServer.Hosting
return opts.NetworkProvider;
});
services.TryAddSingleton<AppsHelper>();
services.TryAddSingleton<AppService>();
services.TryAddSingleton<Ganss.XSS.HtmlSanitizer>(o =>
{
var htmlSanitizer = new Ganss.XSS.HtmlSanitizer();
htmlSanitizer.RemovingAtRule += (sender, args) =>
{
};
htmlSanitizer.RemovingTag += (sender, args) =>
{
if (args.Tag.TagName.Equals("img", StringComparison.InvariantCultureIgnoreCase))
{
if (!args.Tag.ClassList.Contains("img-fluid"))
{
args.Tag.ClassList.Add("img-fluid");
}
args.Cancel = true;
}
};
htmlSanitizer.RemovingAttribute += (sender, args) =>
{
if (args.Tag.TagName.Equals("img", StringComparison.InvariantCultureIgnoreCase) &&
args.Attribute.Name.Equals("src", StringComparison.InvariantCultureIgnoreCase) &&
args.Reason == Ganss.XSS.RemoveReason.NotAllowedUrlValue)
{
args.Cancel = true;
}
};
htmlSanitizer.RemovingStyle += (sender, args) => { args.Cancel = true; };
htmlSanitizer.AllowedAttributes.Add("class");
htmlSanitizer.AllowedTags.Add("iframe");
htmlSanitizer.AllowedTags.Remove("img");
htmlSanitizer.AllowedAttributes.Add("webkitallowfullscreen");
htmlSanitizer.AllowedAttributes.Add("allowfullscreen");
return htmlSanitizer;
});
services.TryAddSingleton<LightningConfigurationProvider>();
services.TryAddSingleton<LanguageService>();
@ -114,7 +154,7 @@ namespace BTCPayServer.Hosting
services.TryAddSingleton<CurrencyNameTable>();
services.TryAddSingleton<IFeeProviderFactory>(o => new NBXplorerFeeProviderFactory(o.GetRequiredService<ExplorerClientProvider>())
{
Fallback = new FeeRate(100, 1),
Fallback = new FeeRate(100L, 1),
BlockTarget = 20
});
@ -142,6 +182,10 @@ namespace BTCPayServer.Hosting
services.AddSingleton<IHostedService, InvoiceNotificationManager>();
services.AddSingleton<IHostedService, InvoiceWatcher>();
services.AddSingleton<IHostedService, RatesHostedService>();
services.AddSingleton<IHostedService, BackgroundJobSchedulerHostedService>();
services.AddSingleton<IHostedService, AppHubStreamer>();
services.AddSingleton<IBackgroundJobClient, BackgroundJobClient>();
services.AddTransient<IConfigureOptions<MvcOptions>, BTCPayClaimsFilter>();
services.TryAddSingleton<ExplorerClientProvider>();
@ -158,8 +202,9 @@ namespace BTCPayServer.Hosting
services.TryAddScoped<IHttpContextAccessor, HttpContextAccessor>();
services.AddTransient<AccessTokenController>();
services.AddTransient<InvoiceController>();
services.AddTransient<AppsPublicController>();
// Add application services.
services.AddTransient<IEmailSender, EmailSender>();
services.AddSingleton<EmailSenderFactory>();
// bundling
services.AddAuthorization(o => Policies.AddBTCPayPolicies(o));

View File

@ -38,9 +38,20 @@ namespace BTCPayServer.Hosting
{
var bitpayAuth = GetBitpayAuth(httpContext, out bool isBitpayAuth);
var isBitpayAPI = IsBitpayAPI(httpContext, isBitpayAuth);
if (isBitpayAPI && httpContext.Request.Method == "OPTIONS")
{
httpContext.Response.StatusCode = 200;
httpContext.Response.SetHeader("Access-Control-Allow-Origin", "*");
if (httpContext.Request.Headers.ContainsKey("Access-Control-Request-Headers"))
{
httpContext.Response.SetHeader("Access-Control-Allow-Headers", httpContext.Request.Headers["Access-Control-Request-Headers"].FirstOrDefault());
}
return; // We bypass MVC completely
}
httpContext.SetIsBitpayAPI(isBitpayAPI);
if (isBitpayAPI)
{
httpContext.Response.SetHeader("Access-Control-Allow-Origin", "*");
httpContext.SetBitpayAuth(bitpayAuth);
}
await _Next(httpContext);
@ -81,32 +92,34 @@ namespace BTCPayServer.Hosting
var isJson = (httpContext.Request.ContentType ?? string.Empty).StartsWith("application/json", StringComparison.OrdinalIgnoreCase);
var path = httpContext.Request.Path.Value;
var method = httpContext.Request.Method;
var isCors = method == "OPTIONS";
if (
bitpayAuth &&
(isCors || bitpayAuth) &&
(path == "/invoices" || path == "/invoices/") &&
httpContext.Request.Method == "POST" &&
isJson)
(isCors || (method == "POST" && isJson)))
return true;
if (
bitpayAuth &&
(isCors || bitpayAuth) &&
(path == "/invoices" || path == "/invoices/") &&
httpContext.Request.Method == "GET")
(isCors || method == "GET"))
return true;
if (
path.StartsWith("/invoices/", StringComparison.OrdinalIgnoreCase) &&
httpContext.Request.Method == "GET" &&
(isJson || httpContext.Request.Query.ContainsKey("token")))
path.StartsWith("/invoices/", StringComparison.OrdinalIgnoreCase) &&
(isCors || method == "GET") &&
(isCors || isJson || httpContext.Request.Query.ContainsKey("token")))
return true;
if (path.StartsWith("/rates", StringComparison.OrdinalIgnoreCase) &&
httpContext.Request.Method == "GET")
(isCors || method == "GET"))
return true;
if (
path.Equals("/tokens", StringComparison.Ordinal) &&
(httpContext.Request.Method == "GET" || httpContext.Request.Method == "POST"))
(isCors || method == "GET" || method == "POST"))
return true;
return false;

View File

@ -19,7 +19,6 @@ using BTCPayServer.Models;
using Microsoft.AspNetCore.Identity;
using BTCPayServer.Data;
using Microsoft.Extensions.Logging;
using Hangfire;
using BTCPayServer.Logging;
using Microsoft.AspNetCore.Authorization;
using System.Threading.Tasks;
@ -27,11 +26,8 @@ using BTCPayServer.Controllers;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Mails;
using Microsoft.Extensions.Configuration;
using Hangfire.AspNetCore;
using BTCPayServer.Configuration;
using System.IO;
using Hangfire.Dashboard;
using Hangfire.Annotations;
using Microsoft.Extensions.DependencyInjection.Extensions;
using System.Threading;
using Microsoft.Extensions.Options;
@ -40,23 +36,12 @@ using Microsoft.AspNetCore.Server.Kestrel.Core;
using System.Net;
using Meziantou.AspNetCore.BundleTagHelpers;
using BTCPayServer.Security;
using BTCPayServer.Services.Apps;
namespace BTCPayServer.Hosting
{
public class Startup
{
class NeedRole : IDashboardAuthorizationFilter
{
string _Role;
public NeedRole(string role)
{
_Role = role;
}
public bool Authorize([NotNull] DashboardContext context)
{
return context.GetHttpContext().User.IsInRole(_Role);
}
}
public Startup(IConfiguration conf, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
Configuration = conf;
@ -78,7 +63,7 @@ namespace BTCPayServer.Hosting
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddSignalR();
services.AddBTCPayServer();
services.AddMvc(o =>
{
@ -99,7 +84,7 @@ namespace BTCPayServer.Hosting
services.Configure<IdentityOptions>(options =>
{
options.Password.RequireDigit = false;
options.Password.RequiredLength = 7;
options.Password.RequiredLength = 6;
options.Password.RequireLowercase = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
@ -107,21 +92,6 @@ namespace BTCPayServer.Hosting
options.Lockout.MaxFailedAccessAttempts = 5;
options.Lockout.AllowedForNewUsers = true;
});
services.AddHangfire((o) =>
{
var scope = AspNetCoreJobActivator.Current.BeginScope(null);
var options = (ApplicationDbContextFactory)scope.Resolve(typeof(ApplicationDbContextFactory));
options.ConfigureHangfireBuilder(o);
});
services.AddCors(o =>
{
o.AddPolicy("BitpayAPI", b =>
{
b.AllowAnyMethod().AllowAnyHeader().AllowAnyOrigin();
});
});
// If the HTTPS certificate path is not set this logic will NOT be used and the default Kestrel binding logic will be.
string httpsCertificateFilePath = Configuration.GetOrDefault<string>("HttpsCertificateFilePath", null);
bool useDefaultCertificate = Configuration.GetOrDefault<bool>("HttpsUseDefaultCertificate", false);
@ -192,11 +162,9 @@ namespace BTCPayServer.Hosting
app.UsePayServer();
app.UseStaticFiles();
app.UseAuthentication();
app.UseHangfireServer();
app.UseHangfireDashboard("/hangfire", new DashboardOptions()
app.UseSignalR(route =>
{
AppPath = options.GetRootUri(),
Authorization = new[] { new NeedRole(Roles.ServerAdmin) }
route.MapHub<AppHub>("/apps/hub");
});
app.UseWebSockets();
app.UseStatusCodePages();

33
BTCPayServer/IDelay.cs Normal file
View File

@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace BTCPayServer
{
public interface IDelay
{
Task Wait(TimeSpan delay, CancellationToken cancellationToken);
}
public class TaskDelay : IDelay
{
TaskDelay()
{
}
private static readonly TaskDelay _Instance = new TaskDelay();
public static TaskDelay Instance
{
get
{
return _Instance;
}
}
public Task Wait(TimeSpan delay, CancellationToken cancellationToken)
{
return Task.Delay(delay, cancellationToken);
}
}
}

View File

@ -0,0 +1,580 @@
// <auto-generated />
using System;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20190219032533_AppsTagging")]
partial class AppsTagging
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.1.8-servicing-32085");
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
b.Property<string>("Address")
.ValueGeneratedOnAdd();
b.Property<DateTimeOffset?>("CreatedTime");
b.Property<string>("InvoiceDataId");
b.HasKey("Address");
b.HasIndex("InvoiceDataId");
b.ToTable("AddressInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasMaxLength(50);
b.Property<string>("StoreId")
.HasMaxLength(50);
b.HasKey("Id");
b.HasIndex("StoreId");
b.ToTable("ApiKeys");
});
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("AppType");
b.Property<DateTimeOffset>("Created");
b.Property<string>("Name");
b.Property<string>("Settings");
b.Property<string>("StoreDataId");
b.Property<bool>("TagAllInvoices");
b.HasKey("Id");
b.HasIndex("StoreDataId");
b.ToTable("Apps");
});
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
{
b.Property<string>("InvoiceDataId");
b.Property<string>("Address");
b.Property<DateTimeOffset>("Assigned");
b.Property<string>("CryptoCode");
b.Property<DateTimeOffset?>("UnAssigned");
b.HasKey("InvoiceDataId", "Address");
b.ToTable("HistoricalAddressInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<byte[]>("Blob");
b.Property<DateTimeOffset>("Created");
b.Property<string>("CustomerEmail");
b.Property<string>("ExceptionStatus");
b.Property<string>("ItemCode");
b.Property<string>("OrderId");
b.Property<string>("Status");
b.Property<string>("StoreDataId");
b.HasKey("Id");
b.HasIndex("StoreDataId");
b.ToTable("Invoices");
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
{
b.Property<string>("InvoiceDataId");
b.Property<string>("UniqueId");
b.Property<string>("Message");
b.Property<DateTimeOffset>("Timestamp");
b.HasKey("InvoiceDataId", "UniqueId");
b.ToTable("InvoiceEvents");
});
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Facade");
b.Property<string>("Label");
b.Property<DateTimeOffset>("PairingTime");
b.Property<string>("SIN");
b.Property<string>("StoreDataId");
b.HasKey("Id");
b.HasIndex("SIN");
b.HasIndex("StoreDataId");
b.ToTable("PairedSINData");
});
modelBuilder.Entity("BTCPayServer.Data.PairingCodeData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("DateCreated");
b.Property<DateTimeOffset>("Expiration");
b.Property<string>("Facade");
b.Property<string>("Label");
b.Property<string>("SIN");
b.Property<string>("StoreDataId");
b.Property<string>("TokenValue");
b.HasKey("Id");
b.ToTable("PairingCodes");
});
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<bool>("Accounted");
b.Property<byte[]>("Blob");
b.Property<string>("InvoiceDataId");
b.HasKey("Id");
b.HasIndex("InvoiceDataId");
b.ToTable("Payments");
});
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
{
b.Property<string>("Id");
b.HasKey("Id");
b.ToTable("PendingInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<byte[]>("Blob");
b.Property<string>("InvoiceDataId");
b.HasKey("Id");
b.HasIndex("InvoiceDataId");
b.ToTable("RefundAddresses");
});
modelBuilder.Entity("BTCPayServer.Data.SettingData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Value");
b.HasKey("Id");
b.ToTable("Settings");
});
modelBuilder.Entity("BTCPayServer.Data.StoreData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("DefaultCrypto");
b.Property<string>("DerivationStrategies");
b.Property<string>("DerivationStrategy");
b.Property<int>("SpeedPolicy");
b.Property<byte[]>("StoreBlob");
b.Property<byte[]>("StoreCertificate");
b.Property<string>("StoreName");
b.Property<string>("StoreWebsite");
b.HasKey("Id");
b.ToTable("Stores");
});
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
{
b.Property<string>("ApplicationUserId");
b.Property<string>("StoreDataId");
b.Property<string>("Role");
b.HasKey("ApplicationUserId", "StoreDataId");
b.HasIndex("StoreDataId");
b.ToTable("UserStore");
});
modelBuilder.Entity("BTCPayServer.Models.ApplicationUser", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("AccessFailedCount");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Email")
.HasMaxLength(256);
b.Property<bool>("EmailConfirmed");
b.Property<bool>("LockoutEnabled");
b.Property<DateTimeOffset?>("LockoutEnd");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256);
b.Property<string>("NormalizedUserName")
.HasMaxLength(256);
b.Property<string>("PasswordHash");
b.Property<string>("PhoneNumber");
b.Property<bool>("PhoneNumberConfirmed");
b.Property<bool>("RequiresEmailConfirmation");
b.Property<string>("SecurityStamp");
b.Property<bool>("TwoFactorEnabled");
b.Property<string>("UserName")
.HasMaxLength(256);
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasName("UserNameIndex");
b.ToTable("AspNetUsers");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Name")
.HasMaxLength(256);
b.Property<string>("NormalizedName")
.HasMaxLength(256);
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasName("RoleNameIndex");
b.ToTable("AspNetRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ClaimType");
b.Property<string>("ClaimValue");
b.Property<string>("RoleId")
.IsRequired();
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ClaimType");
b.Property<string>("ClaimValue");
b.Property<string>("UserId")
.IsRequired();
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider");
b.Property<string>("ProviderKey");
b.Property<string>("ProviderDisplayName");
b.Property<string>("UserId")
.IsRequired();
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("RoleId");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("LoginProvider");
b.Property<string>("Name");
b.Property<string>("Value");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens");
});
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("AddressInvoices")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("APIKeys")
.HasForeignKey("StoreId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("Apps")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("HistoricalAddressInvoices")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("Invoices")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("Events")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("PairedSINs")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("Payments")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("PendingInvoices")
.HasForeignKey("Id")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("RefundAddresses")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser")
.WithMany("UserStores")
.HasForeignKey("ApplicationUserId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("UserStores")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,23 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace BTCPayServer.Migrations
{
public partial class AppsTagging : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "TagAllInvoices",
table: "Apps",
nullable: false,
defaultValue: false);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "TagAllInvoices",
table: "Apps");
}
}
}

View File

@ -14,7 +14,7 @@ namespace BTCPayServer.Migrations
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.1.0-rtm-30799");
.HasAnnotation("ProductVersion", "2.1.8-servicing-32085");
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
@ -63,6 +63,8 @@ namespace BTCPayServer.Migrations
b.Property<string>("StoreDataId");
b.Property<bool>("TagAllInvoices");
b.HasKey("Id");
b.HasIndex("StoreDataId");

View File

@ -0,0 +1,83 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using BTCPayServer.Services.Apps;
namespace BTCPayServer.Models.AppViewModels
{
public class UpdateCrowdfundViewModel
{
[Required] [MaxLength(30)] public string Title { get; set; }
[MaxLength(50)] public string Tagline { get; set; }
[Required] public string Description { get; set; }
[Display(Name = "Featured Image")]
public string MainImageUrl { get; set; }
public string NotificationUrl { get; set; }
[Required]
[Display(Name = "Allow crowdfund to be publicly visible (still visible to you)")]
public bool Enabled { get; set; } = false;
[Required]
[Display(Name = "Enable background animations on new payments")]
public bool AnimationsEnabled { get; set; } = true;
[Required]
[Display(Name = "Enable sounds on new payments")]
public bool SoundsEnabled { get; set; } = true;
[Required]
[Display(Name = "Enable Disqus Comments")]
public bool DisqusEnabled { get; set; } = true;
[Display(Name = "Disqus Shortname")] public string DisqusShortname { get; set; }
public DateTime? StartDate { get; set; }
public DateTime? EndDate { get; set; }
[Required]
[MaxLength(5)]
[Display(Name = "Primary currency used for targets and stats. (e.g. BTC, LTC, USD, etc.)")]
public string TargetCurrency { get; set; } = "BTC";
[Display(Name = "Set a Target amount ")]
public decimal? TargetAmount { get; set; }
public IEnumerable<string> ResetEveryValues = Enum.GetNames(typeof(CrowdfundResetEvery));
[Display(Name = "Reset goal every")] public string ResetEvery { get; set; } = nameof(CrowdfundResetEvery.Never);
public int ResetEveryAmount { get; set; } = 1;
[Display(Name = "Do not allow additional contributions after target has been reached")]
public bool EnforceTargetAmount { get; set; }
[Display(Name = "Contribution Perks Template")]
public string PerksTemplate { get; set; }
[MaxLength(500)]
[Display(Name = "Custom bootstrap CSS file")]
public string CustomCSSLink { get; set; }
[Display(Name = "Custom CSS Code")]
public string EmbeddedCSS { get; set; }
[Display(Name = "Count all invoices created on the store as part of the crowdfunding goal")]
public bool UseAllStoreInvoices { get; set; }
public string AppId { get; set; }
public string SearchTerm { get; set; }
[Display(Name = "Sort contribution perks by popularity")]
public bool SortPerksByPopularity { get; set; }
[Display(Name = "Display contribution ranking")]
public bool DisplayPerksRanking { get; set; }
}
}

View File

@ -1,8 +1,4 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using System.ComponentModel.DataAnnotations;
namespace BTCPayServer.Models.AppViewModels
{
@ -45,5 +41,7 @@ namespace BTCPayServer.Models.AppViewModels
[MaxLength(500)]
[Display(Name = "Custom bootstrap CSS file")]
public string CustomCSSLink { get; set; }
public string Id { get; set; }
}
}

View File

@ -0,0 +1,69 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
namespace BTCPayServer.Models.AppViewModels
{
public class ViewCrowdfundViewModel
{
public string StatusMessage{ get; set; }
public string StoreId { get; set; }
public string AppId { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string MainImageUrl { get; set; }
public string EmbeddedCSS { get; set; }
public string CustomCSSLink { get; set; }
public DateTime? StartDate { get; set; }
public DateTime? EndDate { get; set; }
public string TargetCurrency { get; set; }
public decimal? TargetAmount { get; set; }
public bool EnforceTargetAmount { get; set; }
public CrowdfundInfo Info { get; set; }
public string Tagline { get; set; }
public ViewPointOfSaleViewModel.Item[] Perks { get; set; }
public bool DisqusEnabled { get; set; }
public bool SoundsEnabled { get; set; }
public string DisqusShortname { get; set; }
public bool AnimationsEnabled { get; set; }
public int ResetEveryAmount { get; set; }
public bool NeverReset { get; set; }
public Dictionary<string, int> PerkCount { get; set; }
public CurrencyData CurrencyData { get; set; }
public class CrowdfundInfo
{
public int TotalContributors { get; set; }
public decimal CurrentPendingAmount { get; set; }
public decimal CurrentAmount { get; set; }
public decimal? ProgressPercentage { get; set; }
public decimal? PendingProgressPercentage { get; set; }
public DateTime LastUpdated { get; set; }
public Dictionary<string, decimal> PaymentStats { get; set; }
public Dictionary<string, decimal> PendingPaymentStats { get; set; }
public DateTime? LastResetDate { get; set; }
public DateTime? NextResetDate { get; set; }
}
public bool Started => !StartDate.HasValue || DateTime.Now.ToUniversalTime() > StartDate;
public bool Ended => !EndDate.HasValue || DateTime.Now.ToUniversalTime() > EndDate;
public bool DisplayPerksRanking { get; set; }
}
public class ContributeToCrowdfund
{
public ViewCrowdfundViewModel ViewCrowdfundViewModel { get; set; }
[Required] public decimal Amount { get; set; }
public string Email { get; set; }
public string ChoiceKey { get; set; }
public bool RedirectToCheckout { get; set; }
public string RedirectUrl { get; set; }
}
}

View File

@ -7,6 +7,15 @@ namespace BTCPayServer.Models
{
public class ConfirmModel
{
public ConfirmModel() { }
public ConfirmModel(string title, string desc, string action = null)
{
Title = title;
Description = desc;
Action = action;
}
public string Title
{
get; set;

View File

@ -0,0 +1,83 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NBitcoin;
using NBitcoin.JsonConverters;
using NBitpayClient;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Models
{
public class CreateInvoiceRequest
{
[JsonProperty(PropertyName = "buyer")]
public Buyer Buyer { get; set; }
[JsonProperty(PropertyName = "buyerEmail", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string BuyerEmail { get; set; }
[JsonProperty(PropertyName = "buyerCountry", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string BuyerCountry { get; set; }
[JsonProperty(PropertyName = "buyerZip", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string BuyerZip { get; set; }
[JsonProperty(PropertyName = "buyerState", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string BuyerState { get; set; }
[JsonProperty(PropertyName = "buyerCity", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string BuyerCity { get; set; }
[JsonProperty(PropertyName = "buyerAddress2", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string BuyerAddress2 { get; set; }
[JsonProperty(PropertyName = "buyerAddress1", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string BuyerAddress1 { get; set; }
[JsonProperty(PropertyName = "buyerName", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string BuyerName { get; set; }
[JsonProperty(PropertyName = "physical", DefaultValueHandling = DefaultValueHandling.Ignore)]
public bool Physical { get; set; }
[JsonProperty(PropertyName = "redirectURL", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string RedirectURL { get; set; }
[JsonProperty(PropertyName = "notificationURL", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string NotificationURL { get; set; }
[JsonProperty(PropertyName = "extendedNotifications", DefaultValueHandling = DefaultValueHandling.Ignore)]
public bool ExtendedNotifications { get; set; }
[JsonProperty(PropertyName = "fullNotifications", DefaultValueHandling = DefaultValueHandling.Ignore)]
public bool FullNotifications { get; set; }
[JsonProperty(PropertyName = "transactionSpeed", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string TransactionSpeed { get; set; }
[JsonProperty(PropertyName = "buyerPhone", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string BuyerPhone { get; set; }
[JsonProperty(PropertyName = "posData", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string PosData { get; set; }
[JsonProperty(PropertyName = "itemCode", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string ItemCode { get; set; }
[JsonProperty(PropertyName = "itemDesc", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string ItemDesc { get; set; }
[JsonProperty(PropertyName = "orderId", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string OrderId { get; set; }
[JsonProperty(PropertyName = "currency", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Currency { get; set; }
[JsonProperty(PropertyName = "price", DefaultValueHandling = DefaultValueHandling.Ignore)]
public decimal Price { get; set; }
[JsonProperty(PropertyName = "notificationEmail", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string NotificationEmail { get; set; }
[JsonConverter(typeof(DateTimeJsonConverter))]
[JsonProperty(PropertyName = "expirationTime", DefaultValueHandling = DefaultValueHandling.Ignore)]
public DateTimeOffset? ExpirationTime { get; set; }
[JsonProperty(PropertyName = "status", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Status { get; set; }
[JsonProperty(PropertyName = "minerFees", DefaultValueHandling = DefaultValueHandling.Ignore)]
public Dictionary<string, MinerFeeInfo> MinerFees { get; set; }
[JsonProperty(PropertyName = "supportedTransactionCurrencies", DefaultValueHandling = DefaultValueHandling.Ignore)]
public Dictionary<string, InvoiceSupportedTransactionCurrency> SupportedTransactionCurrencies { get; set; }
[JsonProperty(PropertyName = "exchangeRates", DefaultValueHandling = DefaultValueHandling.Ignore)]
public Dictionary<string, Dictionary<string, decimal>> ExchangeRates { get; set; }
[JsonProperty(PropertyName = "refundable", DefaultValueHandling = DefaultValueHandling.Ignore)]
public bool Refundable { get; set; }
[JsonProperty(PropertyName = "taxIncluded", DefaultValueHandling = DefaultValueHandling.Ignore)]
public decimal? TaxIncluded { get; set; }
[JsonProperty(PropertyName = "nonce", DefaultValueHandling = DefaultValueHandling.Ignore)]
public long Nonce { get; set; }
[JsonProperty(PropertyName = "guid", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Guid { get; set; }
[JsonProperty(PropertyName = "token", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Token { get; set; }
}
}

View File

@ -90,6 +90,12 @@ namespace BTCPayServer.Models
get; set;
}
[JsonProperty("taxIncluded", DefaultValueHandling = DefaultValueHandling.Ignore)]
public decimal TaxIncluded
{
get; set;
}
//"currency":"USD"
[JsonProperty("currency")]
public string Currency

View File

@ -105,6 +105,7 @@ namespace BTCPayServer.Models.InvoicingModels
get;
set;
}
public string TaxIncluded { get; set; }
public BuyerInformation BuyerInformation
{
get;
@ -143,6 +144,6 @@ namespace BTCPayServer.Models.InvoicingModels
public DateTimeOffset MonitoringDate { get; internal set; }
public List<Data.InvoiceEventData> Events { get; internal set; }
public string NotificationEmail { get; internal set; }
public Dictionary<string, string> PosData { get; set; }
public Dictionary<string, object> PosData { get; set; }
}
}

View File

@ -15,6 +15,10 @@ namespace BTCPayServer.Models.InvoicingModels
{
get; set;
}
public int Total
{
get; set;
}
public string SearchTerm
{
get; set;

View File

@ -24,7 +24,6 @@ namespace BTCPayServer.Models.InvoicingModels
public bool IsModal { get; set; }
public bool IsLightning { get; set; }
public string CryptoCode { get; set; }
public string ServerUrl { get; set; }
public string InvoiceId { get; set; }
public string BtcAddress { get; set; }
public string BtcDue { get; set; }

View File

@ -5,9 +5,10 @@ using System.Threading.Tasks;
namespace BTCPayServer.Models.ServerViewModels
{
public class SparkServicesViewModel
public class LightningWalletServices
{
public string SparkLink { get; set; }
public string ServiceLink { get; set; }
public bool ShowQR { get; set; }
public string WalletName { get; internal set; }
}
}

View File

@ -0,0 +1,80 @@
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Models
{
public class StatusMessageModel
{
public StatusMessageModel()
{
}
public StatusMessageModel(string s)
{
if (string.IsNullOrEmpty(s))
return;
try
{
if (s.StartsWith("{", StringComparison.InvariantCultureIgnoreCase) &&
s.EndsWith("}", StringComparison.InvariantCultureIgnoreCase))
{
var model = JObject.Parse(s).ToObject<StatusMessageModel>();
Html = model.Html;
Message = model.Message;
Severity = model.Severity;
}
else
{
ParseNonJsonStatus(s);
}
}
catch (Exception)
{
ParseNonJsonStatus(s);
}
}
public string Message { get; set; }
public string Html { get; set; }
public StatusSeverity Severity { get; set; }
public string SeverityCSS
{
get
{
switch (Severity)
{
case StatusSeverity.Info:
return "info";
case StatusSeverity.Error:
return "danger";
case StatusSeverity.Success:
return "success";
default:
throw new ArgumentOutOfRangeException();
}
}
}
public override string ToString()
{
return JObject.FromObject(this).ToString(Formatting.None);
}
private void ParseNonJsonStatus(string s)
{
Message = s;
Severity = s.StartsWith("Error", StringComparison.InvariantCultureIgnoreCase)
? StatusSeverity.Error
: StatusSeverity.Success;
}
public enum StatusSeverity
{
Info,
Error,
Success
}
}
}

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Payments;
using BTCPayServer.Services;
using BTCPayServer.Validation;
using Microsoft.AspNetCore.Mvc.Rendering;
@ -11,16 +12,17 @@ namespace BTCPayServer.Models.StoreViewModels
{
public class CheckoutExperienceViewModel
{
class Format
public class Format
{
public string Name { get; set; }
public string Value { get; set; }
public PaymentMethodId PaymentId { get; set; }
}
public SelectList CryptoCurrencies { get; set; }
public SelectList Languages { get; set; }
[Display(Name = "Default crypto currency on checkout")]
public string DefaultCryptoCurrency { get; set; }
[Display(Name = "Default the default payment method on checkout")]
public string DefaultPaymentMethod { get; set; }
[Display(Name = "Default language on checkout")]
public string DefaultLang { get; set; }
[Display(Name = "Do not propose lightning payment if value of the invoice is above...")]
@ -47,15 +49,6 @@ namespace BTCPayServer.Models.StoreViewModels
[Display(Name = "Custom HTML title to display on Checkout page")]
public string HtmlTitle { get; set; }
public void SetCryptoCurrencies(ExplorerClientProvider explorerProvider, string defaultCrypto)
{
var choices = explorerProvider.GetAll().Select(o => new Format() { Name = o.Item1.CryptoCode, Value = o.Item1.CryptoCode }).ToArray();
var chosen = choices.FirstOrDefault(f => f.Value == defaultCrypto) ?? choices.FirstOrDefault();
CryptoCurrencies = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
DefaultCryptoCurrency = chosen.Name;
}
public void SetLanguages(LanguageService langService, string defaultLang)
{
defaultLang = langService.GetLanguages().Any(language => language.Code == defaultLang)? defaultLang : "en";

View File

@ -72,5 +72,6 @@ namespace BTCPayServer.Models.StoreViewModels
[Display(Name = "API Key")]
public string ApiKey { get; set; }
public string EncodedApiKey { get; set; }
public bool StoreNotConfigured { get; set; }
}
}

View File

@ -11,7 +11,7 @@ namespace BTCPayServer.Models.StoreViewModels
public bool Enabled { get; set; }
[Display(Name = "Integration Mode")]
public string Mode { get; set; } = "popup";
public string Mode { get; set; } = "inline";
public List<SelectListItem> Modes { get; } = new List<SelectListItem>
{

View File

@ -11,5 +11,6 @@ namespace BTCPayServer.Models.WalletViewModels
public bool SubstractFees { get; set; }
public decimal Amount { get; set; }
public string Destination { get; set; }
public bool NoChange { get; set; }
}
}

View File

@ -28,6 +28,10 @@ namespace BTCPayServer.Models.WalletViewModels
[Display(Name = "Fee rate (satoshi per byte)")]
[Required]
public int FeeSatoshiPerByte { get; set; }
[Display(Name = "Make sure no change UTXO is created")]
public bool NoChange { get; set; }
public bool AdvancedMode { get; set; }
public decimal? Rate { get; set; }
public int Divisibility { get; set; }
public string Fiat { get; set; }

View File

@ -33,12 +33,10 @@ namespace BTCPayServer.Payments.Bitcoin
private TaskCompletionSource<bool> _RunningTask;
private CancellationTokenSource _Cts;
BTCPayWalletProvider _Wallets;
BTCPayNetworkProvider _NetworkProvider;
public NBXplorerListener(ExplorerClientProvider explorerClients,
BTCPayWalletProvider wallets,
InvoiceRepository invoiceRepository,
BTCPayNetworkProvider networkProvider,
EventAggregator aggregator, Microsoft.Extensions.Hosting.IApplicationLifetime lifetime)
{
PollInterval = TimeSpan.FromMinutes(1.0);
@ -47,7 +45,6 @@ namespace BTCPayServer.Payments.Bitcoin
_ExplorerClients = explorerClients;
_Aggregator = aggregator;
_Lifetime = lifetime;
_NetworkProvider = networkProvider;
}
CompositeDisposable leases = new CompositeDisposable();
@ -373,7 +370,7 @@ namespace BTCPayServer.Payments.Bitcoin
invoice.SetPaymentMethod(paymentMethod);
}
wallet.InvalidateCache(strategy);
_Aggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1002, InvoiceEvent.ReceivedPayment){Payment = payment});
_Aggregator.Publish(new InvoiceEvent(invoice, 1002, InvoiceEvent.ReceivedPayment){Payment = payment});
return invoice;
}
public Task StopAsync(CancellationToken cancellationToken)

View File

@ -12,6 +12,21 @@ namespace BTCPayServer.Payments
public class PaymentFilter
{
class OrPaymentFilter : IPaymentFilter
{
private readonly IPaymentFilter _a;
private readonly IPaymentFilter _b;
public OrPaymentFilter(IPaymentFilter a, IPaymentFilter b)
{
_a = a;
_b = b;
}
public bool Match(PaymentMethodId paymentMethodId)
{
return _a.Match(paymentMethodId) || _b.Match(paymentMethodId);
}
}
class NeverPaymentFilter : IPaymentFilter
{
@ -54,6 +69,34 @@ namespace BTCPayServer.Payments
return paymentMethodId == _paymentMethodId;
}
}
class PredicateFilter : IPaymentFilter
{
private Func<PaymentMethodId, bool> predicate;
public PredicateFilter(Func<PaymentMethodId, bool> predicate)
{
this.predicate = predicate;
}
public bool Match(PaymentMethodId paymentMethodId)
{
return this.predicate(paymentMethodId);
}
}
public static IPaymentFilter Where(Func<PaymentMethodId, bool> predicate)
{
if (predicate == null)
throw new ArgumentNullException(nameof(predicate));
return new PredicateFilter(predicate);
}
public static IPaymentFilter Or(IPaymentFilter a, IPaymentFilter b)
{
if (a == null)
throw new ArgumentNullException(nameof(a));
if (b == null)
throw new ArgumentNullException(nameof(b));
return new OrPaymentFilter(a, b);
}
public static IPaymentFilter Never()
{
return NeverPaymentFilter.Instance;

View File

@ -7,6 +7,7 @@ using BTCPayServer.Lightning;
using BTCPayServer.Lightning.JsonConverters;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services.Invoices;
using NBitcoin;
using Newtonsoft.Json;
namespace BTCPayServer.Payments.Lightning
@ -16,17 +17,21 @@ namespace BTCPayServer.Payments.Lightning
[JsonConverter(typeof(LightMoneyJsonConverter))]
public LightMoney Amount { get; set; }
public string BOLT11 { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.UInt256JsonConverter))]
public uint256 PaymentHash { get; set; }
public string GetDestination(BTCPayNetwork network)
{
return GetPaymentId();
return BOLT11;
}
public decimal NetworkFee { get; set; }
public string GetPaymentId()
{
return BOLT11;
// Legacy, some old payments don't have the PaymentHash set
return PaymentHash?.ToString() ?? BOLT11;
}
public PaymentTypes GetPaymentType()

View File

@ -47,7 +47,7 @@ namespace BTCPayServer.Payments.Lightning
}
catch (OperationCanceledException) when (cts.IsCancellationRequested)
{
throw new PaymentMethodUnavailableException($"The lightning node did not replied in a timely maner");
throw new PaymentMethodUnavailableException($"The lightning node did not reply in a timely maner");
}
catch (Exception ex)
{
@ -78,7 +78,7 @@ namespace BTCPayServer.Payments.Lightning
}
catch (OperationCanceledException) when (cts.IsCancellationRequested)
{
throw new PaymentMethodUnavailableException($"The lightning node did not replied in a timely manner");
throw new PaymentMethodUnavailableException($"The lightning node did not reply in a timely manner");
}
catch (Exception ex)
{
@ -115,7 +115,7 @@ namespace BTCPayServer.Payments.Lightning
}
if (address == null)
throw new PaymentMethodUnavailableException($"DNS did not resolved {nodeInfo.Host}");
throw new PaymentMethodUnavailableException($"DNS did not resolve {nodeInfo.Host}");
using (var tcp = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp))
{

View File

@ -191,13 +191,14 @@ namespace BTCPayServer.Payments.Lightning
var payment = await _InvoiceRepository.AddPayment(listenedInvoice.InvoiceId, notification.PaidAt.Value, new LightningLikePaymentData()
{
BOLT11 = notification.BOLT11,
PaymentHash = BOLT11PaymentRequest.Parse(notification.BOLT11, network.NBitcoinNetwork).PaymentHash,
Amount = notification.Amount
}, network, accounted: true);
if (payment != null)
{
var invoice = await _InvoiceRepository.GetInvoice(listenedInvoice.InvoiceId);
if (invoice != null)
_Aggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1002, InvoiceEvent.ReceivedPayment){Payment = payment});
_Aggregator.Publish(new InvoiceEvent(invoice, 1002, InvoiceEvent.ReceivedPayment){Payment = payment});
}
}

View File

@ -67,10 +67,37 @@ namespace BTCPayServer.Payments
return CryptoCode + "_" + PaymentType.ToString();
}
public static bool TryParse(string str, out PaymentMethodId paymentMethodId)
{
paymentMethodId = null;
var parts = str.Split('_', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 0 || parts.Length > 2)
return false;
PaymentTypes type = PaymentTypes.BTCLike;
if (parts.Length == 2)
{
switch (parts[1].ToLowerInvariant())
{
case "btclike":
case "onchain":
type = PaymentTypes.BTCLike;
break;
case "lightninglike":
case "offchain":
type = PaymentTypes.LightningLike;
break;
default:
return false;
}
}
paymentMethodId = new PaymentMethodId(parts[0], type);
return true;
}
public static PaymentMethodId Parse(string str)
{
var parts = str.Split('_');
return new PaymentMethodId(parts[0], parts.Length == 1 ? PaymentTypes.BTCLike : Enum.Parse<PaymentTypes>(parts[1]));
if (!TryParse(str, out var result))
throw new FormatException("Invalid PaymentMethodId");
return result;
}
}
}

View File

@ -11,6 +11,7 @@
"BTCPAY_BTCEXTERNALLNDGRPC": "type=lnd-grpc;server=https://lnd:lnd@127.0.0.1:53280/;allowinsecure=true",
"BTCPAY_BTCEXTERNALLNDREST": "type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/lnd-rest/btc/;allowinsecure=true;macaroonfilepath=D:\\admin.macaroon",
"BTCPAY_BTCEXPLORERURL": "http://127.0.0.1:32838/",
"BTCPAY_DISABLE-REGISTRATION": "false",
"ASPNETCORE_ENVIRONMENT": "Development",
"BTCPAY_CHAINS": "btc,ltc",
"BTCPAY_POSTGRES": "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver"
@ -32,6 +33,7 @@
"BTCPAY_BTCEXTERNALSPARK": "server=https://127.0.0.1:53280/spark/btc/;cookiefile=fake",
"BTCPAY_BTCEXTERNALCHARGE": "server=https://127.0.0.1:53280/spark/btc/;cookiefilepath=fake",
"BTCPAY_BTCEXPLORERURL": "http://127.0.0.1:32838/",
"BTCPAY_DISABLE-REGISTRATION": "false",
"ASPNETCORE_ENVIRONMENT": "Development",
"BTCPAY_CHAINS": "btc,ltc",
"BTCPAY_POSTGRES": "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver",

View File

@ -198,44 +198,6 @@ namespace BTCPayServer.Security
}
yield return token;
}
private bool IsBitpayAPI(HttpContext httpContext, bool bitpayAuth)
{
if (!httpContext.Request.Path.HasValue)
return false;
var isJson = (httpContext.Request.ContentType ?? string.Empty).StartsWith("application/json", StringComparison.OrdinalIgnoreCase);
var path = httpContext.Request.Path.Value;
if (
bitpayAuth &&
(path == "/invoices" || path == "/invoices/") &&
httpContext.Request.Method == "POST" &&
isJson)
return true;
if (
bitpayAuth &&
(path == "/invoices" || path == "/invoices/") &&
httpContext.Request.Method == "GET")
return true;
if (
path.StartsWith("/invoices/", StringComparison.OrdinalIgnoreCase) &&
httpContext.Request.Method == "GET" &&
(isJson || httpContext.Request.Query.ContainsKey("token")))
return true;
if (path.Equals("/rates", StringComparison.OrdinalIgnoreCase) &&
httpContext.Request.Method == "GET")
return true;
if (
path.Equals("/tokens", StringComparison.Ordinal) &&
(httpContext.Request.Method == "GET" || httpContext.Request.Method == "POST"))
return true;
return false;
}
}
internal static void AddAuthentication(IServiceCollection services, Action<BitpayAuthOptions> bitpayAuth = null)
{

View File

@ -0,0 +1,66 @@
using System;
using System.Threading.Tasks;
using BTCPayServer.Controllers;
using BTCPayServer.Models.AppViewModels;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
namespace BTCPayServer.Services.Apps
{
public class AppHub: Hub
{
public const string InvoiceCreated = "InvoiceCreated";
public const string PaymentReceived = "PaymentReceived";
public const string InfoUpdated = "InfoUpdated";
public const string InvoiceError = "InvoiceError";
private readonly AppsPublicController _AppsPublicController;
public AppHub(AppsPublicController appsPublicController)
{
_AppsPublicController = appsPublicController;
}
public async Task ListenToCrowdfundApp(string appId)
{
if (Context.Items.ContainsKey("app"))
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, Context.Items["app"].ToString());
Context.Items.Remove("app");
}
Context.Items.Add("app", appId);
await Groups.AddToGroupAsync(Context.ConnectionId, appId);
}
public async Task CreateInvoice(ContributeToCrowdfund model)
{
model.RedirectToCheckout = false;
_AppsPublicController.ControllerContext.HttpContext = Context.GetHttpContext();
try
{
var result =
await _AppsPublicController.ContributeToCrowdfund(Context.Items["app"].ToString(), model);
switch (result)
{
case OkObjectResult okObjectResult:
await Clients.Caller.SendCoreAsync(InvoiceCreated, new[] {okObjectResult.Value.ToString()});
break;
case ObjectResult objectResult:
await Clients.Caller.SendCoreAsync(InvoiceError, new[] {objectResult.Value});
break;
default:
await Clients.Caller.SendCoreAsync(InvoiceError, System.Array.Empty<object>());
break;
}
}
catch (Exception)
{
await Clients.Caller.SendCoreAsync(InvoiceError, System.Array.Empty<object>());
}
}
}
}

View File

@ -0,0 +1,78 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.HostedServices;
using BTCPayServer.Logging;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Rating;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using NBitcoin;
namespace BTCPayServer.Services.Apps
{
public class AppHubStreamer : EventHostedServiceBase
{
private readonly AppService _appService;
private IHubContext<AppHub> _HubContext;
public AppHubStreamer(EventAggregator eventAggregator,
IHubContext<AppHub> hubContext,
AppService appService) : base(eventAggregator)
{
_appService = appService;
_HubContext = hubContext;
}
protected override void SubscibeToEvents()
{
Subscribe<InvoiceEvent>();
Subscribe<AppsController.AppUpdated>();
}
protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken)
{
if (evt is InvoiceEvent invoiceEvent)
{
foreach (var appId in AppService.GetAppInternalTags(invoiceEvent.Invoice.InternalTags))
{
if (invoiceEvent.Name == InvoiceEvent.ReceivedPayment)
{
var data = invoiceEvent.Payment.GetCryptoPaymentData();
await _HubContext.Clients.Group(appId).SendCoreAsync(AppHub.PaymentReceived, new object[]
{
data.GetValue(),
invoiceEvent.Payment.GetCryptoCode(),
Enum.GetName(typeof(PaymentTypes),
invoiceEvent.Payment.GetPaymentMethodId().PaymentType)
}, cancellationToken);
}
await InfoUpdated(appId);
}
}
else if (evt is AppsController.AppUpdated app)
{
await InfoUpdated(app.AppId);
}
}
private async Task InfoUpdated(string appId)
{
var info = await _appService.GetAppInfo(appId);
await _HubContext.Clients.Group(appId).SendCoreAsync(AppHub.InfoUpdated, new object[] { info });
}
}
}

View File

@ -0,0 +1,400 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Filters;
using BTCPayServer.Models;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Rating;
using BTCPayServer.Security;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using Ganss.XSS;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using NBitpayClient;
using YamlDotNet.RepresentationModel;
using static BTCPayServer.Controllers.AppsController;
namespace BTCPayServer.Services.Apps
{
public class AppService
{
ApplicationDbContextFactory _ContextFactory;
private readonly InvoiceRepository _InvoiceRepository;
CurrencyNameTable _Currencies;
private readonly RateFetcher _RateFetcher;
private readonly HtmlSanitizer _HtmlSanitizer;
private readonly BTCPayNetworkProvider _Networks;
public CurrencyNameTable Currencies => _Currencies;
public AppService(ApplicationDbContextFactory contextFactory,
InvoiceRepository invoiceRepository,
BTCPayNetworkProvider networks,
CurrencyNameTable currencies,
RateFetcher rateFetcher,
HtmlSanitizer htmlSanitizer)
{
_ContextFactory = contextFactory;
_InvoiceRepository = invoiceRepository;
_Currencies = currencies;
_RateFetcher = rateFetcher;
_HtmlSanitizer = htmlSanitizer;
_Networks = networks;
}
public async Task<object> GetAppInfo(string appId)
{
var app = await GetApp(appId, AppType.Crowdfund, true);
return await GetInfo(app);
}
private async Task<ViewCrowdfundViewModel> GetInfo(AppData appData, string statusMessage = null)
{
var settings = appData.GetSettings<CrowdfundSettings>();
var resetEvery = settings.StartDate.HasValue ? settings.ResetEvery : CrowdfundResetEvery.Never;
DateTime? lastResetDate = null;
DateTime? nextResetDate = null;
if (resetEvery != CrowdfundResetEvery.Never)
{
lastResetDate = settings.StartDate.Value;
nextResetDate = lastResetDate.Value;
while (DateTime.Now >= nextResetDate)
{
lastResetDate = nextResetDate;
switch (resetEvery)
{
case CrowdfundResetEvery.Hour:
nextResetDate = lastResetDate.Value.AddHours(settings.ResetEveryAmount);
break;
case CrowdfundResetEvery.Day:
nextResetDate = lastResetDate.Value.AddDays(settings.ResetEveryAmount);
break;
case CrowdfundResetEvery.Month:
nextResetDate = lastResetDate.Value.AddMonths(settings.ResetEveryAmount);
break;
case CrowdfundResetEvery.Year:
nextResetDate = lastResetDate.Value.AddYears(settings.ResetEveryAmount);
break;
}
}
}
var invoices = await GetInvoicesForApp(appData, lastResetDate);
var completeInvoices = invoices.Where(entity => entity.Status == InvoiceStatus.Complete || entity.Status == InvoiceStatus.Confirmed).ToArray();
var pendingInvoices = invoices.Where(entity => !(entity.Status == InvoiceStatus.Complete || entity.Status == InvoiceStatus.Confirmed)).ToArray();
var rateRules = appData.StoreData.GetStoreBlob().GetRateRules(_Networks);
var pendingPaymentStats = GetCurrentContributionAmountStats(pendingInvoices, !settings.EnforceTargetAmount);
var paymentStats = GetCurrentContributionAmountStats(completeInvoices, !settings.EnforceTargetAmount);
var currentAmount = await GetCurrentContributionAmount(
paymentStats,
settings.TargetCurrency, rateRules);
var currentPendingAmount = await GetCurrentContributionAmount(
pendingPaymentStats,
settings.TargetCurrency, rateRules);
var perkCount = invoices
.Where(entity => !string.IsNullOrEmpty(entity.ProductInformation.ItemCode))
.GroupBy(entity => entity.ProductInformation.ItemCode)
.ToDictionary(entities => entities.Key, entities => entities.Count());
var perks = Parse(settings.PerksTemplate, settings.TargetCurrency);
if (settings.SortPerksByPopularity)
{
var ordered = perkCount.OrderByDescending(pair => pair.Value);
var newPerksOrder = ordered
.Select(keyValuePair => perks.SingleOrDefault(item => item.Id == keyValuePair.Key))
.Where(matchingPerk => matchingPerk != null)
.ToList();
var remainingPerks = perks.Where(item => !newPerksOrder.Contains(item));
newPerksOrder.AddRange(remainingPerks);
perks = newPerksOrder.ToArray();
}
return new ViewCrowdfundViewModel()
{
Title = settings.Title,
Tagline = settings.Tagline,
Description = settings.Description,
CustomCSSLink = settings.CustomCSSLink,
MainImageUrl = settings.MainImageUrl,
EmbeddedCSS = settings.EmbeddedCSS,
StoreId = appData.StoreDataId,
AppId = appData.Id,
StartDate = settings.StartDate?.ToUniversalTime(),
EndDate = settings.EndDate?.ToUniversalTime(),
TargetAmount = settings.TargetAmount,
TargetCurrency = settings.TargetCurrency,
EnforceTargetAmount = settings.EnforceTargetAmount,
StatusMessage = statusMessage,
Perks = perks,
DisqusEnabled = settings.DisqusEnabled,
SoundsEnabled = settings.SoundsEnabled,
DisqusShortname = settings.DisqusShortname,
AnimationsEnabled = settings.AnimationsEnabled,
ResetEveryAmount = settings.ResetEveryAmount,
DisplayPerksRanking = settings.DisplayPerksRanking,
PerkCount = perkCount,
NeverReset = settings.ResetEvery == CrowdfundResetEvery.Never,
CurrencyData = _Currencies.GetCurrencyData(settings.TargetCurrency, true),
Info = new ViewCrowdfundViewModel.CrowdfundInfo()
{
TotalContributors = invoices.Length,
CurrentPendingAmount = currentPendingAmount,
CurrentAmount = currentAmount,
ProgressPercentage = (currentAmount / settings.TargetAmount) * 100,
PendingProgressPercentage = (currentPendingAmount / settings.TargetAmount) * 100,
LastUpdated = DateTime.Now,
PaymentStats = paymentStats,
PendingPaymentStats = pendingPaymentStats,
LastResetDate = lastResetDate,
NextResetDate = nextResetDate
}
};
}
public static string GetCrowdfundOrderId(string appId) => $"crowdfund-app_{appId}";
public static string GetAppInternalTag(string appId) => $"APP#{appId}";
public static string[] GetAppInternalTags(IEnumerable<string> tags)
{
return tags == null ? Array.Empty<string>() : tags
.Where(t => t.StartsWith("APP#", StringComparison.InvariantCulture))
.Select(t => t.Substring("APP#".Length)).ToArray();
}
private async Task<InvoiceEntity[]> GetInvoicesForApp(AppData appData, DateTime? startDate = null)
{
var invoices = await _InvoiceRepository.GetInvoices(new InvoiceQuery()
{
StoreId = new[] { appData.StoreData.Id },
OrderId = appData.TagAllInvoices ? null : new[] { GetCrowdfundOrderId(appData.Id) },
Status = new string[]{
InvoiceState.ToString(InvoiceStatus.New),
InvoiceState.ToString(InvoiceStatus.Paid),
InvoiceState.ToString(InvoiceStatus.Confirmed),
InvoiceState.ToString(InvoiceStatus.Complete)},
StartDate = startDate
});
// Old invoices may have invoices which were not tagged
invoices = invoices.Where(inv => inv.Version < InvoiceEntity.InternalTagSupport_Version ||
inv.InternalTags.Contains(GetAppInternalTag(appData.Id))).ToArray();
return invoices;
}
public async Task<StoreData[]> GetOwnedStores(string userId)
{
using (var ctx = _ContextFactory.CreateContext())
{
return await ctx.UserStore
.Where(us => us.ApplicationUserId == userId && us.Role == StoreRoles.Owner)
.Select(u => u.StoreData)
.ToArrayAsync();
}
}
public async Task<bool> DeleteApp(AppData appData)
{
using (var ctx = _ContextFactory.CreateContext())
{
ctx.Apps.Add(appData);
ctx.Entry<AppData>(appData).State = EntityState.Deleted;
return await ctx.SaveChangesAsync() == 1;
}
}
public async Task<ListAppsViewModel.ListAppViewModel[]> GetAllApps(string userId, bool allowNoUser = false)
{
using (var ctx = _ContextFactory.CreateContext())
{
return await ctx.UserStore
.Where(us => (allowNoUser && string.IsNullOrEmpty(userId)) || us.ApplicationUserId == userId)
.Join(ctx.Apps, us => us.StoreDataId, app => app.StoreDataId,
(us, app) =>
new ListAppsViewModel.ListAppViewModel()
{
IsOwner = us.Role == StoreRoles.Owner,
StoreId = us.StoreDataId,
StoreName = us.StoreData.StoreName,
AppName = app.Name,
AppType = app.AppType,
Id = app.Id
})
.ToArrayAsync();
}
}
public async Task<AppData> GetApp(string appId, AppType appType, bool includeStore = false)
{
using (var ctx = _ContextFactory.CreateContext())
{
var query = ctx.Apps
.Where(us => us.Id == appId &&
us.AppType == appType.ToString());
if (includeStore)
{
query = query.Include(data => data.StoreData);
}
return await query.FirstOrDefaultAsync();
}
}
public async Task<StoreData> GetStore(AppData app)
{
using (var ctx = _ContextFactory.CreateContext())
{
return await ctx.Stores.FirstOrDefaultAsync(s => s.Id == app.StoreDataId);
}
}
public ViewPointOfSaleViewModel.Item[] Parse(string template, string currency)
{
if (string.IsNullOrWhiteSpace(template))
return Array.Empty<ViewPointOfSaleViewModel.Item>();
var input = new StringReader(template);
YamlStream stream = new YamlStream();
stream.Load(input);
var root = (YamlMappingNode)stream.Documents[0].RootNode;
return root
.Children
.Select(kv => new PosHolder { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlMappingNode })
.Where(kv => kv.Value != null)
.Select(c => new ViewPointOfSaleViewModel.Item()
{
Description = _HtmlSanitizer.Sanitize(c.GetDetailString("description")),
Id = c.Key,
Image = _HtmlSanitizer.Sanitize(c.GetDetailString("image")),
Title = _HtmlSanitizer.Sanitize(c.GetDetailString("title") ?? c.Key),
Price = c.GetDetail("price")
.Select(cc => new ViewPointOfSaleViewModel.Item.ItemPrice()
{
Value = decimal.Parse(cc.Value.Value, CultureInfo.InvariantCulture),
Formatted = Currencies.FormatCurrency(cc.Value.Value, currency)
}).Single(),
Custom = c.GetDetailString("custom") == "true"
})
.ToArray();
}
public async Task<decimal> GetCurrentContributionAmount(Dictionary<string, decimal> stats, string primaryCurrency, RateRules rateRules)
{
var result = new List<decimal>();
var ratesTask = _RateFetcher.FetchRates(
stats.Keys
.Select((x) => new CurrencyPair(primaryCurrency, PaymentMethodId.Parse(x).CryptoCode))
.Distinct()
.ToHashSet(),
rateRules).Select(async rateTask =>
{
var (key, value) = rateTask;
var tResult = await value;
var rate = tResult.BidAsk?.Bid;
if (rate == null)
return;
foreach (var stat in stats)
{
if (string.Equals(PaymentMethodId.Parse(stat.Key).CryptoCode, key.Right,
StringComparison.InvariantCultureIgnoreCase))
{
result.Add((1m / rate.Value) * stat.Value);
}
}
});
await Task.WhenAll(ratesTask);
return result.Sum();
}
public Dictionary<string, decimal> GetCurrentContributionAmountStats(InvoiceEntity[] invoices, bool softcap)
{
return invoices
.SelectMany(p =>
{
// For hardcap, we count newly created invoices as part of the contributions
if (!softcap && p.Status == InvoiceStatus.New)
return new[] { (Key: p.ProductInformation.Currency, Value: p.ProductInformation.Price) };
// If the user get a donation via other mean, he can register an invoice manually for such amount
// then mark the invoice as complete
var payments = p.GetPayments();
if (payments.Count == 0 &&
p.ExceptionStatus == InvoiceExceptionStatus.Marked &&
p.Status == InvoiceStatus.Complete)
return new[] { (Key: p.ProductInformation.Currency, Value: p.ProductInformation.Price) };
// If an invoice has been marked invalid, remove the contribution
if (p.ExceptionStatus == InvoiceExceptionStatus.Marked &&
p.Status == InvoiceStatus.Invalid)
return new[] { (Key: p.ProductInformation.Currency, Value: 0m) };
// Else, we just sum the payments
return payments
.Select(pay => (Key: pay.GetPaymentMethodId().ToString(), Value: pay.GetCryptoPaymentData().GetValue()))
.ToArray();
})
.GroupBy(p => p.Key)
.ToDictionary(p => p.Key, p => p.Select(v => v.Value).Sum());
}
private class PosHolder
{
public string Key { get; set; }
public YamlMappingNode Value { get; set; }
public IEnumerable<PosScalar> GetDetail(string field)
{
var res = Value.Children
.Where(kv => kv.Value != null)
.Select(kv => new PosScalar { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlScalarNode })
.Where(cc => cc.Key == field);
return res;
}
public string GetDetailString(string field)
{
return GetDetail(field).FirstOrDefault()?.Value?.Value;
}
}
private class PosScalar
{
public string Key { get; set; }
public YamlScalarNode Value { get; set; }
}
public async Task<AppData> GetAppDataIfOwner(string userId, string appId, AppType? type = null)
{
if (userId == null || appId == null)
return null;
using (var ctx = _ContextFactory.CreateContext())
{
var app = await ctx.UserStore
.Where(us => us.ApplicationUserId == userId && us.Role == StoreRoles.Owner)
.SelectMany(us => us.StoreData.Apps.Where(a => a.Id == appId))
.FirstOrDefaultAsync();
if (app == null)
return null;
if (type != null && type.Value.ToString() != app.AppType)
return null;
return app;
}
}
}
}

View File

@ -7,6 +7,7 @@ namespace BTCPayServer.Services.Apps
{
public enum AppType
{
PointOfSale
PointOfSale,
Crowdfund
}
}

View File

@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Services.Apps
{
public class CrowdfundSettings
{
public string Title { get; set; }
public string Description { get; set; }
public bool Enabled { get; set; } = false;
public DateTime? StartDate { get; set; }
public DateTime? EndDate { get; set; }
public string TargetCurrency { get; set; }
public decimal? TargetAmount { get; set; }
public bool EnforceTargetAmount { get; set; }
public string CustomCSSLink { get; set; }
public string MainImageUrl { get; set; }
public string NotificationUrl { get; set; }
public string Tagline { get; set; }
public string EmbeddedCSS { get; set; }
public string PerksTemplate { get; set; }
public bool DisqusEnabled { get; set; } = false;
public bool SoundsEnabled { get; set; } = true;
public string DisqusShortname { get; set; }
public bool AnimationsEnabled { get; set; } = true;
public int ResetEveryAmount { get; set; } = 1;
public CrowdfundResetEvery ResetEvery { get; set; } = CrowdfundResetEvery.Never;
[Obsolete("Use AppData.TagAllInvoices instead")]
public bool UseAllStoreInvoices { get; set; }
public bool DisplayPerksRanking { get; set; }
public bool SortPerksByPopularity { get; set; }
}
public enum CrowdfundResetEvery
{
Never,
Hour,
Day,
Month,
Year
}
}

View File

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Services
{
public interface IBackgroundJobClient
{
void Schedule(Func<Task> act, TimeSpan zero);
}
}

View File

@ -5,6 +5,7 @@ using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using Newtonsoft.Json;
namespace BTCPayServer.Services.Invoices.Export
@ -12,10 +13,12 @@ namespace BTCPayServer.Services.Invoices.Export
public class InvoiceExport
{
public BTCPayNetworkProvider Networks { get; }
public CurrencyNameTable Currencies { get; }
public InvoiceExport(BTCPayNetworkProvider networks)
public InvoiceExport(BTCPayNetworkProvider networks, CurrencyNameTable currencies)
{
Networks = networks;
Currencies = currencies;
}
public string Process(InvoiceEntity[] invoices, string fileFormat)
{
@ -52,6 +55,7 @@ namespace BTCPayServer.Services.Invoices.Export
private IEnumerable<ExportInvoiceHolder> convertFromDb(InvoiceEntity invoice)
{
var exportList = new List<ExportInvoiceHolder>();
var currency = Currencies.GetNumberFormatInfo(invoice.ProductInformation.Currency, true);
var invoiceDue = invoice.ProductInformation.Price;
// in this first version we are only exporting invoices that were paid
@ -60,7 +64,6 @@ namespace BTCPayServer.Services.Invoices.Export
// not accounted payments are payments which got double spent like RBfed
if (!payment.Accounted)
continue;
var cryptoCode = payment.GetPaymentMethodId().CryptoCode;
var pdata = payment.GetCryptoPaymentData();
@ -77,13 +80,13 @@ namespace BTCPayServer.Services.Invoices.Export
PaymentType = payment.GetPaymentMethodId().PaymentType == Payments.PaymentTypes.BTCLike ? "OnChain" : "OffChain",
Destination = payment.GetCryptoPaymentData().GetDestination(Networks.GetNetwork(cryptoCode)),
Paid = pdata.GetValue().ToString(CultureInfo.InvariantCulture),
PaidCurrency = (pdata.GetValue() * pmethod.Rate).ToString(CultureInfo.InvariantCulture),
PaidCurrency = Math.Round(pdata.GetValue() * pmethod.Rate, currency.NumberDecimalDigits).ToString(CultureInfo.InvariantCulture),
// Adding NetworkFee because Paid doesn't take into account network fees
// so if fee is 10000 satoshis, customer can essentially send infinite number of tx
// and merchant effectivelly would receive 0 BTC, invoice won't be paid
// while looking just at export you could sum Paid and assume merchant "received payments"
NetworkFee = payment.NetworkFee.ToString(CultureInfo.InvariantCulture),
InvoiceDue = invoiceDue,
InvoiceDue = Math.Round(invoiceDue, currency.NumberDecimalDigits),
OrderId = invoice.OrderId,
StoreId = invoice.StoreId,
InvoiceId = invoice.Id,
@ -99,6 +102,7 @@ namespace BTCPayServer.Services.Invoices.Export
InvoiceItemDesc = invoice.ProductInformation.ItemDesc,
InvoicePrice = invoice.ProductInformation.Price,
InvoiceCurrency = invoice.ProductInformation.Currency,
BuyerEmail = invoice.BuyerInformation?.BuyerEmail
};
exportList.Add(target);
@ -136,5 +140,6 @@ namespace BTCPayServer.Services.Invoices.Export
public string InvoiceFullStatus { get; set; }
public string InvoiceStatus { get; set; }
public string InvoiceExceptionStatus { get; set; }
public string BuyerEmail { get; set; }
}
}

View File

@ -91,6 +91,12 @@ namespace BTCPayServer.Services.Invoices
get; set;
}
[JsonProperty(PropertyName = "taxIncluded", DefaultValueHandling = DefaultValueHandling.Ignore)]
public decimal TaxIncluded
{
get; set;
}
[JsonProperty(PropertyName = "currency")]
public string Currency
{
@ -107,6 +113,8 @@ namespace BTCPayServer.Services.Invoices
}
public class InvoiceEntity
{
public const int InternalTagSupport_Version = 1;
public int Version { get; set; } = 1;
public string Id
{
get; set;
@ -158,6 +166,9 @@ namespace BTCPayServer.Services.Invoices
set;
}
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public HashSet<string> InternalTags { get; set; } = new HashSet<string>();
[Obsolete("Use GetDerivationStrategies instead")]
public string DerivationStrategy
{
@ -344,6 +355,7 @@ namespace BTCPayServer.Services.Invoices
InvoiceResponse dto = new InvoiceResponse
{
Id = Id,
StoreId = StoreId,
OrderId = OrderId,
PosData = PosData,
CurrentTime = DateTimeOffset.UtcNow,

View File

@ -93,6 +93,16 @@ retry:
}
}
public async Task<AppData[]> GetAppsTaggingStore(string storeId)
{
if (storeId == null)
throw new ArgumentNullException(nameof(storeId));
using (var ctx = _ContextFactory.CreateContext())
{
return await ctx.Apps.Where(a => a.StoreDataId == storeId && a.TagAllInvoices).ToArrayAsync();
}
}
public async Task UpdateInvoice(string invoiceId, UpdateCustomerModel data)
{
using (var ctx = _ContextFactory.CreateContext())
@ -178,7 +188,6 @@ retry:
textSearch.Add(invoice.StoreId);
AddToTextSearch(invoice.Id, textSearch.ToArray());
return invoice;
}
@ -420,91 +429,108 @@ retry:
return entity;
}
private IQueryable<Data.InvoiceData> GetInvoiceQuery(ApplicationDbContext context, InvoiceQuery queryObject)
{
IQueryable<Data.InvoiceData> query = context.Invoices;
if (!string.IsNullOrEmpty(queryObject.InvoiceId))
{
query = query.Where(i => i.Id == queryObject.InvoiceId);
}
if (queryObject.StoreId != null && queryObject.StoreId.Length > 0)
{
var stores = queryObject.StoreId.ToHashSet();
query = query.Where(i => stores.Contains(i.StoreDataId));
}
if (queryObject.UserId != null)
{
query = query.Where(i => i.StoreData.UserStores.Any(u => u.ApplicationUserId == queryObject.UserId));
}
if (!string.IsNullOrEmpty(queryObject.TextSearch))
{
var ids = new HashSet<string>(SearchInvoice(queryObject.TextSearch));
if (ids.Count == 0)
{
// Hacky way to return an empty query object. The nice way is much too elaborate:
// https://stackoverflow.com/questions/33305495/how-to-return-empty-iqueryable-in-an-async-repository-method
return query.Where(x => false);
}
query = query.Where(i => ids.Contains(i.Id));
}
if (queryObject.StartDate != null)
query = query.Where(i => queryObject.StartDate.Value <= i.Created);
if (queryObject.EndDate != null)
query = query.Where(i => i.Created <= queryObject.EndDate.Value);
if (queryObject.OrderId != null && queryObject.OrderId.Length > 0)
{
var statusSet = queryObject.OrderId.ToHashSet();
query = query.Where(i => statusSet.Contains(i.OrderId));
}
if (queryObject.ItemCode != null && queryObject.ItemCode.Length > 0)
{
var statusSet = queryObject.ItemCode.ToHashSet();
query = query.Where(i => statusSet.Contains(i.ItemCode));
}
if (queryObject.Status != null && queryObject.Status.Length > 0)
{
var statusSet = queryObject.Status.ToHashSet();
query = query.Where(i => statusSet.Contains(i.Status));
}
if (queryObject.Unusual != null)
{
var unused = queryObject.Unusual.Value;
query = query.Where(i => unused == (i.Status == "invalid" || i.ExceptionStatus != null));
}
if (queryObject.ExceptionStatus != null && queryObject.ExceptionStatus.Length > 0)
{
var exceptionStatusSet = queryObject.ExceptionStatus.Select(s => NormalizeExceptionStatus(s)).ToHashSet();
query = query.Where(i => exceptionStatusSet.Contains(i.ExceptionStatus));
}
query = query.OrderByDescending(q => q.Created);
if (queryObject.Skip != null)
query = query.Skip(queryObject.Skip.Value);
if (queryObject.Count != null)
query = query.Take(queryObject.Count.Value);
return query;
}
public async Task<int> GetInvoicesTotal(InvoiceQuery queryObject)
{
using (var context = _ContextFactory.CreateContext())
{
var query = GetInvoiceQuery(context, queryObject);
return await query.CountAsync();
}
}
public async Task<InvoiceEntity[]> GetInvoices(InvoiceQuery queryObject)
{
using (var context = _ContextFactory.CreateContext())
{
IQueryable<Data.InvoiceData> query = context
.Invoices
.Include(o => o.Payments)
var query = GetInvoiceQuery(context, queryObject);
query = query.Include(o => o.Payments)
.Include(o => o.RefundAddresses);
if (queryObject.IncludeAddresses)
query = query.Include(o => o.HistoricalAddressInvoices).Include(o => o.AddressInvoices);
if (queryObject.IncludeEvents)
query = query.Include(o => o.Events);
if (!string.IsNullOrEmpty(queryObject.InvoiceId))
{
query = query.Where(i => i.Id == queryObject.InvoiceId);
}
if (queryObject.StoreId != null && queryObject.StoreId.Length > 0)
{
var stores = queryObject.StoreId.ToHashSet();
query = query.Where(i => stores.Contains(i.StoreDataId));
}
if (queryObject.UserId != null)
{
query = query.Where(i => i.StoreData.UserStores.Any(u => u.ApplicationUserId == queryObject.UserId));
}
if (!string.IsNullOrEmpty(queryObject.TextSearch))
{
var ids = new HashSet<string>(SearchInvoice(queryObject.TextSearch));
if (ids.Count == 0)
return Array.Empty<InvoiceEntity>();
query = query.Where(i => ids.Contains(i.Id));
}
if (queryObject.StartDate != null)
query = query.Where(i => queryObject.StartDate.Value <= i.Created);
if (queryObject.EndDate != null)
query = query.Where(i => i.Created <= queryObject.EndDate.Value);
if (queryObject.OrderId != null && queryObject.OrderId.Length > 0)
{
var statusSet = queryObject.OrderId.ToHashSet();
query = query.Where(i => statusSet.Contains(i.OrderId));
}
if (queryObject.ItemCode != null && queryObject.ItemCode.Length > 0)
{
var statusSet = queryObject.ItemCode.ToHashSet();
query = query.Where(i => statusSet.Contains(i.ItemCode));
}
if (queryObject.Status != null && queryObject.Status.Length > 0)
{
var statusSet = queryObject.Status.ToHashSet();
query = query.Where(i => statusSet.Contains(i.Status));
}
if (queryObject.Unusual != null)
{
var unused = queryObject.Unusual.Value;
query = query.Where(i => unused == (i.Status == "invalid" || i.ExceptionStatus != null));
}
if (queryObject.ExceptionStatus != null && queryObject.ExceptionStatus.Length > 0)
{
var exceptionStatusSet = queryObject.ExceptionStatus.Select(s => NormalizeExceptionStatus(s)).ToHashSet();
query = query.Where(i => exceptionStatusSet.Contains(i.ExceptionStatus));
}
query = query.OrderByDescending(q => q.Created);
if (queryObject.Skip != null)
query = query.Skip(queryObject.Skip.Value);
if (queryObject.Count != null)
query = query.Take(queryObject.Count.Value);
var data = await query.ToArrayAsync().ConfigureAwait(false);
return data.Select(ToEntity).ToArray();
}
}
private string NormalizeExceptionStatus(string status)

View File

@ -1,48 +1,40 @@
using BTCPayServer.Logging;
using Microsoft.Extensions.Logging;
using Hangfire;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Mail;
using System.Threading.Tasks;
namespace BTCPayServer.Services.Mails
{
// This class is used by the application to send email for account confirmation and password reset.
// For more details see https://go.microsoft.com/fwlink/?LinkID=532713
public class EmailSender : IEmailSender
public abstract class EmailSender : IEmailSender
{
IBackgroundJobClient _JobClient;
SettingsRepository _Repository;
public EmailSender(IBackgroundJobClient jobClient, SettingsRepository repository)
public EmailSender(IBackgroundJobClient jobClient)
{
if (jobClient == null)
throw new ArgumentNullException(nameof(jobClient));
_JobClient = jobClient;
_Repository = repository;
}
public async Task SendEmailAsync(string email, string subject, string message)
{
var settings = await _Repository.GetSettingAsync<EmailSettings>() ?? new EmailSettings();
if (!settings.IsComplete())
{
Logs.Configuration.LogWarning("Should have sent email, but email settings are not configured");
return;
}
_JobClient.Schedule(() => SendMailCore(email, subject, message), TimeSpan.Zero);
return;
_JobClient = jobClient ?? throw new ArgumentNullException(nameof(jobClient));
}
public async Task SendMailCore(string email, string subject, string message)
public void SendEmail(string email, string subject, string message)
{
var settings = await _Repository.GetSettingAsync<EmailSettings>() ?? new EmailSettings();
if (!settings.IsComplete())
throw new InvalidOperationException("Email settings not configured");
var smtp = settings.CreateSmtpClient();
MailMessage mail = new MailMessage(settings.From, email, subject, message);
mail.IsBodyHtml = true;
await smtp.SendMailAsync(mail);
_JobClient.Schedule(async () =>
{
var emailSettings = await GetEmailSettings();
if (emailSettings?.IsComplete() != true)
{
Logs.Configuration.LogWarning("Should have sent email, but email settings are not configured");
return;
}
var smtp = emailSettings.CreateSmtpClient();
var mail = new MailMessage(emailSettings.From, email, subject, message)
{
IsBodyHtml = true
};
await smtp.SendMailAsync(mail);
}, TimeSpan.Zero);
}
public abstract Task<EmailSettings> GetEmailSettings();
}
}

View File

@ -0,0 +1,31 @@
using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;
using BTCPayServer.Services.Stores;
namespace BTCPayServer.Services.Mails
{
public class EmailSenderFactory
{
private readonly IBackgroundJobClient _JobClient;
private readonly SettingsRepository _Repository;
private readonly StoreRepository _StoreRepository;
public EmailSenderFactory(IBackgroundJobClient jobClient,
SettingsRepository repository,
StoreRepository storeRepository)
{
_JobClient = jobClient;
_Repository = repository;
_StoreRepository = storeRepository;
}
public IEmailSender GetEmailSender(string storeId = null)
{
var serverSender = new ServerEmailSender(_Repository, _JobClient);
if (string.IsNullOrEmpty(storeId))
return serverSender;
return new StoreEmailSender(_StoreRepository, serverSender, _JobClient, storeId);
}
}
}

View File

@ -7,6 +7,6 @@ namespace BTCPayServer.Services.Mails
{
public interface IEmailSender
{
Task SendEmailAsync(string email, string subject, string message);
void SendEmail(string email, string subject, string message);
}
}

View File

@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Services.Mails
{
class ServerEmailSender : EmailSender
{
public ServerEmailSender(SettingsRepository settingsRepository,
IBackgroundJobClient backgroundJobClient) : base(backgroundJobClient)
{
if (settingsRepository == null)
throw new ArgumentNullException(nameof(settingsRepository));
SettingsRepository = settingsRepository;
}
public SettingsRepository SettingsRepository { get; }
public override Task<EmailSettings> GetEmailSettings()
{
return SettingsRepository.GetSettingAsync<EmailSettings>();
}
}
}

View File

@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Stores;
namespace BTCPayServer.Services.Mails
{
class StoreEmailSender : EmailSender
{
public StoreEmailSender(StoreRepository storeRepository,
EmailSender fallback,
IBackgroundJobClient backgroundJobClient,
string storeId) : base(backgroundJobClient)
{
if (storeId == null)
throw new ArgumentNullException(nameof(storeId));
StoreRepository = storeRepository;
FallbackSender = fallback;
StoreId = storeId;
}
public StoreRepository StoreRepository { get; }
public EmailSender FallbackSender { get; }
public string StoreId { get; }
public override async Task<EmailSettings> GetEmailSettings()
{
var store = await StoreRepository.FindStore(StoreId);
var emailSettings = store.GetStoreBlob().EmailSettings;
if (emailSettings?.IsComplete() == true)
{
return emailSettings;
}
return await FallbackSender.GetEmailSettings();
}
}
}

View File

@ -11,5 +11,6 @@ namespace BTCPayServer.Services
public bool DeprecatedLightningConnectionStringCheck { get; set; }
public bool ConvertMultiplierToSpread { get; set; }
public bool ConvertNetworkFeeProperty { get; set; }
public bool ConvertCrowdfundOldSettings { get; set; }
}
}

View File

@ -42,6 +42,15 @@ namespace BTCPayServer.Services.Rates
static Dictionary<string, IFormatProvider> _CurrencyProviders = new Dictionary<string, IFormatProvider>();
public string FormatCurrency(string price, string currency)
{
return FormatCurrency(decimal.Parse(price, CultureInfo.InvariantCulture), currency);
}
public string FormatCurrency(decimal price, string currency)
{
return price.ToString("C", GetCurrencyProvider(currency));
}
public NumberFormatInfo GetNumberFormatInfo(string currency, bool useFallback)
{
var data = GetCurrencyProvider(currency);
@ -121,15 +130,18 @@ namespace BTCPayServer.Services.Rates
var provider = GetNumberFormatInfo(currency, true);
var currencyData = GetCurrencyData(currency, true);
var divisibility = currencyData.Divisibility;
while (true)
if (value != 0m)
{
var rounded = decimal.Round(value, divisibility, MidpointRounding.AwayFromZero);
if ((Math.Abs(rounded - value) / value) < 0.001m)
while (true)
{
value = rounded;
break;
var rounded = decimal.Round(value, divisibility, MidpointRounding.AwayFromZero);
if ((Math.Abs(rounded - value) / value) < 0.001m)
{
value = rounded;
break;
}
divisibility++;
}
divisibility++;
}
if (divisibility != provider.CurrencyDecimalDigits)
{

View File

@ -104,7 +104,8 @@ namespace BTCPayServer.Services.Rates
Providers.Add("hitbtc", new ExchangeSharpRateProvider("hitbtc", new ExchangeHitbtcAPI(), false));
// Cryptopia is often not available
Providers.Add("cryptopia", new ExchangeSharpRateProvider("cryptopia", new ExchangeCryptopiaAPI(), false));
// Disabled because of https://twitter.com/Cryptopia_NZ/status/1085084168852291586
// Providers.Add("cryptopia", new ExchangeSharpRateProvider("cryptopia", new ExchangeCryptopiaAPI(), false));
// Handmade providers
Providers.Add(QuadrigacxRateProvider.QuadrigacxName, new QuadrigacxRateProvider());

View File

@ -162,6 +162,8 @@ namespace BTCPayServer.Services.Stores
{
if (string.IsNullOrEmpty(name))
throw new ArgumentException("name should not be empty", nameof(name));
if (ownerId == null)
throw new ArgumentNullException(nameof(ownerId));
using (var ctx = _ContextFactory.CreateContext())
{
StoreData store = new StoreData

View File

@ -0,0 +1,251 @@
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
@model UpdateCrowdfundViewModel
@{
ViewData["Title"] = "Update Crowdfund";
}
<section>
<div class="modal" id="product-modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Contribution Perks Management</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="js-product-save btn btn-primary">Save Changes</button>
</div>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-lg-12 text-center">
<h2 class="section-heading">@ViewData["Title"]</h2>
<hr class="primary">
</div>
</div>
<div class="row">
<div class="col-lg-12 text-center">
<partial name="_StatusMessage" for="@TempData["TempDataProperty-StatusMessage"]" />
</div>
</div>
<div class="row">
<div class="col-lg-12">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Title" class="control-label"></label>*
<input asp-for="Title" class="form-control" />
<span asp-validation-for="Title" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Tagline" class="control-label"></label>
<input asp-for="Tagline" class="form-control" />
<span asp-validation-for="Tagline" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Description" class="control-label"></label>*
<textarea asp-for="Description" rows="20" cols="40" class="form-control richtext"></textarea>
<span asp-validation-for="Description" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="TargetCurrency" class="control-label"></label>*
<input asp-for="TargetCurrency" class="form-control" />
<span asp-validation-for="TargetCurrency" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="TargetAmount" class="control-label"></label>
<input asp-for="TargetAmount" class="form-control" />
<span asp-validation-for="TargetAmount" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="StartDate" class="control-label"></label>
<div class="input-group ">
<input asp-for="StartDate" class="form-control datetime"/>
<div class="input-group-append only-for-js">
<button class="btn btn-secondary input-group-clear" type="button" title="Clear">
<span class=" fa fa-times"></span>
</button>
</div>
</div>
<span asp-validation-for="StartDate" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ResetEvery" class="control-label"></label>
<div class="input-group">
<input type="number" asp-for="ResetEveryAmount" placeholder="Amount" class="form-control">
<select class="custom-select" asp-for="ResetEvery">
@foreach (var opt in Model.ResetEveryValues)
{
<option value="@opt">@opt</option>
}
</select>
</div>
</div>
<div class="form-group">
<label asp-for="EndDate" class="control-label"></label>
<div class="input-group ">
<input asp-for="EndDate" class="form-control datetime" />
<div class="input-group-append only-for-js">
<button class="btn btn-secondary input-group-clear" type="button" title="Clear">
<span class=" fa fa-times"></span>
</button>
</div>
</div>
<span asp-validation-for="EndDate" class="text-danger"></span>
</div>
<div class="form-group">
<label class="control-label">Contribution Perks</label>
<div class="mb-3">
<a class="js-product-add btn btn-secondary" href="#" data-toggle="modal" data-target="#product-modal"><i class="fa fa-plus fa-fw"></i> Add Perk</a>
</div>
<div class="js-products bg-light row p-3">
</div>
</div>
<div class="form-group">
<label asp-for="PerksTemplate" class="control-label"></label>
<textarea asp-for="PerksTemplate" rows="10" cols="40" class="js-product-template form-control"></textarea>
<span asp-validation-for="PerksTemplate" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="CustomCSSLink" class="control-label"></label>
<a href="https://docs.btcpayserver.org/development/theme#bootstrap-themes" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
<input asp-for="CustomCSSLink" class="form-control" />
<span asp-validation-for="CustomCSSLink" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="MainImageUrl" class="control-label"></label>
<input asp-for="MainImageUrl" class="form-control" />
<span asp-validation-for="MainImageUrl" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="EmbeddedCSS" class="control-label"></label>
<textarea asp-for="EmbeddedCSS" rows="10" cols="40" class="form-control"></textarea>
<span asp-validation-for="EmbeddedCSS" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="NotificationUrl" class="control-label"></label>
<input asp-for="NotificationUrl" class="form-control" />
<span asp-validation-for="NotificationUrl" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Enabled"></label>
<input asp-for="Enabled" type="checkbox" class="form-check"/>
<span asp-validation-for="Enabled" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="SortPerksByPopularity"></label>
<input asp-for="SortPerksByPopularity" type="checkbox" class="form-check"/>
<span asp-validation-for="SortPerksByPopularity" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="DisplayPerksRanking"></label>
<input asp-for="DisplayPerksRanking" type="checkbox" class="form-check"/>
<span asp-validation-for="DisplayPerksRanking" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="EnforceTargetAmount"></label>
<input asp-for="EnforceTargetAmount" type="checkbox" class="form-check"/>
<span asp-validation-for="EnforceTargetAmount" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="UseAllStoreInvoices"></label>
<input asp-for="UseAllStoreInvoices" type="checkbox" class="form-check"/>
<span asp-validation-for="UseAllStoreInvoices" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="SoundsEnabled"></label>
<input asp-for="SoundsEnabled" type="checkbox" class="form-check"/>
<span asp-validation-for="SoundsEnabled" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="AnimationsEnabled"></label>
<input asp-for="AnimationsEnabled" type="checkbox" class="form-check"/>
<span asp-validation-for="AnimationsEnabled" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="DisqusEnabled"></label>
<input asp-for="DisqusEnabled" type="checkbox" class="form-check"/>
<span asp-validation-for="DisqusEnabled" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="DisqusShortname" class="control-label"></label>
<input asp-for="DisqusShortname" class="form-control" />
<span asp-validation-for="DisqusShortname" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" class="btn btn-primary" value="Save Settings" />
<a class="btn btn-secondary" target="_blank" asp-action="ListInvoices" asp-controller="Invoice" asp-route-searchterm="@Model.SearchTerm">Invoices</a>
<a class="btn btn-secondary" target="_blank" asp-action="ViewCrowdfund" asp-controller="AppsPublic" asp-route-appId="@Model.AppId">View App</a>
<a class="btn btn-secondary" target="_blank" asp-action="ListApps">Back to the app list</a>
</div>
</form>
</div>
</div>
</div>
</section>
@section Scripts {
<bundle name="wwwroot/bundles/crowdfund-admin-bundle.min.js"></bundle>
<bundle name="wwwroot/bundles/crowdfund-admin-bundle.min.css"></bundle>
<script id="template-product-item" type="text/template">
<div class="col-sm-4 col-md-3 mb-3">
<div class="card">
{image}
<div class="card-body">
<h6 class="card-title">{title}</h6>
<a href="#" class="js-product-edit btn btn-primary" data-toggle="modal" data-target="#product-modal">Edit</a>
<a href="#" class="js-product-remove btn btn-danger"><i class="fa fa-trash"></i></a>
</div>
</div>
</div>
</script>
<script id="template-product-content" type="text/template">
<div class="mb-3">
<input class="js-product-id" type="hidden" name="id" value="{id}">
<input class="js-product-index" type="hidden" name="index" value="{index}">
<div class="form-row">
<div class="col-sm-6">
<label>Title</label>*
<input type="text" class="js-product-title form-control mb-2" value="{title}" autofocus />
</div>
<div class="col-sm-3">
<label>Price</label>*
<input type="number" step="any" class="js-product-price form-control mb-2" value="{price}" />
</div>
<div class="col-sm-3">
<label>Custom price</label>
<select class="js-product-custom form-control">
{custom}
</select>
</div>
</div>
<div class="form-row">
<div class="col">
<label>Image</label>
<input type="text" class="js-product-image form-control mb-2" value="{image}" />
</div>
</div>
<div class="form-row">
<div class="col">
<label>Description</label>
<textarea rows="3" cols="40" class="js-product-description form-control mb-2">{description}</textarea>
</div>
</div>
</div>
</script>
}

View File

@ -97,34 +97,83 @@
<span asp-validation-for="Template" class="text-danger"></span>
</div>
<div class="form-group">
<h5>Host button externally</h5>
<p>You can host point of sale buttons in an external website with the following code.</p>
@if (Model.Example1 != null)
{
<span>For anything with a custom amount</span>
<pre><code class="html">@Model.Example1</code></pre>
}
@if (Model.Example2 != null)
{
<span>For a specific item of your template</span>
<pre><code class="html">@Model.Example2</code></pre>
}
<p>A <code>POST</code> callback will be sent to notification with the following form will be sent to <code>notificationUrl</code> once the enough is paid and once again once there is enough confirmations to the payment:</p>
<pre><code class="json">@Model.ExampleCallback</code></pre>
<p><strong>Never</strong> trust anything but <code>id</code>, <strong>ignore</strong> the other fields completely, an attacker can spoof those, they are present only for backward compatibility reason:</p>
<p>
<ul>
<li>Send a <code>GET</code> request to <code>https://btcpay.example.com/invoices/{invoiceId}</code> with <code>Content-Type: application/json</code></li>
<li>Verify that the <code>orderId</code> is from your backend, that the <code>price</code> is correct and that <code>status</code> is either <code>confirmed</code> or <code>complete</code></li>
<li>You can then ship your order</li>
</ul>
</p>
<input type="submit" class="btn btn-primary" value="Save Settings" />
</div>
<div class="form-group">
<input type="submit" class="btn btn-primary" value="Save Settings" />
<div class="accordion" id="accordian-dev-info">
<div class="card">
<div class="card-header" id="accordian-dev-info-embed-payment-button-header">
<h2 class="mb-0">
<button class="btn btn-link" type="button" data-toggle="collapse" data-target="#accordian-dev-info-embed-payment-button" aria-expanded="true" aria-controls="accordian-dev-info-embed-payment-button">
Embed Payment Button linking to POS item
</button>
</h2>
</div>
<div id="accordian-dev-info-embed-payment-button" class="collapse" aria-labelledby="accordian-dev-info-embed-payment-button-header" data-parent="#accordian-dev-info">
<div class="card-body">
<p>You can host point of sale buttons in an external website with the following code.</p>
@if (Model.Example1 != null)
{
<span>For anything with a custom amount</span>
<pre><code class="html">@Model.Example1</code></pre>
}
@if (Model.Example2 != null)
{
<span>For a specific item of your template</span>
<pre><code class="html">@Model.Example2</code></pre>
}
</div>
</div>
</div>
<div class="card">
<div class="card-header" id="accordian-dev-info-embed-pos-iframe-header">
<h2 class="mb-0">
<button class="btn btn-link collapsed" type="button" data-toggle="collapse" data-target="#accordian-dev-info-embed-pos-iframe" aria-expanded="false" aria-controls="accordian-dev-info-embed-pos-iframe">
Embed POS with Iframe
</button>
</h2>
</div>
<div id="accordian-dev-info-embed-pos-iframe" class="collapse" aria-labelledby="accordian-dev-info-embed-pos-iframe-header" data-parent="#accordian-dev-info">
<div class="card-body">
You can embed the POS using an iframe
@{
var iframe = $"<iframe src='{(Url.Action("ViewPointOfSale", "AppsPublic", new {appId = Model.Id}, Context.Request.Scheme))}' style='max-width: 100%; border: 0;'></iframe>";
}
<pre><code class="html">@iframe</code></pre>
</div>
</div>
</div>
<div class="card">
<div class="card-header" id="accordian-dev-info-notification-header">
<h2 class="mb-0">
<button class="btn btn-link collapsed" type="button" data-toggle="collapse" data-target="#accordian-dev-info-notification" aria-expanded="false" aria-controls="accordian-dev-info-notification">
Notification Url Callbacks
</button>
</h2>
</div>
<div id="accordian-dev-info-notification" class="collapse" aria-labelledby="accordian-dev-info-notification-header" data-parent="#accordian-dev-info">
<div class="card-body">
<p>A <code>POST</code> callback will be sent to notification with the following form will be sent to <code>notificationUrl</code> once the enough is paid and once again once there is enough confirmations to the payment:</p>
<pre><code class="json">@Model.ExampleCallback</code></pre>
<p><strong>Never</strong> trust anything but <code>id</code>, <strong>ignore</strong> the other fields completely, an attacker can spoof those, they are present only for backward compatibility reason:</p>
<p>
<ul>
<li>Send a <code>GET</code> request to <code>https://btcpay.example.com/invoices/{invoiceId}</code> with <code>Content-Type: application/json</code></li>
<li>Verify that the <code>orderId</code> is from your backend, that the <code>price</code> is correct and that <code>status</code> is either <code>confirmed</code> or <code>complete</code></li>
<li>You can then ship your order</li>
</ul>
</p> </div>
</div>
</div>
</div>
</div>
</form>
<a asp-action="ListApps">Back to the app list</a>
</div>
</div>
</div>
@ -159,7 +208,7 @@
</div>
<div class="col-sm-3">
<label>Price</label>*
<input type="text" class="js-product-price form-control mb-2" value="{price}" />
<input type="number" class="js-product-price form-control mb-2" value="{price}" />
</div>
<div class="col-sm-3">
<label>Custom price</label>

View File

@ -0,0 +1,78 @@
@using Microsoft.EntityFrameworkCore.Internal
@model BTCPayServer.Models.AppViewModels.ContributeToCrowdfund
<form method="post">
@foreach (var item in Model.ViewCrowdfundViewModel.Perks)
{
<div class="card mb-4 perk expanded" id="@item.Id">
@if (Model.ViewCrowdfundViewModel.DisplayPerksRanking && Model.ViewCrowdfundViewModel.PerkCount.ContainsKey(item.Id))
{
<span class="btn btn-sm rounded-circle px-0 btn-primary perk-badge">#@(Model.ViewCrowdfundViewModel.Perks.IndexOf(item)+1)</span>
}
@if (!string.IsNullOrEmpty(item.Image))
{
<img class="card-img-top" src="@item.Image"/>
}
<div class="card-body">
<div class="card-title d-flex justify-content-between">
<label class="h5">
@if (Model.ViewCrowdfundViewModel.Started && !Model.ViewCrowdfundViewModel.Ended && (item.Price.Value > 0 || item.Custom))
{
<input type="radio" asp-for="ChoiceKey" value="@item.Id"/>
}
@(string.IsNullOrEmpty(item.Title) ? item.Id : item.Title)
</label>
<span class="text-muted">
@if (item.Price.Value > 0)
{
@item.Price.Value
if (item.Custom)
{
Html.Raw("or more");
}
}
else if (item.Custom)
{
Html.Raw("Any amount");
}
</span>
</div>
<p class="card-text overflow-hidden">@Html.Raw(item.Description)</p>
</div>
@if (Model.ViewCrowdfundViewModel.PerkCount.ContainsKey(item.Id))
{
<div class="card-footer d-flex justify-content-between">
<span></span>
<span> @Model.ViewCrowdfundViewModel.PerkCount[item.Id] Contributors</span>
</div>
}
</div>
}
@if (Model.ViewCrowdfundViewModel.Started && !Model.ViewCrowdfundViewModel.Ended)
{
<div class="form-group">
<label asp-for="Email"></label>
<input asp-for="Email" type="email" class="form-control"/>
<span asp-validation-for="Email" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Amount"></label>
<div class="input-group mb-3">
<input asp-for="Amount" type="number" step="any" class="form-control"/>
<div class="input-group-append">
<span class="input-group-text">@Model.ViewCrowdfundViewModel.TargetCurrency.ToUpperInvariant()</span>
</div>
</div>
<span asp-validation-for="Amount" class="text-danger"></span>
</div>
<input type="hidden" asp-for="RedirectToCheckout"/>
<button type="submit" class="btn btn-primary" >Contribute</button>
}
</form>

View File

@ -0,0 +1,154 @@
@using BTCPayServer.Models.AppViewModels
@using Microsoft.CodeAnalysis.CSharp.Syntax
@model BTCPayServer.Models.AppViewModels.ViewCrowdfundViewModel
<div class="container p-0">
<div class="row h-100 w-100 py-sm-0 py-md-4 mx-0">
<div class="card w-100 p-0 mx-0">
@if (!string.IsNullOrEmpty(Model.MainImageUrl))
{
<img class="card-img-top" src="@Model.MainImageUrl"/>
}
<div class="d-flex justify-content-between px-2">
<h1>
@Model.Title
@if (!Model.Started && Model.StartDate.HasValue)
{
<span class="h6 text-muted" >
Starts @Model.StartDate.Value.Subtract(DateTime.Now.ToUniversalTime())
</span>
}
else if (Model.Started && !Model.Ended && Model.EndDate.HasValue)
{
<span class="h6 text-muted" >
Ends @Model.EndDate.Value.Subtract(DateTime.Now.ToUniversalTime())
</span>
}
else if (Model.Started && !Model.Ended && !Model.EndDate.HasValue)
{
<span class="h6 text-muted" :title="startDate" title="No set end date">
Currently Active!
</span>
}
</h1>
@if (Model.TargetAmount.HasValue)
{
<span class="mt-3">
<span class="h5">@Model.TargetAmount @Model.TargetCurrency</span>
@if (Model.ResetEveryAmount > 0 && !Model.NeverReset)
{
<span> Dynamic</span>
}
@if (Model.EnforceTargetAmount)
{
<span >Hardcap Goal <span class="fa fa-question-circle" ></span></span>
}
else
{
<span >Softcap Goal <span class="fa fa-question-circle" ></span> </span>
}
</span>
}
</div>
@if (Model.TargetAmount.HasValue)
{
<div class="progress w-100 rounded-0 " >
<div class="progress-bar" role="progressbar"
style="width:@(Model.Info.ProgressPercentage + "%")"
aria-valuemax="100">
</div>
<div class="progress-bar bg-warning" role="progressbar"
style="width:@(Model.Info.PendingProgressPercentage + "%")"
aria-valuemin="0"
aria-valuemax="100">
</div>
</div>
}
<div class="card-body">
<div class="row py-2 text-center">
<div class="col-sm border-right" id="raised-amount">
<h5>@(Model.Info.CurrentAmount + Model.Info.CurrentPendingAmount) @Model.TargetCurrency </h5>
<h5 class="text-muted">Raised</h5>
</div>
<div class="col-sm border-right">
<h5>@(Model.Info.PendingProgressPercentage.GetValueOrDefault(0) + Model.Info.ProgressPercentage.GetValueOrDefault(0))%</h5>
<h5 class="text-muted">Of Goal</h5>
</div>
<div class="col-sm text-right">
<h5>
@Model.Info.TotalContributors
</h5>
<h5 class="text-muted">Contributors</h5>
</div>
@if (Model.Started && !Model.Ended)
{
<div class="col-sm">
<h5>
@TimeZoneInfo.ConvertTimeFromUtc(Model.EndDate.Value, TimeZoneInfo.Local)
</h5>
<h5 class="text-muted">Ends</h5>
</div>
} else if (!Model.Started)
{
<div class="col-sm">
<h5>
@TimeZoneInfo.ConvertTimeFromUtc(Model.StartDate.Value, TimeZoneInfo.Local)
</h5>
<h5 class="text-muted">Starts</h5>
</div>
}else if (Model.Ended)
{
<div class="col-sm" id="inactive-campaign">
<h5>
Campaign
</h5>
<h5 >not active</h5>
</div>
}
</div>
<div class="card-title">
<div class="row">
<div class="col-sm-12 ">
<h2 class="text-muted" >@Model.Tagline</h2>
</div>
</div>
</div>
<hr/>
<div class="row">
<div class="col-md-8 col-sm-12">
<div class="card-text overflow-hidden">@Html.Raw(Model.Description)</div>
</div>
<div class="col-md-4 col-sm-12">
<partial
name="Crowdfund/ContributeForm"
model="@(new ContributeToCrowdfund()
{
ViewCrowdfundViewModel = Model,
RedirectToCheckout = true,
})"></partial>
</div>
</div>
</div>
<div class="card-footer text-muted d-flex" >
<div class="align-self-end pr-4">Updated @Model.Info.LastUpdated</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,319 @@
<div class="container p-0" id="app" v-cloak>
<div class="row h-100 w-100 py-sm-0 py-md-4 mx-0">
<div class="card w-100 p-0 mx-0">
<img class="card-img-top" :src="srvModel.mainImageUrl" v-if="srvModel.mainImageUrl" id="crowdfund-main-image">
<div class="d-flex justify-content-between px-2" id="crowdfund-header-container">
<h1>
{{srvModel.title}}
<span class="h6 text-muted" v-if="!started && srvModel.startDate" v-b-tooltip :title="startDate" id="crowdfund-header-start-date">
Starts {{startDateRelativeTime}}
</span>
<span class="h6 text-muted" v-else-if="started && !ended && srvModel.endDate" v-b-tooltip :title="endDate" id="crowdfund-header-end-date">
Ends {{endDateRelativeTime}}
</span>
<span class="h6 text-muted" v-else-if="started && !ended && !srvModel.endDate" v-b-tooltip title="No set end date" id="crowdfund-header-active">
Currently Active!
</span>
</h1>
<span v-if="srvModel.targetAmount" class="mt-3" id="crowdfund-header-target">
<span class="h5" id="crowdfund-header-target-amount">{{srvModel.targetAmount}} {{targetCurrency}}</span>
<span v-if="srvModel.resetEvery !== 'Never'"
id="crowdfund-header-target-dynamic"
v-b-tooltip
:title="'Goal resets every ' + srvModel.resetEveryAmount + ' ' + srvModel.resetEvery + ((srvModel.resetEveryAmount>1)?'s': '')" >Dynamic </span>
<span v-if="srvModel.enforceTargetAmount"
id="crowdfund-header-target-softcap">Hardcap Goal <span class="fa fa-question-circle" v-b-tooltip title="No contributions allowed after the goal has been reached"></span></span>
<span v-else
id="crowdfund-header-target-hardcap">Softcap Goal <span class="fa fa-question-circle" v-b-tooltip title="Contributions allowed even after goal is reached"></span> </span>
</span>
</div>
<div class="progress w-100 rounded-0 " v-if="srvModel.targetAmount"
id="crowdfund-progress-bar">
<div class="progress-bar" role="progressbar"
:aria-valuenow="srvModel.info.progressPercentage"
v-bind:style="{ width: srvModel.info.progressPercentage + '%' }"
aria-valuemin="0"
id="crowdfund-progress-bar-confirmed-bar"
v-b-tooltip :title="parseFloat(srvModel.info.progressPercentage).toFixed(2) + '% contributions'"
aria-valuemax="100">
</div>
<div class="progress-bar bg-warning" role="progressbar"
id="crowdfund-progress-bar-pending-bar"
:aria-valuenow="srvModel.info.pendingProgressPercentage"
v-bind:style="{ width: srvModel.info.pendingProgressPercentage + '%' }"
v-b-tooltip :title="parseFloat(srvModel.info.pendingProgressPercentage).toFixed(2) + '% contributions pending confirmation'"
aria-valuemin="0"
aria-valuemax="100">
</div>
</div>
<div class="card-body">
<div class="row py-2 text-center">
<div class="col-sm border-right" id="crowdfund-body-raised-amount">
<h5>{{ raisedAmount }} {{targetCurrency}} </h5>
<h5 class="text-muted">Raised</h5>
</div>
<div class="col-sm border-right" id="crowdfund-body-goal-raised">
<h5>{{ percentageRaisedAmount }}%</h5>
<h5 class="text-muted">Of Goal</h5>
</div>
<div class="col-sm border-right" id="crowdfund-body-total-contributors">
<h5>
{{srvModel.info.totalContributors}}
</h5>
<h5 class="text-muted">Contributors</h5>
</div>
<div class="col-sm" v-if="endDiff" id="crowdfund-body-campaign-dates-started">
<h5>
{{endDiff}}
</h5>
<h5 class="text-muted">Left</h5>
<b-tooltip target="crowdfund-body-campaign-dates-started" >
<ul class="p-0">
<li v-if="startDate" class="list-unstyled">
{{started? "Started" : "Starts"}} {{startDate}}
</li>
<li v-if="endDate" class="list-unstyled">
{{ended? "Ended" : "Ends"}} {{endDate}}
</li>
</ul>
</b-tooltip>
</div>
<div class="col-sm" v-if="startDiff" id="crowdfund-body-campaign-dates-not-started">
<h5>
{{startDiff}}
</h5>
<h5 class="text-muted">Left to start</h5>
<b-tooltip target="crowdfund-body-campaign-dates-not-started" >
<ul class="p-0">
<li v-if="startDate" class="list-unstyled">
{{started? "Started" : "Starts"}} {{startDate}}
</li>
<li v-if="endDate" class="list-unstyled">
{{ended? "Ended" : "Ends"}} {{endDate}}
</li>
</ul>
</b-tooltip>
</div>
<div class="col-sm" v-if="ended" id="crowdfund-body-campaign-dates-not-active">
<h5>
Campaign
</h5>
<h5 >not active</h5>
<b-tooltip target="crowdfund-body-campaign-dates-not-active" >
<ul class="p-0">
<li v-if="startDate" class="list-unstyled">
{{started? "Started" : "Starts"}} {{startDate}}
</li>
<li v-if="endDate" class="list-unstyled">
{{ended? "Ended" : "Ends"}} {{endDate}}
</li>
</ul>
</b-tooltip>
</div>
</div>
<b-tooltip target="crowdfund-body-raised-amount" v-if="paymentStats && paymentStats.length > 0">
<ul class="p-0 text-uppercase">
<li v-for="stat of paymentStats" class="list-unstyled">
{{stat.label}} <span v-if="stat.lightning" class="fa fa-bolt"></span> {{stat.value}}
</li>
</ul>
</b-tooltip>
<b-tooltip target="crowdfund-body-goal-raised" v-if="srvModel.resetEvery !== 'Never'">
Goal resets every {{srvModel.resetEveryAmount}} {{srvModel.resetEvery}} {{srvModel.resetEveryAmount>1?'s': ''}}
</b-tooltip>
<div class="card-title" id="crowdfund-body-header">
<div class="row">
<div class="col-sm-12 col-md-8 col-lg-9" id="crowdfund-body-header-tagline-container">
<h2 class="text-muted" v-if="srvModel.tagline" id="crowdfund-body-header-tagline">{{srvModel.tagline}}</h2>
</div>
<div class="col-sm-12 col-md-4 col-lg-3" id="crowdfund-body-header-cta-container">
<button v-if="active" id="crowdfund-body-header-cta" class="btn btn-lg btn-primary w-100 font-weight-bold" v-on:click="contributeModalOpen = true">Contribute</button>
</div>
</div>
</div>
<template v-if="srvModel.disqusEnabled">
<b-tabs>
<b-tab title="Details"active>
<div class="row mt-2">
<div class="col-md-8 col-sm-12" id="crowdfund-body-description-container">
<div class="card-text overflow-hidden" v-html="srvModel.description" id="crowdfund-body-description"></div>
</div>
<div class="col-md-4 col-sm-12" id="crowdfund-body-contribution-container">
<contribute :target-currency="srvModel.targetCurrency"
:display-perks-ranking="srvModel.displayPerksRanking"
:active="active"
:loading="loading"
:in-modal="false"
:perks="perks">
</contribute>
</div>
</div>
</b-tab>
<b-tab title="Discussion" >
<div id="disqus_thread" class=" mt-2"></div>
</b-tab>
</b-tabs>
</template>
<template v-else>
<hr/>
<div class="row mt-2">
<div class="col-md-8 col-sm-12" id="crowdfund-body-description-container">
<div class="card-text overflow-hidden" v-html="srvModel.description" id="crowdfund-body-description"></div>
</div>
<div class="col-md-4 col-sm-12" id="crowdfund-body-contribution-container">
<contribute :target-currency="srvModel.targetCurrency"
:loading="loading"
:display-perks-ranking="srvModel.displayPerksRanking"
:active="active"
:in-modal="false"
:perks="perks">
</contribute>
</div>
</div>
</template>
</div>
<div class="card-footer text-muted d-flex" v-if="srvModel.animationsEnabled || srvModel.soundsEnabled">
<div class="align-self-end pr-4">Updated {{lastUpdated}}</div>
<div class="form-check mx-1" v-if="srvModel.animationsEnabled || animation">
<input class="form-check-input" type="checkbox" id="cbAnime" v-model="animation">
<label class="form-check-label" for="cbAnime">
Animations
</label>
</div>
<div class="form-check mx-1" v-if="srvModel.soundsEnabled|| sound">
<input class="form-check-input" type="checkbox" id="cbSounds" v-model="sound">
<label class="form-check-label" for="cbSounds">
Sounds
</label>
</div>
</div>
</div>
</div>
<b-modal title="Contribute" v-model="contributeModalOpen" size="lg" ok-only="true" ok-variant="secondary" ok-title="Close" ref="modalContribute">
<contribute v-if="contributeModalOpen"
:target-currency="srvModel.targetCurrency"
:active="active"
:perks="srvModel.perks"
:loading="loading"
:in-modal="true">
</contribute>
</b-modal>
</div>
<script type="text/x-template" id="perks-template">
<div class="perks-container">
<perk v-if="!perks || perks.length ===0"
:perk="{title: 'Donate Custom Amount', price: { value: null}, custom: true}"
:target-currency="targetCurrency"
:active="active"
:loading="loading"
:in-modal="inModal">
</perk>
<perk v-for="(perk, index) in perks" :perk="perk" :key="perk.id"
:target-currency="targetCurrency"
:active="active"
:display-perks-ranking="displayPerksRanking"
:index="index"
:loading="loading"
:in-modal="inModal">
</perk>
</div>
</script>
<script type="text/x-template" id="perk-template">
<div class="card mb-4 perk" v-bind:class="{ 'expanded': expanded, 'unexpanded': !expanded }" v-on:click="expand" :id="perk.id">
<span v-if="displayPerksRanking && perk.sold"
class="btn btn-sm rounded-circle px-0 perk-badge"
v-bind:class="{ 'btn-primary': index==0, 'btn-secondary': index!=0}">#{{index+1}}</span>
<div class="perk-zoom " v-if="canExpand">
<div class="perk-zoom-bg bg-primary"> </div>
<div class="perk-zoom-text w-100 text-center text-white font-weight-bold">
Select this contribution perk
</div>
</div>
<form v-on:submit='onContributeFormSubmit'>
<input type="hidden" :value="perk.id" id="choiceKey"/>
<img v-if="perk.image && perk.image != 'null' " class="card-img-top" :src="perk.image" />
<div class="card-body">
<div class="card-title d-flex justify-content-between" >
<span class="h5">{{perk.title? perk.title : perk.id}} </span>
<span class="text-muted" >
<template v-if="perk.price.value">{{perk.price.value.noExponents()}}
{{targetCurrency}}
<template v-if="perk.custom">or more</template>
</template>
<template v-else-if="!perk.price.value && perk.custom">
Any amount
</template>
</span>
</div>
<p class="card-text overflow-hidden" v-if="perk.description" v-html="perk.description"></p>
<div class="input-group" style="max-width: 500px;" v-if="expanded" :id="'perk-form'+ perk.id">
<input
:disabled="!active"
:readonly="!perk.custom"
class="form-control"
type="number"
v-model="amount"
:min="perk.price.value"
step="any"
placeholder="Contribution Amount"
required >
<div class="input-group-append">
<span class='input-group-text'>{{targetCurrency}}</span>
<button
class="btn btn-primary"
v-bind:class="{ 'btn-disabled': loading}"
:disabled="!active || loading"
type="submit">
<div v-if="loading" class="spinner-grow spinner-grow-sm" role="status">
<span class="sr-only">Loading...</span>
</div>
Continue
</button>
</div>
</div>
</div>
<div class="card-footer d-flex justify-content-between" v-if="perk.sold">
<span ></span>
<span x >{{perk.sold}} Contributor{{perk.sold > 1? "s": ""}}</span>
</div>
</form>
</div>
</script>
<script type="text/x-template" id="contribute-template">
<div>
<h3 v-if="!inModal" class="mb-3">Contribute</h3>
<perks
:perks="perks"
:loading="loading"
:in-modal="inModal"
:display-perks-ranking="displayPerksRanking"
:target-currency="targetCurrency"
:active="active">
</perks>
</div>
</script>

View File

@ -0,0 +1,58 @@
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
@inject BTCPayServer.HostedServices.CssThemeManager themeManager
@model BTCPayServer.Models.AppViewModels.ViewCrowdfundViewModel
@{
ViewData["Title"] = Model.Title;
Layout = null;
}
<!DOCTYPE html>
<html class="h-100">
<head>
<title>@Model.Title</title>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="apple-mobile-web-app-capable" content="yes">
<link href="@Context.Request.GetRelativePathOrAbsolute(themeManager.BootstrapUri)" rel="stylesheet"/>
@if (Model.CustomCSSLink != null)
{
<link href="@Model.CustomCSSLink" rel="stylesheet"/>
}
@if (!Context.Request.Query.ContainsKey("simple"))
{
<script type="text/javascript">
var srvModel = @Html.Raw(Json.Serialize(Model));
</script>
<bundle name="wwwroot/bundles/crowdfund-bundle-1.min.js"></bundle>
<bundle name="wwwroot/bundles/crowdfund-bundle-2.min.js"></bundle>
}
<bundle name="wwwroot/bundles/crowdfund-bundle.min.css"></bundle>
@if (!string.IsNullOrEmpty(Model.EmbeddedCSS))
{
<style>
@Html.Raw(Model.EmbeddedCSS);
</style>
}
</head>
<body>
@if (Context.Request.Query.ContainsKey("simple"))
{
@await Html.PartialAsync("Crowdfund/MinimalCrowdfund", Model)
}
else
{
<noscript>
@await Html.PartialAsync("Crowdfund/MinimalCrowdfund", Model)
</noscript>
if (Model.AnimationsEnabled)
{
<canvas id="fireworks"></canvas>
}
@await Html.PartialAsync("Crowdfund/VueCrowdfund", Model)
}
</body>
</html>

View File

@ -20,7 +20,7 @@
<link rel="manifest" href="~/manifest.json">
<link href="@this.Context.Request.GetAbsoluteUri(themeManager.BootstrapUri)" rel="stylesheet" />
<link href="@this.Context.Request.GetRelativePathOrAbsolute(themeManager.BootstrapUri)" rel="stylesheet" />
@if (Model.CustomCSSLink != null)
{
<link href="@Model.CustomCSSLink" rel="stylesheet" />
@ -194,6 +194,7 @@
<div class="modal-footer bg-light">
<form method="post" asp-antiforgery="false" data-buy>
<input id="js-cart-amount" class="form-control" type="hidden" name="amount">
<input id="js-cart-posdata" class="form-control" type="hidden" name="posdata">
<button id="js-cart-pay" class="btn btn-primary btn-lg" type="submit"><b>@Model.CustomButtonText</b></button>
</form>
</div>

View File

@ -24,7 +24,7 @@
<div class="col-lg-12 text-center">
<h2 class="section-heading">The Bitpay Translator</h2>
<hr class="primary">
<p>Bitpay is using deprecated standard in their invoices which multiple wallet do not support, use this transform their invoices to regular address/amount.</p>
<p>Bitpay is using a deprecated standard in their invoices that most wallets do not support. Use this tool to transform their invoices to a regular address/amount.</p>
</div>
</div>
<div class="row">

View File

@ -10,7 +10,7 @@
<h1>Welcome to BTCPay Server</h1>
<hr />
<p>BTCPay Server is a free and open source server for merchants wanting to accept Bitcoin for their business.</p>
<a style="background-color: #fff;color: #222;display:inline-block;text-align: center;white-space: nowrap;vertical-align: middle;user-select: none;line-height: 1.25;font-size: 1rem;text-decoration:none;font-weight: 700; text-transform: uppercase;border: none;border-radius: 300px;padding: 15px 30px;" href="https://docs.btcpayserver.org">Getting started</a>
<a style="background-color: #fff;color: #222;display:inline-block;text-align: center;white-space: nowrap;vertical-align: middle;user-select: none;line-height: 1.25;font-size: 1rem;text-decoration:none;font-weight: 700; text-transform: uppercase;border: none;border-radius: 300px;padding: 15px 30px;" href="https://btcpayserver.org" target="_blank">Official website</a>
</div>
</div>
</header>
@ -128,13 +128,19 @@
</div>
</div>
<div class="row">
<div class="col-lg-4 ml-auto text-center">
<a href="http://slack.forkbitpay.ninja/">
<div class="col-lg-3 ml-auto text-center">
<a href="https://chat.btcpayserver.org/">
<img src="~/img/mattermost.png" height="100" />
</a>
<p><a href="https://chat.btcpayserver.org/">On Mattermost</a></p>
</div>
<div class="col-lg-3 ml-auto text-center">
<a href="https://slack.btcpayserver.org/">
<img src="~/img/slack.png" height="100" />
</a>
<p><a href="http://slack.forkbitpay.ninja/">On Slack</a></p>
</div>
<div class="col-lg-4 mr-auto text-center">
<div class="col-lg-3 mr-auto text-center">
<a href="https://twitter.com/BtcpayServer">
<img src="~/img/twitter.png" height="100" />
</a>
@ -142,7 +148,7 @@
<a href="https://twitter.com/BtcpayServer">On Twitter</a>
</p>
</div>
<div class="col-lg-4 mr-auto text-center">
<div class="col-lg-3 mr-auto text-center">
<a href="https://github.com/btcpayserver/btcpayserver">
<img src="~/img/github.png" height="100" />
</a>

View File

@ -299,9 +299,16 @@
:autoload="selectedThirdPartyProcessor === 'coinswitch'"
:to-currency-address="srvModel.btcAddress">
<div>
<a v-on:click="openDialog($event)" :href="url" class="action-button" v-show="url && !opened">
{{$t("Pay with CoinSwitch")}}
</a>
<a v-on:click="openDialog($event)" :href="url" class="action-button" v-show="url && !opened">
{{$t("Pay with CoinSwitch")}}
</a>
@if (Model.ChangellyEnabled)
{
<button v-show="!opened" v-on:click="$parent.selectedThirdPartyProcessor = 'changelly'" class="btn-link mt-2">
{{$t("Pay with Changelly")}}
</button>
}
<iframe
v-if="showInlineIFrame"
@ -342,6 +349,12 @@
<a v-on:click="openDialog($event)" :href="url" class="action-button" v-show="url">
{{$t("Pay with Changelly")}}
</a>
@if (Model.CoinSwitchEnabled)
{
<button v-on:click="$parent.selectedThirdPartyProcessor = 'coinswitch'" class="btn-link mt-2">
{{$t("Pay with CoinSwitch")}}
</button>
}
<button class="retry-button" v-if="calculateError" v-on:click="retry('calculateAmount')">
{{$t("ConversionTab_CalculateAmount_Error")}}
</button>
@ -374,10 +387,10 @@
</div>
<div class="success-message">{{$t("This invoice has been paid")}}</div>
<a class="action-button" :href="srvModel.merchantRefLink" v-show="!isModal">
<span>{{$t("Return to StoreName", srvModel)}}</span>
<span v-html="$t('Return to StoreName', srvModel)"></span>
</a>
<button class="action-button close-action" v-show="isModal">
<span>{{$t("Return to StoreName", srvModel)}}</span>
<span v-html="$t('Close')">{{$t("Return to StoreName", srvModel)}}</span>
</button>
</div>
</div>

View File

@ -155,7 +155,11 @@
</tr>
<tr>
<th>Price</th>
<td>@Model.ProductInformation.Price @Model.ProductInformation.Currency</td>
<td>@Model.Fiat</td>
</tr>
<tr>
<th>Tax included</th>
<td>@Model.TaxIncluded</td>
</tr>
</table>
}
@ -178,30 +182,17 @@
</tr>
<tr>
<th>Price</th>
<td>@Model.ProductInformation.Price @Model.ProductInformation.Currency</td>
<td>@Model.Fiat</td>
</tr>
<tr>
<th>Tax included</th>
<td>@Model.TaxIncluded</td>
</tr>
</table>
</div>
<div class="col-md-6">
<h3>Point of Sale Data</h3>
<table class="table table-sm table-responsive-md">
@foreach (var posDataItem in Model.PosData)
{
<tr>
@if (!string.IsNullOrEmpty(posDataItem.Key))
{
<th>@posDataItem.Key</th>
<td>@posDataItem.Value</td>
}
else
{
<td colspan="2">@posDataItem.Value</td>
}
</tr>
}
</table>
<partial name="PosData" model="@Model.PosData" />
</div>
</div>
}

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