Compare commits

...

1451 Commits

Author SHA1 Message Date
3a05f7e294 PSBT export support in send from wallet screen 2019-05-08 14:40:16 +09:00
03713f9bd8 Add PSBT support in the send screen 2019-05-08 14:39:37 +09:00
2a145f4350 Replace noob button in wallet send by an advanced settings accordion 2019-05-08 12:34:33 +09:00
d049da696c Fix exception thrown if user does not exist on login 2019-05-08 12:34:13 +09:00
5a46d0e80d Add cmd tools to generate blocks 2019-05-08 12:19:16 +09:00
926250a967 Remove warnings 2019-05-07 23:34:31 +09:00
139b588795 fix coinswitch...yet again 2019-05-07 23:23:29 +09:00
909f18f9c7 Update language 2019-05-07 18:02:14 +09:00
f598495198 bump 2019-05-07 17:57:04 +09:00
95d746504d Changing invoice state and updating display from js 2019-05-07 17:29:19 +09:00
9a2e1d43ea Triggering optional confirmation update only on Invoice details page 2019-05-07 17:29:19 +09:00
be844978c1 Allow cancelling a non paid pending invoice in payment requests (#815)
* allow cancel on un paid new invoices in payment requests

* start work on cancel pr payment

* finish up cancel action

* final touch and add tests
2019-05-07 17:26:40 +09:00
60a361f963 Trying to make sure Azure tests does not run on PRs 2019-05-07 17:11:23 +09:00
93f50451e6 bump deps 2019-05-07 17:05:45 +09:00
0936812df0 Fix date time issues on crowdfund.payment requests (#808)
* fix some conditional display bugs in crowdfund

* bump flatpickr

* make clear button show up even with flatpickt fake input ui

* update uis to specify date value in specific format and use custom format for flatpickr display and use moment to parse date instead

* fix remaining public ui date issues
2019-05-07 17:01:37 +09:00
50351f56f8 Fix grammar (#817)
* Fix grammar

* Fix typo
2019-05-07 16:55:22 +09:00
232ceed8b0 Prettify the bitcoin core node page 2019-05-07 14:44:26 +09:00
b6c37a73b1 Fix duplicated entries on Services. Fix formatting of P2P page. 2019-05-07 14:31:49 +09:00
5967666df6 Add green wallet info 2019-05-07 14:16:44 +09:00
bf035333cf Add service type P2P 2019-05-07 14:07:36 +09:00
f93d1173e2 Show tor exposed bitcoin node 2019-05-07 13:58:55 +09:00
08bf4faeee Pass the hint change address to hardware wallet (useful in care of send-to-self where the underlying wallet support only output belonging to self) 2019-05-07 08:21:34 +09:00
e2b2cf0175 Do not drop column in u2f migration if not possible (#813)
closes #812
2019-05-05 00:57:44 +09:00
d32a24004e Fix test 2019-05-03 12:59:11 +09:00
1f04e4e6be Add rpcport for bitcoin-cli 2019-05-03 11:10:01 +09:00
778dcf97b1 update docker compose for bitcoin 2019-05-03 11:04:19 +09:00
957fbdb907 Update NBitcoin, NBXplorer, Bitcoin Core 2019-05-03 10:18:08 +09:00
e169b851ee Remove another warning 2019-05-02 21:44:16 +09:00
7fadb4c5ad Remove some annoying warnings 2019-05-02 21:38:39 +09:00
a20db7f341 bump nbx 2019-05-02 21:35:28 +09:00
b5f4739ae5 Allow invoice creation to only allow specific payment methods in UI (#792)
* allow invoice creation to only allow specific payment methods

* add test

* reuse existing feature

* final fixes
2019-05-02 21:29:51 +09:00
4bc03fbf06 Code coloring invoice states 2019-05-02 21:11:56 +09:00
1d3ff143d2 Tweaking UI, expanding details and max width on order id 2019-05-02 21:11:56 +09:00
6918b8a291 Extracting payment details population, refactoring invoice data load 2019-05-02 21:11:56 +09:00
3cd37682d3 [BUG FIX]: Coinswitch exchange with altcoins popup not showing bug fix (#804) 2019-05-02 21:02:01 +09:00
19a990b095 Add U2f Login Support (#787)
* init u2f

* ux fixes

* Cleanup Manage Controller

* final changes

* remove logs

* remove console log

* fix text for u2f

* Use Is Secure instead of IsHttps

* add some basic u2f tests

* move loaders to before title

* missing commit

* refactor after nicolas wtf moment
2019-05-02 21:01:08 +09:00
87a4f02f18 bump NBXplorer 2019-05-02 20:46:27 +09:00
8a99fc0505 Fix Azure Storage (#803) 2019-05-02 20:39:12 +09:00
bac99deb6c Do not run external integration if PR 2019-05-02 20:38:45 +09:00
e65850b1eb Refactor Send money from ledger using PSBT 2019-05-02 18:56:01 +09:00
a6e52ed3df bump NBitcoin 2019-05-02 17:31:57 +09:00
4a9eadf71a Bump NBXplorer 2019-05-02 17:28:54 +09:00
b8f6cf4f23 Execute ExternalIntegration tests after 2019-05-02 15:31:51 +09:00
e8abc1137b remove duplicate view code for email and fix password bug (#788)
closes #786
2019-05-01 12:17:25 +09:00
8507688c50 add azure storage config validation (#791) 2019-05-01 12:16:55 +09:00
5718096224 Revert "Merge branch 'sonarqube'"
This reverts commit d76e61e6f468b006659eebeff84be8bdedc02822.
2019-04-29 07:53:34 +02:00
d76e61e6f4 Merge branch 'sonarqube' 2019-04-29 07:21:50 +02:00
232817c00d add sonarqube 2019-04-29 07:18:21 +02:00
86af585df3 bump nbx in tests 2019-04-29 12:31:21 +09:00
9e770ea484 bump dbriize 2019-04-29 12:30:47 +09:00
9670f11554 Fix HTTP 500 errors if querying the website in tests 2019-04-28 23:11:24 +09:00
dc369d52cb Use fa fa-user for profile menu item 2019-04-28 16:07:42 +09:00
33c755fc54 Replace log out text by an icon 2019-04-28 15:59:44 +09:00
c5adc0eb71 Rename ShowEmailWarningForStore(storeId) => IsEmailConfigured(storeId) 2019-04-28 15:28:22 +09:00
fcb1de8a86 Show email warning on apps when settings are not complete (#794)
* Show email warning on apps when settings are not complete

closes #693

* refactor email warning logic
2019-04-28 15:27:10 +09:00
6df83ad148 Replace DBreeze by DBriize 2019-04-28 15:16:11 +09:00
857a436677 Clarifying comma is required for splitting params, providing example 2019-04-26 22:00:12 -05:00
c6091750b0 Displaying switchable datetimes on wallet transactions page 2019-04-26 22:00:12 -05:00
64e7324285 Fixing CanUsePoSApp test 2019-04-26 22:00:12 -05:00
d5bd0ee781 Filtering invoices by StartDate and EndDate
Now it's required to separate parameters with comma. Forced to do
this because dates have spaces between date and time part
2019-04-26 22:00:12 -05:00
3b91b38014 do not run external integration tests if in a PR 2019-04-24 22:40:36 +09:00
165d4e2732 remove unused parameter 2019-04-24 22:40:36 +09:00
098dfacce8 Remove segwit limitation for rescan 2019-04-24 22:40:35 +09:00
44d1419af9 Add itemCode to Invoice Response (#784)
closes #779
2019-04-24 22:36:35 +09:00
d0d077642d Make sure we returns only one token in GetTokens 2019-04-23 16:05:11 +09:00
dc04839fab Run Azure tests in CircleCI 2019-04-22 17:19:04 +09:00
4ce0cb4b35 Remove useless code in storage providers 2019-04-22 16:57:22 +09:00
5100c36c06 Uncomment google/amazon code, just disable it in the service registration 2019-04-22 16:45:50 +09:00
b184360eb7 Abstracted cloud storage - Amazon/Google/Azure/Local (#708)
* wip

* add in storage system

* ui fixes

* fix settings ui

* Add Files Crud UI

* add titles

* link files to users

* add migration

* set blob to public

* remove base 64 read code

* fix file query model init

* move view model to own file

* fix local root path

* use datadir for local storage

* move to services

* add direct file url

* try fix tests

* remove magic string

* remove other magic strings

* show error message on unsupported provider

* fix asp net version

* redirect to storage settings if provider is not supported

* start writing tests

* fix tests

* fix test again

* add some more to the tests

* more tests

* try making local provider work on tests

* fix formfile

* fix small issue with returning deleted file

* check if returned data is null for deleted file

* validate azure Container name

* more state fixes

* change azure test trait

* add tmp file url generator

* fix tests

* small clean

* disable amazon and  google


comment out unused code for now


comment out google/amazon
2019-04-22 16:41:20 +09:00
02d79de17c Merge pull request #773 from pavlenex/readme
Add TOC, Intro Video, Getting Started to readme
2019-04-22 14:16:53 +09:00
cf27c66132 wordiness 2019-04-19 15:59:56 +02:00
53d9ed5adb add proper video and img size 2019-04-19 13:23:35 +02:00
3fe5051098 Update README.md
- Add Table of Contents
- Add Getting Started Section
- Add Intro video
2019-04-19 13:17:51 +02:00
f7c8a989b6 bump lnd 2019-04-17 13:51:43 +09:00
65dcfd3549 bump 2019-04-15 15:28:05 +09:00
6976fc54ca Merge pull request #765 from Kukks/bugfix/crowdfund
Fix dynamic  crowdfund labelling
2019-04-15 15:26:58 +09:00
0e077ff5c4 Merge pull request #768 from Kukks/feature/invoicesearchsession
Make invoice list search term persistent for session
2019-04-15 15:26:05 +09:00
c2f171a729 Merge pull request #766 from Kukks/bugfix/crowdfund_orderid
fix redirect uri for crowdfund invoices
2019-04-15 15:24:53 +09:00
fea38758e4 Merge pull request #767 from Kukks/bugfix/unusual-filter
fix unusual filter
2019-04-15 15:24:41 +09:00
444733565b Updating altcoins section 2019-04-13 22:24:46 -05:00
96d28f00cc Make invoice list search term persistent for session 2019-04-13 14:00:48 +02:00
70cc79a77f fix unusual filter
closes #763
2019-04-13 13:50:14 +02:00
8d10186fdf fix redirect uri for crowdfund invoices
closes #759
2019-04-13 13:43:47 +02:00
6f7e0205f8 Fix dynamic crowdfund labelling
closes #760
2019-04-13 13:22:19 +02:00
7ef11817c1 Add britt and rockstar video in readme 2019-04-12 18:03:05 +09:00
c387c84861 bump 2019-04-12 15:02:28 +09:00
ae7ad9f667 Filter the apps by the user id 2019-04-12 14:54:59 +09:00
c55f1185e6 Revert "Do not show all apps in Server settings policy"
This reverts commit 1619666befd4bf8931a11a8c2927de4c5b1add86.
2019-04-12 14:43:07 +09:00
1619666bef Do not show all apps in Server settings policy 2019-04-12 14:29:56 +09:00
bf784f6fd7 Merge pull request #758 from rockstardev/dynamicroot
Allowing for displaying of app directly on website root
2019-04-12 14:19:19 +09:00
13e330fa65 Better UI for selection of app to be displayed on root 2019-04-12 00:13:14 -05:00
827b133534 Allowing for displaying of app directly on website root 2019-04-11 16:30:23 -05:00
4067d4b00f Remove the Facade concept 2019-04-11 23:55:20 +09:00
359d8c5c6a Merge pull request #745 from Kukks/feature/invoicepaymentdata
Add payment data to crypto info in invoice api model
2019-04-11 19:10:52 +09:00
265b7364e8 Merge pull request #756 from Kukks/invoice-auto-redirect
Allow POS to redirect invoices automatically after paid
2019-04-11 19:10:21 +09:00
dc2b8c9e4c bump to nbitpay and use for payments 2019-04-11 12:00:10 +02:00
37869fd049 Add payment data to crypto info in invoice api model
Depends on https://github.com/MetacoSA/NBitpayClient/pull/22
2019-04-11 11:54:56 +02:00
d7ada4d493 add redirect automatically to checkout experience/ store settings 2019-04-11 11:53:31 +02:00
f093f85dbf Merge pull request #753 from britttttk/fix/defaultText
Improve default payment method dropdown
2019-04-11 18:11:46 +09:00
1cf17872ab Allow POS to redirect invoices automatically after paid
closes #730
2019-04-11 11:08:42 +02:00
c79751829b Merge pull request #754 from Kukks/fix-pos-notif
fix pos settings savings for notifications
2019-04-11 17:34:42 +09:00
7a21c03896 fix pos settings savings for notifications
closes #751
2019-04-11 09:14:39 +02:00
54f07139db Improve default payment method dropdown text 2019-04-11 00:41:30 -06:00
d78990fbd5 Changing queries: Using FirstOrDefaultAsync result in suboptimal queries 2019-04-11 15:05:30 +09:00
9ed7dbc838 Remove Quadrigacx (bankrupt) 2019-04-11 14:57:31 +09:00
9b12c7bc57 Add missing file 2019-04-11 12:41:38 +09:00
8973c75bbc fix build 2019-04-11 10:06:36 +09:00
f425df7b6d Add Contributing section 2019-04-10 16:05:07 -05:00
a44b600c5e Improving documentation and altcoins sections 2019-04-10 12:48:02 -05:00
7f0a42c2d5 Updating altcoins section 2019-04-10 12:30:54 -05:00
60cd864226 Inject HttpClient inside lightning client instances 2019-04-11 01:10:46 +09:00
71cf02915e Merge pull request #740 from rockstardev/uifixes
Button to switch between time formats, width fix
2019-04-09 18:05:18 +09:00
327d2298fb Merge pull request #746 from Kukks/tag-pos-invoices
tag pos invoices too
2019-04-09 18:04:14 +09:00
2ca11ed692 Fix PoS decimal issue (Fix #747) 2019-04-09 11:10:27 +09:00
0224815a60 workaround tight coupling of crowdfund to apps mechanics 2019-04-08 16:02:53 +02:00
df824c36d2 tag pos invoices too 2019-04-08 15:46:24 +02:00
66e7777b1a bump nbx 2019-04-08 22:26:52 +09:00
7ff85a86bf Merge pull request #736 from Dolu89/master
New Pay Button type (Custom amount and Slider)
2019-04-08 22:19:05 +09:00
7b3700c2c6 Fix bitbank API weirdness (Fix #741) 2019-04-08 21:57:12 +09:00
04679aefd6 Merge pull request #743 from Kukks/fix-coinswitch
fix coinswitch
2019-04-08 17:23:47 +09:00
5190639b77 Simplify InvoiceWatcher logic and remove unused code 2019-04-08 13:28:13 +09:00
0bf73abb39 Fix Custom amount under 0 in Pay button 2019-04-06 15:22:09 +02:00
7e0211924d Replace inline js by templates in pay button 2019-04-06 15:02:02 +02:00
a8a857a7ce Move Slider settings below radio buttons 2019-04-06 13:47:22 +02:00
1d18965a26 fix coinswitch 2019-04-06 08:10:27 +02:00
e020b86a3f Button to switch between time formats, width fix 2019-04-05 09:44:49 -05:00
1b80b90609 Update lang, bump 2019-04-05 18:50:20 +09:00
efc3512994 Merge pull request #739 from Kukks/pos-fixes
fix malformed html in pos + align price/button to card bottom
2019-04-05 16:53:33 +09:00
acb8ca982f fix malformed html in pos + align price/button to card bottom 2019-04-05 09:50:41 +02:00
adc42cbba4 Put timeout on tests 2019-04-05 16:28:18 +09:00
7edcb7ef5f Update NBitcoin 2019-04-05 16:21:00 +09:00
656017c6df SocketFactory uses NBitcoin implementation of Socks 2019-04-05 16:19:04 +09:00
35db6d4a8b Fix test CanScheduleBackgroundTasks 2019-04-05 15:32:30 +09:00
2741187546 Merge pull request #737 from rockstardev/uifixes
New batch of UI fixes
2019-04-05 15:23:41 +09:00
c3a7ab647c Increase reliability of test CanScheduleBackgroundTasks 2019-04-05 15:16:36 +09:00
92da0ec2d2 fix tests 2019-04-05 14:59:46 +09:00
c767a49f2d Increase notitifcation timeout to 1 minute, make sure that BackgroundJobScheduler is correctly cancelling tasks 2019-04-05 14:58:25 +09:00
ea8196b532 Do not use HttpClient singleton for the InvoiceNotifcationManager 2019-04-05 14:31:09 +09:00
58f138e854 Invoice list improvements
Items 5 and 9 from #349
2019-04-04 22:25:38 -05:00
b4b6939498 Coin switching on no-script invoices 2019-04-04 21:48:52 -05:00
bc97c07670 Add currency select in Pay Button 2019-04-04 21:32:16 +02:00
cf27fe5a53 Merge remote-tracking branch 'upstream/master' 2019-04-04 20:57:16 +02:00
449066449b Add a new Pay Button Type : Slider 2019-04-04 20:56:12 +02:00
eb5e32a07f Remove exception thrown by binance provider 2019-04-04 19:39:37 +09:00
708cdbe23f Remove bunch of catched exception when BTCPay starts 2019-04-04 19:35:46 +09:00
333de52c33 Merge branch 'feature/sync-video' 2019-04-04 18:17:39 +09:00
6b9932fa14 Escape css selector 2019-04-04 18:16:54 +09:00
6c45689e6a Fix point of sale search (Fix #734) 2019-04-04 16:32:56 +09:00
1e3307c84c Add link to andreas video during IBD 2019-04-04 15:58:28 +09:00
d0eed9857d Prevent user to log in or register via unsecured network 2019-04-04 14:28:50 +09:00
4221763f48 Merge remote-tracking branch 'upstream/master' 2019-04-03 21:45:41 +02:00
184c797b0e Add a new Pay Button Type : Custom amount 2019-04-03 21:43:53 +02:00
4853e15d8a Better timing measurement during invoice creation 2019-04-03 15:00:09 +09:00
6b4b903669 Improve invoice logs, make sure logs are saved as fire and forget 2019-04-03 14:38:35 +09:00
05da63f2a5 Merge pull request #721 from Kukks/expose-notif-to-pos
add notif Email to crowdfund and pos + add notif url to pos
2019-04-02 17:34:56 +09:00
5b4b073fc8 Merge pull request #731 from Kukks/coinswitch-shitcoin-tax
add coinswitch shitcoin tax
2019-04-02 17:33:37 +09:00
4723a83dbb Merge pull request #732 from rockstardev/uifixes
UI fixes
2019-04-02 17:32:47 +09:00
78350db62d Add a Clean button in the Maintenance page 2019-04-01 17:10:05 +09:00
e8b71f36b2 Adding support for noscript invoices 2019-03-31 13:46:38 -05:00
6db9061dd1 add coinswitch shitcoin tax 2019-03-31 18:55:14 +02:00
320826a4b9 Returning empty payload to fix JSON parse error thrown in JS 2019-03-31 11:48:53 -05:00
ad0edb5f4c Make sure arm build have /sbin/ip 2019-03-31 14:08:53 +09:00
2856c10bc3 Revert "Do not use /sbin/ip to fetch the current ip (fix #712)"
This reverts commit 561644f75b3b83cae3b1cc8db0ceb7ee657dbe82.
2019-03-31 13:54:10 +09:00
24aa18e9ed bump 2019-03-31 13:32:26 +09:00
767eca97cb Fix tests 2019-03-31 13:31:50 +09:00
73d5415ea9 Use NBitcoin's socks implementation 2019-03-31 13:16:05 +09:00
e5a26cfca8 Update dependencies 2019-03-31 12:08:08 +09:00
e40cd1fc0c Update publish docker 2019-03-29 18:39:02 +09:00
978b7d930e Catch operation cancelled exception on the BackgroundJobScheduler 2019-03-29 18:09:54 +09:00
0f2e3ef957 Make latest branch 2019-03-29 17:48:24 +09:00
275b590e80 add notif Email to crowdfund and pos + add notif url to pos
closes #720
2019-03-29 07:51:00 +01:00
5d9da82d8e fix build 2019-03-27 18:58:56 +09:00
1a122726b7 Add more timeout for lightning tests 2019-03-27 18:57:51 +09:00
0bd02a9272 Fix some exceptions raised if port is already used 2019-03-27 18:56:43 +09:00
3cce7b8b35 Refactor the lightning listener, some users complain payments are not detected (should fix #676) 2019-03-27 15:53:38 +09:00
e3ab1f5228 Merge pull request #707 from Kukks/user-email-sync
set username on email change
2019-03-25 14:59:02 +09:00
4c875d9c7c update doc sdk 2019-03-25 14:10:16 +09:00
e79334a6f6 Fix: if anyone can create invoice and /invoices has storeId parameters, then it should be allowed 2019-03-25 12:59:42 +09:00
a09c6d51e6 fix exception which can be thrown if the store is not found 2019-03-25 12:24:48 +09:00
312c7b7193 Fix anonymous bitpay api access 2019-03-25 12:22:17 +09:00
ee733fee28 If AnyoneCanInvoice and the storeId is passed as a parameter to the Bitpay API, then allow request 2019-03-25 12:18:39 +09:00
4d7e9d3f8a Rewrite the BitpayAuthHandler more clearly 2019-03-25 12:09:18 +09:00
873c0a183a Merge pull request #713 from pavlenex/link
Update Readme, Fix Broken Pebble Link
2019-03-25 10:20:08 +09:00
ea53ae8f20 Update ManageController.cs 2019-03-24 16:09:36 +01:00
686bc3380d Update ManageController.cs 2019-03-24 16:09:20 +01:00
67da20bcea Merge pull request #706 from Kukks/pos-max-length
remove template max length in pos app
2019-03-24 23:58:08 +09:00
561644f75b Do not use /sbin/ip to fetch the current ip (fix #712) 2019-03-24 23:56:31 +09:00
1abc89858f Fix broken Pebble Link
Fix broken Pebble Link
2019-03-24 14:15:47 +01:00
91c63a8ee6 bump 2019-03-24 13:37:14 +09:00
563882d30b Merge pull request #711 from rockstardev/bugfix/satround
Javascript floating point math fix, closes #701
2019-03-24 13:35:13 +09:00
9a5eeee794 Javascript floating point math fix, closes #701 2019-03-23 22:28:54 -05:00
0578a692db Updating bundler to new version that will also support .NET Core 2.2 2019-03-23 19:10:16 -05:00
f74f06338a Update bundle minifier, trying to fix (#710 with bundle on) 2019-03-23 23:24:29 +09:00
1281f348bf set username on email change
closes #673
2019-03-22 12:53:56 +01:00
5e76d4bfc1 remove template max length in pos app
closes #704
2019-03-22 09:14:27 +01:00
2a302ea346 Do not spam logs if we can't connect to lightning because lightning is not started 2019-03-20 14:35:33 +09:00
be90172840 bump Microsoft.AspNetCore.App 2019-03-20 14:13:12 +09:00
b85ee895f5 Update nbxplorer in the tests 2019-03-20 14:08:25 +09:00
93de408e07 bump 2019-03-20 12:37:53 +09:00
d3662ae734 fix dockerfile 2019-03-20 12:34:47 +09:00
132d7795ea bump .net 2019-03-20 12:22:46 +09:00
bf5a624209 Remove curl dependency on alpine image 2019-03-20 11:49:43 +09:00
abbdbda03a Add Bitbank provider 2019-03-20 00:49:44 +09:00
8a8593437a Add TOR
- Adds Tor
- Clarifies alts (once again)
- adds payment requests
2019-03-18 12:00:31 +01:00
e203cada54 Merge pull request #675 from Kukks/css-grid-pos-fix
attempt fix pos css grid
2019-03-18 19:47:11 +09:00
00c11c7ee9 bump 2019-03-18 18:58:15 +09:00
82126b85d2 Fix tests 2019-03-18 18:52:19 +09:00
4f428c8ed1 Fix other tor services display 2019-03-18 17:13:02 +09:00
9868af4db8 Show onion link in the navbar 2019-03-18 17:07:39 +09:00
0a5d7c5efa Add .local as a localnetwork link 2019-03-18 16:48:04 +09:00
c2754b324d Show other tor services 2019-03-18 16:45:46 +09:00
5c618233cb fix singular item in pos stretched width 2019-03-18 07:39:45 +01:00
9e91259b9e attempt fix pos css grid 2019-03-18 07:07:31 +01:00
014d08f38a Add socks support on BTCPay Server to query onion endpoints 2019-03-18 00:03:02 +09:00
7998ea142b Fix tor services not appearing 2019-03-17 21:47:08 +09:00
a4051dac72 Make sure BTCPay show TOR lightning node info if the site is accessed through TOR 2019-03-17 21:28:47 +09:00
e3a8892d24 Check tor services in the background 2019-03-17 21:07:24 +09:00
ea02d77e69 Parse torrc file to know virtual port of hidden services 2019-03-17 20:49:26 +09:00
4f582a6712 Show onion hosts instead of url (can't know about ports) 2019-03-17 13:42:54 +09:00
4769b1d452 Show tor services inside hidden directories in Server Settings/Services 2019-03-17 13:01:47 +09:00
17b18d820f bump 2019-03-17 12:28:38 +09:00
26f34e75c2 Language update 2019-03-17 12:28:19 +09:00
6f50ac50ec Merge pull request #689 from rockstardev/rock-lnsats
Allowing for lightning payment amounts to be displayed in Satoshis
2019-03-17 12:26:13 +09:00
5261cfcdd3 Allowing for lightning payment amounts to be displayed in Satoshis 2019-03-15 22:43:57 -05:00
675920697f Merge pull request #685 from britttttk/fix/PayReqMessage
Improve delete payment request message
2019-03-16 12:36:24 +09:00
24699bf2ba Updating QR Code on public node info page to be SVG
Co-authored-by: Sebastian Kippe <sebastian@kip.pe>
2019-03-15 22:16:12 -05:00
5ab92ed794 Switching to SVG for QR code so it looks OK on higher density display
Co-authored-by: Sebastian Kippe <sebastian@kip.pe>
2019-03-15 21:45:55 -05:00
8e83f0faa1 Improve delete payment request message 2019-03-14 23:26:39 -06:00
30d5add2ea bump 2019-03-15 14:04:17 +09:00
6e47babf45 Overpayment of lightning invoice now properly appears (Fix #486) 2019-03-14 18:48:05 +09:00
9b95fa1f20 Add links for Payment Requests and Apps docs in UI 2019-03-12 21:50:55 +09:00
c67aa14a87 Fix payment requests and crowdfund app not working if ROOTPATH is specified (fix #659) 2019-03-12 15:48:24 +09:00
23f296ef34 The store owner can define default currency pairs when using rate API without parameter 2019-03-11 18:39:21 +09:00
c6ce676ad3 Fix exception on Payment Request (#672) 2019-03-11 16:06:27 +09:00
fafb02b0dc bump 2019-03-11 15:20:10 +09:00
53cecc8c8a Improve error message for POST /tokens without body 2019-03-10 15:10:30 +09:00
db0e9ee8f8 Merge pull request #654 from Kukks/robots-suck
Add policy to discourage search engines + build robots.txt dynamcally
2019-03-10 15:00:11 +09:00
2fac794d96 Merge pull request #667 from pavlenex/pavlenex-patch-issue-templates
Add Issue Templates
2019-03-10 14:58:58 +09:00
ffcd716906 Create feature_request.md 2019-03-09 23:21:03 +01:00
85f50724db Create Bug Report template 2019-03-09 23:16:29 +01:00
808a995741 Merge pull request #665 from rockstardev/rock-qrcodefix
Rendering QR code only if there is data, plus loading indicator
2019-03-10 00:40:07 +09:00
4deb853914 remo useless mode now 2019-03-09 16:35:53 +01:00
470ec3354e Rendering QR code only if there is data, plus loading indicator 2019-03-09 09:33:05 -06:00
053c2da9f1 use thememanager instead of view component 2019-03-09 16:29:04 +01:00
c0e28ce66e Revert "Remove error in console logs on checkout page"
This reverts commit 08dd94e267c4810837b2fccd33b9001a3e406b30.
2019-03-10 00:28:47 +09:00
08dd94e267 Remove error in console logs on checkout page 2019-03-10 00:18:34 +09:00
7497865d1f Pay button was not working properly if the server was not en-US (Fix #638) 2019-03-09 23:40:22 +09:00
1888e4fe2b remove robots and remove nofollow 2019-03-09 15:01:56 +01:00
6746a5cbd5 add meta for noindex,nofollow if policy set 2019-03-09 14:55:37 +01:00
baecb7bb0c Fix coinswitch issue (#664)
Coinswitch did a breaking change, this fixes it. Closes #660
2019-03-09 22:36:25 +09:00
28bf4b42bb Fix tests 2019-03-09 22:36:07 +09:00
c73dc425ad Remove externalurl in command line options --help (Fix #658) 2019-03-09 22:30:49 +09:00
2138b7dcb8 Fix invoice popup not showing up if btcpay has a rootpath 2019-03-09 22:28:20 +09:00
8b6c4a9383 simplifed robots generator 2019-03-09 14:23:55 +01:00
344755cbd0 Add policy to discourage search engines + build robots.txt dynamcally
closes #390
2019-03-09 14:13:10 +01:00
63a975267c Fix coinswitch issue
Coinswitch did a breaking change, this fixes it. Closes #660
2019-03-09 14:11:56 +01:00
3c7d93e88d No ROOTPATH in launchsettings 2019-03-09 18:40:41 +09:00
75974037bc Fix SignalR paths if RootPath is used 2019-03-09 16:08:31 +09:00
e8a346182b Fix vue-qrcode inside the lightning network info vue 2019-03-09 12:51:05 +09:00
e96d34f741 New QR Code component, fixes scanning of long lightning invoices 2019-03-08 16:19:39 -06:00
7dad814f19 Fix some checkout page if RootPath is set (#659) 2019-03-09 00:48:33 +09:00
7e67ca1413 Fix RootPath was not working correctly on Linux 2019-03-08 23:38:00 +09:00
603263549b Document LND supported wallets in services (#657)
* Document LND supported wallets in services

* Add Zeus

* Open links in new tab
2019-03-08 18:12:14 +09:00
a82b971ce7 bump 2019-03-07 20:51:51 +09:00
b58c8ef2f0 Bump libraries 2019-03-07 20:33:05 +09:00
274533bfdf Fix spot not using HttpClient created by the httpclientfactory 2019-03-07 19:41:20 +09:00
cd6ce401e1 Fix logs getting spammed by HTTP requests 2019-03-07 19:41:19 +09:00
0c0809101d Datetime picker and small edit UI changes (#647)
* do not allow negative amounts for crowdfund and payment requests

* remove currency placeholder in payment requests

* Improve date picker ui 

Clear button only appears when a value is set. If no value is set, display a placeholder indicating it. closes #625
2019-03-07 14:29:29 +09:00
4b342376a8 Pos experimental card deck (#651)
* Try out experimental card deck

* apply card deck to shopping cart version
2019-03-07 14:28:42 +09:00
fd963b9ad0 fix no store error message for payment request (#646)
* fix no store error message for payment request 

closes #628

* Update PaymentRequestController.cs
2019-03-07 14:28:14 +09:00
06406c0695 Clone Payment Requests (#648)
* Clone Payment Requests

closes #615

* Do not save clone instantly
2019-03-07 14:27:16 +09:00
2b567de5c1 Allow sounds and animation colors in crowdfund to be configured (#653)
closes #652
2019-03-07 14:25:09 +09:00
ef46d03760 fix lightning typo (#655)
closes #622
2019-03-07 13:14:47 +09:00
b174f299fa Fix LND QR code (Fix #656) 2019-03-07 10:26:27 +09:00
09837966b9 bump 2019-03-06 17:53:03 +09:00
465dce1d02 Running check of submitted SMTP data on both Test and Save 2019-03-05 13:00:14 -06:00
067dbad546 Fix build 2019-03-05 17:29:52 +09:00
522970fdb9 Fix build 2019-03-05 17:21:44 +09:00
e67aa499a6 Fix build 2019-03-05 17:20:26 +09:00
3b68d81507 Small refactoring 2019-03-05 17:13:34 +09:00
051248f2fc Add CancellationToken to GetRatesAsync and propagate it from the controllers to the rate fetcher 2019-03-05 17:09:17 +09:00
9a239f99f4 Add bylls as supported exchange 2019-03-05 16:07:23 +09:00
86c431d66e Only get contributions from invoices matching the currency of the Payment request or crowdfund 2019-03-05 14:26:27 +09:00
b35fe0e8e3 Fix CurrentPendingAmount/CurrentAmount not being set 2019-03-05 14:06:40 +09:00
54905f5ceb Fix currencyValue 2019-03-05 13:58:13 +09:00
1c9b05d992 Fix: Payment requests and crowdfund were estimating current contributions based on the current rate 2019-03-05 13:54:34 +09:00
a89c71df38 Fix bundleconfig 2019-03-04 22:38:06 +09:00
3db1f2af12 Fix warning when compiling bundles 2019-03-04 22:35:56 +09:00
5399ff2751 Fix forwarding options 2019-03-04 22:34:14 +09:00
bcea6027e9 Replace Forwarded Headers via ASP.NET Core middleware 2019-03-04 20:48:19 +09:00
a9722df7e4 bump 2019-03-04 18:39:16 +09:00
474be6f7be French update 2019-03-04 18:39:03 +09:00
ada9a7264b Fix typo in AppsController.cs (#630)
Fixing a tiny typo in the AppsController.cs
2019-03-04 18:36:47 +09:00
e991b302d0 Fix bug: "BTCPay is expecting you to access this website" being cached 2019-03-04 18:33:57 +09:00
eef301c6ec Fix error message if X-Forwarded-Proto not set correctly 2019-03-04 17:26:08 +09:00
3fdfd0adfd Removed exponentiation on invoice request amount
Fixes #620
2019-03-03 17:44:28 -06:00
358f1ffc43 Removed border when there is only one currency 2019-03-03 17:44:28 -06:00
349c3409df Preserve password when sending test email 2019-03-03 17:40:40 -06:00
0263a2950c added enabled to ViewCrowdfundViewModel, added warning on preview page 2019-03-03 17:39:53 -06:00
0364a57cae Added a space between Enable and SSL. 2019-03-03 17:39:22 -06:00
e232dd7d7e Add a space between "Test" and "Email" in the UI. 2019-03-03 17:38:57 -06:00
fee936b569 Only checking for last admin if user being deleted is admin
Bugfixing issue #632
2019-03-03 17:38:38 -06:00
420115c54d bump nbx 2019-03-03 01:37:34 +09:00
312e961098 Update c-lightning, nbxplorer and lightning libs 2019-03-02 23:38:26 +09:00
223213857f Do not expose internal IP on SSH connection settings 2019-03-01 16:41:36 +09:00
c0da81557b Fix missing authenticated URI for charge 2019-03-01 15:38:11 +09:00
81945c0737 Fix bug on spark external config parsing 2019-03-01 15:34:30 +09:00
9664e3d6a1 bump version and deps 2019-03-01 14:55:53 +09:00
0a8bd38e76 Danish support 2019-03-01 14:52:45 +09:00
013054fb82 Small refactor 2019-03-01 14:46:32 +09:00
d898f716d1 Add some tests on externalConnectionString 2019-03-01 14:33:32 +09:00
1d3f144d21 Support relative path for external services, simplify the code in Services 2019-03-01 13:20:21 +09:00
de29d87487 Fix QR code not showing full uri if using relative path 2019-02-28 23:01:25 +09:00
ebef085a9c Support relative path for Spark and RTL external url, check in server settings if we are using a secure protocol 2019-02-28 22:20:14 +09:00
2c1f159d72 Document error of reverse proxy configuration 2019-02-28 21:43:44 +09:00
1ef59e05a5 fix dockerfile 2019-02-27 21:58:28 +09:00
5e09992637 Fix dockerfile 2019-02-27 21:56:36 +09:00
30448233b1 Fix dockerfile 2019-02-27 21:49:25 +09:00
a1601a17aa Fix permission 2019-02-27 21:44:16 +09:00
7522f7d0f7 fix remaining edgecase with payment request pay endpoint (#619) 2019-02-27 21:42:25 +09:00
d04b9c4c09 Remove last reference to externalurl 2019-02-27 21:41:02 +09:00
1a24ff9a49 bump 2019-02-27 21:41:01 +09:00
13d72de82d fix payment request redirect url (#617) 2019-02-27 20:25:13 +09:00
3728fdab3f improve warning message 2019-02-27 18:54:19 +09:00
2317e3d50c Make sure we rewrite the request scheme 2019-02-27 18:52:11 +09:00
5f15976c02 bump 2019-02-27 18:46:15 +09:00
7f592639c5 Remove URI rewritting and ExternalUri stuff 2019-02-27 18:38:11 +09:00
a98402af12 Making currency switching indicator more obvious with button style (#616) 2019-02-27 13:45:58 +09:00
316ffa91d1 bump 2019-02-26 23:10:58 +09:00
c24953b57e Making hamburger light to see it on dark background (#613) 2019-02-26 14:08:03 +09:00
7a1b1b7e5e Merge branch 'payment-requests' 2019-02-25 17:59:02 +09:00
70f71f64c4 Use internal tags, not order id in the streamer to know if the incoming invoice is for the payment request 2019-02-25 17:56:29 +09:00
5bccd07d7d Make sure the invoiceEvent is from a payment request. 2019-02-25 17:56:29 +09:00
d818baa6d1 Fix crowdfund test 2019-02-25 17:56:29 +09:00
249b8abf03 deduct network from contributions + removed unsued Enabled properties 2019-02-25 17:56:29 +09:00
c134277514 remove creating state from payment requests 2019-02-25 17:56:29 +09:00
f5d366cf7f Fix final bugs 2019-02-25 17:56:29 +09:00
ad25a2ed08 Add payment requests 2019-02-25 17:56:28 +09:00
1e7a2ffe97 Enable/Disable tips and discount. Fix custom amount. (#612) 2019-02-25 15:11:03 +09:00
dd52075ff1 Add backoff delay if fetching exchange rate is failing 2019-02-24 22:00:30 +09:00
0253e42bd5 Do not poll in the invoice page if websocket are working 2019-02-23 15:22:17 +09:00
d99774f8d9 fix tests 2019-02-22 22:52:43 +09:00
d563a2ec89 Fix tests 2019-02-22 22:48:39 +09:00
b4b4523193 Round currency up to significant decimal 2019-02-22 22:15:25 +09:00
fbcb69f447 Do not prevent btcpayserver from starting if using insecure protocol for lightning services 2019-02-22 18:24:27 +09:00
8ae5a9c1f7 Fix old crowdfunding invoices 2019-02-22 17:51:38 +09:00
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
5c074f6f5f Update translations 2019-01-07 22:51:26 +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
679942159e bump 2019-01-07 22:37:55 +09: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
63c309bd12 Merge pull request #499 from Kukks/node-info-page
Add Node Info Page
2019-01-07 22:04:08 +09:00
561ec57cc8 Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2019-01-07 11:13:02 +01:00
3cefd7bd1e Merge pull request #467 from Kukks/feature/coinswitch
CoinSwitch Integration
2019-01-07 19:11:55 +09:00
c63feb488c Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2019-01-07 10:58:52 +01:00
12c418d84d Add Node Info Page 2019-01-07 09:52:27 +01:00
4b982f815c Renaming 2019-01-07 15:35:18 +09:00
d4d3346b6d Merge pull request #463 from sipsorcery/455-disablereg
Set disable registration as default true
2019-01-07 15:20:04 +09:00
6010a103e0 Added new disable-registration command line option. 2019-01-06 16:43:55 +01:00
5dc1da2af0 Don't disable user registrations if debug for unit tests. 2019-01-06 14:55:18 +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
5fd77d9fcc Merge pull request #492 from Kukks/crowdfund-part-1
Crowdfund Part 1: JS Dependencies
2019-01-06 21:59:52 +09: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
540414d8f5 Merge pull request #495 from Kukks/crowdfund-part-3
Enhance Invoice Events
2019-01-06 18:14:56 +09:00
abcdb8ced0 Merge pull request #494 from Kukks/crowdfund-part-2
Crowdfund Part 2: Expand Invoice Searching
2019-01-06 18:14:23 +09:00
5076d73695 Enhance Invoice Events 2019-01-06 10:12:45 +01:00
d88735f84e Merge remote-tracking branch 'upstream/master' into 455-disablereg 2019-01-06 10:05:33 +01:00
2244f0ab76 Merge pull request #493 from btcpayserver/feature/fastertests
[WIP] Make tests fast to execute
2019-01-06 18:04:38 +09:00
40c85d6104 Expand Invoice Searching 2019-01-06 10:00:55 +01:00
42892e24f4 Remove uneeded database call during derivation scheme registration 2019-01-06 17:58:11 +09: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
c27557826b add vendors 2019-01-06 09:08:05 +01:00
88150b6535 Improve IPN tests 2019-01-06 15:04:30 +09: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
b2aebcc5d3 Merge pull request #480 from britttttk/fix/PaymentButton
Fix payment button size
2019-01-05 22:32:11 +09:00
ae9ad0fa65 Merge pull request #489 from btcpayserver/feature/networkfee
Add support for removing network fee on first payment
2019-01-05 22:09:10 +09: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
a05cd5678b Add support for removing network fee on first payment 2019-01-05 17:45:49 +09:00
2ccf007b9a fix permissions 2019-01-05 09:38:27 +01:00
895b8c2c80 ux fixes 2019-01-05 09:18:15 +01:00
0f175174f6 Rename TxFee to NetworkFee and save the Network Fee of each payment under PaymentEntity 2019-01-05 13:31:05 +09: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
fa8993191e Update coinswitch.html 2019-01-03 15:31:56 +01:00
239ce28575 Update coinswitch.html 2019-01-02 21:50:43 +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
1e26926350 Fix payment button size 2018-12-30 00:27:22 -07: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
fff39d9879 Merge remote-tracking branch 'origin/feature/coinswitch' into feature/coinswitch 2018-12-28 12:34:01 +01:00
444e761d41 Merge remote-tracking branch 'btcpayserver/master' into feature/coinswitch 2018-12-28 12:33:55 +01:00
68a3def35a save state for coinswitch started order 2018-12-28 12:33:47 +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
3effdf0f4d Update UpdateCoinSwitchSettings.cshtml 2018-12-28 10:49:12 +01:00
3c122bcf53 Merge pull request #478 from mariodian/pos-fix-search-btn
PoS: Fix z-index of search cancel button that overlaps modal confirmation
2018-12-28 16:12:59 +09:00
037ff52f4f Fix z-index of search cancel button that overlaps modal confirmation 2018-12-28 11:13:04 +08: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
fa506b5bf8 bump 2018-12-27 18:52:01 +09:00
e3193a92d0 Add PaidCurrency in the excel export 2018-12-27 16:48:33 +09:00
d76dabdca6 Remove warning 2018-12-27 16:19:00 +09:00
d219f50912 Merge branch 'pos-new-design' 2018-12-27 16:17:07 +09:00
e08710a19c Fix tests 2018-12-27 16:11:58 +09:00
4e167b35be Bug fixes
- fix `tip reset` when cart content changes
- fix negative cart value when deleting empty cart items
2018-12-27 13:19:51 +08:00
16873384a8 - fix cart item removal
- fix empty qty field
- remove tip when total changes
2018-12-27 12:57:31 +08:00
f87339f9fa Change CustomTipPercentages type to int[] 2018-12-27 12:57:31 +08:00
5f5e5e3211 Add missing AppId 2018-12-27 12:57:31 +08:00
f724db8226 Fix cart table widths 2018-12-27 12:57:31 +08:00
c7e90cd7df New PoS design 2018-12-27 12:57:31 +08:00
8c5b00b1a3 Merge pull request #470 from Kukks/feature/bootstrapbump
update bootstrap to 4.2.1
2018-12-27 13:40:42 +09:00
a3b79fbcd8 Merge pull request #462 from rockstardev/master
InvoiceDue field in export
2018-12-26 16:47:38 +09:00
9db77e6351 Rewrite and comment non obvious code for ledger 2018-12-26 15:10:00 +09:00
5bc1eaec9f bump 2018-12-26 15:04:43 +09:00
81c9ce7284 Limit the number of time the wallet need to export the xpub 2018-12-26 15:04:11 +09:00
caa6978d80 Save the KeyPath of the WalletKeyPathRoot of the hardware wallet so we don't have to scan for it 2018-12-26 14:04:00 +09:00
af22d6a4e3 Remove preliminary test to know if the ledger can handle the store. If it can't signing will fail anyway. 2018-12-25 19:33:03 +09:00
0eabb3c37c Remove useless query to ledger xpub in the Add derivation scheme screen 2018-12-25 19:02:11 +09:00
2b84791391 fix raw html 2018-12-22 21:03:43 +01:00
6f896cb096 update bootstrap to 4.2.1 2018-12-22 17:32:03 +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
a63ed4d3b4 bump 2018-12-21 16:45:05 +09:00
968c820702 Add turkish translation 2018-12-21 14:15:05 +09:00
3061b4dfd2 Add comments 2018-12-21 13:33:26 +09:00
ed4de612dd Fix layout if customized to an absolute uri 2018-12-21 13:31:02 +09:00
d4bdd5fd9c Do not use absolute link to link theme files on layout.cshtml 2018-12-21 13:24:01 +09:00
8b71556425 Merge branch 'master' into 455-disablereg 2018-12-20 21:58:07 +01:00
ae6e1bfd85 Update UpdateCoinSwitchSettings.cshtml 2018-12-20 21:01:10 +01:00
6d1f3b73ef update link 2018-12-20 20:40:33 +01:00
0dcaf80c7f Changed disable register mechanism to apply policy setting after admin user created rather than using DB user count checks. 2018-12-20 20:39:48 +01:00
fc9cd5bdf0 Merge remote-tracking branch 'btcpayserver/master' into feature/coinswitch 2018-12-20 17:57:04 +01:00
a434c45196 fix function names 2018-12-20 17:56:57 +01:00
87b316ec23 Merge pull request #439 from Kukks/grs-clightning
Add lightning icon for GRS
2018-12-20 22:43:28 +09:00
9c99ffae57 Lightning charge integration 2018-12-20 22:40:32 +09:00
30a3a84ec9 fix final issues with integration 2018-12-20 14:33:31 +01:00
d0f585df9d fix tests 2018-12-20 21:34:09 +09:00
bac2db5cda Add timeout to lightning tests 2018-12-20 21:27:08 +09:00
c35bf2f483 fix docker compose 2018-12-20 21:23:24 +09:00
2e04c5e39c Update docker-compose test 2018-12-20 21:20:30 +09:00
9dcf16e819 Add xunit diagnostic message 2018-12-20 21:00:06 +09:00
361d494cde Accept cookiefilepath as alternative to cookiefile for spark connection string 2018-12-20 20:24:06 +09:00
4d7015294e Fix configuration of lnd rest 2018-12-20 20:12:36 +09:00
5f16fb4668 report slow tests 2018-12-20 18:44:39 +09:00
4bf2228675 Show test logs in CircleCI 2018-12-20 18:35:32 +09:00
2ba823f192 Merge pull request #465 from Kukks/master
do not add ln payment if amount couldn't be parsed
2018-12-20 18:09:57 +09:00
27fa2d5b69 do not add ln payment if amount couldn't be parsed 2018-12-20 09:21:20 +01:00
47ef7661d8 Fix search for macaroon 2018-12-20 17:08:32 +09:00
3f6ff25322 update es-ES 2018-12-20 17:00:30 +09:00
f56c23009a bump 2018-12-20 16:57:59 +09:00
e80593fb7b Expose LND's other macaroon if possible 2018-12-20 16:52:04 +09:00
57324345ac Remove remaining of RestrictedMacaroon 2018-12-20 16:24:36 +09:00
73e280157d Show the gRPC cypher suites for gRPC consumption 2018-12-20 14:16:23 +09:00
cfaa5766ed Always allow user registration if there are no user records. 2018-12-19 20:03:27 +01:00
8b08db308b Merge branch 'master' into 455-disablereg 2018-12-19 19:40:13 +01:00
3e2ff55954 Merge remote-tracking branch 'btcpayserver/master' into feature/coinswitch 2018-12-19 10:41:36 +01:00
70d1d0d230 Remove volumes before running tests 2018-12-19 15:50:20 +09:00
94e0048a3b Redirect users to docs.btcpayserver.org on home page 2018-12-19 15:30:10 +09:00
a34d1641b3 Set disable registration as default true. 2018-12-18 20:16:48 +01:00
40c645e433 coinswitch integration 2018-12-18 20:14:59 +01:00
b2e5415a35 coinswitch integration 2018-12-18 20:00:30 +01:00
365ee4cf0b Fixing CSV test now that we have new field / reorders 2018-12-18 12:35:59 -06:00
2b4603a234 coinswitch integration 2018-12-18 19:01:58 +01:00
ec23eae21d Ensuding that payments are always ordered by time for consistency 2018-12-18 11:56:51 -06:00
7a9229628a InvoiceDue field in export 2018-12-18 11:56:12 -06:00
9db5c0f375 Hack tests to make currency formatting work on linux 2018-12-19 00:28:06 +09:00
27bde55f54 work on building the viewmodel for crowdfund 2018-12-18 16:27:03 +01:00
2bb24282d2 Clean previous run with dock-compose 2018-12-19 00:15:33 +09:00
998472e463 Fix symbol display on linux 2018-12-19 00:11:15 +09:00
63ff46a768 cache docker on circleCI for tests 2018-12-18 23:27:57 +09:00
660f43e3b7 Add fast test for JPY formatting 2018-12-18 23:26:35 +09:00
0ba96aa4b8 Fix export tests 2018-12-18 23:24:22 +09:00
d85247d2ad Run tests inside container 2018-12-18 22:35:58 +09:00
9ca85ed365 Change column order 2018-12-18 21:44:51 +09:00
93113fd871 Fix payment exports to reflect correctly payment data, rename fields. 2018-12-18 21:35:52 +09:00
b5d360594a Merge remote-tracking branch 'origin/master' into feature/crowdfund 2018-12-18 13:29:22 +01:00
d5ae79c38c Add more information about status in the CSV export 2018-12-18 19:33:14 +09:00
7cf07b27e3 Invoice export should not prefix amounts with crypto code 2018-12-18 19:20:10 +09:00
bb0f986b0c Add additional test on euro formatting 2018-12-18 19:09:55 +09:00
2c2a85327f Add test logs 2018-12-18 01:02:27 +09:00
7bf03e497b In cart js, add space to symbol if needed (fix #450) 2018-12-18 00:38:59 +09:00
7a4dee3d38 Point of Sale returns correct currency information (#450) 2018-12-18 00:25:17 +09:00
7b27d6f0bb Merge branch 'mariodian-pos-product-management' 2018-12-15 23:40:04 +09:00
83dc95a0a7 Remove dollar sign in textbox 2018-12-15 23:39:45 +09:00
d60889f952 Merge branch 'pos-product-management' of https://github.com/mariodian/btcpayserver into mariodian-pos-product-management 2018-12-15 23:22:07 +09:00
8c9952973d Merge pull request #449 from sipsorcery/uxpwdreset
HTML formatting fix for issue #448.
2018-12-15 23:20:09 +09:00
00673bdb7f Fix product width on smaller screens 2018-12-14 16:16:08 +08:00
d039890a9b Create js-only product management in PoS 2018-12-14 16:03:02 +08:00
41e88c07fe update languages 2018-12-14 13:14:03 +09:00
67c5027b16 bump 2018-12-14 13:12:27 +09:00
a341d4f800 Show Spark QR Code pairing 2018-12-14 13:12:27 +09: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
3ad1834439 Better fix this as well or the user gets a blank page after the reset. 2018-12-12 22:39:37 +01:00
4b492eae85 HTML formatting fix for issue #448. 2018-12-12 22:29:40 +01:00
f0ff47af8d Merge pull request #447 from britttttk/translations/disable
Check for disable flag in Transifex
2018-12-12 23:40:47 +09:00
991826b686 do not show restricted macaroon 2018-12-12 18:52:01 +09:00
22d59a1ed7 Do not leak access key in browser 2018-12-12 18:37:50 +09:00
475ea68696 Can attach external spark 2018-12-12 18:19:13 +09:00
864e84706a check for disable flag 2018-12-11 22:18:17 -07:00
9c93e76eeb Remove temporary nuget down hack 2018-12-12 12:20:17 +09:00
7fa1b65af0 initial commit 2018-12-11 16:36:25 +01:00
c00c95efcf initial coinswitch work 2018-12-11 12:47:38 +01:00
94be2b46d5 docker build should use right api.nuget.org server 2018-12-10 23:36:54 +09:00
4b4d0d2d19 Adding working server for api.nuget.org 2018-12-10 22:35:43 +09:00
0d06cf63b7 Use enum for invoice status and invoice exception 2018-12-10 21:48:28 +09:00
7b24c02d51 bump 2018-12-10 20:34:34 +09:00
becf488714 Merge remote-tracking branch 'btcpayserver/master' into grs-clightning 2018-12-10 10:42:08 +01:00
e89e8226e4 Fix build 2018-12-10 17:34:27 +09:00
a533a96598 Remove XFrame for PoS 2018-12-10 16:39:21 +09:00
27321c0919 bump 2018-12-10 16:04:28 +09:00
058472d325 Show restricted macaroon for LND 2018-12-10 16:03:58 +09:00
b5c9a03052 Can mark invoice as complete 2018-12-10 15:34:48 +09:00
07dad3affa bump 2018-12-07 19:35:25 +09:00
8afc103ae7 Show REST connection information for LND in a QR Code 2018-12-07 19:31:07 +09:00
591d7b4b80 Can show external service link with BTCPAY_EXTERNALSERVICES 2018-12-07 18:42:39 +09:00
2162afc78e Lightning network warnings 2018-12-07 17:54:10 +09:00
25e226d219 Clarify the code 2018-12-07 14:37:07 +09:00
8472bfe90d Add test for bad bitid signature 2018-12-07 14:34:07 +09:00
93645b2fbe Fix error 500 if token not found 2018-12-07 13:48:39 +09:00
d53c987f2e bump 2018-12-06 17:25:50 +09:00
682693a9f0 Update translations 2018-12-06 17:23:42 +09:00
e836faf792 Stop setting BIP70 link info 2018-12-06 17:12:51 +09:00
6e27233be8 Remove BIP70 support 2018-12-06 17:08:28 +09:00
9209984a2f Remove useless argument from GetInvoice 2018-12-06 17:05:27 +09:00
1477630c78 Remove anonymous access to invoice data 2018-12-06 16:58:04 +09:00
ab670080c7 bump 2018-12-06 12:29:13 +09:00
8198f98376 Code simplification 2018-12-06 12:26:42 +09:00
65b4697229 Properly error 401 if request is not signed correctly 2018-12-06 12:22:05 +09:00
e75a1a8b70 Improve ledger feedback for asking authorization to access xpub 2018-12-04 21:22:27 +09:00
580494fea7 add correct icon 2018-12-04 11:46:01 +01:00
5a958da84d bump 2018-12-04 13:04:56 +09:00
cad602ad14 Fix several issues in cart
* Fix: Only USD currency with 2 decimals were properly handled for tips
* Fix: All PoS apps would were sharing the same basket
* Fix: Currency formatting was not using server side information
* Fix: Various bug of formatting for decimal 0 and more than 2.
2018-12-04 13:04:26 +09:00
1f14bd6188 Add button and qr code to the bitpay translator 2018-12-04 11:53:25 +09:00
156f52b76f Add bitpay translator 2018-12-03 23:59:08 +09:00
d674b8ac71 Merge pull request #430 from dalijolijo/master
bump
2018-12-01 22:40:48 +09:00
861150971f bump 2018-12-01 10:15:57 +00:00
a653421514 Merge pull request #428 from mariodian/fix-pos-cart-currency
Fix currency format for total amount
2018-12-01 14:09:01 +09:00
8f234a02cb Add currency formats for major currencies 2018-12-01 12:59:45 +08:00
92ecf99427 bump 2018-12-01 13:23:56 +09:00
705dbf12d7 Change translation of the expiration screen 2018-12-01 13:19:35 +09:00
fe11b11c13 Add Polski and Srpski 2018-12-01 12:02:53 +09:00
f2a43ad1f3 Escape js properties in html template 2018-11-30 21:14:09 +08:00
cbbe5cfb25 - fix currency format for numbers over 999
- fix cart table
2018-11-30 20:01:47 +08:00
0eccc6085b bump 2018-11-30 04:34:38 -06:00
a89da1f705 Recoding test to respect new ordering in CSV 2018-11-30 04:34:18 -06:00
5b297e539a Additional fields and ordering based on feedback 2018-11-30 04:18:37 -06:00
1d932c3753 Improve invoice script if no PoS data available 2018-11-30 04:17:57 -06:00
5a77fc74ba quickly fix changelly button style (#423)
Fix the button for now so it doesn't appear broken.
2018-11-30 04:17:44 -06:00
7b47b96252 Always using quotes for CSV export 2018-11-30 03:15:23 -06:00
a4bec83ecc Fixing warnings on invariant culture, hate this for being so verbose 2018-11-30 02:51:23 -06:00
8509a0de18 Basic export CSV and JSON tests 2018-11-30 02:34:43 -06:00
8e30b7430d Adding PaymentType and destination, CSV export 2018-11-30 02:04:26 -06:00
9235d32a45 Export of payments made on invoices 2018-11-30 01:22:39 -06:00
dd503570ac bump 2018-11-30 11:30:30 +09:00
613281a1e7 Fix form processing when cart is enabled (#424) 2018-11-30 11:29:27 +09:00
bab7bf6633 bump 2018-11-27 15:17:32 +09:00
1831692761 Enable shopping cart, add items to cart, enable tips (#410)
Modal cart, remove items, checkout

Fix removal and adding of cart items

Improve cart UI

Add cart bundle, remove unused js files from the view when cart isn't used

Do not enable cart by default

Do not put modal into the view when the cart is disabled

Escape js properties

Work with amounts as cents

Make animation speed look constant

Enable tips in the cart

Fix cart UI
2018-11-27 15:14:32 +09:00
e144d2479b Add POS Data to Invoice UI (#409)
* Add POS Data in Invoice UI

* fix build

* extract in helper and add UTs

* add in unit test coverage through mvc view too
2018-11-27 15:13:09 +09:00
c25831316e bump nbx 2018-11-26 12:02:50 +09:00
60b72aabe8 fix test 2018-11-24 13:38:23 +09:00
c8fcb0ab18 Use framework dependent build for ARM 2018-11-23 16:14:13 +09:00
9911d18390 Do not push latest images to dockerhub 2018-11-23 14:12:47 +09:00
e24630ac1e Remove qemu install requirement for the host 2018-11-23 14:08:14 +09:00
4c1fd3edae More comment on ARM build 2018-11-23 14:02:44 +09:00
f65492dd66 Use stretch slim for arm 2018-11-23 14:00:33 +09:00
5d978c7670 Use manifest image for building arm images 2018-11-23 13:58:21 +09:00
11788cece9 No need to create latest tag 2018-11-23 13:18:35 +09:00
1aaa55dc62 Make test less flaky 2018-11-23 13:09:30 +09:00
ce57a2b8fb Do not tag latest 2018-11-23 12:59:48 +09:00
0604cc5bd0 bump 2018-11-23 11:37:05 +09:00
3d2c0bcc6c Use specific sdk and runtime version for arm 2018-11-23 11:23:27 +09:00
0f222979a6 CircleCI multiarch Docker images (#416)
* Preparing final version of CircleCI docker building

* Removing test job requirement for building Docker images

We'll already monitor build before tagging, would be too many checks

* Adding pushing of manifest for tag

* Easy access to docker/circle config files for edit

* Generalizing script with $DOCKERHUB_REPO variable
2018-11-23 11:21:01 +09:00
a1eb6a14f5 Fix all script because of docker-compose team screwing up (https://github.com/docker/compose/issues/6316) 2018-11-22 16:16:10 +09:00
186ce01022 add pairing code to tokens page after authorize (#412) 2018-11-22 15:13:35 +09:00
0096ec1d12 bump nbxplorer 2018-11-21 20:41:51 +09:00
2929d7bf51 Fixing MONA_BTC rate breaking tests and CircleCI because zaif is down 2018-11-20 15:42:45 -06:00
d90fb5764d Add noindex,nofollow to invoices, checkouts and fix create invoice ui bug (#407)
* add noindex,nofollow on invoices

* fix create invoice button and add noindex,nofollow to checkout and invoice pages
2018-11-19 13:20:48 +09:00
4dccd0c733 Add better instruction on how to customize the theme 2018-11-17 12:43:11 +09:00
300d912331 bump 2018-11-17 11:43:41 +09:00
9d21c89151 Preserving title with custom amount (#403)
* Preserving title with custom amount

* Custom button texts for complete localization

* Update tests, now checking custom amount description and button text

* Support for Custom CSS in POS
2018-11-17 11:39:43 +09:00
24a8c4015c Bump 2018-11-17 11:35:20 +09:00
5eb40d6b7f Bugfixing redirect button (#405) 2018-11-17 11:32:31 +09:00
36f486e91b Add test for the parser 2018-11-17 01:45:59 +09:00
5b684ac26e Make really sure we don't generate segwit addresses for non segwit coins 2018-11-17 01:39:32 +09:00
85062725bd bump 2018-11-17 01:21:56 +09:00
401d9c8565 DerivationSchemeParser should not override a label 2018-11-17 01:21:34 +09:00
6f276ac1bc Do not crash if derivation strategy is empty 2018-11-17 01:09:28 +09:00
4350785cef Remove double slash 2018-11-17 00:23:51 +09:00
9a2a85ac3d Update translations 2018-11-17 00:22:18 +09:00
d030a61322 bump 2018-11-17 00:16:31 +09:00
dacb6dca41 bump .net core 2018-11-17 00:13:22 +09:00
c40fc69087 Use the choiceKey of PoS item as ItemCode 2018-11-16 23:16:44 +09:00
eff983135c showcase the custom field in PoS template 2018-11-16 18:36:18 +09:00
479303dd9e Tweaking UI for custom amounts (#398)
* Tweaking appearance of custom amount card

* Allowing POS items to have custom amounts, good for donations/tips

* Prepending currency symbol in POS

* Fixing regression, thanks unit test
2018-11-16 12:31:38 +09:00
e9b2088f7d change default title for pointofsale 2018-11-14 17:45:46 +09:00
4af5b94013 Add tooling which pull transifex translation automatically, add Slovenčina. (Close #386) 2018-11-14 16:48:25 +09:00
441398402d Remove global.json because .403 became suddenly unavailable 2018-11-13 16:41:49 +09:00
258d4fda3f bump 2018-11-13 16:37:43 +09:00
8e667f6c3f Allow empty template (Fix #303) 2018-11-13 16:32:13 +09:00
a996cc2e6d Fix margins, change template (#397) 2018-11-13 16:29:18 +09:00
9b8a8690e7 Change links to gitbook 2018-11-13 16:21:58 +09:00
f2387fd6b5 Workaround to compile on circle 2018-11-13 16:16:57 +09:00
888036a99d use docker on Circle CI 2018-11-13 15:55:10 +09:00
539c0ed7f0 show dotnet info on CI 2018-11-13 15:47:25 +09:00
95e065a462 Add tooltip to update store (#382) 2018-11-13 15:36:07 +09:00
087f20cb6c Fix small view error in logs (#392) 2018-11-12 22:25:39 +09:00
7adf321956 Checkout Experience Language Setting (#393)
* fix check out experience default language validation of preset value not found

* Update CheckoutExperienceViewModel.cs
2018-11-12 22:17:00 +09:00
dc749462ec automatically detect the btcpay server url in btcpay.js 2018-11-10 23:43:48 +09:00
16b57f24a2 Fix #383 2018-11-10 23:25:11 +09:00
b16b1c3e8b - add item image and description (#391)
- fix margins
2018-11-10 15:38:26 +09:00
fee56873b5 Handle exception if log file do not exists. 2018-11-09 21:43:10 +09:00
e1b2b72cd2 bump 2018-11-09 21:16:09 +09:00
daf4e5ce6c I am sorry for so many prs <3 (#389)
* make language loading more solid

* disable browser lang preferences

* pr fix

* pr fixes

* pr fixes

* make sure language files are named correctly

* fix dropdown width issue when in modal form

* fix issue from jquery hell
2018-11-09 21:13:00 +09:00
2ec2c7263f Make language loading more efficient and solid (#388)
* make language loading more solid

* disable browser lang preferences

* pr fix

* pr fixes

* pr fixes
2018-11-09 19:02:53 +09:00
abfcab552f bump (#384) 2018-11-09 17:34:30 +09:00
cfdf8b1670 Example modal in invoice list (#387) 2018-11-09 17:13:45 +09:00
f23e2a3ec4 async i18n and json translation format (#369)
* start working on loading locales async and as json

* finish off langs and UI

* fix path

* fix tests
2018-11-09 16:48:38 +09:00
aa1ac3da50 Modal invoice through btcpay.js (#381)
* Modal through btcpay.js

* Handling close action depending on whether is modal or not

* Tweaking button position

* Stripping trailing slashes if present when setting site root
2018-11-09 16:09:09 +09:00
c9c7316b7d Logs UI in Server Admin (#374)
* add in ui

* add in logging viewer

* Revert "add in ui"

This reverts commit 9614721fa8a439f7097adca69772b5d41f6585d6.

* finish basic feature

* clean up

* improve and fix build

* add in debug log level command option

* use paging for log file list, use extension to select log files, show message for setting up logging

* make paging a little better

* add very basic UT for logs

* Update ServerController.cs
2018-11-07 22:29:35 +09:00
d152d5cd90 fix build 2018-11-06 16:08:42 +09:00
6fd37710e1 Rename validators namespace 2018-11-06 15:38:07 +09:00
0419a3c19a do not affect Buyer for every paymentid 2018-11-05 17:37:55 +09:00
0c382da561 Show unconf transactions with low opacity 2018-11-05 17:26:49 +09:00
9fc7f287d2 Expose buyer object to conform to bitpay API 2018-11-05 17:02:12 +09:00
dd7c4850f0 bump nbxplorer 2018-11-05 14:12:05 +09:00
93992ec3ed bump 2018-11-05 12:15:05 +09:00
15d9adfbf1 Fix rate fetching for kraken doge and dash 2018-11-05 12:14:39 +09:00
676a914c40 Fix, allow rescan if other crypto nodes are not synched 2018-11-04 22:46:27 +09:00
b423b4eec1 Do not allow rescan of wallet which are not segwit 2018-11-04 14:59:28 +09:00
9784a89112 limit apdu size to ledger 2018-11-04 00:36:48 +09:00
7b596e6d9c Add Bitcore BTX support (#259) 2018-11-03 13:42:17 +09:00
76febcf238 bump NBXplorer.Client (#378)
NBXplorer.Client Version 1.0.3.5 is available: da7df86019
2018-11-02 21:38:41 +09:00
a57a72de88 bump clightning 2018-11-02 19:02:07 +09:00
235b307b06 bump deps 2018-11-02 18:05:48 +09:00
05b0f6d0f7 Fix invoice search not working on transaction id 2018-11-02 14:26:13 +09:00
1d7081d8b8 bump 2018-11-01 21:51:16 +09:00
c0174c0c2c inverse DASH rate 2018-11-01 16:33:53 +09:00
fa8324c1f9 Fix DASH rate for kraken 2018-11-01 14:48:46 +09:00
4b0951caec trim destination in WalletSend 2018-11-01 12:54:25 +09:00
0d51c99717 Properly configure the logger to log what happen in ConfigureServices, add https profile adapted for debugging ledger wallet. 2018-11-01 12:52:32 +09:00
24623c59d7 Adjusted mechanism for setting https binding configuration option (#372)
* Adjusted mechanism for setting https binding configuration option.

* Modified the https binding logic to use default bind and port options.

* Removed dedicated https certification config properties and instead used direct access via setting name.
2018-11-01 12:07:28 +09:00
88044f6b76 Decouple Wallet Send screen from Ledger Wallet 2018-11-01 00:19:25 +09:00
38edbf8362 Improve token UX (Fix #353) 2018-10-31 17:59:09 +09:00
bc0acf5701 make test more reliable 2018-10-31 16:57:31 +09:00
a82f181126 Reactivate cryptopia 2018-10-31 13:31:03 +09:00
be0139a46f bump 2018-10-31 13:06:36 +09:00
4db5b4f2b1 Wait for the nodes to be fully synched before starting tests 2018-10-31 13:06:17 +09:00
93cefced80 bump .NET core and dependencies 2018-10-31 13:03:12 +09:00
85f586f623 bump dependencies 2018-10-31 11:56:21 +09:00
2be1f97419 Remove cryptopedia as direct provider, add estimated time to wallet rescan page, bump nbx 2018-10-30 15:40:27 +09:00
63014231ab Revert "Added configuration options for BtcPayServer https binding. (#360)"
This reverts commit 3ac37497ab9b5ff2c28eaab54c7f2a12356659dd.
2018-10-30 00:25:05 +09:00
3ac37497ab Added configuration options for BtcPayServer https binding. (#360) 2018-10-30 00:11:02 +09:00
d0cafb020f Add an invoices list to store list 2018-10-29 12:44:20 +09:00
d3b3198b68 For lightning payments tests, add small delay after creating the invoice before sending the payment 2018-10-29 00:22:30 +09:00
c1f17ff63b Add some test logs to flaky test 2018-10-28 23:43:48 +09:00
dafd958f69 bump 2018-10-28 23:07:58 +09:00
f51af6c61c fix issue with changelly rates and cover with UTs (#368) 2018-10-28 23:07:36 +09:00
254db22063 Change test trait name 2018-10-28 22:51:02 +09:00
8be4256278 Fix unreliable tests 2018-10-28 22:46:03 +09:00
8e8669d63f Warning as errors 2018-10-28 22:15:32 +09:00
4625ff92f1 Run unreliable tests, attempt to make them a bit more reliable 2018-10-28 22:10:37 +09:00
6aa84326af Make sure tests run sequentially 2018-10-28 21:46:12 +09:00
9a384d81fe Run only dev time containers 2018-10-28 21:25:42 +09:00
0cbe36c048 Run reliable tests, remove the docker build 2018-10-28 21:19:18 +09:00
7f16aa8c7e Run only fast tests on CI 2018-10-28 20:59:59 +09:00
872f8a6229 Add circleCI badge 2018-10-28 20:28:16 +09:00
9b261daa6d Add circleci file 2018-10-28 20:06:04 +09:00
c46c15c258 Fix changelly tests 2018-10-28 01:10:07 +09:00
a8ba1ed1ed Removing Kukks changelly credential from the source code 2018-10-28 01:02:24 +09:00
ff4056d4f3 bump 2018-10-27 23:32:04 +09:00
ae152c3ffa bump NBXplorer 2018-10-27 23:30:57 +09:00
e2ff33d7db Document how to test mysql 2018-10-27 23:20:50 +09:00
ce94c05fd3 MySQL Support (#345)
* MySQL EF support added using Pomelo MySQL provider.

* MySQL EF support added using Pomelo MySQL provider.
2018-10-27 23:15:21 +09:00
9cde4dc7e2 Restart containers if crash 2018-10-27 23:14:26 +09:00
ca571cd756 Add authorization on WalletRescan 2018-10-27 22:52:09 +09:00
e5eb0c79c0 Exposing LND Rest, providing info in Server/Services (#363)
* Displaying LND Rest connection info in Services

* Code cleanup

* Tweaking UI

* Fix typo
2018-10-27 22:49:39 +09:00
43bd6587d3 re-enable changelly 2018-10-27 22:41:37 +09:00
3bb059ab74 refactor changelly & improve tests (#366) 2018-10-27 22:41:07 +09:00
4c963d6edf bump 2018-10-26 23:10:45 +09:00
396bc7f7b4 Commenting Changelly 2018-10-26 23:10:29 +09:00
2896a9b26f Add ScanUTXOSet support 2018-10-26 23:07:39 +09:00
9267a45449 Remove useless test 2018-10-26 19:07:19 +09:00
c430d470c4 Fix warnings and bump nbxplorer 2018-10-26 19:06:06 +09:00
3921a3ca22 Fix warnings, update libs 2018-10-26 18:36:58 +09:00
1ff0a98d30 Adding Ukrainian Translation (#352) 2018-10-24 15:18:31 +09:00
f0efd52cb7 Adding Kazakh Language (#350) 2018-10-24 15:17:09 +09:00
bb8fa88688 Adding Vietnamese (#351) 2018-10-24 15:16:30 +09:00
4b976c13c1 Changelly v2 (#343)
* Disable shapeshift and use changelly

* UI to manage changelly payment method

* wip on changelly api

* Add in Vue component for changelly and remove target currency from payment method

* add changelly merhcant id

* Small fixes to get Conversion to load

* wip fixing the component

* fix merge conflict

* fixes to UI

* remove debug, fix fee calc and move changelly to own partials

* Update ChangellyController.cs

* move original vue setup back to checkout

* Update core.js

* Extracting Changelly component to js file

* Proposal for loading spinner

* remove zone

* imrpove changelly ui

* add in changelly config checks

* try new method to calculate amount + remove to currency from list

* abstract changelly lofgic to provider and reduce dependency on js component

* Add UTs for Changelly

* refactor changelly backend

* fix failing UT

* add shitcoin tax

* pr changes

* pr changes

* WIP: getting rid of changelly dependency

* client caching, compiling code, cleaner code

* Cleaner changelly

* fiat!

* updat i18n, css and error styler

* default keys

* pr changes part 1

* part2

* fix tests

* fix loader alignment and retry button responsiveness

* final pr change
2018-10-24 14:52:19 +09:00
f68d4efcdd update to 0.17.0 2018-10-19 19:05:12 +09:00
fea247b218 Fixing broken link in Wallets/WalletSend.cshtml (#342)
Removing the earlier Yubico link, since it's broken and the article no longer exists.
Furthermore, I tested this integration with other U2F supporting browsers (Firefox Nightly, Firefox) and it only works in Google Chrome, so I suggest we only suggest what works, and for now, that's Chrome only.
2018-10-18 12:43:41 +09:00
f419c56a3c Revert "Changelly Support (#267)"
This reverts commit a5fca7a1c43c4d23ad1a825253fa1fe3ab26677c.
2018-10-18 12:27:46 +09:00
a5fca7a1c4 Changelly Support (#267)
* Disable shapeshift and use changelly

* UI to manage changelly payment method

* wip on changelly api

* Add in Vue component for changelly and remove target currency from payment method

* add changelly merhcant id

* Small fixes to get Conversion to load

* wip fixing the component

* fix merge conflict

* fixes to UI

* remove debug, fix fee calc and move changelly to own partials

* Update ChangellyController.cs

* move original vue setup back to checkout

* Update core.js

* Extracting Changelly component to js file

* Proposal for loading spinner

* remove zone

* imrpove changelly ui

* add in changelly config checks

* try new method to calculate amount + remove to currency from list

* abstract changelly lofgic to provider and reduce dependency on js component

* Add UTs for Changelly

* refactor changelly backend

* fix failing UT

* add shitcoin tax

* pr changes

* pr changes
2018-10-18 12:13:39 +09:00
e18d0b5d51 Updating Yaml (#336) 2018-10-17 13:30:43 +09:00
9952cdca7f bump 2018-10-17 12:06:37 +09:00
6278145374 Removing old QR update code (#337) 2018-10-17 11:55:49 +09:00
84018a5caa Bugfixing race condition for QR code switch (#335)
Ref: #334
2018-10-17 11:49:30 +09:00
d7785fe2d2 Added Serilog file logger for debug file support. (#323) 2018-10-16 00:37:42 +09:00
e1751c4d91 [Fix] Querying rate with authenticated request should be successfull 2018-10-15 18:11:20 +09:00
913da79ff4 Remove dups lang 2018-10-14 21:35:21 +09:00
a4fbb2de7e creating italian localization file (#310) 2018-10-14 21:34:15 +09:00
b5601ed5e6 fix typo (#304) 2018-10-14 21:28:09 +09:00
42c4f15f22 fixed typos (#331) 2018-10-14 21:26:47 +09:00
6fbd9b2628 bump 2018-10-12 14:02:27 +09:00
d04bfb58a2 Resolving issue with long translations breaking layout (#330) 2018-10-12 14:01:44 +09:00
cded2548f5 bump 2018-10-12 13:32:04 +09:00
dcc859a86a Disable export to JSON 2018-10-12 13:17:38 +09:00
9bec38559f Nepali Translation (#311)
* Nepali Language

* Delete Checkout.cshtml

* Delete LanguageService.cs

* add np.js

* Match Language File Style
2018-10-12 10:11:03 +09:00
2856454d41 [langs-fr] Correction and some minor improvement (#316) 2018-10-12 10:10:42 +09:00
b3c4fc4003 Added Russian (#312) 2018-10-12 10:10:20 +09:00
c2bbc04c4c Various bugfixes (#308)
* NotifyEmail field on Invoice, sending email when triggered

* Styling invoices page

* Exporting Invoices in JSON

* Recoding based on feedback

* Fixing image breaking responsive layout on mobile

* Reducing amount of data sent in email notification

* Turning bundling on by default
2018-10-12 10:09:13 +09:00
db40c7bc32 Solving the new version of btcpayserver caused btcpay-python not to create an order problem (#327) 2018-10-11 23:50:28 +09:00
60707fdbd1 Add simplified chinese translation (#326)
* Add Chinese Simplified translation

* Add Chinese simplified translation
2018-10-11 14:25:16 +09:00
f05614f4da bump 2018-10-11 00:51:43 +09:00
a10c382bd4 [Tests] return WalletId when registering scheme 2018-10-10 00:13:37 +09:00
da2fb876cb Can pass pre filled amount and address to Send Wallet 2018-10-09 23:48:14 +09:00
3c58bff803 Comparable WalletId 2018-10-09 23:44:32 +09:00
a28814bc0e Fix RateRules crash if dups 2018-10-09 23:43:03 +09:00
3cff8261ae Set the id for apps only to 20 bytes, and output the Id in the controller so we can use it in tests 2018-10-09 23:38:56 +09:00
b16e8f7b76 Move GetAppDataIfOwner inside AppHelper 2018-10-09 23:34:51 +09:00
57ab001c0c [Tests] Pass Port to the fake http context 2018-10-09 23:32:47 +09:00
3d85dace38 Move currency display method into CurrencyNameTable 2018-10-09 23:30:26 +09:00
7a04c2974f Center QR Authenticator QR Code 2018-10-09 23:30:26 +09:00
d459839bf7 Displaying Node Info as QR code for Lightning payments (#318)
* Styling elements required for Node info

* Allowing switching QR between bolt11 and node info for lightning

* Equal width for Bolt11 and Node info buttons

* Certain languages were too verbose for display of "Pay with"

Fixes: #317
2018-10-08 07:19:55 +09:00
657cfe1b23 bump 2018-10-06 23:21:33 +09:00
f4eaa0f01f Make sure X-Forwarded-Port does not override ExternalUrl 2018-10-06 23:20:32 +09:00
1e2ffcadf0 Add GRS lightning image (#309) 2018-10-03 11:25:52 +09:00
dcc05af02e fix typo 2018-10-02 22:11:01 +09:00
4b7f78f38b document .net version update 2018-10-02 22:06:06 +09:00
f94ff4cc74 Update dotnet version + nbxplorer 2018-10-02 19:33:25 +09:00
b750663a1f update lnd/clightning 2018-09-28 17:08:51 +09:00
4c4b76e995 Remove Bitpay direct provider 2018-09-28 17:08:51 +09:00
da19d2c1a7 Bugfixing display of custom amount entry in POS (#302)
Ref: #293
2018-09-28 13:31:59 +09:00
fb15c5b354 Increase timeout for wallet, update references 2018-09-28 10:15:35 +09:00
6ffe1cfcab bump LedgerWallet lib 2018-09-27 16:31:33 +09:00
87678c58ac Fix error message for ledger signing 2018-09-27 15:43:34 +09:00
feab4cc48a update ledgerwebsocket 2018-09-27 15:21:26 +09:00
712946f512 bump 2018-09-22 05:16:24 -05:00
a7bfceae05 Reverting to 2.1.0 until we update docker images 2018-09-22 05:14:36 -05:00
8a26cd549a bump 2018-09-22 01:11:14 -05:00
1cf3ce0617 Footer with server version now visible only for logged in users
Per #282
2018-09-22 01:11:14 -05:00
73c65fada2 Fixing display on Lockout page 2018-09-22 01:11:14 -05:00
92ea923c03 Updating reference to Bitcoin docker image 2018-09-22 01:11:14 -05:00
e7db453717 Removing network fee line item if fee is 0
Per discussion in #265
2018-09-22 01:11:14 -05:00
10ee09f052 Bugfixing broken link in footer
Div overlay caused it, fixes #269
2018-09-22 01:11:14 -05:00
be7d91a138 VueJs cloak until loading is finished
Addressing #255
2018-09-22 01:11:14 -05:00
3278c80d3f Fixing problem with View dynamic recompiling
The type 'RazorViewAttribute' exists in both 'Microsoft.AspNetCore.Mvc.Razor' error resolved
2018-09-22 01:11:14 -05:00
65e1edb0b8 Merge pull request #274 from Kukks/feature/lockout
enable account lockout
2018-09-22 00:42:33 -05:00
e05c88370f enable account lockout 2018-09-12 13:36:44 +02:00
15c29f8419 Update dependencies 2018-09-09 23:04:16 +09:00
fc722731d3 bump 2018-09-08 14:54:23 +09:00
1c9c564e90 Merge branch 'rockstardev-master' 2018-09-08 14:54:06 +09:00
872b60f8ea Merge branch 'master' of github.com:btcpayserver/btcpayserver 2018-09-08 14:53:53 +09:00
0d3364b3da Change button path to api/v1/invoices 2018-09-08 14:53:42 +09:00
fed53661b3 Add btcpay.store.cancreateinvoice claim, and use that for the store 2018-09-08 14:53:41 +09:00
e86b4d89ca remove paybuttontest 2018-09-08 14:53:41 +09:00
c5cb32f6dd Providing option to disable Pay Button at later date 2018-09-08 14:53:41 +09:00
deb56e16ec Confirmation page for enabling Pay Button 2018-09-08 14:53:41 +09:00
b5626ef01c Validating that Store has Pay Button enabled 2018-09-08 14:53:41 +09:00
e39d9067f2 Updating Unit tests 2018-09-08 14:53:41 +09:00
43d34d5d35 Now requiring Authorization on AppsController 2018-09-08 14:53:41 +09:00
7341be76bb Extracting public portion of app controller 2018-09-08 14:53:41 +09:00
0abd62dfe8 Moving PayButton handler to public controller 2018-09-08 14:53:41 +09:00
735012e3d7 Refactoring Invoice to cleanup unused code 2018-09-08 14:53:41 +09:00
b5efb8d2e6 Add exchange name to expired rate 2018-09-08 14:53:40 +09:00
1dbeabb716 Update broken link of third-party host (#271) 2018-09-08 14:05:37 +09:00
671f9e56e2 Merge branch 'master' of github.com:btcpayserver/btcpayserver 2018-09-04 19:04:47 +09:00
dc6c189948 Update broken link (#264)
Current link goes to a 404 page, updated to current .NET Core SDK 2.1 page
2018-09-03 22:04:14 +09:00
4501824f3f Updating Readme (#262)
* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md

reduced merchant guide link for better readability

* Update README.md

* Update README.md

* Update README.md
2018-09-03 22:03:50 +09:00
4568d2a98e Add exchange name to expired rate 2018-08-31 10:45:21 +09:00
f5d81334f8 Remove Lightning Specific logic from BTCPay, and use BTCPayServer.Lightning packages instead 2018-08-30 12:24:00 +09:00
f3ed90399b Merge pull request #257 from dalijolijo/master
Bring supported coins in alphabetic order
2018-08-30 10:45:01 +09:00
fada01cec9 Bring supported coins in alphabetic order 2018-08-30 01:07:30 +02:00
1b4b9fb4cc bump 2018-08-29 00:31:39 +09:00
6eeef8a866 Remove XFrame on the checkout page 2018-08-29 00:31:23 +09:00
24979a0af2 add lnd trickledelay 2018-08-28 18:13:28 +09:00
be1a44f018 Fix bug in LND client 2018-08-28 18:06:07 +09:00
9fcc2903fc Revert "Disable color in logs"
This reverts commit 06df63b2835d77a83c8897368f91491e008f57b5.
2018-08-28 16:13:26 +09:00
06df63b283 Disable color in logs 2018-08-28 09:56:17 +09:00
0f1efc16f5 fx build 2018-08-27 18:45:05 +09:00
da8a06952c Fix bug about btcpay showing wrong port 0 as public lightning port for charge and clightning 2018-08-27 18:36:08 +09:00
38d810cef7 Fix bundling 2018-08-25 23:08:46 +09:00
393a3a2b8f Remove obsolete code in checkout 2018-08-25 22:50:17 +09:00
aaddc580d1 bump 2018-08-25 21:49:41 +09:00
957d478865 Fixing coinaverage, ValidityTime was faster than the refresh rate 2018-08-25 21:49:20 +09:00
023913a852 Rate limit per IP the number of login attempt 2018-08-25 20:28:46 +09:00
6c51d83f61 Fix tests 2018-08-25 15:49:04 +09:00
0edaedb6ab Report if BackgroundFetcherRateProvider has expired entry 2018-08-25 15:09:42 +09:00
13f21aa0d6 bump 2018-08-25 14:48:06 +09:00
929a0c37bd Better handle errors on BackgroundFetcherRateProvider 2018-08-25 14:44:56 +09:00
058ccf56d0 Fix uncaught exception on when getting rates of invoice 2018-08-25 14:44:55 +09:00
162ac572da Merge pull request #252 from Onurrr/patch-1
Update Checkout.cshtml
2018-08-24 18:37:07 +09:00
c7f3fdb46d Update Checkout.cshtml 2018-08-23 13:11:07 +02:00
29af07b3f9 Make sure we do not return outdated rates 2018-08-23 13:47:56 +09:00
758436a428 bump 2018-08-23 12:16:43 +09:00
e0cadb4f62 Do not send not found if invoices does not belong to logged on user 2018-08-23 12:13:27 +09:00
013dfa1b61 Properly escape attributes, make the preview an actual form rather than an image 2018-08-23 11:55:29 +09:00
e0f1c50534 Making Currency a textbox instead of dropdown 2018-08-23 11:17:54 +09:00
d50dc2e68e ServerIPN is an email 2018-08-23 11:12:25 +09:00
8b5b18c97e Make sure no trailing slash bug 2018-08-23 11:11:39 +09:00
f7383b4cc8 Fixing error on CheckoutExperience if no crypto is set 2018-08-23 11:08:53 +09:00
1bc32285ba Merge branch 'master' of https://github.com/rockstardev/btcpayserver into rockstardev-master 2018-08-23 11:01:48 +09:00
f12114f9aa Poll and cache rates in parallel 2018-08-23 00:24:33 +09:00
2b6faa8d20 Migrating generator to work on store path 2018-08-22 14:05:12 +02:00
45b7df6ac9 Removing traces of PayButton being an app... shhhh... 2018-08-22 14:04:59 +02:00
5a43ce2719 Transfering Pay Button from App directly to Store 2018-08-22 13:57:54 +02:00
fe31dc8606 Setting titles for new pill navigation 2018-08-22 13:56:55 +02:00
f0615482d9 Refactoring pill navigation to use new subnav code with enums 2018-08-22 13:50:29 +02:00
03c47e6f7d Merge remote-tracking branch 'source/master' 2018-08-22 13:01:34 +02:00
4111b8a5a3 Maintaining AppId reference 2018-08-22 12:59:55 +02:00
5b5a2e8c25 Reorganizing Javascript code and references 2018-08-22 11:10:46 +02:00
9ec0c23c52 Folder for pay buttons, donate button 2018-08-22 10:59:24 +02:00
9a5034c13c URL of image for pay button 2018-08-22 10:52:17 +02:00
b1fcf4524a Separate app for PayButton 2018-08-22 10:26:49 +02:00
87d384dba5 Decouple RateProviderFactory with RateFetcher 2018-08-22 16:53:40 +09:00
4f5a8f7953 Use direct provider for Kraken, update packages 2018-08-21 15:59:57 +09:00
8728356698 Use HttpClientFactory for coinaverage 2018-08-21 14:33:13 +09:00
9c30476fc8 Making BTCPayServer a bit faster when creating invoices 2018-08-21 13:54:52 +09:00
09beb57eaf Fixing csproj 2018-08-17 16:27:37 +02:00
76a36d1829 Merge remote-tracking branch 'source/master'
# Conflicts:
#	BTCPayServer/BTCPayServer.csproj
2018-08-17 13:35:56 +02:00
0d4036efa2 Dynamically determining site url 2018-08-17 13:33:47 +02:00
cb4562aad5 Model validation attributes added for email and url 2018-08-17 13:26:33 +02:00
0084d4766b Server side validation of PayButton POST 2018-08-17 13:21:00 +02:00
74ddcfa01e Handling payment button post and providing test form 2018-08-17 12:38:03 +02:00
ed36fba0d7 Defining input names for validation errors 2018-08-17 10:30:49 +02:00
af015d435b Integrating Clipboard.js and implemeting copy code 2018-08-16 22:22:15 +02:00
ec59980e6f Validation and conditional rendering of Generated Code section 2018-08-16 21:55:24 +02:00
f0f4247c5d Field validation 2018-08-15 23:58:39 +02:00
b562094956 Currency dropdown as part of page model 2018-08-14 23:47:41 +02:00
4afd55c441 Rendering button code on load from srvModel 2018-08-14 23:47:28 +02:00
d7404f418d Returning intialized model and inputchange on all attribs 2018-08-14 19:40:57 +02:00
893410911c Button width influenced by generator 2018-08-14 18:59:59 +02:00
556b581b6a Updating display of generated HTML 2018-08-14 14:57:46 +02:00
1685ccaca8 Fixing success message for DNS 2018-08-13 17:04:37 +09:00
522abcfdfd Fix setting up DNS name 2018-08-13 16:48:10 +09:00
ed1827ff28 Fix not showing ssh service if no LND 2018-08-13 15:10:59 +09:00
214b2d1c1c Fix SSH fingerprint checking 2018-08-13 09:43:59 +09:00
322518e9dc Ensure the ssh connection is trusted 2018-08-12 23:23:26 +09:00
ea2dd536b4 bump 2018-08-12 21:40:56 +09:00
6a1eca760a Can configure BTCPay SSH connection at startup 2018-08-12 21:38:45 +09:00
29513d4ded Who network type in the conf file of gRPC, fix #246 2018-08-12 16:19:18 +09:00
57daf27fdd Completion of UI for rest of Pay Button gneerator 2018-08-11 13:34:52 +02:00
e698d90e3c Pay Button page foundation 2018-08-10 20:26:51 +02:00
86ca081030 Fix #244 2018-08-08 17:32:16 +09:00
14841ad7c9 bump 2018-08-08 14:46:46 +09:00
2c6aa12aab Fix #240 2018-08-08 14:45:46 +09:00
ef4d39db3c bump 2018-08-06 12:08:55 +09:00
7a566c477d Allow CORS for creating a new invoice via AJAX through the PoS app (fix #238) 2018-08-06 12:04:36 +09:00
85c40aef23 Merge pull request #241 from rockstardev/master
Always showing currency name to prevent LND confusion
2018-08-06 11:47:01 +09:00
d00fa42553 Always showing currency name to prevent LND confusion
Discussion: https://forkbitpay.slack.com/archives/C6PSCRFAM/p1533125977000141
2018-08-05 22:45:48 +02:00
e9e94f5e99 Show nice error if attempting to access lnd before being fully synched 2018-08-03 12:14:09 +09:00
5e2d4407ca Update doc for update point of sale 2018-08-02 10:30:47 +09:00
846bd08e20 Server admin can add new user 2018-08-02 00:16:16 +09:00
a1a4eed860 Add % to spread box 2018-08-01 23:38:52 +09:00
1e582625f3 fix migration bug 2018-08-01 18:50:33 +09:00
39b018fdf3 fix test 2018-08-01 18:42:28 +09:00
83304de1c6 Remove the concept of "Rate multiplier" and replace it with the concept of "Spread" 2018-08-01 18:38:46 +09:00
5c8e03dcbf More user friendly Update Store screen 2018-08-01 15:59:29 +09:00
4dddc539f6 bump 2018-07-31 00:27:17 +09:00
9950b781b4 Slight UI adjustment to disable or enable method of payment 2018-07-31 00:26:49 +09:00
7a32f692d1 Add test for Disabling PaymentMethod 2018-07-31 00:18:58 +09:00
d480be925b Can disable method of payments 2018-07-30 23:54:31 +09:00
3775317047 Fix CanGetRates test, and fix #178 2018-07-30 23:22:26 +09:00
500bdd9bf1 Fix GetRates 2018-07-30 23:07:29 +09:00
57bda24664 Fix other DDOS related to GetRate 2018-07-30 22:51:39 +09:00
6401af00fe Fix potential DDOS on get rate 2018-07-30 22:45:28 +09:00
3b3a18bbbc Update README.md 2018-07-30 13:43:46 +09:00
b4e9dfeeaf Merge pull request #234 from DeltaEngine/master
Added Dash support
2018-07-28 23:21:20 +09:00
16f5def245 Reverted Dash_btc rule parsing check on request 2018-07-28 16:11:02 +02:00
26e7de534b Added Dash to readme 2018-07-27 21:07:43 +02:00
a3ae694048 Added Dash support for BTCPayServer 2018-07-27 21:06:19 +02:00
b04d70f141 bump 2018-07-27 18:18:27 +09:00
101d6131c7 Merge pull request #197 from Kukks/feature/bitpayrates
[WIP] Bitpay rates api
2018-07-27 18:18:05 +09:00
faabd68f6f Merge remote-tracking branch 'origin/master' into feature/bitpayrates 2018-07-27 11:16:52 +02:00
aa72b814da bump 2018-07-27 18:04:57 +09:00
0dcda0f289 Fix: Inverse rule was not found in BTCPay with X_X 2018-07-27 18:04:41 +09:00
a2b039f983 Report errors of LND 2018-07-27 15:49:57 +09:00
1a54f2d01a remove redundant cryptocode field in payment method interface 2018-07-27 08:41:36 +02:00
4276994265 fix bitpayconstraint for rates 2018-07-27 07:55:42 +02:00
2c6133b4f7 let X_X rates show in rates api as bitpay does 2018-07-27 07:55:18 +02:00
64181d1a93 use default crypto for /rates route 2018-07-27 07:54:55 +02:00
25e9a27a78 add in bitpay api constraints to actions 2018-07-27 06:38:54 +02:00
f3edaf5160 Merge remote-tracking branch 'btcpayserver/master' into feature/bitpayrates 2018-07-27 05:57:25 +02:00
7bfdf2d11d Order transactions in transaction list view 2018-07-27 12:03:56 +09:00
d2808cf662 bump 2018-07-27 01:35:07 +09:00
b68fcec692 Fix rate in the WalletSend 2018-07-27 01:17:43 +09:00
86644d38d7 Show rate error to the model in WalletSend 2018-07-27 00:32:09 +09:00
52f60b0457 Can show the transaction list in wallet menu 2018-07-27 00:08:07 +09:00
638b58ab48 remove debug u2f 2018-07-26 23:26:06 +09:00
1606f43609 Show the fiat price when sending coins 2018-07-26 23:23:28 +09:00
ad1307746c Add a "Wallet" menu 2018-07-26 22:32:50 +09:00
22307b8a2b Merge pull request #233 from danielalexiuc/master
Fix small typo on server settings page
2018-07-25 10:59:43 +09:00
f1c244bf6a Fix small typo 2018-07-25 11:44:26 +10:00
c425e0439d Fix build error 2018-07-25 01:01:05 +09:00
cdf78f0463 Fix mixing commands 2018-07-25 00:51:45 +09:00
7d997f7d60 disown the launched command 2018-07-25 00:35:18 +09:00
0edbbe6ae3 Use nohup to update domain 2018-07-24 23:56:41 +09:00
4f7bfbf451 fix typo 2018-07-24 23:36:59 +09:00
cd416dac60 nohup the update and changedomain 2018-07-24 23:27:52 +09:00
b58173967d Fix typo 2018-07-24 22:55:10 +09:00
4b743bb998 bump 2018-07-24 22:26:48 +09:00
0af1adcfb8 Can update server 2018-07-24 22:10:37 +09:00
c048e4eeaf bump 2018-07-24 19:05:11 +09:00
64194896ba Add curl dependency because of https://github.com/dotnet/corefx/issues/30003 2018-07-24 19:04:48 +09:00
1d4472cc08 Show inner exception if SSL connection fail when changing domain 2018-07-24 18:47:55 +09:00
d8bc7ef4b8 Better description for changing domain 2018-07-24 18:23:16 +09:00
27cea81cb2 Add ability to change domain from server settings 2018-07-24 17:04:57 +09:00
b0d6216ffc Better timestamp for invoice logs, fix bugs which can happen if invoice get deleted 2018-07-24 12:19:43 +09:00
060876d07f Do not round satoshis to 8 decimal 2018-07-23 18:15:40 +09:00
96b7373d88 Update NBitcoin 2018-07-23 18:10:54 +09:00
c2f282f85c Fix rounding bug 2018-07-23 17:59:12 +09:00
7afa726ddc Fix, macaroonfilepath should be read in order to create the qr code 2018-07-23 12:20:11 +09:00
6eb7bbf853 Fix some invoice failing to create because of rounding issues 2018-07-23 12:01:20 +09:00
11a26c940d Do not expose the config secret in URL, and use {net.CryptoCode}.external.lnd.grpc argument 2018-07-23 11:54:33 +09:00
624252e4ad LightningConnectionString can parse gRPC 2018-07-23 11:11:00 +09:00
57bb980526 Update packages 2018-07-23 00:21:40 +09:00
e0c718f4ba Fix wallet alert 2018-07-23 00:21:40 +09:00
c58e015bfb Merge pull request #230 from viacoin/master
Viacoin: add to README
2018-07-22 21:45:28 +09:00
648829644a Add QR code information 2018-07-22 21:28:21 +09:00
1b0f8c7aca Viacoin: add to README 2018-07-22 14:18:31 +02:00
4f8e0b0393 Can get lnd config without being logged 2018-07-22 18:43:11 +09:00
466f65d6cd bump 2018-07-22 18:39:22 +09:00
022b4f115d Expose LND gRPC settings 2018-07-22 18:38:14 +09:00
71f6aaabbd Merge pull request #229 from rockstardev/master
Changing Lightning suffix per suggestion
2018-07-21 22:21:49 +09:00
79b06bce42 Changing Lightning suffix per suggestion 2018-07-20 23:33:54 -05:00
480afebcd9 bump 2018-07-20 15:25:45 +09:00
96721e95a2 Clean unreachable store if user is deleted 2018-07-20 15:24:19 +09:00
883cd41232 Fix bug where store was not properly deleted 2018-07-19 22:46:55 +09:00
3f48a478af Add delete button in update store settings 2018-07-19 22:23:14 +09:00
8d3b45bdec Delete store if no owner 2018-07-19 21:38:55 +09:00
bbd19a96ec Make sure we don't delete store on Sqlite 2018-07-19 21:32:33 +09:00
ce17e3212a Can delete stores 2018-07-19 19:31:17 +09:00
c3ea63c6ce bump 2018-07-19 14:49:47 +09:00
1ee4bd9c92 Fix tests, and make sure Listen does not block for LND 2018-07-19 14:49:30 +09:00
e6bb6619e5 bump 2018-07-19 13:55:12 +09:00
cc29d863d7 Merge pull request #228 from rockstardev/dev-currency-selection
Showing currency name next to icon
2018-07-19 13:54:19 +09:00
8cb2c93abd Adding UTF8 lightning icon to Lightning payments methods 2018-07-18 23:53:00 -05:00
2187e05a10 Renaming BTG image to conform to new naming scheme for onchain/offchain 2018-07-17 23:32:44 -05:00
4c07483383 Merge remote-tracking branch 'source/master' into dev-currency-selection 2018-07-17 23:29:52 -05:00
65916755b6 Bitcoin always first selection in currency list 2018-07-17 23:29:40 -05:00
3cefbc89e4 Conditional display not necessary since whole block is hidden 2018-07-17 23:10:54 -05:00
c40b47b1dd Hover indicator and handling case with only one currency 2018-07-17 22:54:09 -05:00
a2d17bfa7e Closing currency selection dialog once invoice expired or paid 2018-07-17 22:30:02 -05:00
cdf0c6d27d Merge pull request #227 from Vutov/master
Added BTG lightning svg
2018-07-17 22:57:18 +09:00
8154986102 Added BTG lightning svg 2018-07-17 13:43:37 +03:00
203494e809 Tweaking CSS styles and display of payment method selection 2018-07-17 00:14:56 -05:00
d49bbc95af Tweaking display with display name and crypto code 2018-07-16 23:43:52 -05:00
c44132fd35 Using same icon for onchain and offchain per user feedback 2018-07-16 23:29:55 -05:00
c75512303d Getting display names directly from NetworkProvider 2018-07-16 23:25:28 -05:00
97d50df13e bump 2018-07-14 12:46:29 +09:00
0e1ef78af1 Fix auth for listening invoices 2018-07-14 12:45:45 +09:00
464ab30fea Ordering currencies by name 2018-07-13 22:35:34 -05:00
3ee1c05646 Merge pull request #225 from rockstardev/master
Fitting longer wallet addresses, displaying ellipsis for overflow
2018-07-14 12:24:38 +09:00
c54c39926b Fitting longer wallet addresses, displaying ellipsis for overflow
Ref: https://github.com/btcpayserver/btcpayserver/issues/223
2018-07-13 16:45:32 -05:00
33d18a3278 Displaying payment method name during checkout
Ref: https://github.com/btcpayserver/btcpayserver/issues/152
2018-07-13 15:58:59 -05:00
97e564901e Merge pull request #222 from martinbehrens/tweak-help-options
tweaking help option wording
2018-07-14 03:14:18 +09:00
832069dd44 bump 2018-07-14 03:08:45 +09:00
1ac17e96c3 Throw lnd exception if any issue with lnd 2018-07-14 02:56:36 +09:00
d907031ec7 Cancel the infinite delay 2018-07-14 02:41:48 +09:00
4b5af9cb5c tweaking help option wording 2018-07-13 19:00:03 +02:00
0057146fee bump 2018-07-14 01:54:10 +09:00
0c8925d2a2 Correctly dispose the session when listening lightning invoices 2018-07-14 01:45:14 +09:00
b9e5b0d56e Merge pull request #206 from viacoin/master
Viacoin: add support
2018-07-13 23:04:03 +09:00
eb6dbd1247 Merge branch 'master' into master 2018-07-13 15:07:15 +02:00
fe8428b8b0 make sure the LndInvoiceClientSession get disposed, even if it fails at initialization 2018-07-13 19:56:19 +09:00
f2aa15310a Viacoin: add support 2018-07-13 12:53:04 +02:00
1814cb2d6e bump 2018-07-13 19:48:01 +09:00
94a6f20a05 Refactor the LndInvoiceClient which might solve memory leak 2018-07-13 19:45:50 +09:00
22e700a53e Fix NullReferenceException when setting lightning connectionString without externalurl 2018-07-13 15:02:31 +09:00
cd78e559cf Merge pull request #221 from romanornr/master
[Translation Dutch]: improve translation
2018-07-12 23:55:19 +09:00
f0257fb8f7 [Translation Dutch]: improve translation 2018-07-12 12:57:15 +02:00
34cdbf73f0 bump 2018-07-12 18:20:01 +09:00
b291a6d25a removing csp 2018-07-12 18:19:43 +09:00
fa7e974e73 bump 2018-07-12 17:38:43 +09:00
976d9d0cda Add CSP (Disable it if custom theming) 2018-07-12 17:38:21 +09:00
6ea2d9175d hamburger menu should not be black 2018-07-12 16:34:09 +09:00
10ceddc709 ReferrerPolicy 2018-07-12 02:38:08 +09:00
5dd57c8064 X-XSS-Protection 2018-07-12 02:23:54 +09:00
a256dd3277 x-content-type-options=nosniff 2018-07-12 01:43:16 +09:00
5ee9a92f1e Do not use external website for highlightjs 2018-07-12 01:20:44 +09:00
65c7c85c14 Do not put youtube on the front page (doing suspicious ads requests from the website) 2018-07-12 00:55:06 +09:00
27b686095c bump 2018-07-11 22:40:25 +09:00
cd2fef0dab Add a error if the browser access BTCPay with the wrong url 2018-07-11 22:40:10 +09:00
743288fa47 add instruction for the lightning connection string 2018-07-11 19:23:23 +09:00
270ebead49 fix error message 2018-07-11 17:47:29 +09:00
145e3bec83 bump 2018-07-11 16:46:31 +09:00
563e931468 simplify the docker-compose 2018-07-11 10:42:20 +09:00
3113097c4f Update to https, use new dockerfile 2018-07-10 19:33:54 +09:00
cdbbad1694 Fix misleading error when using http on internalNode 2018-07-10 12:58:17 +09:00
c9c2730409 check macaroonfilepath is rooted 2018-07-10 12:51:23 +09:00
310a9a6d59 Remove ctor in LndSwaggerClient 2018-07-10 12:49:25 +09:00
1a1078782e Suppoort macaroonfilepath in connection string 2018-07-10 12:46:04 +09:00
73cb3dc4ee Fix listen loop for LND 2018-07-09 01:08:09 +09:00
9eb36a8b40 Clean the LND listener, and make sure it correctly ends. 2018-07-08 22:20:59 +09:00
6307aa8665 Use SHA256 cert thumprint in connection string, allowInsecure=true 2018-07-08 20:58:37 +09:00
b9e8408db5 Simplify LND implementation 2018-07-08 18:55:48 +09:00
0879895678 Fix tests and rename type=lnd to type=lnd-rest 2018-07-08 15:34:19 +09:00
a4ecf070b0 Merge pull request #217 from rockstardev/master
Handling unlikely state transition from paid to invalid
2018-07-08 12:24:33 +09:00
162d76206e Handling unlikely state transition from paid to invalid
Ref: https://github.com/btcpayserver/btcpayserver/issues/216
2018-07-07 10:38:07 -05:00
5af14ef2ec When creating PoS app, redirect to settings. When updating an app, redirect to List of apps. 2018-07-05 21:11:18 +09:00
7210eebeca Create Store redirect to store settings 2018-07-05 21:05:13 +09:00
25dbf6445f LND Support 2018-07-01 21:45:21 +09:00
0828c60537 Deactivate support for UFO (default rate rules are failing CanGetRateCryptoCurrenciesByDefault ) 2018-07-01 16:11:29 +09:00
34deb17f3d Fix tests 2018-07-01 16:10:17 +09:00
06b02b8691 Fix missing logs 2018-07-01 15:52:11 +09:00
b7abc08c27 Create a new format for LightningConnectionString 2018-07-01 15:45:08 +09:00
399ae2cd9e Fix: If DOGE fee becomes higher that 1 DOGE, the transaction would fail to broadcast 2018-07-01 13:46:28 +09:00
63fe0f6612 Make sure that DOGECOIN pays min amount of fee 2018-06-30 22:05:41 +09:00
42d60ef84b Fix: Could not send money from wallet of a coin without segwit 2018-06-30 21:26:10 +09:00
1784c30787 Merge remote-tracking branch 'source/master' into dev-lndrpc
# Conflicts:
#	BTCPayServer.Tests/UnitTest1.cs
2018-06-26 01:08:01 -05:00
ac8feceaf2 bump 2018-06-26 14:19:54 +09:00
3d8c5195ae Update CLightning and charge 2018-06-26 14:18:47 +09:00
9a5259510b Merge remote-tracking branch 'source/master' into dev-lndrpc 2018-06-25 22:31:42 -05:00
caecb26420 fix typos and sentences referencing Bitcoin 2018-06-25 11:58:07 +09:00
ecc8b3d9ed Fix spelling 2018-06-24 21:51:32 +09:00
d313395751 Show rule evaluation in invoice logs 2018-06-24 21:01:29 +09:00
9e698a8004 Commenting few tricky lines of code 2018-06-23 23:37:58 -05:00
3c4c99ee42 Saving of Macaroon and Tls for LND connection 2018-06-23 23:16:39 -05:00
d34ffc0d9a Refactoring conditional method, separating into two properties 2018-06-23 22:50:50 -05:00
039303bfaa Fixing typos during code review 2018-06-23 22:03:51 -05:00
273cf1adc9 Fix checkout if only one currency is present 2018-06-24 00:53:56 +09:00
5feb520843 Add support for groestlcoin 2018-06-24 00:45:57 +09:00
17e914778d Make sure that lightning payments events are using the state of the invoice when they got issued (#205) 2018-06-21 14:15:36 +09:00
db24ab792f update clightning 2018-06-18 23:07:55 +09:00
42475ec7b7 Switching to lnd image from Docker Hub 2018-06-15 18:28:01 -05:00
4972f0ab7b Labeling issue with rapid testing of lightning payments 2018-06-15 18:27:37 -05:00
07e13747cf Merge remote-tracking branch 'source/master' into dev-lndrpc 2018-06-15 17:21:21 -05:00
2465eb7e36 Revering debug param 2018-06-15 17:20:56 -05:00
4ddcd7a4c8 Will depend on lnd bitcoin.defaultremotedelay=720 param
Ref: https://github.com/lightningnetwork/lnd/pull/788
2018-06-15 17:14:20 -05:00
89d9658e82 Bugfixing amount in invoice data, we need to set Satoshis 2018-06-15 17:12:59 -05:00
66ecb32538 Need param so that funding channels can be opened between LND and CL 2018-06-15 16:29:09 -05:00
a22576da0a Streamlining flow of interaction between test lnd customer / merchant 2018-06-15 15:56:02 -05:00
69bd888bab Refactoring ServerTester so that ClightningRPCClient can use LND 2018-06-15 15:02:40 -05:00
9b540273fc Parsing of node info and returning it for GetInfo 2018-06-15 13:57:39 -05:00
cfd083bed5 Providing port for peer-to-peer connection for local tests 2018-06-15 11:53:53 -05:00
55c9314cdd Reference to lnd docker image updated to point to latest
Also helps with building image locally for debugging
2018-06-15 11:53:34 -05:00
8cafa8a483 Merge remote-tracking branch 'origin/master' into feature/bitpayrates 2018-06-12 15:34:09 +02:00
448cc06a11 Merge pull request #203 from ChekaZ/master
Support UFO
2018-06-12 10:43:53 +09:00
0780df4fd7 Support UFO 2018-06-09 17:25:45 +02:00
04174b7431 Fix authentication 2018-06-06 16:02:37 +09:00
b7c58c2083 Fix bug of authentication caused by previous refactoring on authentication 2018-06-06 14:46:41 +09:00
cd75fd6842 bump 2018-06-05 12:53:55 +09:00
370951a3bd make sure postgres DB is created with C locale 2018-06-05 12:51:37 +09:00
2c08b0137b Update NBitpayClient 2018-06-05 12:29:45 +09:00
724af44e41 Merge branch 'master' into feature/bitpayrates 2018-06-04 15:09:14 +02:00
1eee31e9f1 Fix rate bug: Sometimes coinaverage is sending null bid and ask 2018-06-04 12:01:30 +09:00
01cf579530 Use proper custom authentication handler for bitpay 2018-06-04 12:00:03 +09:00
f72705935a Merge pull request #201 from ChekaZ/master
Support Feathercoin
2018-06-04 01:45:07 +09:00
a29ab6b6b0 Support Feathercoin 2018-06-03 14:30:43 +02:00
4784518235 update link 2018-06-01 13:21:56 +09:00
f8c88bd44f Providing ability to increase lightning timeout for tests/debugging 2018-05-31 16:31:39 -05:00
0d1d0d57f4 Logging Swagger errors for logging and easier debugging 2018-05-31 16:31:19 -05:00
2bd1238668 Rounding TotalSeconds expiry so it doesn't break invoice creation 2018-05-31 16:31:00 -05:00
d1fb51b412 Reactivating LND end to end test 2018-05-31 16:07:59 -05:00
279de1b869 Passing CancelToken and properly parsing invoice response 2018-05-31 15:53:14 -05:00
ce9189caf8 Listen / WaitInvoice for Lnd 2018-05-31 15:08:22 -05:00
431147784e Merge branch 'master' into dev-lndrpc 2018-05-31 12:11:31 -05:00
0697b8bf86 update images 2018-05-31 23:54:03 +09:00
5050b59014 bump 2018-05-31 18:41:33 +09:00
665cf4c3b1 Updating BTCPayServer to .NET Core 2.1 2018-05-31 18:41:03 +09:00
bac9ef4f93 add some UT and fix error message + bump Nbitpayclient 2018-05-29 17:12:07 +02:00
98e81ab0fd Merge branch 'rockstardev-master' 2018-05-29 12:27:55 +09:00
6ce70237fc Merge branch 'master' of github.com:btcpayserver/btcpayserver 2018-05-29 12:27:45 +09:00
4f23fc99a1 Renaming method to better communicate function 2018-05-29 12:27:04 +09:00
d7fccae452 Generalizing TimeSpan to Time Ago, reverting invoice list to ago 2018-05-29 12:27:04 +09:00
d7a5021ed2 Updating Invoice details to show local date time 2018-05-29 12:27:03 +09:00
63ec832667 Support for localizing datetimes base on browser's timezone 2018-05-29 12:27:03 +09:00
8d95b9fa04 Renaming method to better communicate function 2018-05-28 09:36:49 -05:00
ada6f3b844 Finish rate api 2018-05-28 15:30:43 +02:00
c8a26ce952 api fixes 2018-05-28 14:55:49 +02:00
6cf80b7533 small rename 2018-05-28 14:29:23 +02:00
79df523bb2 reorder methods 2018-05-28 10:20:18 +02:00
e921f9757a Merge remote-tracking branch 'btcpayserver/master' into feature/bitpayrates 2018-05-28 09:05:11 +02:00
b497d1871e Merge pull request #195 from andres-pinilla/patch-3
Updated es.js
2018-05-28 10:50:30 +09:00
c7cd029482 Update es.js
Various strings updated.
2018-05-27 20:36:32 -05:00
68f2cba60d Generalizing TimeSpan to Time Ago, reverting invoice list to ago 2018-05-26 09:42:55 -05:00
5c4200b036 Updating Invoice details to show local date time 2018-05-26 09:35:42 -05:00
bc06114023 Support for localizing datetimes base on browser's timezone 2018-05-26 09:32:20 -05:00
bed9737d64 Merge remote-tracking branch 'btcpayserver/master' into feature/bitpayrates 2018-05-26 14:42:17 +02:00
556082c4c9 fix json serialization 2018-05-26 18:29:57 +09:00
6a46d02fc6 Add dummy policies field 2018-05-26 18:26:02 +09:00
d75e5b8b12 Merge pull request #129 from cronos-polis/master
Polis Integration
2018-05-26 14:50:32 +09:00
d293bc3947 Throwing NotImplementedException for Listen / WaitInvoice 2018-05-25 12:19:15 -05:00
e634700913 Changing the way we signal that LightningConnectingString is Lnd 2018-05-25 12:18:47 -05:00
ce81136c88 Adding LndMockTester for passing end to end tests 2018-05-25 10:44:59 -05:00
a97ef2eee8 MinerFee matching Bitpay API 2018-05-25 22:49:49 +09:00
be33ebc168 fix typo 2018-05-25 17:35:01 +09:00
789193a0c8 fix typo 2018-05-25 12:55:29 +09:00
01792cf299 Merge pull request #190 from yashbhutwala/fix_typo
Fix spelling of lightning
2018-05-25 11:41:46 +09:00
ff9265f721 Fix spelling of lightning 2018-05-24 16:26:01 -04:00
8d61314852 Use FullNotification and improve instructions 2018-05-25 00:02:24 +09:00
1ce6ae8727 fix typo 2018-05-24 23:58:24 +09:00
dec5dbc0d2 Ability to pass fields to POS app #181 2018-05-24 23:54:48 +09:00
4e32dad1ea Can solve inverses at exchange level 2018-05-23 19:29:01 +09:00
127ca7582f Make sure inverse rules have priority 2018-05-23 19:13:12 +09:00
b98993f84b fix typo 2018-05-23 11:16:19 +09:00
e35f074b66 Better label for Rate Multiplier 2018-05-23 02:43:22 +09:00
ba3d13d56c Make sure the rate of the merchant is using the ask of a divided exchange 2018-05-23 02:18:38 +09:00
ead67887ab Enable SSL was ignored 2018-05-23 00:59:50 +09:00
437f27f107 Merge pull request #188 from andres-pinilla/master
Better spanish translation (tu)
2018-05-22 14:11:25 +09:00
8d41a8e98d Better spanish translation (tú) 2018-05-21 23:15:04 -05:00
7e6ab015a6 Fix bug on Error page 2018-05-22 01:05:45 +09:00
2583eb15ec Merge remote-tracking branch 'btcpayserver/master' into feature/bitpayrates 2018-05-21 16:49:43 +02:00
1879ea55e8 init bitpay rates api 2018-05-21 16:49:37 +02:00
f8bc3a5081 bump 2018-05-21 20:47:07 +09:00
dd1a93ee0e Revert "Remove unused Error.cshtml"
This reverts commit 7b2ef9aec2c5f5afbecf1b639995b4a645390928.
2018-05-21 20:44:03 +09:00
093ae39e61 Custom HTTPS certificates accepted for lnd connection 2018-05-20 10:27:49 -05:00
cac58808f0 Renaming file to LndInvoiceClient and commenting Dispose 2018-05-20 10:27:35 -05:00
a063f10778 Checking for nulls during channel opening in tests 2018-05-20 10:27:11 -05:00
3cf3aa63f6 CurrencyNameTable can use fallback 2018-05-20 23:37:18 +09:00
011dd5574f Add a fallback currency format info 2018-05-20 23:22:20 +09:00
365911286b bump 2018-05-20 17:04:03 +09:00
fe5347aa86 Maintaining BitPay compatibility
Ref: https://github.com/btcpayserver/btcpayserver/issues/180
2018-05-20 17:00:54 +09:00
f22c8a72cd Rebase 2018-05-16 11:21:57 -05:00
eeb522fe7d Remove special case for showing crypto currency 2018-05-16 21:19:48 +09:00
f9e40b209a Merge pull request #179 from rockstardev/fiat
Showing exchange rate for cryptos
2018-05-16 21:14:23 +09:00
20635ea3d6 Showing exchange rate for cryptos
Ref: https://github.com/btcpayserver/btcpayserver/pull/170#issuecomment-389462155
2018-05-16 05:46:11 -05:00
6cefd9c3e7 Merge remote-tracking branch 'source/master' into dev-lndrpc 2018-05-16 04:50:46 -05:00
7062705d6f bump 2018-05-16 10:40:48 +09:00
58b994e043 fix tests 2018-05-16 10:40:25 +09:00
640ff36fa2 fix build 2018-05-16 10:26:45 +09:00
39ec5242d7 Bump NBitpayClient 2018-05-16 10:22:43 +09:00
1c50210e61 fix build 2018-05-16 10:22:42 +09:00
a1ffda0151 Merge pull request #168 from Kukks/feature/extended-invoice
[WIP] Feature/extended invoice
2018-05-16 10:21:52 +09:00
fd15348551 Merge pull request #174 from monaco-ex/pr-support-monacoin
Support Monacoin.
2018-05-16 10:21:07 +09:00
989c99c550 Merge pull request #170 from rockstardev/fiat
Display fiat value of invoice during checkout
2018-05-16 10:18:24 +09:00
bcf97b1474 Hiding display of payment-tabs now that we have flex line-items 2018-05-15 15:17:59 -05:00
67abbed66a Removing display of exchange if invoice source amount is in crypto 2018-05-15 15:17:58 -05:00
eb01e91e13 Only show Order Amount in Fiat if Invoice is created with fiat value 2018-05-15 15:17:58 -05:00
12ceb9e0bc Simplifying CSS styles for line-items box
Now that we'll have different box sizes it's not possible to rely on exact height specification
2018-05-15 15:17:57 -05:00
ecf03f90aa Fix UriAttribute bug, and currency formatting crash 2018-05-16 02:24:59 +09:00
1747414a57 update clightning of docker compose 2018-05-16 01:37:20 +09:00
3a02f16c6e Fix bug where exchange name in rate rules were uncorrectly considered a currency 2018-05-16 01:27:15 +09:00
a6ee337ed0 more coverage 2018-05-15 16:25:43 +02:00
559f535257 add some coverage for bitpay fields 2018-05-15 16:18:37 +02:00
2952ccc7fd Merge remote-tracking branch 'origin/master' into feature/extended-invoice 2018-05-15 15:44:51 +02:00
a0243fa569 Lnd support for passing macaroon and tls as hex 2018-05-14 22:18:08 -05:00
789b9168ad Adding Lnd to connection types and supporting parsing 2018-05-14 15:54:44 -05:00
7c29cb62ef Enabling dual support - clients with or without macaroons/tls 2018-05-14 15:05:03 -05:00
f81ca1888d Merge remote-tracking branch 'upstream/master' 2018-05-14 11:23:54 -05:00
ed02e0f4d6 Merge pull request #1 from zeusthealmighty/patch-1
KeyPath Updated
2018-05-14 11:23:08 -05:00
0a83f21af5 KeyPath Updated 2018-05-14 10:04:12 -05:00
23a3c145ed fix run.sh 2018-05-14 22:08:35 +09:00
4184c6c208 Convert in UriAttribute use invariant culture 2018-05-14 21:28:33 +09:00
29c28b1841 Merge pull request #175 from Kukks/bugfix/real-url-validation
use alternative uri validation
2018-05-14 17:31:56 +09:00
de48fb4077 add direct file test cases 2018-05-14 09:34:19 +02:00
bcd79c5882 use alternative uri validation 2018-05-14 09:32:04 +02:00
b8c513aa2b Support Monacoin. 2018-05-14 05:44:17 +00:00
ad67f4ef18 update to use longs 2018-05-13 09:47:42 +02:00
2c0bcfc0ec Merge remote-tracking branch 'btcpayserver/master' into feature/extended-invoice 2018-05-13 08:34:36 +02:00
0ba1072d54 bump 2018-05-13 15:09:38 +09:00
f7fe855274 Do not roundup rates 2018-05-13 15:09:17 +09:00
449738414b Add cryptopia 2018-05-12 19:37:32 +09:00
a34842585d bump 2018-05-12 19:19:40 +09:00
eb882c2c46 Update package 2018-05-12 19:15:54 +09:00
ca65c6bd8f fix #171 2018-05-12 18:38:43 +09:00
f97173e9e7 Testing invoice payment with Lnd 2018-05-12 00:43:13 -05:00
8fc1b0c856 Ensuring lightning channel is open for testing 2018-05-12 00:23:10 -05:00
cabd7c4e64 Lnd requires zmqpubrawblock setting, and port 9735 for peer connections 2018-05-12 00:19:26 -05:00
f8540dc78c Providing merchant_lnd and customer_lnd for testing 2018-05-11 16:59:24 -05:00
b03d271f85 Refactoring LndClient, enabling passing of Swagger instance 2018-05-11 14:07:46 -05:00
3770adb7d3 Displaying fiat value of invoice's order amount in details 2018-05-11 12:15:26 -05:00
7fdf19ca22 Remove cryptopia from CoinAverage 2018-05-11 11:07:42 -05:00
4e776adb03 Merge remote-tracking branch 'upstream/master' 2018-05-11 11:06:51 -05:00
26db946392 BTCPayRateProviderFactory is responsible for getting the supported exchange list 2018-05-12 00:54:17 +09:00
d102c142b9 Typo 2018-05-11 10:46:49 -05:00
f7989541b9 Change Polis Rates - Add Cryptopia 2018-05-11 10:26:08 -05:00
b7f0ce18b3 Make sure Lightning charge can't hang out the payment 2018-05-12 00:23:25 +09:00
e1dfbfe3b0 Rebase 2018-05-11 10:20:23 -05:00
786d129452 Make sure to not freeze if ligthning does not respond 2018-05-12 00:14:39 +09:00
a37a8e8fcd Merge remote-tracking branch 'btcpayserver/master' into feature/extended-invoice 2018-05-11 16:46:38 +02:00
355989c278 bump 2018-05-11 23:34:42 +09:00
af3dee95de round up rates sent back by the RateProviderFactory 2018-05-11 23:31:50 +09:00
70a6bd6a01 bump 2018-05-11 22:42:29 +09:00
4afb0acc84 does not generate antiforgery 2018-05-11 22:41:11 +09:00
9afc143801 Use decimals and fix invoices 2018-05-11 22:38:31 +09:00
8e4943df65 low-medium speed policy 2018-05-11 22:12:45 +09:00
9b3bd8343d bump nuget packages 2018-05-11 21:33:46 +09:00
ee4f83ddba small fixes 2018-05-11 12:21:25 +02:00
c326998381 bump nbitpayclient dependency to .20 2018-05-11 11:42:13 +02:00
239a011e60 add new properties and change types to decimal 2018-05-11 11:31:21 +02:00
5ffe118159 Merge remote-tracking branch 'btcpayserver/master' into feature/extended-invoice 2018-05-11 11:24:32 +02:00
6f07849e1d Use policies security for controlling access to bitpay api 2018-05-11 17:16:18 +09:00
dbe5c62d11 Merge remote-tracking branch 'btcpayserver/master' into feature/extended-invoice 2018-05-11 10:08:29 +02:00
199db01eaf No need of authentication for GetInvoice API (#166) 2018-05-11 17:05:08 +09:00
a3c46c8f67 Use hangfire in-memory provider until the postgres binding to hangfire get updated. 2018-05-11 15:06:11 +09:00
66a68d6180 Rename LockSubscription, remove the link if not available 2018-05-10 16:02:49 +09:00
be1128a886 Support for displaying fiat value of invoice 2018-05-09 22:39:13 -05:00
d41a5a65a2 Update posgres and clightning in tests 2018-05-10 11:56:46 +09:00
d5cab938ee bump 2018-05-09 14:10:06 +09:00
9dddfb65f0 Add globalization to the alpine package 2018-05-09 14:09:41 +09:00
6bd5976d90 Update SQLite package 2018-05-08 19:17:06 +09:00
b3385bf901 update tests image 2018-05-08 18:09:12 +09:00
bba268b5e2 Upgrade to .NET Core 2.1 2018-05-08 17:57:53 +09:00
70c98b6901 use the theme manager for ViewPointOfSale 2018-05-08 00:19:28 +09:00
2d3b7fea2e update packages 2018-05-07 23:02:55 +09:00
3bdf1c9a00 Merge pull request #156 from Vutov/master
Added BTG Support
2018-05-07 14:16:31 +09:00
a52665ea80 bump 2018-05-07 12:29:56 +09:00
3d943d49e6 Make sure if the default crypto is no available, we don't get error 404 2018-05-07 12:25:50 +09:00
6ca8ba9231 Allow signing on non segwit transactions via the ledger 2018-05-07 12:17:46 +09:00
75d685ae6c fix grammar 2018-05-07 00:24:23 +09:00
7b2ef9aec2 Remove unused Error.cshtml 2018-05-06 22:49:00 +09:00
ca8af5047a Changed DefaultRateRules 2018-05-06 14:59:49 +03:00
bca68986f3 Added BTG Support 2018-05-05 23:06:09 +03:00
7c3ddf904c Merge remote-tracking branch 'upstream/master' 2018-05-04 09:47:03 -05:00
cfbcf0947a Switching to using Dockerfile from Docker Hub 2018-05-01 21:12:04 -05:00
fcfba7f5e1 Refactoring connection to Lnd now there is HTTP support 2018-05-01 20:33:43 -05:00
f4f9fabfd3 Building docker compose with our custom lnd 2018-05-01 19:02:57 -05:00
25208915eb Merge remote-tracking branch 'upstream/master' 2018-04-30 10:29:30 -05:00
75f4a39ef2 Adding script to build lnd Docker container for testing
Obviously when we publish to Docker Hub this whole folder is bye-bye
2018-04-29 02:57:08 -05:00
f9f4d93191 Lnd Dockerfile that integrates with BtcPayServer 2018-04-29 02:52:33 -05:00
69050f7a56 Lnd sends some integers as strings, testing invoice creation 2018-04-28 12:49:56 -05:00
1743919cd4 Conversion of LnrpcInvoice into LightningInvoice 2018-04-28 00:39:43 -05:00
131328b42c Foundation integration with Lnd 2018-04-27 23:36:58 -05:00
ad3b605148 Adding ZMQ settings Lnd needs 2018-04-27 23:36:58 -05:00
f32e225fa6 Generating Lnd wrapper using NSwag
https://github.com/lightningnetwork/lnd/blob/master/lnrpc/rpc.swagger.json
2018-04-27 23:36:58 -05:00
52e0845fc5 Merge remote-tracking branch 'upstream/master' 2018-04-21 09:59:46 -05:00
daf1a0a4bc Revert to origin csproj 2018-04-17 11:19:28 -05:00
bc8978182e Add Polis 2018-04-17 11:13:50 -05:00
615 changed files with 99179 additions and 16391 deletions

102
.circleci/config.yml Normal file
View File

@ -0,0 +1,102 @@
version: 2
jobs:
build:
machine:
docker_layer_caching: true
steps:
- checkout
test:
machine:
docker_layer_caching: true
steps:
- checkout
- run:
command: |
cd BTCPayServer.Tests
docker-compose down --v
docker-compose build
TESTS_RUN_EXTERNAL_INTEGRATION="true"
if [ -n "$CIRCLE_PULL_REQUEST" ] || [ -n "$CIRCLE_PR_NUMBER" ]; then
TESTS_RUN_EXTERNAL_INTEGRATION="false"
fi
docker-compose run -e TESTS_RUN_EXTERNAL_INTEGRATION=$TESTS_RUN_EXTERNAL_INTEGRATION tests
# publish jobs require $DOCKERHUB_REPO, $DOCKERHUB_USER, $DOCKERHUB_PASS defined
publish_docker_linuxamd64:
machine:
docker_layer_caching: true
steps:
- checkout
- run:
command: |
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
#
sudo docker build --pull -t $DOCKERHUB_REPO:$LATEST_TAG-amd64 -f Dockerfile.linuxamd64 .
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-amd64
publish_docker_linuxarm:
machine:
docker_layer_caching: true
steps:
- checkout
- run:
command: |
sudo docker run --rm --privileged multiarch/qemu-user-static:register --reset
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
#
sudo docker build --pull -t $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 -f Dockerfile.linuxarm32v7 .
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-arm32v7
publish_docker_multiarch:
machine:
enabled: true
image: circleci/classic:201808-01
steps:
- run:
command: |
# Turn on Experimental features
sudo mkdir $HOME/.docker
sudo sh -c 'echo "{ \"experimental\": \"enabled\" }" >> $HOME/.docker/config.json'
#
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
#
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
sudo docker manifest create --amend $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-amd64 $DOCKERHUB_REPO:$LATEST_TAG-arm32v7
sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-amd64 --os linux --arch amd64
sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 --os linux --arch arm --variant v7
sudo docker manifest push $DOCKERHUB_REPO:$LATEST_TAG -p
workflows:
version: 2
build_and_test:
jobs:
- test
publish:
jobs:
- publish_docker_linuxamd64:
filters:
# ignore any commit on any branch by default
branches:
ignore: /.*/
# only act on version tags
tags:
only: /v[1-9]+(\.[0-9]+)*/
- publish_docker_linuxarm:
filters:
branches:
ignore: /.*/
tags:
only: /v[1-9]+(\.[0-9]+)*/
- publish_docker_multiarch:
requires:
- publish_docker_linuxamd64
- publish_docker_linuxarm
filters:
branches:
ignore: /.*/
tags:
only: /v[1-9]+(\.[0-9]+)*/

34
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,34 @@
---
name: Report a problem
about: File a technical problem or report a bug
---
**Describe the problem/bug**
A clear and concise description of what the bug is.
**Your environment**
* Version of BTCPay Server:
* Deployment method:
* Other relevant environment details:
**Logs (if applicable)**
Basic logs can be found in Server Settings > Logs.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Actual behavior**
Tell us what happens instead
**Screenshots/Links**
If applicable, add screenshots or links to help explain your problem.
**Additional context**
Add any other context about the problem here.

View File

@ -0,0 +1,20 @@
---
name: Feature request
about: Ideas and feature requests
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Provide examples**
If applicable provide examples, wireframes, sketches or images to better explain your idea.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@ -1,20 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
<TargetFramework>netcoreapp2.1</TargetFramework>
<IsPackable>false</IsPackable>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
<LangVersion>7.2</LangVersion>
<UserSecretsId>AB0AC1DD-9D26-485B-9416-56A33F268117</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.0" />
<PackageReference Include="xunit" Version="2.3.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BTCPayServer\BTCPayServer.csproj" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.0.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
@ -24,6 +25,13 @@
<None Update="docker-compose.yml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="xunit.runner.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BTCPayServer\BTCPayServer.csproj" />
</ItemGroup>
</Project>

View File

@ -1,4 +1,6 @@
using BTCPayServer.Configuration;
using System.Linq;
using BTCPayServer.HostedServices;
using BTCPayServer.Hosting;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
@ -32,9 +34,17 @@ using System.Security.Principal;
using System.Text;
using System.Threading;
using Xunit;
using BTCPayServer.Services;
using System.Net.Http;
namespace BTCPayServer.Tests
{
public enum TestDatabases
{
Postgres,
MySQL,
}
public class BTCPayServerTester : IDisposable
{
private string _Directory;
@ -57,6 +67,11 @@ namespace BTCPayServer.Tests
set;
}
public string MySQL
{
get; set;
}
public string Postgres
{
get; set;
@ -68,6 +83,10 @@ namespace BTCPayServer.Tests
get; set;
}
public TestDatabases TestDatabase
{
get; set;
}
public bool MockRates { get; set; } = true;
@ -91,24 +110,28 @@ namespace BTCPayServer.Tests
config.AppendLine($"ltc.explorer.url={LTCNBXplorerUri.AbsoluteUri}");
config.AppendLine($"ltc.explorer.cookiefile=0");
config.AppendLine($"btc.lightning={IntegratedLightning.AbsoluteUri}");
if (Postgres != null)
if (TestDatabase == TestDatabases.MySQL && !String.IsNullOrEmpty(MySQL))
config.AppendLine($"mysql=" + MySQL);
else if (!String.IsNullOrEmpty(Postgres))
config.AppendLine($"postgres=" + Postgres);
var confPath = Path.Combine(chainDirectory, "settings.config");
File.WriteAllText(confPath, config.ToString());
ServerUri = new Uri("http://" + HostName + ":" + Port + "/");
HttpClient = new HttpClient();
HttpClient.BaseAddress = ServerUri;
Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development");
var conf = new DefaultConfiguration() { Logger = Logs.LogProvider.CreateLogger("Console") }.CreateConfiguration(new[] { "--datadir", _Directory, "--conf", confPath });
var conf = new DefaultConfiguration() { Logger = Logs.LogProvider.CreateLogger("Console") }.CreateConfiguration(new[] { "--datadir", _Directory, "--conf", confPath, "--disable-registration", "false" });
_Host = new WebHostBuilder()
.UseConfiguration(conf)
.UseContentRoot(FindBTCPayServerDirectory())
.ConfigureServices(s =>
{
s.AddLogging(l =>
{
l.AddFilter("System.Net.Http.HttpClient", LogLevel.Critical);
l.SetMinimumLevel(LogLevel.Information)
.AddFilter("Microsoft", LogLevel.Error)
.AddFilter("Hangfire", LogLevel.Error)
@ -120,38 +143,89 @@ namespace BTCPayServer.Tests
.Build();
_Host.Start();
InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository));
StoreRepository = (StoreRepository)_Host.Services.GetService(typeof(StoreRepository));
var dashBoard = (NBXplorerDashboard)_Host.Services.GetService(typeof(NBXplorerDashboard));
while(!dashBoard.IsFullySynched())
{
Thread.Sleep(10);
}
var rateProvider = (BTCPayRateProviderFactory)_Host.Services.GetService(typeof(BTCPayRateProviderFactory));
rateProvider.DirectProviders.Clear();
if (MockRates)
{
var rateProvider = (RateProviderFactory)_Host.Services.GetService(typeof(RateProviderFactory));
rateProvider.Providers.Clear();
var coinAverageMock = new MockRateProvider();
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("BTC_USD"),
Value = 5000m
});
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("BTC_CAD"),
Value = 4500m
});
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("LTC_USD"),
Value = 500m
});
rateProvider.DirectProviders.Add("coinaverage", coinAverageMock);
var coinAverageMock = new MockRateProvider();
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("BTC_USD"),
BidAsk = new BidAsk(5000m)
});
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("BTC_CAD"),
BidAsk = new BidAsk(4500m)
});
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("LTC_BTC"),
BidAsk = new BidAsk(0.001m)
});
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("LTC_USD"),
BidAsk = new BidAsk(500m)
});
rateProvider.Providers.Add("coinaverage", coinAverageMock);
var bitflyerMock = new MockRateProvider();
bitflyerMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "bitflyer",
CurrencyPair = CurrencyPair.Parse("BTC_JPY"),
BidAsk = new BidAsk(700000m)
});
rateProvider.Providers.Add("bitflyer", bitflyerMock);
var quadrigacx = new MockRateProvider();
quadrigacx.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "quadrigacx",
CurrencyPair = CurrencyPair.Parse("BTC_CAD"),
BidAsk = new BidAsk(6000m)
});
rateProvider.Providers.Add("quadrigacx", quadrigacx);
var bittrex = new MockRateProvider();
bittrex.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "bittrex",
CurrencyPair = CurrencyPair.Parse("DOGE_BTC"),
BidAsk = new BidAsk(0.004m)
});
rateProvider.Providers.Add("bittrex", bittrex);
}
}
private string FindBTCPayServerDirectory()
{
var solutionDirectory = LanguageService.TryGetSolutionDirectoryInfo(Directory.GetCurrentDirectory());
return Path.Combine(solutionDirectory.FullName, "BTCPayServer");
}
public HttpClient HttpClient { get; set; }
public string HostName
{
get;
internal set;
}
public InvoiceRepository InvoiceRepository { get; private set; }
public StoreRepository StoreRepository { get; private set; }
public Uri IntegratedLightning { get; internal set; }
public bool InContainer { get; internal set; }
@ -160,17 +234,21 @@ namespace BTCPayServer.Tests
return _Host.Services.GetRequiredService<T>();
}
public T GetController<T>(string userId = null, string storeId = null) where T : Controller
public T GetController<T>(string userId = null, string storeId = null, Claim[] additionalClaims = null) where T : Controller
{
var context = new DefaultHttpContext();
context.Request.Host = new HostString("127.0.0.1");
context.Request.Host = new HostString("127.0.0.1", Port);
context.Request.Scheme = "http";
context.Request.Protocol = "http";
if (userId != null)
{
context.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, userId) }, Policies.CookieAuthentication));
List<Claim> claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.NameIdentifier, userId));
if (additionalClaims != null)
claims.AddRange(additionalClaims);
context.User = new ClaimsPrincipal(new ClaimsIdentity(claims.ToArray(), Policies.CookieAuthentication));
}
if(storeId != null)
if (storeId != null)
{
context.SetStoreData(GetService<StoreRepository>().FindStore(storeId, userId).GetAwaiter().GetResult());
}

View File

@ -0,0 +1,253 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments.Changelly;
using BTCPayServer.Payments.Changelly.Models;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Tests.Logging;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Xunit;
using Xunit.Abstractions;
namespace BTCPayServer.Tests
{
public class ChangellyTests
{
public ChangellyTests(ITestOutputHelper helper)
{
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
Logs.LogProvider = new XUnitLogProvider(helper);
}
[Fact]
[Trait("Integration", "Integration")]
public async void CanSetChangellyPaymentMethod()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
var controller = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
var storeBlob = controller.StoreData.GetStoreBlob();
Assert.Null(storeBlob.ChangellySettings);
var updateModel = new UpdateChangellySettingsViewModel()
{
ApiSecret = "secret",
ApiKey = "key",
ApiUrl = "http://gozo.com",
ChangellyMerchantId = "aaa",
};
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
await controller.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
var store = await tester.PayTester.StoreRepository.FindStore(user.StoreId);
storeBlob = controller.StoreData.GetStoreBlob();
Assert.NotNull(storeBlob.ChangellySettings);
Assert.NotNull(storeBlob.ChangellySettings);
Assert.IsType<ChangellySettings>(storeBlob.ChangellySettings);
Assert.Equal(storeBlob.ChangellySettings.ApiKey, updateModel.ApiKey);
Assert.Equal(storeBlob.ChangellySettings.ApiSecret,
updateModel.ApiSecret);
Assert.Equal(storeBlob.ChangellySettings.ApiUrl, updateModel.ApiUrl);
Assert.Equal(storeBlob.ChangellySettings.ChangellyMerchantId,
updateModel.ChangellyMerchantId);
}
}
[Fact]
[Trait("Integration", "Integration")]
public async void CanToggleChangellyPaymentMethod()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
var controller = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
var updateModel = new UpdateChangellySettingsViewModel()
{
ApiSecret = "secret",
ApiKey = "key",
ApiUrl = "http://gozo.com",
ChangellyMerchantId = "aaa",
Enabled = true
};
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
await controller.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
var store = await tester.PayTester.StoreRepository.FindStore(user.StoreId);
Assert.True(store.GetStoreBlob().ChangellySettings.Enabled);
updateModel.Enabled = false;
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
await controller.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
store = await tester.PayTester.StoreRepository.FindStore(user.StoreId);
Assert.False(store.GetStoreBlob().ChangellySettings.Enabled);
}
}
[Fact]
[Trait("Integration", "Integration")]
public async void CannotUseChangellyApiWithoutChangellyPaymentMethodSet()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
var changellyController =
tester.PayTester.GetController<ChangellyController>(user.UserId, user.StoreId);
changellyController.IsTest = true;
//test non existing payment method
Assert.IsType<BitpayErrorModel>(Assert
.IsType<BadRequestObjectResult>(await changellyController.GetCurrencyList(user.StoreId))
.Value);
var updateModel = CreateDefaultChangellyParams(false);
var storesController = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
//set payment method but disabled
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
await storesController.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
Assert.IsType<BitpayErrorModel>(Assert
.IsType<BadRequestObjectResult>(await changellyController.GetCurrencyList(user.StoreId))
.Value);
updateModel.Enabled = true;
//test with enabled method
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
await storesController.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
Assert.IsNotType<BitpayErrorModel>(Assert
.IsType<OkObjectResult>(await changellyController.GetCurrencyList(user.StoreId))
.Value);
}
}
UpdateChangellySettingsViewModel CreateDefaultChangellyParams(bool enabled)
{
return new UpdateChangellySettingsViewModel()
{
ApiKey = "6ed02cdf1b614d89a8c0ceb170eebb61",
ApiSecret = "8fbd66a2af5fd15a6b5f8ed0159c5842e32a18538521ffa145bd6c9e124d3483",
ChangellyMerchantId = "804298eb5753",
Enabled = enabled
};
}
[Fact]
[Trait("Integration", "Integration")]
public async void CanGetCurrencyListFromChangelly()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
//save changelly settings
var updateModel = CreateDefaultChangellyParams(true);
var storesController = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
//confirm saved
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
await storesController.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
var factory = UnitTest1.CreateBTCPayRateFactory();
var fetcher = new RateFetcher(factory);
var httpClientFactory = new MockHttpClientFactory();
var changellyController = new ChangellyController(
new ChangellyClientProvider(tester.PayTester.StoreRepository, httpClientFactory),
tester.NetworkProvider, fetcher);
changellyController.IsTest = true;
var result = Assert
.IsType<OkObjectResult>(await changellyController.GetCurrencyList(user.StoreId))
.Value as IEnumerable<CurrencyFull>;
Assert.True(result.Any());
}
}
[Fact]
[Trait("Integration", "Integration")]
public async void CanCalculateToAmountForChangelly()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
var updateModel = CreateDefaultChangellyParams(true);
var storesController = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
await storesController.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
var factory = UnitTest1.CreateBTCPayRateFactory();
var fetcher = new RateFetcher(factory);
var httpClientFactory = new MockHttpClientFactory();
var changellyController = new ChangellyController(
new ChangellyClientProvider(tester.PayTester.StoreRepository, httpClientFactory),
tester.NetworkProvider, fetcher);
changellyController.IsTest = true;
Assert.IsType<decimal>(Assert
.IsType<OkObjectResult>(await changellyController.CalculateAmount(user.StoreId, "ltc", "btc", 1.0m, default))
.Value);
}
}
[Fact]
[Trait("Integration", "Integration")]
public void CanComputeBaseAmount()
{
Assert.Equal(1, ChangellyCalculationHelper.ComputeBaseAmount(1, 1));
Assert.Equal(0.5m, ChangellyCalculationHelper.ComputeBaseAmount(1, 0.5m));
Assert.Equal(2, ChangellyCalculationHelper.ComputeBaseAmount(0.5m, 1));
Assert.Equal(4m, ChangellyCalculationHelper.ComputeBaseAmount(1, 4));
}
[Fact]
[Trait("Integration", "Integration")]
public void CanComputeCorrectAmount()
{
Assert.Equal(1, ChangellyCalculationHelper.ComputeCorrectAmount(0.5m, 1, 2));
Assert.Equal(0.25m, ChangellyCalculationHelper.ComputeCorrectAmount(0.5m, 1, 0.5m));
Assert.Equal(20, ChangellyCalculationHelper.ComputeCorrectAmount(10, 1, 2));
}
}
public class MockHttpClientFactory : IHttpClientFactory
{
public HttpClient CreateClient(string name)
{
return new HttpClient();
}
}
}

View File

@ -1,8 +1,9 @@
using System;
using System.Collections.Generic;
using System.Text;
using BTCPayServer.Payments.Lightning.Charge;
using BTCPayServer.Payments.Lightning.CLightning;
using BTCPayServer.Lightning;
using BTCPayServer.Lightning.Charge;
using BTCPayServer.Payments.Lightning;
using NBitcoin;
namespace BTCPayServer.Tests
@ -15,7 +16,7 @@ namespace BTCPayServer.Tests
{
this._Parent = serverTester;
var url = serverTester.GetEnvironment(environmentName, defaultValue);
Client = new ChargeClient(new Uri(url), network);
Client = (ChargeClient)LightningClientFactory.CreateClient(url, network);
P2PHost = _Parent.GetEnvironment(environmentName + "_HOST", defaultHost);
}
public ChargeClient Client { get; set; }

View File

@ -0,0 +1,89 @@
using BTCPayServer.Controllers;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments.CoinSwitch;
using BTCPayServer.Tests.Logging;
using Microsoft.AspNetCore.Mvc;
using Xunit;
using Xunit.Abstractions;
namespace BTCPayServer.Tests
{
public class CoinSwitchTests
{
public CoinSwitchTests(ITestOutputHelper helper)
{
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
Logs.LogProvider = new XUnitLogProvider(helper);
}
[Fact]
[Trait("Integration", "Integration")]
public async void CanSetCoinSwitchPaymentMethod()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
var controller = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
var storeBlob = controller.StoreData.GetStoreBlob();
Assert.Null(storeBlob.CoinSwitchSettings);
var updateModel = new UpdateCoinSwitchSettingsViewModel()
{
MerchantId = "aaa",
};
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
await controller.UpdateCoinSwitchSettings(user.StoreId, updateModel, "save")).ActionName);
var store = await tester.PayTester.StoreRepository.FindStore(user.StoreId);
storeBlob = controller.StoreData.GetStoreBlob();
Assert.NotNull(storeBlob.CoinSwitchSettings);
Assert.NotNull(storeBlob.CoinSwitchSettings);
Assert.IsType<CoinSwitchSettings>(storeBlob.CoinSwitchSettings);
Assert.Equal(storeBlob.CoinSwitchSettings.MerchantId,
updateModel.MerchantId);
}
}
[Fact]
[Trait("Integration", "Integration")]
public async void CanToggleCoinSwitchPaymentMethod()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
var controller = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
var updateModel = new UpdateCoinSwitchSettingsViewModel()
{
MerchantId = "aaa",
Enabled = true
};
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
await controller.UpdateCoinSwitchSettings(user.StoreId, updateModel, "save")).ActionName);
var store = await tester.PayTester.StoreRepository.FindStore(user.StoreId);
Assert.True(store.GetStoreBlob().CoinSwitchSettings.Enabled);
updateModel.Enabled = false;
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
await controller.UpdateCoinSwitchSettings(user.StoreId, updateModel, "save")).ActionName);
store = await tester.PayTester.StoreRepository.FindStore(user.StoreId);
Assert.False(store.GetStoreBlob().CoinSwitchSettings.Enabled);
}
}
}
}

View File

@ -0,0 +1,304 @@
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)
}, default));
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)
}, default));
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)
}, default));
//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)
}, default));
//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)
}, default));
//Scenario 6: Allowed
Assert.IsType<OkObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
{
Amount = new decimal(0.05)
}, default));
}
}
[Fact]
[Trait("Integration", "Integration")]
public void CanComputeCrowdfundModel()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
user.ModifyStore(s => s.NetworkFeeMode = NetworkFeeMode.Never);
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

@ -8,30 +8,27 @@ using Microsoft.AspNetCore.Builder;
using System.Threading.Tasks;
using System.Threading;
using Microsoft.AspNetCore.Hosting.Server.Features;
using System.Threading.Channels;
using System.IO;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json;
namespace BTCPayServer.Tests
{
public class CustomServer : IDisposable
{
TaskCompletionSource<bool> _Evt = null;
IWebHost _Host = null;
CancellationTokenSource _Closed = new CancellationTokenSource();
Channel<JObject> _Requests = Channel.CreateUnbounded<JObject>();
public CustomServer()
{
{
var port = Utils.FreeTcpPort();
_Host = new WebHostBuilder()
.Configure(app =>
{
app.Run(req =>
{
while (_Act == null)
{
Thread.Sleep(10);
_Closed.Token.ThrowIfCancellationRequested();
}
_Act(req);
_Act = null;
_Evt.TrySetResult(true);
_Requests.Writer.WriteAsync(JsonConvert.DeserializeObject<JObject>(new StreamReader(req.Request.Body).ReadToEnd()), _Closed.Token);
req.Response.StatusCode = 200;
return Task.CompletedTask;
});
@ -47,22 +44,24 @@ namespace BTCPayServer.Tests
return new Uri(_Host.ServerFeatures.Get<IServerAddressesFeature>().Addresses.First());
}
Action<HttpContext> _Act;
public void ProcessNextRequest(Action<HttpContext> act)
public async Task<JObject> GetNextRequest()
{
var source = new TaskCompletionSource<bool>();
CancellationTokenSource cancellation = new CancellationTokenSource(20000);
cancellation.Token.Register(() => source.TrySetCanceled());
source = new TaskCompletionSource<bool>();
_Evt = source;
_Act = act;
try
using (CancellationTokenSource cancellation = new CancellationTokenSource(2000000))
{
_Evt.Task.GetAwaiter().GetResult();
}
catch (TaskCanceledException)
{
throw new Xunit.Sdk.XunitException("Callback to the webserver was expected, check if the callback url is accessible from internet");
try
{
JObject req = null;
while(!await _Requests.Reader.WaitToReadAsync(cancellation.Token) ||
!_Requests.Reader.TryRead(out req))
{
}
return req;
}
catch (TaskCanceledException)
{
throw new Xunit.Sdk.XunitException("Callback to the webserver was expected, check if the callback url is accessible from internet");
}
}
}

View File

@ -1,12 +1,14 @@
FROM microsoft/dotnet:2.0.6-sdk-2.1.101-stretch
WORKDIR /app
# caches restore result by copying csproj file separately
COPY BTCPayServer.Tests/BTCPayServer.Tests.csproj BTCPayServer.Tests/BTCPayServer.Tests.csproj
FROM mcr.microsoft.com/dotnet/core/sdk:2.1.505-alpine3.7 AS builder
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT false
RUN apk add --no-cache icu-libs
ENV LC_ALL en_US.UTF-8
ENV LANG en_US.UTF-8
WORKDIR /source
COPY BTCPayServer/BTCPayServer.csproj BTCPayServer/BTCPayServer.csproj
WORKDIR /app/BTCPayServer.Tests
RUN dotnet restore
# copies the rest of your code
COPY . ../.
ENTRYPOINT ["dotnet", "test"]
COPY BTCPayServer.Tests/BTCPayServer.Tests.csproj BTCPayServer.Tests/BTCPayServer.Tests.csproj
RUN dotnet restore BTCPayServer.Tests/BTCPayServer.Tests.csproj
COPY . .
RUN dotnet build
WORKDIR /source/BTCPayServer.Tests
ENTRYPOINT ["./docker-entrypoint.sh"]

View File

@ -2,7 +2,7 @@
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Payments.Lightning.CLightning;
using BTCPayServer.Lightning.CLightning;
using NBitcoin;
namespace BTCPayServer.Tests
@ -13,10 +13,10 @@ namespace BTCPayServer.Tests
public LightningDTester(ServerTester parent, string environmentName, string defaultRPC, string defaultHost, Network network)
{
this.parent = parent;
RPC = new CLightningRPCClient(new Uri(parent.GetEnvironment(environmentName, defaultRPC)), network);
RPC = new CLightningClient(new Uri(parent.GetEnvironment(environmentName, defaultRPC)), network);
}
public CLightningRPCClient RPC { get; }
public CLightningClient RPC { get; }
public string P2PHost { get; }
}

View File

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Text;
using BTCPayServer.Lightning.LND;
using NBitcoin;
namespace BTCPayServer.Tests.Lnd
{
public class LndMockTester
{
private ServerTester _Parent;
public LndMockTester(ServerTester serverTester, string environmentName, string defaultValue, string defaultHost, Network network)
{
this._Parent = serverTester;
var url = serverTester.GetEnvironment(environmentName, defaultValue);
Swagger = new LndSwaggerClient(new LndRestSettings(new Uri(url)) { AllowInsecure = true });
Client = new LndClient(Swagger, network);
P2PHost = _Parent.GetEnvironment(environmentName + "_HOST", defaultHost);
}
public LndSwaggerClient Swagger { get; set; }
public LndClient Client { get; set; }
public string P2PHost { get; }
}
}

View File

@ -0,0 +1,67 @@
using System;
using System.Linq;
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 async Task Advance(TimeSpan time)
{
_Now += time;
List<WaitObj> overdue = new List<WaitObj>();
lock (waits)
{
foreach (var wait in waits.ToArray())
{
if (_Now >= wait.Expiration)
{
overdue.Add(wait);
waits.Remove(wait);
}
}
}
foreach (var o in overdue)
o.CTS.TrySetResult(true);
try
{
await Task.WhenAll(overdue.Select(o => o.CTS.Task).ToArray());
}
catch { }
}
public override string ToString()
{
return _Now.Millisecond.ToString(CultureInfo.InvariantCulture);
}
}
}

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Rating;
using BTCPayServer.Services.Rates;
@ -10,7 +11,7 @@ namespace BTCPayServer.Tests.Mocks
public class MockRateProvider : IRateProvider
{
public ExchangeRates ExchangeRates { get; set; } = new ExchangeRates();
public Task<ExchangeRates> GetRatesAsync()
public Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
{
return Task.FromResult(ExchangeRates);
}

View File

@ -0,0 +1,216 @@
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.PaymentRequestViewModels;
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;
namespace BTCPayServer.Tests
{
public class PaymentRequestTests
{
public PaymentRequestTests(ITestOutputHelper helper)
{
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
Logs.LogProvider = new XUnitLogProvider(helper);
}
[Fact]
[Trait("Integration", "Integration")]
public void CanCreateViewUpdateAndDeletePaymentRequest()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
var user2 = tester.NewAccount();
user2.GrantAccess();
var paymentRequestController = user.GetController<PaymentRequestController>();
var guestpaymentRequestController = user2.GetController<PaymentRequestController>();
var request = new UpdatePaymentRequestViewModel()
{
Title = "original juice",
Currency = "BTC",
Amount = 1,
StoreId = user.StoreId,
Description = "description"
};
var id = (Assert
.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(null, request).Result).RouteValues.Values.First().ToString());
//permission guard for guests editing
Assert
.IsType<NotFoundResult>(guestpaymentRequestController.EditPaymentRequest(id).Result);
request.Title = "update";
Assert.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(id, request).Result);
Assert.Equal(request.Title, Assert.IsType<ViewPaymentRequestViewModel>( Assert.IsType<ViewResult>(paymentRequestController.ViewPaymentRequest(id).Result).Model).Title);
Assert.False(string.IsNullOrEmpty(id));
Assert.IsType<ViewPaymentRequestViewModel>(Assert
.IsType<ViewResult>(paymentRequestController.ViewPaymentRequest(id).Result).Model);
//Delete
Assert.IsType<ConfirmModel>(Assert
.IsType<ViewResult>(paymentRequestController.RemovePaymentRequestPrompt(id).Result).Model);
Assert.IsType<RedirectToActionResult>(paymentRequestController.RemovePaymentRequest(id).Result);
Assert
.IsType<NotFoundResult>(paymentRequestController.ViewPaymentRequest(id).Result);
}
}
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")]
public async Task CanPayPaymentRequestWhenPossible()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
var paymentRequestController = user.GetController<PaymentRequestController>();
Assert.IsType<NotFoundResult>(await paymentRequestController.PayPaymentRequest(Guid.NewGuid().ToString()));
var request = new UpdatePaymentRequestViewModel()
{
Title = "original juice",
Currency = "BTC",
Amount = 1,
StoreId = user.StoreId,
Description = "description"
};
var response = Assert
.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(null, request).Result)
.RouteValues.First();
var invoiceId = Assert
.IsType<OkObjectResult>(await paymentRequestController.PayPaymentRequest(response.Value.ToString(), false)).Value
.ToString();
var actionResult = Assert
.IsType<RedirectToActionResult>(await paymentRequestController.PayPaymentRequest(response.Value.ToString()));
Assert.Equal("Checkout", actionResult.ActionName);
Assert.Equal("Invoice", actionResult.ControllerName);
Assert.Contains(actionResult.RouteValues, pair => pair.Key == "Id" && pair.Value.ToString() == invoiceId);
var invoice = user.BitPay.GetInvoice(invoiceId, Facade.Merchant);
Assert.Equal(1, invoice.Price);
request = new UpdatePaymentRequestViewModel()
{
Title = "original juice with expiry",
Currency = "BTC",
Amount = 1,
ExpiryDate = DateTime.Today.Subtract( TimeSpan.FromDays(2)),
StoreId = user.StoreId,
Description = "description"
};
response = Assert
.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(null, request).Result)
.RouteValues.First();
Assert
.IsType<BadRequestObjectResult>(await paymentRequestController.PayPaymentRequest(response.Value.ToString(), false));
}
}
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")]
public async Task CanCancelPaymentWhenPossible()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
var paymentRequestController = user.GetController<PaymentRequestController>();
Assert.IsType<NotFoundResult>(await
paymentRequestController.CancelUnpaidPendingInvoice(Guid.NewGuid().ToString(), false));
var request = new UpdatePaymentRequestViewModel()
{
Title = "original juice",
Currency = "BTC",
Amount = 1,
StoreId = user.StoreId,
Description = "description"
};
var response = Assert
.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(null, request).Result)
.RouteValues.First();
var paymentRequestId = response.Value.ToString();
var invoiceId = Assert
.IsType<OkObjectResult>(await paymentRequestController.PayPaymentRequest(paymentRequestId, false)).Value
.ToString();
var actionResult = Assert
.IsType<RedirectToActionResult>(await paymentRequestController.PayPaymentRequest(response.Value.ToString()));
Assert.Equal("Checkout", actionResult.ActionName);
Assert.Equal("Invoice", actionResult.ControllerName);
Assert.Contains(actionResult.RouteValues, pair => pair.Key == "Id" && pair.Value.ToString() == invoiceId);
var invoice = user.BitPay.GetInvoice(invoiceId, Facade.Merchant);
Assert.Equal(InvoiceState.ToString(InvoiceStatus.New), invoice.Status);
Assert.IsType<OkObjectResult>(await
paymentRequestController.CancelUnpaidPendingInvoice(paymentRequestId, false));
invoice = user.BitPay.GetInvoice(invoiceId, Facade.Merchant);
Assert.Equal(InvoiceState.ToString(InvoiceStatus.Invalid), invoice.Status);
Assert.IsType<BadRequestObjectResult>(await
paymentRequestController.CancelUnpaidPendingInvoice(paymentRequestId, false));
}
}
}
}

View File

@ -29,11 +29,7 @@ If you want to stop, and remove all existing data
docker-compose down --v
```
You can run the tests inside a container by running
```
docker-compose run --rm tests
```
You can run tests on `MySql` database instead of `Postgres` by setting environnement variable `TESTS_DB` equals to `MySql`.
## How to manually test payments
@ -45,10 +41,15 @@ You can call bitcoin-cli inside the container with `docker exec`, for example, i
```
If you are using Powershell:
```
```powershell
.\docker-bitcoin-cli.ps1 sendtoaddress "mohu16LH66ptoWGEL1GtP6KHTBJYXMWhEf" 0.23111090
```
You can also generate blocks:
```powershell
.\docker-bitcoin-generate.ps1 3
```
### Using the test litecoin-cli
Same as bitcoin-cli, but with `.\docker-litecoin-cli.ps1` and `.\docker-litecoin-cli.sh` instead.

View File

@ -4,12 +4,28 @@ using System.Collections.Generic;
using System.Text;
using BTCPayServer.Rating;
using Xunit;
using System.Globalization;
namespace BTCPayServer.Tests
{
public class RateRulesTest
{
[Fact]
[Trait("Fast", "Fast")]
public void SecondDuplicatedRuleIsIgnored()
{
StringBuilder builder = new StringBuilder();
builder.AppendLine("DOGE_X = 1.1");
builder.AppendLine("DOGE_X = 1.2");
Assert.True(RateRules.TryParse(builder.ToString(), out var rules));
var rule = rules.GetRuleFor(new CurrencyPair("DOGE", "BTC"));
rule.Reevaluate();
Assert.True(!rule.HasError);
Assert.Equal(1.1m, rule.BidAsk.Ask);
}
[Fact]
[Trait("Fast", "Fast")]
public void CanParseRateRules()
{
// Check happy path
@ -35,9 +51,9 @@ namespace BTCPayServer.Tests
rules.ToString());
var tests = new[]
{
(Pair: "DOGE_USD", Expected: "bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1"),
(Pair: "BTC_USD", Expected: "gdax(BTC_USD)"),
(Pair: "BTC_CAD", Expected: "coinbase(BTC_CAD)"),
(Pair: "DOGE_USD", Expected: "bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1"),
(Pair: "DOGE_CAD", Expected: "bittrex(DOGE_BTC) * coinbase(BTC_CAD) * 1.1"),
(Pair: "LTC_CAD", Expected: "coinaverage(LTC_CAD) * 1.02"),
};
@ -45,8 +61,8 @@ namespace BTCPayServer.Tests
{
Assert.Equal(test.Expected, rules.GetRuleFor(CurrencyPair.Parse(test.Pair)).ToString());
}
rules.GlobalMultiplier = 2.32m;
Assert.Equal("(bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1) * 2.32", rules.GetRuleFor(CurrencyPair.Parse("DOGE_USD")).ToString());
rules.Spread = 0.2m;
Assert.Equal("(bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1) * (0.8, 1.2)", rules.GetRuleFor(CurrencyPair.Parse("DOGE_USD")).ToString());
////////////////
// Check errors conditions
@ -81,9 +97,9 @@ namespace BTCPayServer.Tests
var tests2 = new[]
{
(Pair: "DOGE_USD", Expected: "bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1", ExpectedExchangeRates: "bittrex(DOGE_BTC),gdax(BTC_USD)"),
(Pair: "BTC_USD", Expected: "gdax(BTC_USD)", ExpectedExchangeRates: "gdax(BTC_USD)"),
(Pair: "BTC_CAD", Expected: "coinbase(BTC_CAD)", ExpectedExchangeRates: "coinbase(BTC_CAD)"),
(Pair: "DOGE_USD", Expected: "bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1", ExpectedExchangeRates: "bittrex(DOGE_BTC),gdax(BTC_USD)"),
(Pair: "DOGE_CAD", Expected: "bittrex(DOGE_BTC) * coinbase(BTC_CAD) * 1.1", ExpectedExchangeRates: "bittrex(DOGE_BTC),coinbase(BTC_CAD)"),
(Pair: "LTC_CAD", Expected: "coinaverage(LTC_CAD) * 1.02", ExpectedExchangeRates: "coinaverage(LTC_CAD)"),
};
@ -94,16 +110,16 @@ namespace BTCPayServer.Tests
Assert.Equal(test.ExpectedExchangeRates, string.Join(',', rule.ExchangeRates.OfType<object>().ToArray()));
}
var rule2 = rules.GetRuleFor(CurrencyPair.Parse("DOGE_CAD"));
rule2.ExchangeRates.SetRate("bittrex", CurrencyPair.Parse("DOGE_BTC"), 5000);
rule2.ExchangeRates.SetRate("bittrex", CurrencyPair.Parse("DOGE_BTC"), new BidAsk(5000m));
rule2.Reevaluate();
Assert.True(rule2.HasError);
Assert.Equal("5000 * ERR_RATE_UNAVAILABLE(coinbase, BTC_CAD) * 1.1", rule2.ToString(true));
Assert.Equal("bittrex(DOGE_BTC) * coinbase(BTC_CAD) * 1.1", rule2.ToString(false));
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), 2000.4m);
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), new BidAsk(2000.4m));
rule2.Reevaluate();
Assert.False(rule2.HasError);
Assert.Equal("5000 * 2000.4 * 1.1", rule2.ToString(true));
Assert.Equal(rule2.Value, 5000m * 2000.4m * 1.1m);
Assert.Equal(rule2.BidAsk.Bid, 5000m * 2000.4m * 1.1m);
////////
// Make sure parenthesis are correctly calculated
@ -112,23 +128,67 @@ namespace BTCPayServer.Tests
builder.AppendLine("BTC_USD = -3 + coinbase(BTC_CAD) + 50 - 5");
builder.AppendLine("DOGE_BTC = 2000");
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
rules.GlobalMultiplier = 1.1m;
rules.Spread = 0.1m;
rule2 = rules.GetRuleFor(CurrencyPair.Parse("DOGE_USD"));
Assert.Equal("(2000 * (-3 + coinbase(BTC_CAD) + 50 - 5)) * 1.1", rule2.ToString());
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), 1000m);
Assert.Equal("(2000 * (-3 + coinbase(BTC_CAD) + 50 - 5)) * (0.9, 1.1)", rule2.ToString());
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), new BidAsk(1000m));
Assert.True(rule2.Reevaluate());
Assert.Equal("(2000 * (-3 + 1000 + 50 - 5)) * 1.1", rule2.ToString(true));
Assert.Equal((2000m * (-3m + 1000m + 50m - 5m)) * 1.1m, rule2.Value.Value);
Assert.Equal("(2000 * (-3 + 1000 + 50 - 5)) * (0.9, 1.1)", rule2.ToString(true));
Assert.Equal((2000m * (-3m + 1000m + 50m - 5m)) * 0.9m, rule2.BidAsk.Bid);
// Test inverse
rule2 = rules.GetRuleFor(CurrencyPair.Parse("USD_DOGE"));
Assert.Equal("(1 / (2000 * (-3 + coinbase(BTC_CAD) + 50 - 5))) * 1.1", rule2.ToString());
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), 1000m);
Assert.Equal("(1 / (2000 * (-3 + coinbase(BTC_CAD) + 50 - 5))) * (0.9, 1.1)", rule2.ToString());
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), new BidAsk(1000m));
Assert.True(rule2.Reevaluate());
Assert.Equal("(1 / (2000 * (-3 + 1000 + 50 - 5))) * 1.1", rule2.ToString(true));
Assert.Equal(( 1.0m / (2000m * (-3m + 1000m + 50m - 5m))) * 1.1m, rule2.Value.Value);
Assert.Equal("(1 / (2000 * (-3 + 1000 + 50 - 5))) * (0.9, 1.1)", rule2.ToString(true));
Assert.Equal((1.0m / (2000m * (-3m + 1000m + 50m - 5m))) * 0.9m, rule2.BidAsk.Bid);
////////
// Make sure kraken is not converted to CurrencyPair
builder = new StringBuilder();
builder.AppendLine("BTC_USD = kraken(BTC_USD)");
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
rule2 = rules.GetRuleFor(CurrencyPair.Parse("BTC_USD"));
rule2.ExchangeRates.SetRate("kraken", CurrencyPair.Parse("BTC_USD"), new BidAsk(1000m));
Assert.True(rule2.Reevaluate());
// Make sure can handle pairs
builder = new StringBuilder();
builder.AppendLine("BTC_USD = kraken(BTC_USD)");
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
rule2 = rules.GetRuleFor(CurrencyPair.Parse("BTC_USD"));
rule2.ExchangeRates.SetRate("kraken", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m));
Assert.True(rule2.Reevaluate());
Assert.Equal("(6000, 6100)", rule2.ToString(true));
Assert.Equal(6000m, rule2.BidAsk.Bid);
rule2 = rules.GetRuleFor(CurrencyPair.Parse("USD_BTC"));
rule2.ExchangeRates.SetRate("kraken", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m));
Assert.True(rule2.Reevaluate());
Assert.Equal("1 / (6000, 6100)", rule2.ToString(true));
Assert.Equal(1m / 6100m, rule2.BidAsk.Bid);
// Make sure the inverse has more priority than X_X or CDNT_X
builder = new StringBuilder();
builder.AppendLine("EUR_CDNT = 10");
builder.AppendLine("CDNT_BTC = CDNT_EUR * EUR_BTC;");
builder.AppendLine("CDNT_X = CDNT_BTC * BTC_X;");
builder.AppendLine("X_X = coinaverage(X_X);");
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
rule2 = rules.GetRuleFor(CurrencyPair.Parse("CDNT_EUR"));
rule2.ExchangeRates.SetRate("coinaverage", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m));
Assert.True(rule2.Reevaluate());
Assert.Equal("1 / 10", rule2.ToString(false));
// Make sure an inverse can be solved on an exchange
builder = new StringBuilder();
builder.AppendLine("X_X = coinaverage(X_X);");
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
rule2 = rules.GetRuleFor(CurrencyPair.Parse("USD_BTC"));
rule2.ExchangeRates.SetRate("coinaverage", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m));
Assert.True(rule2.Reevaluate());
Assert.Equal($"({(1m / 6100m).ToString(CultureInfo.InvariantCulture)}, {(1m / 6000m).ToString(CultureInfo.InvariantCulture)})", rule2.ToString(true));
}
}
}

View File

@ -18,8 +18,12 @@ using System.Text;
using System.Threading.Tasks;
using System.Threading;
using System.Globalization;
using BTCPayServer.Payments.Lightning.CLightning;
using BTCPayServer.Payments.Lightning.Charge;
using BTCPayServer.Tests.Lnd;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Lightning.CLightning;
using BTCPayServer.Lightning;
using BTCPayServer.Services;
using BTCPayServer.Tests.Logging;
namespace BTCPayServer.Tests
{
@ -41,22 +45,27 @@ namespace BTCPayServer.Tests
NetworkProvider = new BTCPayNetworkProvider(NetworkType.Regtest);
ExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_BTCRPCCONNECTION", "server=http://127.0.0.1:43782;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork("BTC").NBitcoinNetwork);
ExplorerNode.ScanRPCCapabilities();
LTCExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_LTCRPCCONNECTION", "server=http://127.0.0.1:43783;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork("LTC").NBitcoinNetwork);
ExplorerClient = new ExplorerClient(NetworkProvider.GetNetwork("BTC").NBXplorerNetwork, new Uri(GetEnvironment("TESTS_BTCNBXPLORERURL", "http://127.0.0.1:32838/")));
LTCExplorerClient = new ExplorerClient(NetworkProvider.GetNetwork("LTC").NBXplorerNetwork, new Uri(GetEnvironment("TESTS_LTCNBXPLORERURL", "http://127.0.0.1:32838/")));
var btc = NetworkProvider.GetNetwork("BTC").NBitcoinNetwork;
CustomerLightningD = new CLightningRPCClient(new Uri(GetEnvironment("TEST_CUSTOMERLIGHTNINGD", "tcp://127.0.0.1:30992/")), btc);
MerchantLightningD = new CLightningRPCClient(new Uri(GetEnvironment("TEST_MERCHANTLIGHTNINGD", "tcp://127.0.0.1:30993/")), btc);
CustomerLightningD = LightningClientFactory.CreateClient(GetEnvironment("TEST_CUSTOMERLIGHTNINGD", "type=clightning;server=tcp://127.0.0.1:30992/"), btc);
MerchantLightningD = LightningClientFactory.CreateClient(GetEnvironment("TEST_MERCHANTLIGHTNINGD", "type=clightning;server=tcp://127.0.0.1:30993/"), btc);
MerchantCharge = new ChargeTester(this, "TEST_MERCHANTCHARGE", "http://api-token:foiewnccewuify@127.0.0.1:54938/", "merchant_lightningd", btc);
MerchantCharge = new ChargeTester(this, "TEST_MERCHANTCHARGE", "type=charge;server=http://127.0.0.1:54938/;api-token=foiewnccewuify", "merchant_lightningd", btc);
MerchantLnd = new LndMockTester(this, "TEST_MERCHANTLND", "https://lnd:lnd@127.0.0.1:53280/", "merchant_lnd", btc);
PayTester = new BTCPayServerTester(Path.Combine(_Directory, "pay"))
{
NBXplorerUri = ExplorerClient.Address,
LTCNBXplorerUri = LTCExplorerClient.Address,
TestDatabase = Enum.Parse<TestDatabases>(GetEnvironment("TESTS_DB", TestDatabases.Postgres.ToString()), true),
Postgres = GetEnvironment("TESTS_POSTGRES", "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver"),
MySQL = GetEnvironment("TESTS_MYSQL", "User ID=root;Host=127.0.0.1;Port=33036;Database=btcpayserver"),
IntegratedLightning = MerchantCharge.Client.Uri
};
PayTester.Port = int.Parse(GetEnvironment("TESTS_PORT", Utils.FreeTcpPort().ToString(CultureInfo.InvariantCulture)), CultureInfo.InvariantCulture);
@ -74,69 +83,26 @@ namespace BTCPayServer.Tests
PayTester.Start();
}
/// <summary>
/// Connect a customer LN node to the merchant LN node
/// </summary>
public void PrepareLightning()
{
PrepareLightningAsync().GetAwaiter().GetResult();
}
/// <summary>
/// Connect a customer LN node to the merchant LN node
/// </summary>
/// <returns></returns>
public async Task PrepareLightningAsync()
public async Task EnsureChannelsSetup()
{
while (true)
{
var skippedStates = new[] { "ONCHAIN", "CHANNELD_SHUTTING_DOWN", "CLOSINGD_SIGEXCHANGE", "CLOSINGD_COMPLETE", "FUNDING_SPEND_SEEN" };
var channel = (await CustomerLightningD.ListPeersAsync())
.SelectMany(p => p.Channels)
.Where(c => !skippedStates.Contains(c.State ?? ""))
.FirstOrDefault();
switch (channel?.State)
{
case null:
var merchantInfo = await WaitLNSynched();
var clightning = new NodeInfo(merchantInfo.Id, MerchantCharge.P2PHost, merchantInfo.Port);
await CustomerLightningD.ConnectAsync(clightning);
var address = await CustomerLightningD.NewAddressAsync();
await ExplorerNode.SendToAddressAsync(address, Money.Coins(0.2m));
ExplorerNode.Generate(1);
await WaitLNSynched();
await Task.Delay(1000);
await CustomerLightningD.FundChannelAsync(clightning, Money.Satoshis(16777215));
break;
case "CHANNELD_AWAITING_LOCKIN":
ExplorerNode.Generate(1);
await WaitLNSynched();
break;
case "CHANNELD_NORMAL":
return;
default:
throw new NotSupportedException(channel?.State ?? "");
}
}
Logs.Tester.LogInformation("Connecting channels");
await BTCPayServer.Lightning.Tests.ConnectChannels.ConnectAll(ExplorerNode, GetLightningSenderClients(), GetLightningDestClients()).ConfigureAwait(false);
Logs.Tester.LogInformation("Channels connected");
}
private async Task<GetInfoResponse> WaitLNSynched()
private IEnumerable<ILightningClient> GetLightningSenderClients()
{
while (true)
{
var merchantInfo = await MerchantCharge.Client.GetInfoAsync();
var blockCount = await ExplorerNode.GetBlockCountAsync();
if (merchantInfo.BlockHeight != blockCount)
{
await Task.Delay(1000);
}
else
{
return merchantInfo;
}
}
yield return CustomerLightningD;
}
private IEnumerable<ILightningClient> GetLightningDestClients()
{
yield return MerchantLightningD;
yield return MerchantLnd.Client;
}
public void SendLightningPayment(Invoice invoice)
@ -148,12 +114,14 @@ namespace BTCPayServer.Tests
{
var bolt11 = invoice.CryptoInfo.Where(o => o.PaymentUrls.BOLT11 != null).First().PaymentUrls.BOLT11;
bolt11 = bolt11.Replace("lightning:", "", StringComparison.OrdinalIgnoreCase);
await CustomerLightningD.SendAsync(bolt11);
await CustomerLightningD.Pay(bolt11);
}
public CLightningRPCClient CustomerLightningD { get; set; }
public CLightningRPCClient MerchantLightningD { get; private set; }
public ILightningClient CustomerLightningD { get; set; }
public ILightningClient MerchantLightningD { get; private set; }
public ChargeTester MerchantCharge { get; private set; }
public LndMockTester MerchantLnd { get; set; }
internal string GetEnvironment(string variable, string defaultValue)
{
@ -185,106 +153,19 @@ namespace BTCPayServer.Tests
HttpClient _Http = new HttpClient();
class MockHttpRequest : HttpRequest
{
Uri serverUri;
public MockHttpRequest(Uri serverUri)
{
this.serverUri = serverUri;
}
public override HttpContext HttpContext => throw new NotImplementedException();
public override string Method
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override string Scheme
{
get => serverUri.Scheme;
set => throw new NotImplementedException();
}
public override bool IsHttps
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override HostString Host
{
get => new HostString(serverUri.Host, serverUri.Port);
set => throw new NotImplementedException();
}
public override PathString PathBase
{
get => "";
set => throw new NotImplementedException();
}
public override PathString Path
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override QueryString QueryString
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override IQueryCollection Query
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override string Protocol
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override IHeaderDictionary Headers => throw new NotImplementedException();
public override IRequestCookieCollection Cookies
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override long? ContentLength
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override string ContentType
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override Stream Body
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override bool HasFormContentType => throw new NotImplementedException();
public override IFormCollection Form
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override Task<IFormCollection> ReadFormAsync(CancellationToken cancellationToken = default(CancellationToken))
{
throw new NotImplementedException();
}
}
public BTCPayServerTester PayTester
{
get; set;
}
public List<string> Stores { get; internal set; } = new List<string>();
public void Dispose()
{
foreach (var store in Stores)
{
Xunit.Assert.True(PayTester.StoreRepository.DeleteStore(store).GetAwaiter().GetResult());
}
if (PayTester != null)
PayTester.Dispose();
}

View File

@ -0,0 +1,250 @@
using System;
using System.IO;
using System.Threading.Tasks;
using BTCPayServer.Controllers;
using BTCPayServer.Models;
using BTCPayServer.Models.ServerViewModels;
using BTCPayServer.Storage.Models;
using BTCPayServer.Storage.Services.Providers.AzureBlobStorage.Configuration;
using BTCPayServer.Storage.Services.Providers.FileSystemStorage.Configuration;
using BTCPayServer.Storage.ViewModels;
using BTCPayServer.Tests.Logging;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Resources;
using Xunit;
using Xunit.Abstractions;
namespace BTCPayServer.Tests
{
public class StorageTests
{
public StorageTests(ITestOutputHelper helper)
{
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
Logs.LogProvider = new XUnitLogProvider(helper);
}
[Fact]
[Trait("Integration", "Integration")]
public async void CanConfigureStorage()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
var controller = tester.PayTester.GetController<ServerController>(user.UserId, user.StoreId);
// //For some reason, the tests cache something on circleci and this is set by default
// //Initially, there is no configuration, make sure we display the choices available to configure
// Assert.IsType<StorageSettings>(Assert.IsType<ViewResult>(await controller.Storage()).Model);
//
// //the file list should tell us it's not configured:
// var viewFilesViewModelInitial =
// Assert.IsType<ViewFilesViewModel>(Assert.IsType<ViewResult>(await controller.Files()).Model);
// Assert.False(viewFilesViewModelInitial.StorageConfigured);
//Once we select a provider, redirect to its view
var localResult = Assert
.IsType<RedirectToActionResult>(controller.Storage(new StorageSettings()
{
Provider = StorageProvider.FileSystem
}));
Assert.Equal(nameof(ServerController.StorageProvider), localResult.ActionName);
Assert.Equal(StorageProvider.FileSystem.ToString(), localResult.RouteValues["provider"]);
var AmazonS3result = Assert
.IsType<RedirectToActionResult>(controller.Storage(new StorageSettings()
{
Provider = StorageProvider.AmazonS3
}));
Assert.Equal(nameof(ServerController.StorageProvider), AmazonS3result.ActionName);
Assert.Equal(StorageProvider.AmazonS3.ToString(), AmazonS3result.RouteValues["provider"]);
var GoogleResult = Assert
.IsType<RedirectToActionResult>(controller.Storage(new StorageSettings()
{
Provider = StorageProvider.GoogleCloudStorage
}));
Assert.Equal(nameof(ServerController.StorageProvider), GoogleResult.ActionName);
Assert.Equal(StorageProvider.GoogleCloudStorage.ToString(), GoogleResult.RouteValues["provider"]);
var AzureResult = Assert
.IsType<RedirectToActionResult>(controller.Storage(new StorageSettings()
{
Provider = StorageProvider.AzureBlobStorage
}));
Assert.Equal(nameof(ServerController.StorageProvider), AzureResult.ActionName);
Assert.Equal(StorageProvider.AzureBlobStorage.ToString(), AzureResult.RouteValues["provider"]);
//Cool, we get redirected to the config pages
//Let's configure this stuff
//Let's try and cheat and go to an invalid storage provider config
Assert.Equal(nameof(Storage), (Assert
.IsType<RedirectToActionResult>(await controller.StorageProvider("I am not a real provider"))
.ActionName));
//ok no more messing around, let's configure this shit.
var fileSystemStorageConfiguration = Assert.IsType<FileSystemStorageConfiguration>(Assert
.IsType<ViewResult>(await controller.StorageProvider(StorageProvider.FileSystem.ToString()))
.Model);
//local file system does not need config, easy days!
Assert.IsType<ViewResult>(
await controller.EditFileSystemStorageProvider(fileSystemStorageConfiguration));
//ok cool, let's see if this got set right
var shouldBeRedirectingToLocalStorageConfigPage =
Assert.IsType<RedirectToActionResult>(await controller.Storage());
Assert.Equal(nameof(StorageProvider), shouldBeRedirectingToLocalStorageConfigPage.ActionName);
Assert.Equal(StorageProvider.FileSystem,
shouldBeRedirectingToLocalStorageConfigPage.RouteValues["provider"]);
//if we tell the settings page to force, it should allow us to select a new provider
Assert.IsType<ChooseStorageViewModel>(Assert.IsType<ViewResult>(await controller.Storage(true)).Model);
//awesome, now let's see if the files result says we're all set up
var viewFilesViewModel =
Assert.IsType<ViewFilesViewModel>(Assert.IsType<ViewResult>(await controller.Files()).Model);
Assert.True(viewFilesViewModel.StorageConfigured);
Assert.Empty(viewFilesViewModel.Files);
}
}
[Fact]
[Trait("Integration", "Integration")]
public async void CanUseLocalProviderFiles()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
var controller = tester.PayTester.GetController<ServerController>(user.UserId, user.StoreId);
var fileSystemStorageConfiguration = Assert.IsType<FileSystemStorageConfiguration>(Assert
.IsType<ViewResult>(await controller.StorageProvider(StorageProvider.FileSystem.ToString()))
.Model);
Assert.IsType<ViewResult>(
await controller.EditFileSystemStorageProvider(fileSystemStorageConfiguration));
var shouldBeRedirectingToLocalStorageConfigPage =
Assert.IsType<RedirectToActionResult>(await controller.Storage());
Assert.Equal(nameof(StorageProvider), shouldBeRedirectingToLocalStorageConfigPage.ActionName);
Assert.Equal(StorageProvider.FileSystem,
shouldBeRedirectingToLocalStorageConfigPage.RouteValues["provider"]);
await CanUploadRemoveFiles(controller);
}
}
[Fact]
[Trait("ExternalIntegration", "ExternalIntegration")]
public async Task CanUseAzureBlobStorage()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
var controller = tester.PayTester.GetController<ServerController>(user.UserId, user.StoreId);
var azureBlobStorageConfiguration = Assert.IsType<AzureBlobStorageConfiguration>(Assert
.IsType<ViewResult>(await controller.StorageProvider(StorageProvider.AzureBlobStorage.ToString()))
.Model);
azureBlobStorageConfiguration.ConnectionString = GetFromSecrets("AzureBlobStorageConnectionString");
azureBlobStorageConfiguration.ContainerName = "testscontainer";
Assert.IsType<ViewResult>(
await controller.EditAzureBlobStorageStorageProvider(azureBlobStorageConfiguration));
var shouldBeRedirectingToAzureStorageConfigPage =
Assert.IsType<RedirectToActionResult>(await controller.Storage());
Assert.Equal(nameof(StorageProvider), shouldBeRedirectingToAzureStorageConfigPage.ActionName);
Assert.Equal(StorageProvider.AzureBlobStorage,
shouldBeRedirectingToAzureStorageConfigPage.RouteValues["provider"]);
//seems like azure config worked, let's see if the conn string was actually saved
Assert.Equal(azureBlobStorageConfiguration.ConnectionString, Assert
.IsType<AzureBlobStorageConfiguration>(Assert
.IsType<ViewResult>(
await controller.StorageProvider(StorageProvider.AzureBlobStorage.ToString()))
.Model).ConnectionString);
await CanUploadRemoveFiles(controller);
}
}
private async Task CanUploadRemoveFiles(ServerController controller)
{
var filename = "uploadtestfile.txt";
var fileContent = "content";
File.WriteAllText(filename, fileContent);
var fileInfo = new FileInfo(filename);
var formFile = new FormFile(
new FileStream(filename, FileMode.OpenOrCreate),
0,
fileInfo.Length, fileInfo.Name, fileInfo.Name)
{
Headers = new HeaderDictionary()
};
formFile.ContentType = "text/plain";
formFile.ContentDisposition = $"form-data; name=\"file\"; filename=\"{fileInfo.Name}\"";
var uploadFormFileResult = Assert.IsType<RedirectToActionResult>(await controller.CreateFile(formFile));
Assert.True(uploadFormFileResult.RouteValues.ContainsKey("fileId"));
var fileId = uploadFormFileResult.RouteValues["fileId"].ToString();
Assert.Equal("Files", uploadFormFileResult.ActionName);
var viewFilesViewModel =
Assert.IsType<ViewFilesViewModel>(Assert.IsType<ViewResult>(await controller.Files(fileId)).Model);
Assert.NotEmpty(viewFilesViewModel.Files);
Assert.Equal(fileId, viewFilesViewModel.SelectedFileId);
Assert.NotEmpty(viewFilesViewModel.DirectFileUrl);
var net = new System.Net.WebClient();
var data = await net.DownloadStringTaskAsync(new Uri(viewFilesViewModel.DirectFileUrl));
Assert.Equal(fileContent, data);
Assert.Equal(StatusMessageModel.StatusSeverity.Success, new StatusMessageModel(Assert
.IsType<RedirectToActionResult>(await controller.DeleteFile(fileId))
.RouteValues["statusMessage"].ToString()).Severity);
viewFilesViewModel =
Assert.IsType<ViewFilesViewModel>(Assert.IsType<ViewResult>(await controller.Files(fileId)).Model);
Assert.Null(viewFilesViewModel.DirectFileUrl);
Assert.Null(viewFilesViewModel.SelectedFileId);
}
private static string GetFromSecrets(string key)
{
var connStr = Environment.GetEnvironmentVariable($"TESTS_{key}");
if (!string.IsNullOrEmpty(connStr) && connStr != "none")
return connStr;
var builder = new ConfigurationBuilder();
builder.AddUserSecrets("AB0AC1DD-9D26-485B-9416-56A33F268117");
var config = builder.Build();
var token = config[key];
Assert.False(token == null, $"{key} is not set.\n Run \"dotnet user-secrets set {key} <value>\"");
return token;
}
}
}

View File

@ -14,6 +14,10 @@ using Xunit;
using NBXplorer.DerivationStrategy;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Tests.Logging;
using BTCPayServer.Lightning;
using BTCPayServer.Lightning.CLightning;
using BTCPayServer.Data;
namespace BTCPayServer.Tests
{
@ -55,6 +59,21 @@ namespace BTCPayServer.Tests
CreateStoreAsync().GetAwaiter().GetResult();
}
public void SetNetworkFeeMode(NetworkFeeMode mode)
{
ModifyStore((store) =>
{
store.NetworkFeeMode = mode;
});
}
public void ModifyStore(Action<StoreViewModel> modify)
{
var storeController = GetController<StoresController>();
StoreViewModel store = (StoreViewModel)((ViewResult)storeController.UpdateStore()).Model;
modify(store);
storeController.UpdateStore(store).GetAwaiter().GetResult();
}
public T GetController<T>(bool setImplicitStore = true) where T : Controller
{
return parent.PayTester.GetController<T>(UserId, setImplicitStore ? StoreId : null);
@ -65,29 +84,28 @@ namespace BTCPayServer.Tests
var store = this.GetController<UserStoresController>();
await store.CreateStore(new CreateStoreViewModel() { Name = "Test Store" });
StoreId = store.CreatedStoreId;
parent.Stores.Add(StoreId);
}
public BTCPayNetwork SupportedNetwork { get; set; }
public void RegisterDerivationScheme(string crytoCode)
public WalletId RegisterDerivationScheme(string crytoCode, bool segwit = false)
{
RegisterDerivationSchemeAsync(crytoCode).GetAwaiter().GetResult();
return RegisterDerivationSchemeAsync(crytoCode, segwit).GetAwaiter().GetResult();
}
public async Task RegisterDerivationSchemeAsync(string cryptoCode)
public async Task<WalletId> RegisterDerivationSchemeAsync(string cryptoCode, bool segwit = false)
{
SupportedNetwork = parent.NetworkProvider.GetNetwork(cryptoCode);
var store = parent.PayTester.GetController<StoresController>(UserId, StoreId);
ExtKey = new ExtKey().GetWif(SupportedNetwork.NBitcoinNetwork);
DerivationScheme = new DerivationStrategyFactory(SupportedNetwork.NBitcoinNetwork).Parse(ExtKey.Neuter().ToString() + "-[legacy]");
var vm = (StoreViewModel)((ViewResult)store.UpdateStore()).Model;
vm.SpeedPolicy = SpeedPolicy.MediumSpeed;
await store.UpdateStore(vm);
DerivationScheme = new DerivationStrategyFactory(SupportedNetwork.NBitcoinNetwork).Parse(ExtKey.Neuter().ToString() + (segwit ? "" : "-[legacy]"));
await store.AddDerivationScheme(StoreId, new DerivationSchemeViewModel()
{
DerivationScheme = DerivationScheme.ToString(),
Confirmation = true
}, cryptoCode);
return new WalletId(StoreId, cryptoCode);
}
public DerivationStrategyBase DerivationScheme { get; set; }
@ -95,15 +113,18 @@ namespace BTCPayServer.Tests
private async Task RegisterAsync()
{
var account = parent.PayTester.GetController<AccountController>();
await account.Register(new RegisterViewModel()
RegisterDetails = new RegisterViewModel()
{
Email = Guid.NewGuid() + "@toto.com",
ConfirmPassword = "Kitten0@",
Password = "Kitten0@",
});
};
await account.Register(RegisterDetails);
UserId = account.RegisteredUserId;
}
public RegisterViewModel RegisterDetails{ get; set; }
public Bitpay BitPay
{
get; set;
@ -117,7 +138,7 @@ namespace BTCPayServer.Tests
{
get; set;
}
public void RegisterLightningNode(string cryptoCode, LightningConnectionType connectionType)
{
RegisterLightningNodeAsync(cryptoCode, connectionType).GetAwaiter().GetResult();
@ -126,15 +147,24 @@ namespace BTCPayServer.Tests
public async Task RegisterLightningNodeAsync(string cryptoCode, LightningConnectionType connectionType)
{
var storeController = this.GetController<StoresController>();
string connectionString = null;
if (connectionType == LightningConnectionType.Charge)
connectionString = "type=charge;server=" + parent.MerchantCharge.Client.Uri.AbsoluteUri;
else if (connectionType == LightningConnectionType.CLightning)
connectionString = "type=clightning;server=" + ((CLightningClient)parent.MerchantLightningD).Address.AbsoluteUri;
else if (connectionType == LightningConnectionType.LndREST)
connectionString = $"type=lnd-rest;server={parent.MerchantLnd.Swagger.BaseUrl};allowinsecure=true";
else
throw new NotSupportedException(connectionType.ToString());
await storeController.AddLightningNode(StoreId, new LightningNodeViewModel()
{
Url = connectionType == LightningConnectionType.Charge ? parent.MerchantCharge.Client.Uri.AbsoluteUri :
connectionType == LightningConnectionType.CLightning ? parent.MerchantLightningD.Address.AbsoluteUri
: throw new NotSupportedException(connectionType.ToString()),
ConnectionString = connectionString,
SkipPortTest = true
}, "save", "BTC");
if (storeController.ModelState.ErrorCount != 0)
Assert.False(true, storeController.ModelState.FirstOrDefault().Value.Errors[0].ErrorMessage);
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,111 @@
using System;
using System.Linq;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using NBitcoin.DataEncoders;
using Newtonsoft.Json.Linq;
using Xunit;
using System.IO;
namespace BTCPayServer.Tests
{
/// <summary>
/// This class hold easy to run utilities for dev time
/// </summary>
public class UtilitiesTests
{
/// <summary>
/// Download transifex transactions and put them in BTCPayServer\wwwroot\locales
/// </summary>
[Trait("Utilities", "Utilities")]
[Fact]
public async Task PullTransifexTranslations()
{
// 1. Generate an API Token on https://www.transifex.com/user/settings/api/
// 2. Run "dotnet user-secrets set TransifexAPIToken <youapitoken>"
var client = new TransifexClient(GetTransifexAPIToken());
var json = await client.GetTransifexAsync("https://api.transifex.com/organizations/btcpayserver/projects/btcpayserver/resources/enjson/");
var langs = new[] { "en" }.Concat(((JObject)json["stats"]).Properties().Select(n => n.Name)).ToArray();
var langsDir = Path.Combine(Services.LanguageService.TryGetSolutionDirectoryInfo().FullName, "BTCPayServer", "wwwroot", "locales");
JObject sourceLang = null;
Task.WaitAll(langs.Select(async l =>
{
bool isSourceLang = l == "en";
var j = await client.GetTransifexAsync($"https://www.transifex.com/api/2/project/btcpayserver/resource/enjson/translation/{l}/");
if(!isSourceLang)
{
while (sourceLang == null)
await Task.Delay(10);
}
var content = j["content"].Value<string>();
if (l == "ne_NP")
l = "np_NP";
if (l == "zh_CN")
l = "zh-SP";
if (l == "kk")
l = "kk-KZ";
var langCode = l.Replace("_", "-");
var langFile = Path.Combine(langsDir, langCode + ".json");
var jobj = JObject.Parse(content);
jobj["code"] = langCode;
if ((string)jobj["currentLanguage"] == "English" && !isSourceLang)
return; // Not translated
if ((string)jobj["currentLanguage"] == "disable")
return; // Not translated
jobj.AddFirst(new JProperty("NOTICE_WARN", "THIS CODE HAS BEEN AUTOMATICALLY GENERATED FROM TRANSIFEX, IF YOU WISH TO HELP TRANSLATION COME ON THE SLACK http://slack.btcpayserver.org TO REQUEST PERMISSION TO https://www.transifex.com/btcpayserver/btcpayserver/"));
if (isSourceLang)
{
sourceLang = jobj;
}
else
{
if(jobj["InvoiceExpired_Body_3"].Value<string>() == sourceLang["InvoiceExpired_Body_3"].Value<string>())
{
jobj["InvoiceExpired_Body_3"] = string.Empty;
}
}
content = jobj.ToString(Newtonsoft.Json.Formatting.Indented);
File.WriteAllText(Path.Combine(langsDir, langFile), content);
}).ToArray());
}
private static string GetTransifexAPIToken()
{
var builder = new ConfigurationBuilder();
builder.AddUserSecrets("AB0AC1DD-9D26-485B-9416-56A33F268117");
var config = builder.Build();
var token = config["TransifexAPIToken"];
Assert.False(token == null, "TransifexAPIToken is not set.\n 1.Generate an API Token on https://www.transifex.com/user/settings/api/ \n 2.Run \"dotnet user-secrets set TransifexAPIToken <youapitoken>\"");
return token;
}
}
public class TransifexClient
{
public TransifexClient(string apiToken)
{
Client = new HttpClient();
APIToken = apiToken;
}
public HttpClient Client { get; }
public string APIToken { get; }
public async Task<JObject> GetTransifexAsync(string uri)
{
var message = new HttpRequestMessage(HttpMethod.Get, uri);
message.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Encoders.Base64.EncodeData(Encoding.ASCII.GetBytes($"api:{APIToken}")));
message.Headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
var response = await Client.SendAsync(message);
return await response.Content.ReadAsAsync<JObject>();
}
}
}

View File

@ -1 +1,2 @@
docker exec -ti btcpayservertests_bitcoind_1 bitcoin-cli -datadir="/data" $args
$bitcoind_container_id=$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=bitcoind)
docker exec -ti $bitcoind_container_id bitcoin-cli -datadir="/data" $args

View File

@ -1,3 +1,4 @@
#!/bin/bash
docker exec -ti btcpayservertests_bitcoind_1 bitcoin-cli -datadir="/data" "$@"
bitcoind_container_id="$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=bitcoind)"
docker exec -ti "$bitcoind_container_id" bitcoin-cli -datadir="/data" "$@"

View File

@ -0,0 +1,3 @@
$bitcoind_container_id=$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=bitcoind)
$address=$(docker exec -ti $bitcoind_container_id bitcoin-cli -datadir="/data" getnewaddress)
docker exec -ti $bitcoind_container_id bitcoin-cli -datadir="/data" generatetoaddress $args $address

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:
@ -14,12 +14,17 @@ services:
TESTS_LTCRPCCONNECTION: server=http://litecoind:43782;ceiwHEbqWI83:DwubwWsoo3
TESTS_BTCNBXPLORERURL: http://nbxplorer:32838/
TESTS_LTCNBXPLORERURL: http://nbxplorer:32838/
TESTS_DB: "Postgres"
TESTS_POSTGRES: User ID=postgres;Host=postgres;Port=5432;Database=btcpayserver
TESTS_MYSQL: User ID=root;Host=mysql;Port=3306;Database=btcpayserver
TESTS_PORT: 80
TESTS_HOSTNAME: tests
TEST_MERCHANTLIGHTNINGD: "/etc/merchant_lightningd_datadir/lightning-rpc"
TEST_CUSTOMERLIGHTNINGD: "/etc/customer_lightningd_datadir/lightning-rpc"
TEST_MERCHANTCHARGE: http://api-token:foiewnccewuify@lightning-charged:9112/
TESTS_RUN_EXTERNAL_INTEGRATION: ${TESTS_RUN_EXTERNAL_INTEGRATION:-false}
TESTS_AzureBlobStorageConnectionString: ${TESTS_AzureBlobStorageConnectionString:-none}
TEST_MERCHANTLIGHTNINGD: "type=clightning;server=unix://etc/merchant_lightningd_datadir/lightning-rpc"
TEST_CUSTOMERLIGHTNINGD: "type=clightning;server=unix://etc/customer_lightningd_datadir/lightning-rpc"
TEST_MERCHANTCHARGE: "type=charge;server=http://lightning-charged:9112/;api-token=foiewnccewuify"
TEST_MERCHANTLND: "https://lnd:lnd@merchant_lnd:8080/"
TESTS_INCONTAINER: "true"
expose:
- "80"
@ -33,20 +38,41 @@ services:
# The dev container is not actually used, it is just handy to run `docker-compose up dev` to start all services
dev:
image: nicolasdorier/docker-bitcoin:0.16.0
image: btcpayserver/bitcoin:0.18.0
environment:
BITCOIN_NETWORK: regtest
BITCOIN_EXTRA_ARGS: |
regtest=1
deprecatedrpc=signrawtransaction
connect=bitcoind:39388
links:
- nbxplorer
- postgres
- mysql
- customer_lightningd
- merchant_lightningd
- lightning-charged
- customer_lnd
- merchant_lnd
devlnd:
image: btcpayserver/bitcoin:0.18.0
environment:
BITCOIN_NETWORK: regtest
BITCOIN_EXTRA_ARGS: |
deprecatedrpc=signrawtransaction
connect=bitcoind:39388
links:
- nbxplorer
- postgres
- mysql
- customer_lnd
- merchant_lnd
nbxplorer:
image: nicolasdorier/nbxplorer:1.0.2.3
image: nicolasdorier/nbxplorer:2.0.0.39
restart: unless-stopped
ports:
- "32838:32838"
expose:
@ -69,36 +95,47 @@ services:
- bitcoind
- litecoind
bitcoind:
image: nicolasdorier/docker-bitcoin:0.16.0
restart: unless-stopped
image: btcpayserver/bitcoin:0.18.0
environment:
BITCOIN_EXTRA_ARGS: |
BITCOIN_NETWORK: regtest
BITCOIN_EXTRA_ARGS: |-
rpcuser=ceiwHEbqWI83
rpcpassword=DwubwWsoo3
regtest=1
server=1
rpcport=43782
rpcbind=0.0.0.0:43782
port=39388
whitelist=0.0.0.0/0
zmqpubrawblock=tcp://0.0.0.0:28332
zmqpubrawtx=tcp://0.0.0.0:28333
deprecatedrpc=signrawtransaction
ports:
- "43782:43782"
expose:
- "43782" # RPC
- "39388" # P2P
- "28332" # ZMQ
- "28333" # ZMQ
volumes:
- "bitcoin_datadir:/data"
customer_lightningd:
image: nicolasdorier/clightning:0.0.0.12-dev
image: btcpayserver/lightning:v0.7.0-1-dev
stop_signal: SIGKILL
restart: unless-stopped
environment:
EXPOSE_TCP: "true"
LIGHTNINGD_OPT: |
bitcoin-datadir=/etc/bitcoin
bitcoin-rpcconnect=bitcoind
network=regtest
ipaddr=customer_lightningd
bind-addr=0.0.0.0
announce-addr=customer_lightningd
log-level=debug
dev-broadcast-interval=1000
dev-bitcoind-poll=1
ports:
- "30992:9835" # api port
expose:
@ -111,7 +148,8 @@ services:
- bitcoind
lightning-charged:
image: shesek/lightning-charge:0.3.9
image: shesek/lightning-charge:0.4.6-standalone
restart: unless-stopped
environment:
NETWORK: regtest
API_TOKEN: foiewnccewuify
@ -130,13 +168,15 @@ services:
- merchant_lightningd
merchant_lightningd:
image: nicolasdorier/clightning:0.0.0.11-dev
image: btcpayserver/lightning:v0.7.0-1-dev
stop_signal: SIGKILL
environment:
EXPOSE_TCP: "true"
LIGHTNINGD_OPT: |
bitcoin-datadir=/etc/bitcoin
bitcoin-rpcconnect=bitcoind
ipaddr=merchant_lightningd
bind-addr=0.0.0.0
announce-addr=merchant_lightningd
network=regtest
log-level=debug
dev-broadcast-interval=1000
@ -152,13 +192,13 @@ services:
- bitcoind
litecoind:
image: nicolasdorier/docker-litecoin:0.15.1
restart: unless-stopped
image: nicolasdorier/docker-litecoin:0.16.3
environment:
BITCOIN_EXTRA_ARGS: |
BITCOIN_EXTRA_ARGS: |-
rpcuser=ceiwHEbqWI83
rpcpassword=DwubwWsoo3
regtest=1
server=1
rpcport=43782
port=39388
whitelist=0.0.0.0/0
@ -174,9 +214,81 @@ services:
- "39372:5432"
expose:
- "5432"
mysql:
image: mysql:8.0.12
expose:
- "3306"
ports:
- "33036:3306"
environment:
- MYSQL_ALLOW_EMPTY_PASSWORD=yes
merchant_lnd:
image: btcpayserver/lnd:v0.6-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
LND_ENVIRONMENT: "regtest"
LND_EXPLORERURL: "http://nbxplorer:32838/"
LND_EXTRA_ARGS: |
restlisten=0.0.0.0:8080
rpclisten=127.0.0.1:10008
rpclisten=0.0.0.0:10009
bitcoin.node=bitcoind
bitcoind.rpchost=bitcoind:43782
bitcoind.zmqpubrawblock=tcp://bitcoind:28332
bitcoind.zmqpubrawtx=tcp://bitcoind:28333
externalip=merchant_lnd:9735
no-macaroons=1
debuglevel=debug
noseedbackup=1
trickledelay=1000
ports:
- "53280:8080"
expose:
- "9735"
volumes:
- "merchant_lnd_datadir:/data"
- "bitcoin_datadir:/deps/.bitcoin"
links:
- bitcoind
customer_lnd:
image: btcpayserver/lnd:v0.6-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
LND_ENVIRONMENT: "regtest"
LND_EXPLORERURL: "http://nbxplorer:32838/"
LND_EXTRA_ARGS: |
restlisten=0.0.0.0:8080
rpclisten=127.0.0.1:10008
rpclisten=0.0.0.0:10009
bitcoin.node=bitcoind
bitcoind.rpchost=bitcoind:43782
bitcoind.zmqpubrawblock=tcp://bitcoind:28332
bitcoind.zmqpubrawtx=tcp://bitcoind:28333
externalip=customer_lnd:10009
no-macaroons=1
debuglevel=debug
noseedbackup=1
trickledelay=1000
ports:
- "53281:8080"
expose:
- "8080"
- "10009"
volumes:
- "customer_lnd_datadir:/root/.lnd"
- "bitcoin_datadir:/deps/.bitcoin"
links:
- bitcoind
volumes:
bitcoin_datadir:
customer_lightningd_datadir:
merchant_lightningd_datadir:
lightning_charge_datadir:
customer_lnd_datadir:
merchant_lnd_datadir:

View File

@ -1 +1,2 @@
docker exec -ti btcpayservertests_customer_lightningd_1 lightning-cli $args
$customer_lightning_container_id=$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=customer_lightningd)
docker exec -ti $customer_lightning_container_id lightning-cli $args

View File

@ -1,3 +1,4 @@
#!/bin/bash
docker exec -ti btcpayservertests_customer_lightningd_1 lightning-cli "$@"
customer_lightning_container_id="$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=customer_lightningd)"
docker exec -ti $customer_lightning_container_id lightning-cli "$@"

View File

@ -0,0 +1,8 @@
#!/bin/sh
set -e
dotnet test --filter Fast=Fast --no-build
dotnet test --filter Integration=Integration --no-build -v n
if [[ "$TESTS_RUN_EXTERNAL_INTEGRATION" == "true" ]]; then
dotnet test --filter ExternalIntegration=ExternalIntegration --no-build -v n
fi

View File

@ -1 +1,2 @@
docker exec -ti btcpayservertests_litecoind_1 litecoin-cli -datadir="/data" $args
$litecoind_container_id=$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=litecoind)
docker exec -ti $litecoind_container_id litecoin-cli -datadir="/data" $args

View File

@ -1,3 +1,4 @@
#!/bin/bash
docker exec -ti btcpayservertests_litecoind_1 litecoin-cli -datadir="/data" "$@"
litecoind_container_id="$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=litecoind)"
docker exec -ti "$litecoind_container_id" litecoin-cli -datadir="/data" "$@"

View File

@ -1 +1,2 @@
docker exec -ti btcpayservertests_merchant_lightningd_1 lightning-cli $args
$merchant_lightning_container_id=$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=merchant_lightningd)
docker exec -ti $merchant_lightning_container_id lightning-cli $args

View File

@ -1,3 +1,4 @@
#!/bin/bash
docker exec -ti btcpayservertests_merchant_lightningd_1 lightning-cli "$@"
merchant_lightning_container_id="$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=merchant_lightningd)"
docker exec -ti $merchant_lightning_container_id lightning-cli "$@"

View File

@ -0,0 +1,5 @@
{
"parallelizeTestCollections": false,
"longRunningTestSeconds": 60,
"diagnosticMessages": true
}

View File

@ -8,10 +8,6 @@ namespace BTCPayServer.Authentication
{
public class BitTokenEntity
{
public string Facade
{
get; set;
}
public string Value
{
get; set;
@ -39,7 +35,6 @@ namespace BTCPayServer.Authentication
return new BitTokenEntity()
{
Label = Label,
Facade = Facade,
StoreId = StoreId,
PairingTime = PairingTime,
SIN = SIN,

View File

@ -11,11 +11,6 @@ namespace BTCPayServer.Authentication
get;
set;
}
public string Facade
{
get;
set;
}
public string Label
{
get;

View File

@ -1,5 +1,5 @@
using BTCPayServer.Data;
using DBreeze;
using DBriize;
using NBitcoin;
using NBitcoin.DataEncoders;
using Newtonsoft.Json;
@ -90,7 +90,6 @@ namespace BTCPayServer.Authentication
return new BitTokenEntity()
{
Label = data.Label,
Facade = data.Facade,
Value = data.Id,
SIN = data.SIN,
PairingTime = data.PairingTime,
@ -129,7 +128,6 @@ namespace BTCPayServer.Authentication
{
var pairingCode = await ctx.PairingCodes.FindAsync(pairingCodeEntity.Id);
pairingCode.Label = pairingCodeEntity.Label;
pairingCode.Facade = pairingCodeEntity.Facade;
await ctx.SaveChangesAsync();
return CreatePairingCodeEntity(pairingCode);
}
@ -178,7 +176,6 @@ namespace BTCPayServer.Authentication
{
Id = pairingCode.TokenValue,
PairingTime = DateTime.UtcNow,
Facade = pairingCode.Facade,
Label = pairingCode.Label,
StoreDataId = pairingCode.StoreDataId,
SIN = pairingCode.SIN
@ -213,7 +210,6 @@ namespace BTCPayServer.Authentication
return null;
return new PairingCodeEntity()
{
Facade = data.Facade,
Id = data.Id,
Label = data.Label,
Expiration = data.Expiration,
@ -242,6 +238,8 @@ namespace BTCPayServer.Authentication
using (var ctx = _Factory.CreateContext())
{
var token = await ctx.PairedSINData.FindAsync(tokenId);
if (token == null)
return null;
return CreateTokenEntity(token);
}
}

View File

@ -44,6 +44,8 @@ namespace BTCPayServer
public string CryptoCode { get; internal set; }
public string BlockExplorerLink { get; internal set; }
public string UriScheme { get; internal set; }
public Money MinFee { get; internal set; }
public string DisplayName { get; set; }
[Obsolete("Should not be needed")]
public bool IsBTC

View File

@ -17,12 +17,13 @@ namespace BTCPayServer
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Bitcoin",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://www.smartbit.com.au/tx/{0}" : "https://testnet.smartbit.com.au/tx/{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "bitcoin",
CryptoImagePath = "imlegacy/bitcoin-symbol.svg",
LightningImagePath = "imlegacy/btc-lightning.svg",
CryptoImagePath = "imlegacy/bitcoin.svg",
LightningImagePath = "imlegacy/bitcoin-lightning.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("0'") : new KeyPath("1'")
});

View File

@ -0,0 +1,30 @@
using NBitcoin;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
public void InitBitcoinGold()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("BTG");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "BGold",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://explorer.bitcoingold.org/insight/tx/{0}/" : "https://test-explorer.bitcoingold.org/insight/tx/{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "bitcoingold",
DefaultRateRules = new[]
{
"BTG_X = BTG_BTC * BTC_X",
"BTG_BTC = bitfinex(BTG_BTC)",
},
CryptoImagePath = "imlegacy/btg.svg",
LightningImagePath = "imlegacy/btg-lightning.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("156'") : new KeyPath("1'")
});
}
}
}

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

@ -0,0 +1,36 @@
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 InitBitcore()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("BTX");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Bitcore",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://insight.bitcore.cc/tx/{0}" : "https://insight.bitcore.cc/tx/{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "bitcore",
DefaultRateRules = new[]
{
"BTX_X = BTX_BTC * BTC_X",
"BTX_BTC = hitbtc(BTX_BTC)"
},
CryptoImagePath = "imlegacy/bitcore.svg",
LightningImagePath = "imlegacy/bitcore-lightning.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("160'") : new KeyPath("1'")
});
}
}
}

View File

@ -0,0 +1,35 @@
using NBitcoin;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
public void InitDash()
{
//not needed: NBitcoin.Altcoins.Dash.Instance.EnsureRegistered();
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("DASH");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Dash",
BlockExplorerLink = NetworkType == NetworkType.Mainnet
? "https://insight.dash.org/insight/tx/{0}"
: "https://testnet-insight.dashevo.org/insight/tx/{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "dash",
DefaultRateRules = new[]
{
"DASH_X = DASH_BTC * BTC_X",
"DASH_BTC = bittrex(DASH_BTC)"
},
CryptoImagePath = "imlegacy/dash.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
//https://github.com/satoshilabs/slips/blob/master/slip-0044.md
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("5'")
: new KeyPath("1'"),
MinFee = Money.Satoshis(1m)
});
}
}
}

View File

@ -16,6 +16,7 @@ namespace BTCPayServer
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Dogecoin",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://dogechain.info/tx/{0}" : "https://dogechain.info/tx/{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
@ -27,7 +28,8 @@ namespace BTCPayServer
},
CryptoImagePath = "imlegacy/dogecoin.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("3'") : new KeyPath("1'")
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("3'") : new KeyPath("1'"),
MinFee = Money.Coins(1m)
});
}
}

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 InitFeathercoin()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("FTC");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Feathercoin",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://explorer.feathercoin.com/tx/{0}" : "https://explorer.feathercoin.com/tx/{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "feathercoin",
DefaultRateRules = new[]
{
"FTC_X = FTC_BTC * BTC_X",
"FTC_BTC = bittrex(FTC_BTC)"
},
CryptoImagePath = "imlegacy/feathercoin.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("8'") : new KeyPath("1'")
});
}
}
}

View File

@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NBitcoin;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
public void InitGroestlcoin()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("GRS");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Groestlcoin",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://chainz.cryptoid.info/grs/tx.dws?{0}.htm" : "https://chainz.cryptoid.info/grs-test/tx.dws?{0}.htm",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "groestlcoin",
DefaultRateRules = new[]
{
"GRS_X = GRS_BTC * BTC_X",
"GRS_BTC = bittrex(GRS_BTC)"
},
CryptoImagePath = "imlegacy/groestlcoin.png",
LightningImagePath = "imlegacy/groestlcoin-lightning.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("17'") : new KeyPath("1'")
});
}
}
}

View File

@ -16,12 +16,13 @@ namespace BTCPayServer
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Litecoin",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://live.blockcypher.com/ltc/tx/{0}/" : "http://explorer.litecointools.com/tx/{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "litecoin",
CryptoImagePath = "imlegacy/litecoin-symbol.svg",
LightningImagePath = "imlegacy/ltc-lightning.svg",
CryptoImagePath = "imlegacy/litecoin.svg",
LightningImagePath = "imlegacy/litecoin-lightning.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("2'") : new KeyPath("1'")
});

View File

@ -0,0 +1,36 @@
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 InitMonacoin()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("MONA");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Monacoin",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://mona.insight.monaco-ex.org/insight/tx/{0}" : "https://testnet-mona.insight.monaco-ex.org/insight/tx/{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "monacoin",
DefaultRateRules = new[]
{
"MONA_X = MONA_BTC * BTC_X",
"MONA_BTC = bittrex(MONA_BTC)"
},
CryptoImagePath = "imlegacy/monacoin.png",
LightningImagePath = "imlegacy/mona-lightning.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("22'") : new KeyPath("1'")
});
}
}
}

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 InitPolis()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("POLIS");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Polis",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://insight.polispay.org/tx/{0}" : "https://insight.polispay.org/tx/{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "polis",
DefaultRateRules = new[]
{
"POLIS_X = POLIS_BTC * BTC_X",
"POLIS_BTC = cryptopia(POLIS_BTC)"
},
CryptoImagePath = "imlegacy/polis.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("1997'") : new KeyPath("1'")
});
}
}
}

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 InitUfo()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("UFO");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Ufo",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://chainz.cryptoid.info/ufo/tx.dws?{0}" : "https://chainz.cryptoid.info/ufo/tx.dws?{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "ufo",
DefaultRateRules = new[]
{
"UFO_X = UFO_BTC * BTC_X",
"UFO_BTC = coinexchange(UFO_BTC)"
},
CryptoImagePath = "imlegacy/ufo.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("202'") : new KeyPath("1'")
});
}
}
}

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 InitViacoin()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("VIA");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Viacoin",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://explorer.viacoin.org/tx/{0}" : "https://explorer.viacoin.org/tx/{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "viacoin",
DefaultRateRules = new[]
{
"VIA_X = VIA_BTC * BTC_X",
"VIA_BTC = bittrex(VIA_BTC)"
},
CryptoImagePath = "imlegacy/viacoin.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("14'") : new KeyPath("1'")
});
}
}
}

View File

@ -47,7 +47,19 @@ namespace BTCPayServer
NetworkType = networkType;
InitBitcoin();
InitLitecoin();
InitBitcore();
InitDogecoin();
InitBitcoinGold();
InitMonacoin();
InitDash();
InitFeathercoin();
InitGroestlcoin();
InitViacoin();
// Disabled because of https://twitter.com/Cryptopia_NZ/status/1085084168852291586
//InitPolis();
//InitBitcoinplus();
//InitUfo();
}
/// <summary>

View File

@ -1,10 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<Version>1.0.2.9</Version>
<TargetFramework>netcoreapp2.1</TargetFramework>
<Version>1.0.3.96</Version>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup>
<PropertyGroup>
<LangVersion>7.3</LangVersion>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Build\dockerfiles\**" />
<Compile Remove="wwwroot\bundles\jqueryvalidate\**" />
@ -27,41 +30,55 @@
<None Remove="Currencies.txt" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="bundleconfig.json" />
<EmbeddedResource Include="Currencies.txt" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BuildBundlerMinifier" Version="2.6.375" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.4.1" />
<PackageReference Include="Hangfire" Version="1.6.19" />
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.2" />
<PackageReference Include="Hangfire.PostgreSql" Version="1.4.8.1" />
<PackageReference Include="LedgerWallet" Version="1.0.1.36" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="2.6.1" />
<PackageReference Include="Meziantou.AspNetCore.BundleTagHelpers" Version="1.0.1" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.18" />
<PackageReference Include="BuildBundlerMinifier" Version="2.9.406" />
<PackageReference Include="BundlerMinifier.Core" Version="2.9.406" />
<PackageReference Include="BundlerMinifier.TagHelpers" Version="2.9.406" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.5.3" />
<PackageReference Include="HtmlSanitizer" Version="4.0.207" />
<PackageReference Include="LedgerWallet" Version="2.0.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="2.6.0" />
<PackageReference Include="NBitcoin" Version="4.1.1.4" />
<PackageReference Include="NBitpayClient" Version="1.0.0.18" />
<PackageReference Include="DBreeze" Version="1.87.0" />
<PackageReference Include="NBXplorer.Client" Version="1.0.2.4" />
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.1" />
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.2" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.13" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.0.1" />
<PackageReference Include="System.ValueTuple" Version="4.4.0" />
<PackageReference Include="System.Xml.XmlSerializer" Version="4.0.11" />
<PackageReference Include="Text.Analyzers" Version="2.6.0" />
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="2.6.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="NBitcoin" Version="4.1.2.14" />
<PackageReference Include="NBitpayClient" Version="1.0.0.34" />
<PackageReference Include="DBriize" Version="1.0.0.4" />
<PackageReference Include="NBXplorer.Client" Version="2.0.0.11" />
<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" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.18" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.1.2" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="2.1.2" />
<PackageReference Include="Serilog" Version="2.7.1" />
<PackageReference Include="Serilog.AspNetCore" Version="2.1.1" />
<PackageReference Include="Serilog.Sinks.File" Version="4.0.0" />
<PackageReference Include="SSH.NET" Version="2016.1.0" />
<PackageReference Include="Text.Analyzers" Version="2.6.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.2" PrivateAssets="All" />
<PackageReference Include="YamlDotNet" Version="4.3.1" />
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.9" />
<PackageReference Include="TwentyTwenty.Storage" Version="2.10.1" />
<PackageReference Include="TwentyTwenty.Storage.Amazon" Version="2.10.1" />
<PackageReference Include="TwentyTwenty.Storage.Azure" Version="2.10.1" />
<PackageReference Include="TwentyTwenty.Storage.Google" Version="2.10.1" />
<PackageReference Include="TwentyTwenty.Storage.Local" Version="2.10.1" />
<PackageReference Include="U2F.Core" Version="2.0.1" />
<PackageReference Include="YamlDotNet" Version="5.2.1" />
</ItemGroup>
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0" />
<DotNetCliToolReference Include="Microsoft.Extensions.SecretManager.Tools" Version="2.0.0" />
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.0" />
</ItemGroup>
@ -113,12 +130,74 @@
<ItemGroup>
<Folder Include="Build\" />
<Folder Include="U2F\Services" />
<Folder Include="wwwroot\vendor\clipboard.js\" />
<Folder Include="wwwroot\vendor\highlightjs\" />
<Folder Include="wwwroot\vendor\summernote" />
<Folder Include="wwwroot\vendor\u2f" />
</ItemGroup>
<ItemGroup>
<None Update="devtest.pfx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<Content Update="Views\Apps\_ViewImports.cshtml">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Home\BitpayTranslator.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\LightningChargeServices.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\LightningWalletServices.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\P2PService.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\SSHService.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Stores\ShowToken.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Stores\PayButtonEnable.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Stores\PayButton.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Public\PayButtonHandle.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\LndServices.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\Maintenance.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\Services.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\ListWallets.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\WalletRescan.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\WalletSendLedger.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\WalletTransactions.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\_Nav.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\_ViewImports.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\_ViewStart.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
</ItemGroup>
</Project>

View File

@ -11,6 +11,11 @@ using StandardConfiguration;
using Microsoft.Extensions.Configuration;
using NBXplorer;
using BTCPayServer.Payments.Lightning;
using Renci.SshNet;
using NBitcoin.DataEncoders;
using BTCPayServer.SSH;
using BTCPayServer.Lightning;
using Serilog.Events;
namespace BTCPayServer.Configuration
{
@ -32,16 +37,18 @@ namespace BTCPayServer.Configuration
get;
private set;
}
public string LogFile
{
get;
private set;
}
public string DataDir
{
get;
private set;
}
public List<IPEndPoint> Listen
{
get;
set;
}
public EndPoint SocksEndpoint { get; set; }
public List<NBXplorerConnectionSetting> NBXplorerConnectionSettings
{
@ -49,6 +56,22 @@ namespace BTCPayServer.Configuration
set;
} = new List<NBXplorerConnectionSetting>();
public bool DisableRegistration
{
get;
private set;
}
public static string GetDebugLog(IConfiguration configuration)
{
return configuration.GetValue<string>("debuglog", null);
}
public static LogEventLevel GetDebugLogLevel(IConfiguration configuration)
{
var raw = configuration.GetValue("debugloglevel", nameof(LogEventLevel.Debug));
return (LogEventLevel)Enum.Parse(typeof(LogEventLevel), raw, true);
}
public void LoadArgs(IConfiguration conf)
{
NetworkType = DefaultConfiguration.GetNetworkType(conf);
@ -74,42 +97,173 @@ namespace BTCPayServer.Configuration
setting.ExplorerUri = conf.GetOrDefault<Uri>($"{net.CryptoCode}.explorer.url", net.NBXplorerNetwork.DefaultSettings.DefaultUrl);
setting.CookieFile = conf.GetOrDefault<string>($"{net.CryptoCode}.explorer.cookiefile", net.NBXplorerNetwork.DefaultSettings.DefaultCookieFile);
NBXplorerConnectionSettings.Add(setting);
var lightning = conf.GetOrDefault<string>($"{net.CryptoCode}.lightning", string.Empty);
if(lightning.Length != 0)
{
if(!LightningConnectionString.TryParse(lightning, out var connectionString, out var error))
var lightning = conf.GetOrDefault<string>($"{net.CryptoCode}.lightning", string.Empty);
if (lightning.Length != 0)
{
throw new ConfigException($"Invalid setting {net.CryptoCode}.lightning, you need to pass either " +
$"the absolute path to the unix socket of a running CLightning instance (eg. /root/.lightning/lightning-rpc), " +
$"or the url to a charge server with crendetials (eg. https://apitoken@API_TOKEN_SECRET:charge.example.com/)");
if (!LightningConnectionString.TryParse(lightning, true, out var connectionString, out var error))
{
Logs.Configuration.LogWarning($"Invalid setting {net.CryptoCode}.lightning, " + 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 +
$"Error: {error}" + Environment.NewLine +
"This service will not be exposed through BTCPay Server");
}
else
{
if (connectionString.IsLegacy)
{
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);
}
}
InternalLightningByCryptoCode.Add(net.CryptoCode, connectionString);
}
ExternalServices.Load(net.CryptoCode, conf);
}
Logs.Configuration.LogInformation("Supported chains: " + String.Join(',', supportedChains.ToArray()));
var services = conf.GetOrDefault<string>("externalservices", null);
if (services != null)
{
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))))
{
if (Uri.TryCreate(service.Link, UriKind.RelativeOrAbsolute, out var uri))
OtherExternalServices.AddOrReplace(service.Name, uri);
}
}
PostgresConnectionString = conf.GetOrDefault<string>("postgres", null);
MySQLConnectionString = conf.GetOrDefault<string>("mysql", null);
BundleJsCss = conf.GetOrDefault<bool>("bundlejscss", true);
ExternalUrl = conf.GetOrDefault<Uri>("externalurl", null);
TorrcFile = conf.GetOrDefault<string>("torrcfile", null);
var socksEndpointString = conf.GetOrDefault<string>("socksendpoint", null);
if(!string.IsNullOrEmpty(socksEndpointString))
{
if (!Utils.TryParseEndpoint(socksEndpointString, 9050, out var endpoint))
throw new ConfigException("Invalid value for socksendpoint");
SocksEndpoint = endpoint;
}
var sshSettings = ParseSSHConfiguration(conf);
if ((!string.IsNullOrEmpty(sshSettings.Password) || !string.IsNullOrEmpty(sshSettings.KeyFile)) && !string.IsNullOrEmpty(sshSettings.Server))
{
int waitTime = 0;
while (!string.IsNullOrEmpty(sshSettings.KeyFile) && !File.Exists(sshSettings.KeyFile))
{
if (waitTime++ < 5)
System.Threading.Thread.Sleep(1000);
else
throw new ConfigException($"sshkeyfile does not exist");
}
if (sshSettings.Port > ushort.MaxValue ||
sshSettings.Port < ushort.MinValue)
throw new ConfigException($"ssh port is invalid");
if (!string.IsNullOrEmpty(sshSettings.Password) && !string.IsNullOrEmpty(sshSettings.KeyFile))
throw new ConfigException($"sshpassword or sshkeyfile should be provided, but not both");
try
{
sshSettings.CreateConnectionInfo();
}
catch
{
throw new ConfigException($"sshkeyfilepassword is invalid");
}
SSHSettings = sshSettings;
}
var fingerPrints = conf.GetOrDefault<string>("sshtrustedfingerprints", "");
if (!string.IsNullOrEmpty(fingerPrints))
{
foreach (var fingerprint in fingerPrints.Split(';', StringSplitOptions.RemoveEmptyEntries))
{
if (!SSHFingerprint.TryParse(fingerprint, out var f))
throw new ConfigException($"Invalid ssh fingerprint format {fingerprint}");
TrustedFingerprints.Add(f);
}
}
RootPath = conf.GetOrDefault<string>("rootpath", "/");
if (!RootPath.StartsWith("/", StringComparison.InvariantCultureIgnoreCase))
RootPath = "/" + RootPath;
var old = conf.GetOrDefault<Uri>("internallightningnode", null);
if(old != null)
throw new ConfigException($"internallightningnode should not be used anymore, use btclightning instead");
if (old != null)
throw new ConfigException($"internallightningnode is deprecated and should not be used anymore, use btclightning instead");
LogFile = GetDebugLog(conf);
if (!string.IsNullOrEmpty(LogFile))
{
Logs.Configuration.LogInformation("LogFile: " + LogFile);
Logs.Configuration.LogInformation("Log Level: " + GetDebugLogLevel(conf));
}
DisableRegistration = conf.GetOrDefault<bool>("disable-registration", true);
}
private SSHSettings ParseSSHConfiguration(IConfiguration conf)
{
var settings = new SSHSettings();
settings.Server = conf.GetOrDefault<string>("sshconnection", null);
if (settings.Server != null)
{
var parts = settings.Server.Split(':');
if (parts.Length == 2 && int.TryParse(parts[1], out int port))
{
settings.Port = port;
settings.Server = parts[0];
}
else
{
settings.Port = 22;
}
parts = settings.Server.Split('@');
if (parts.Length == 2)
{
settings.Username = parts[0];
settings.Server = parts[1];
}
else
{
settings.Username = "root";
}
}
settings.Password = conf.GetOrDefault<string>("sshpassword", "");
settings.KeyFile = conf.GetOrDefault<string>("sshkeyfile", "");
settings.KeyFilePassword = conf.GetOrDefault<string>("sshkeyfilepassword", "");
return settings;
}
internal bool IsTrustedFingerprint(byte[] fingerPrint, byte[] hostKey)
{
return TrustedFingerprints.Any(f => f.Match(fingerPrint, hostKey));
}
public string RootPath { get; set; }
public Dictionary<string, LightningConnectionString> InternalLightningByCryptoCode { get; set; } = new Dictionary<string, LightningConnectionString>();
public Dictionary<string, Uri> OtherExternalServices { get; set; } = new Dictionary<string, Uri>();
public ExternalServices ExternalServices { get; set; } = new ExternalServices();
public BTCPayNetworkProvider NetworkProvider { get; set; }
public string PostgresConnectionString
{
get;
set;
}
public Uri ExternalUrl
public string MySQLConnectionString
{
get;
set;
@ -119,14 +273,12 @@ namespace BTCPayServer.Configuration
get;
set;
}
internal string GetRootUri()
public List<SSHFingerprint> TrustedFingerprints { get; set; } = new List<SSHFingerprint>();
public SSHSettings SSHSettings
{
if (ExternalUrl == null)
return null;
UriBuilder builder = new UriBuilder(ExternalUrl);
builder.Path = RootPath;
return builder.ToString();
get;
set;
}
public string TorrcFile { get; set; }
}
}

View File

@ -6,6 +6,7 @@ using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Microsoft.Extensions.Primitives;
using NBitcoin;
namespace BTCPayServer.Configuration
{
@ -37,6 +38,8 @@ namespace BTCPayServer.Configuration
}
else if (typeof(T) == typeof(string))
return (T)(object)str;
else if (typeof(T) == typeof(IPAddress))
return (T)(object)IPAddress.Parse(str);
else if (typeof(T) == typeof(IPEndPoint))
{
var separator = str.LastIndexOf(":", StringComparison.InvariantCulture);

View File

@ -27,19 +27,33 @@ namespace BTCPayServer.Configuration
};
app.HelpOption("-? | -h | --help");
app.Option("-n | --network", $"Set the network among (mainnet,testnet,regtest) (default: mainnet)", CommandOptionType.SingleValue);
app.Option("--testnet | -testnet", $"Use testnet (Deprecated, use --network instead)", CommandOptionType.BoolValue);
app.Option("--regtest | -regtest", $"Use regtest (Deprecated, use --network instead)", CommandOptionType.BoolValue);
app.Option("--chains | -c", $"Chains to support comma separated (default: btc, available: {chains})", CommandOptionType.SingleValue);
app.Option("--postgres", $"Connection string to postgres database (default: sqlite is used)", CommandOptionType.SingleValue);
app.Option("--externalurl", $"The expected external url of this service, to use if BTCPay is behind a reverse proxy (default: empty, use the incoming HTTP request to figure out)", CommandOptionType.SingleValue);
app.Option("--bundlejscss", $"Bundle javascript and css files for better performance (default: true)", CommandOptionType.SingleValue);
app.Option("--testnet | -testnet", $"Use testnet (deprecated, use --network instead)", CommandOptionType.BoolValue);
app.Option("--regtest | -regtest", $"Use regtest (deprecated, use --network instead)", CommandOptionType.BoolValue);
app.Option("--chains | -c", $"Chains to support as a comma separated (default: btc; available: {chains})", CommandOptionType.SingleValue);
app.Option("--postgres", $"Connection string to a PostgreSQL database (default: SQLite)", CommandOptionType.SingleValue);
app.Option("--mysql", $"Connection string to a MySQL database (default: SQLite)", CommandOptionType.SingleValue);
app.Option("--externalservices", $"Links added to external services inside Server Settings / Services under the format service1:path2;service2:path2.(default: empty)", CommandOptionType.SingleValue);
app.Option("--bundlejscss", $"Bundle JavaScript and CSS files for better performance (default: true)", CommandOptionType.SingleValue);
app.Option("--rootpath", "The root path in the URL to access BTCPay (default: /)", CommandOptionType.SingleValue);
app.Option("--sshconnection", "SSH server to manage BTCPay under the form user@server:port (default: root@externalhost or empty)", CommandOptionType.SingleValue);
app.Option("--sshpassword", "SSH password to manage BTCPay (default: empty)", CommandOptionType.SingleValue);
app.Option("--sshkeyfile", "SSH private key file to manage BTCPay (default: empty)", CommandOptionType.SingleValue);
app.Option("--sshkeyfilepassword", "Password of the SSH keyfile (default: empty)", CommandOptionType.SingleValue);
app.Option("--sshtrustedfingerprints", "SSH Host public key fingerprint or sha256 (default: empty, it will allow untrusted connections)", CommandOptionType.SingleValue);
app.Option("--torrcfile", "Path to torrc file containing hidden services directories (default: empty)", CommandOptionType.SingleValue);
app.Option("--socksendpoint", "Socks endpoint to connect to onion urls (default: empty)", CommandOptionType.SingleValue);
app.Option("--debuglog", "A rolling log file for debug messages.", CommandOptionType.SingleValue);
app.Option("--debugloglevel", "The severity you log (default:information)", CommandOptionType.SingleValue);
app.Option("--disable-registration", "Disables new user registrations (default:true)", CommandOptionType.SingleValue);
foreach (var network in provider.GetAll())
{
var crypto = network.CryptoCode.ToLowerInvariant();
app.Option($"--{crypto}explorerurl", $"Url of the NBxplorer for {network.CryptoCode} (default: {network.NBXplorerNetwork.DefaultSettings.DefaultUrl})", CommandOptionType.SingleValue);
app.Option($"--{crypto}explorerurl", $"URL of the NBXplorer for {network.CryptoCode} (default: {network.NBXplorerNetwork.DefaultSettings.DefaultUrl})", CommandOptionType.SingleValue);
app.Option($"--{crypto}explorercookiefile", $"Path to the cookie file (default: {network.NBXplorerNetwork.DefaultSettings.DefaultCookieFile})", CommandOptionType.SingleValue);
app.Option($"--{crypto}lightning", $"Easy configuration of lightning for the server adnistrator: Must be a unix socket of CLightning (lightning-rpc) or URL to a charge server (default: empty)", CommandOptionType.SingleValue);
app.Option($"--{crypto}lightning", $"Easy configuration of lightning for the server administrator: Must be a UNIX socket of c-lightning (lightning-rpc) or URL to a charge server (default: empty)", CommandOptionType.SingleValue);
app.Option($"--{crypto}externallndgrpc", $"The LND gRPC configuration BTCPay will expose to easily connect to the internal lnd wallet from Zap wallet (default: empty)", CommandOptionType.SingleValue);
app.Option($"--{crypto}externalspark", $"Show spark information in Server settings / Server. The connection string to spark server (default: empty)", CommandOptionType.SingleValue);
app.Option($"--{crypto}externalcharge", $"Show lightning charge information in Server settings/Server. The connection string to charge server (default: empty)", CommandOptionType.SingleValue);
}
return app;
}
@ -98,9 +112,12 @@ namespace BTCPayServer.Configuration
builder.AppendLine("### Server settings ###");
builder.AppendLine("#port=" + defaultSettings.DefaultPort);
builder.AppendLine("#bind=127.0.0.1");
builder.AppendLine("#httpscertificatefilepath=devtest.pfx");
builder.AppendLine("#httpscertificatefilepassword=toto");
builder.AppendLine();
builder.AppendLine("### Database ###");
builder.AppendLine("#postgres=User ID=root;Password=myPassword;Host=localhost;Port=5432;Database=myDataBase;");
builder.AppendLine("#mysql=User ID=root;Password=myPassword;Host=localhost;Port=3306;Database=myDataBase;");
builder.AppendLine();
builder.AppendLine("### NBXplorer settings ###");
foreach (var n in new BTCPayNetworkProvider(networkType).GetAll())

View File

@ -0,0 +1,200 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Controllers;
namespace BTCPayServer.Configuration
{
public class ExternalConnectionString
{
public ExternalConnectionString()
{
}
public ExternalConnectionString(Uri server)
{
Server = server;
}
public Uri Server { get; set; }
public byte[] Macaroon { get; set; }
public Macaroons Macaroons { get; set; }
public string MacaroonFilePath { get; set; }
public string CertificateThumbprint { get; set; }
public string MacaroonDirectoryPath { get; set; }
public string APIToken { get; set; }
public string CookieFilePath { get; set; }
public string AccessKey { get; set; }
/// <summary>
/// Return a connectionString which does not depends on external resources or information like relative path or file path
/// </summary>
/// <returns></returns>
public async Task<ExternalConnectionString> Expand(Uri absoluteUrlBase, ExternalServiceTypes serviceType)
{
var connectionString = this.Clone();
// Transform relative URI into absolute URI
var serviceUri = connectionString.Server.IsAbsoluteUri ? connectionString.Server : ToRelative(absoluteUrlBase, connectionString.Server.ToString());
if (!serviceUri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase) &&
!serviceUri.DnsSafeHost.EndsWith(".onion", StringComparison.OrdinalIgnoreCase))
{
throw new System.Security.SecurityException($"Insecure transport protocol to access this service, please use HTTPS or TOR");
}
connectionString.Server = serviceUri;
if (serviceType == ExternalServiceTypes.LNDGRPC || serviceType == ExternalServiceTypes.LNDRest)
{
// Read the MacaroonDirectory
if (connectionString.MacaroonDirectoryPath != null)
{
try
{
connectionString.Macaroons = await Macaroons.GetFromDirectoryAsync(connectionString.MacaroonDirectoryPath);
connectionString.MacaroonDirectoryPath = null;
}
catch (Exception ex)
{
throw new System.IO.DirectoryNotFoundException("Macaroon directory path not found", ex);
}
}
// Read the MacaroonFilePath
if (connectionString.MacaroonFilePath != null)
{
try
{
connectionString.Macaroon = await System.IO.File.ReadAllBytesAsync(connectionString.MacaroonFilePath);
connectionString.MacaroonFilePath = null;
}
catch (Exception ex)
{
throw new System.IO.FileNotFoundException("Macaroon not found", ex);
}
}
}
if (serviceType == ExternalServiceTypes.Charge || serviceType == ExternalServiceTypes.RTL || serviceType == ExternalServiceTypes.Spark)
{
// Read access key from cookie file
if (connectionString.CookieFilePath != null)
{
string cookieFileContent = null;
bool isFake = false;
try
{
cookieFileContent = await System.IO.File.ReadAllTextAsync(connectionString.CookieFilePath);
isFake = connectionString.CookieFilePath == "fake";
connectionString.CookieFilePath = null;
}
catch (Exception ex)
{
throw new System.IO.FileNotFoundException("Cookie file path not found", ex);
}
if (serviceType == ExternalServiceTypes.RTL)
{
connectionString.AccessKey = cookieFileContent;
}
else if (serviceType == ExternalServiceTypes.Spark)
{
var cookie = (isFake ? "fake:fake:fake" // Hacks for testing
: cookieFileContent).Split(':');
if (cookie.Length >= 3)
{
connectionString.AccessKey = cookie[2];
}
else
{
throw new FormatException("Invalid cookiefile format");
}
}
else if (serviceType == ExternalServiceTypes.Charge)
{
connectionString.APIToken = isFake ? "fake" : cookieFileContent;
}
}
}
return connectionString;
}
private Uri ToRelative(Uri absoluteUrlBase, string path)
{
if (path.StartsWith('/'))
path = path.Substring(1);
return new Uri($"{absoluteUrlBase.AbsoluteUri.WithTrailingSlash()}{path}", UriKind.Absolute);
}
public ExternalConnectionString Clone()
{
return new ExternalConnectionString()
{
MacaroonFilePath = MacaroonFilePath,
CertificateThumbprint = CertificateThumbprint,
Macaroon = Macaroon,
MacaroonDirectoryPath = MacaroonDirectoryPath,
Server = Server,
APIToken = APIToken,
CookieFilePath = CookieFilePath,
AccessKey = AccessKey,
Macaroons = Macaroons?.Clone()
};
}
public static bool TryParse(string str, out ExternalConnectionString result, out string error)
{
if (str == null)
throw new ArgumentNullException(nameof(str));
error = null;
result = null;
var resultTemp = new ExternalConnectionString();
foreach(var kv in str.Split(';')
.Select(part => part.Split('='))
.Where(kv => kv.Length == 2))
{
switch (kv[0].ToLowerInvariant())
{
case "server":
if (resultTemp.Server != null)
{
error = "Duplicated server attribute";
return false;
}
if (!Uri.IsWellFormedUriString(kv[1], UriKind.RelativeOrAbsolute))
{
error = "Invalid URI";
return false;
}
resultTemp.Server = new Uri(kv[1], UriKind.RelativeOrAbsolute);
if (!resultTemp.Server.IsAbsoluteUri && (kv[1].Length == 0 || kv[1][0] != '/'))
resultTemp.Server = new Uri($"/{kv[1]}", UriKind.RelativeOrAbsolute);
break;
case "cookiefile":
case "cookiefilepath":
if (resultTemp.CookieFilePath != null)
{
error = "Duplicated cookiefile attribute";
return false;
}
resultTemp.CookieFilePath = kv[1];
break;
case "macaroondirectorypath":
resultTemp.MacaroonDirectoryPath = kv[1];
break;
case "certthumbprint":
resultTemp.CertificateThumbprint = kv[1];
break;
case "macaroonfilepath":
resultTemp.MacaroonFilePath = kv[1];
break;
case "api-token":
resultTemp.APIToken = kv[1];
break;
case "access-key":
resultTemp.AccessKey = kv[1];
break;
}
}
result = resultTemp;
return true;
}
}
}

View File

@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Lightning;
using Microsoft.Extensions.Configuration;
namespace BTCPayServer.Configuration
{
public class ExternalServices : List<ExternalService>
{
public void Load(string cryptoCode, IConfiguration configuration)
{
Load(configuration, cryptoCode, "lndgrpc", ExternalServiceTypes.LNDGRPC, "Invalid setting {0}, " + Environment.NewLine +
"lnd server: 'server=https://lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
"lnd server: 'server=https://lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
"lnd server: 'server=https://lnd.example.com;macaroondirectorypath=/root/.lnd;certthumbprint=2abdf302...'" + Environment.NewLine +
"Error: {1}",
"LND (gRPC server)");
Load(configuration, cryptoCode, "lndrest", ExternalServiceTypes.LNDRest, "Invalid setting {0}, " + Environment.NewLine +
"lnd server: 'server=https://lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
"lnd server: 'server=https://lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
"lnd server: 'server=https://lnd.example.com;macaroondirectorypath=/root/.lnd;certthumbprint=2abdf302...'" + Environment.NewLine +
"Error: {1}",
"LND (REST server)");
Load(configuration, cryptoCode, "spark", ExternalServiceTypes.Spark, "Invalid setting {0}, " + Environment.NewLine +
$"Valid example: 'server=https://btcpay.example.com/spark/btc/;cookiefile=/etc/clightning_bitcoin_spark/.cookie'" + Environment.NewLine +
"Error: {1}",
"C-Lightning (Spark server)");
Load(configuration, cryptoCode, "rtl", ExternalServiceTypes.RTL, "Invalid setting {0}, " + Environment.NewLine +
$"Valid example: 'server=https://btcpay.example.com/rtl/btc/;cookiefile=/etc/clightning_bitcoin_rtl/.cookie'" + Environment.NewLine +
"Error: {1}",
"LND (Ride the Lightning server)");
Load(configuration, cryptoCode, "charge", ExternalServiceTypes.Charge, "Invalid setting {0}, " + Environment.NewLine +
$"lightning charge server: 'type=charge;server=https://charge.example.com;api-token=2abdf302...'" + Environment.NewLine +
$"lightning charge server: 'type=charge;server=https://charge.example.com;cookiefilepath=/root/.charge/.cookie'" + Environment.NewLine +
"Error: {1}",
"C-Lightning (Charge server)");
}
void Load(IConfiguration configuration, string cryptoCode, string serviceName, ExternalServiceTypes type, string errorMessage, string displayName)
{
var setting = $"{cryptoCode}.external.{serviceName}";
var connStr = configuration.GetOrDefault<string>(setting, string.Empty);
if (connStr.Length != 0)
{
if (!ExternalConnectionString.TryParse(connStr, out var connectionString, out var error))
{
throw new ConfigException(string.Format(CultureInfo.InvariantCulture, errorMessage, setting, error));
}
this.Add(new ExternalService() { Type = type, ConnectionString = connectionString, CryptoCode = cryptoCode, DisplayName = displayName, ServiceName = serviceName });
}
}
public ExternalService GetService(string serviceName, string cryptoCode)
{
return this.FirstOrDefault(o => o.CryptoCode.Equals(cryptoCode, StringComparison.OrdinalIgnoreCase) &&
o.ServiceName.Equals(serviceName, StringComparison.OrdinalIgnoreCase));
}
}
public class ExternalService
{
public string DisplayName { get; set; }
public ExternalServiceTypes Type { get; set; }
public ExternalConnectionString ConnectionString { get; set; }
public string CryptoCode { get; set; }
public string ServiceName { get; set; }
}
public enum ExternalServiceTypes
{
LNDRest,
LNDGRPC,
Spark,
RTL,
Charge,
P2P
}
}

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;
@ -12,7 +13,8 @@ using System.Threading.Tasks;
namespace BTCPayServer.Controllers
{
[BitpayAPIConstraint]
[Authorize(AuthenticationSchemes = Security.Policies.BitpayAuthentication)]
[BitpayAPIConstraint()]
public class AccessTokenController : Controller
{
TokenRepository _TokenRepository;
@ -30,22 +32,22 @@ namespace BTCPayServer.Controllers
[HttpPost]
[Route("tokens")]
[AllowAnonymous]
public async Task<DataWrapper<List<PairingCodeResponse>>> Tokens([FromBody] TokenRequest request)
{
if (request == null)
throw new BitpayHttpException(400, "The request body is missing");
PairingCodeEntity pairingEntity = null;
if (string.IsNullOrEmpty(request.PairingCode))
{
if (string.IsNullOrEmpty(request.Id) || !NBitpayClient.Extensions.BitIdExtensions.ValidateSIN(request.Id))
throw new BitpayHttpException(400, "'id' property is required");
if (string.IsNullOrEmpty(request.Facade))
throw new BitpayHttpException(400, "'facade' property is required");
var pairingCode = await _TokenRepository.CreatePairingCodeAsync();
await _TokenRepository.PairWithSINAsync(pairingCode, request.Id);
pairingEntity = await _TokenRepository.UpdatePairingCode(new PairingCodeEntity()
{
Id = pairingCode,
Facade = request.Facade,
Label = request.Label
});
@ -53,7 +55,7 @@ namespace BTCPayServer.Controllers
else
{
var sin = this.User.GetSIN() ?? request.Id;
if (string.IsNullOrEmpty(request.Id) || !NBitpayClient.Extensions.BitIdExtensions.ValidateSIN(request.Id))
if (string.IsNullOrEmpty(sin) || !NBitpayClient.Extensions.BitIdExtensions.ValidateSIN(sin))
throw new BitpayHttpException(400, "'id' property is required, alternatively, use BitId");
pairingEntity = await _TokenRepository.GetPairingAsync(request.PairingCode);
@ -77,10 +79,11 @@ namespace BTCPayServer.Controllers
{
new PairingCodeResponse()
{
Policies = new Newtonsoft.Json.Linq.JArray(),
PairingCode = pairingEntity.Id,
PairingExpiration = pairingEntity.Expiration,
DateCreated = pairingEntity.CreatedTime,
Facade = pairingEntity.Facade,
Facade = "merchant",
Token = pairingEntity.TokenValue,
Label = pairingEntity.Label
}

View File

@ -17,6 +17,11 @@ using BTCPayServer.Services.Mails;
using BTCPayServer.Services.Stores;
using BTCPayServer.Logging;
using BTCPayServer.Security;
using System.Globalization;
using BTCPayServer.Services.U2F;
using BTCPayServer.Services.U2F.Models;
using Newtonsoft.Json;
using NicolasDorier.RateLimits;
namespace BTCPayServer.Controllers
{
@ -26,10 +31,13 @@ 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;
Configuration.BTCPayServerOptions _Options;
private readonly BTCPayServerEnvironment _btcPayServerEnvironment;
private readonly U2FService _u2FService;
ILogger _logger;
public AccountController(
@ -37,15 +45,21 @@ namespace BTCPayServer.Controllers
RoleManager<IdentityRole> roleManager,
StoreRepository storeRepository,
SignInManager<ApplicationUser> signInManager,
IEmailSender emailSender,
SettingsRepository settingsRepository)
EmailSenderFactory emailSenderFactory,
SettingsRepository settingsRepository,
Configuration.BTCPayServerOptions options,
BTCPayServerEnvironment btcPayServerEnvironment,
U2FService u2FService)
{
this.storeRepository = storeRepository;
_userManager = userManager;
_signInManager = signInManager;
_emailSender = emailSender;
_EmailSenderFactory = emailSenderFactory;
_RoleManager = roleManager;
_SettingsRepository = settingsRepository;
_Options = options;
_btcPayServerEnvironment = btcPayServerEnvironment;
_u2FService = u2FService;
_logger = Logs.PayServer;
}
@ -69,6 +83,7 @@ namespace BTCPayServer.Controllers
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
[RateLimitsFilter(ZoneLimits.Login, Scope = RateLimitsScope.RemoteAddress)]
public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
{
ViewData["ReturnUrl"] = returnUrl;
@ -85,9 +100,45 @@ namespace BTCPayServer.Controllers
return View(model);
}
}
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false);
else
{
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return View(model);
}
if (!await _userManager.IsLockedOutAsync(user) && await _u2FService.HasDevices(user.Id))
{
if (await _userManager.CheckPasswordAsync(user, model.Password))
{
LoginWith2faViewModel twoFModel = null;
if (user.TwoFactorEnabled)
{
// we need to do an actual sign in attempt so that 2fa can function in next step
await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: true);
twoFModel = new LoginWith2faViewModel
{
RememberMe = model.RememberMe
};
}
return View("SecondaryLogin", new SecondaryLoginViewModel()
{
LoginWith2FaViewModel = twoFModel,
LoginWithU2FViewModel = await BuildU2FViewModel(model.RememberMe, user)
});
}
else
{
var incrementAccessFailedResult = await _userManager.AccessFailedAsync(user);
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return View(model);
}
}
var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: true);
if (result.Succeeded)
{
_logger.LogInformation("User logged in.");
@ -95,10 +146,12 @@ namespace BTCPayServer.Controllers
}
if (result.RequiresTwoFactor)
{
return RedirectToAction(nameof(LoginWith2fa), new
return View("SecondaryLogin", new SecondaryLoginViewModel()
{
returnUrl,
model.RememberMe
LoginWith2FaViewModel = new LoginWith2faViewModel()
{
RememberMe = model.RememberMe
}
});
}
if (result.IsLockedOut)
@ -117,6 +170,71 @@ namespace BTCPayServer.Controllers
return View(model);
}
private async Task<LoginWithU2FViewModel> BuildU2FViewModel(bool rememberMe, ApplicationUser user)
{
if (_btcPayServerEnvironment.IsSecure)
{
var u2fChallenge = await _u2FService.GenerateDeviceChallenges(user.Id,
Request.GetAbsoluteUriNoPathBase().ToString().TrimEnd('/'));
return new LoginWithU2FViewModel()
{
Version = u2fChallenge[0].version,
Challenge = u2fChallenge[0].challenge,
Challenges = JsonConvert.SerializeObject(u2fChallenge),
AppId = u2fChallenge[0].appId,
UserId = user.Id,
RememberMe = rememberMe
};
}
return null;
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> LoginWithU2F(LoginWithU2FViewModel viewModel, string returnUrl = null)
{
ViewData["ReturnUrl"] = returnUrl;
var user = await _userManager.FindByIdAsync(viewModel.UserId);
if (user == null)
{
return NotFound();
}
var errorMessage = string.Empty;
try
{
if (await _u2FService.AuthenticateUser(viewModel.UserId, viewModel.DeviceResponse))
{
await _signInManager.SignInAsync(user, viewModel.RememberMe, "U2F");
_logger.LogInformation("User logged in.");
return RedirectToLocal(returnUrl);
}
errorMessage = "Invalid login attempt.";
}
catch (Exception e)
{
errorMessage = e.Message;
}
ModelState.AddModelError(string.Empty, errorMessage);
return View("SecondaryLogin", new SecondaryLoginViewModel()
{
LoginWithU2FViewModel = viewModel,
LoginWith2FaViewModel = !user.TwoFactorEnabled
? null
: new LoginWith2faViewModel()
{
RememberMe = viewModel.RememberMe
}
});
}
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> LoginWith2fa(bool rememberMe, string returnUrl = null)
@ -129,10 +247,13 @@ namespace BTCPayServer.Controllers
throw new ApplicationException($"Unable to load two-factor authentication user.");
}
var model = new LoginWith2faViewModel { RememberMe = rememberMe };
ViewData["ReturnUrl"] = returnUrl;
return View(model);
return View("SecondaryLogin", new SecondaryLoginViewModel()
{
LoginWith2FaViewModel = new LoginWith2faViewModel { RememberMe = rememberMe },
LoginWithU2FViewModel = (await _u2FService.HasDevices(user.Id))? await BuildU2FViewModel(rememberMe, user): null
});
}
[HttpPost]
@ -169,7 +290,11 @@ namespace BTCPayServer.Controllers
{
_logger.LogWarning("Invalid authenticator code entered for user with ID {UserId}.", user.Id);
ModelState.AddModelError(string.Empty, "Invalid authenticator code.");
return View();
return View("SecondaryLogin", new SecondaryLoginViewModel()
{
LoginWith2FaViewModel = model,
LoginWithU2FViewModel = (await _u2FService.HasDevices(user.Id))? await BuildU2FViewModel(rememberMe, user): null
});
}
}
@ -236,23 +361,25 @@ namespace BTCPayServer.Controllers
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> Register(string returnUrl = null)
public async Task<IActionResult> Register(string returnUrl = null, bool logon = true)
{
var policies = await _SettingsRepository.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
if (policies.LockSubscription)
if (policies.LockSubscription && !User.IsInRole(Roles.ServerAdmin))
return RedirectToAction(nameof(HomeController.Index), "Home");
ViewData["ReturnUrl"] = returnUrl;
ViewData["Logon"] = logon.ToString(CultureInfo.InvariantCulture).ToLowerInvariant();
return View();
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Register(RegisterViewModel model, string returnUrl = null)
public async Task<IActionResult> Register(RegisterViewModel model, string returnUrl = null, bool logon = true)
{
ViewData["ReturnUrl"] = returnUrl;
ViewData["Logon"] = logon.ToString(CultureInfo.InvariantCulture).ToLowerInvariant();
var policies = await _SettingsRepository.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
if (policies.LockSubscription)
if (policies.LockSubscription && !User.IsInRole(Roles.ServerAdmin))
return RedirectToAction(nameof(HomeController.Index), "Home");
if (ModelState.IsValid)
{
@ -266,15 +393,24 @@ namespace BTCPayServer.Controllers
{
await _RoleManager.CreateAsync(new IdentityRole(Roles.ServerAdmin));
await _userManager.AddToRoleAsync(user, Roles.ServerAdmin);
if(_Options.DisableRegistration)
{
// Once the admin user has been created lock subsequent user registrations (needs to be disabled for unit tests that require multiple users).
policies.LockSubscription = true;
await _SettingsRepository.UpdateSetting(policies);
}
}
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)
{
await _signInManager.SignInAsync(user, isPersistent: false);
if(logon)
await _signInManager.SignInAsync(user, isPersistent: false);
return RedirectToLocal(returnUrl);
}
else
@ -430,8 +566,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,170 @@
using System;
using System.Linq;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Mails;
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()
{
NotificationEmailWarning = !await IsEmailConfigured(app.StoreDataId),
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,
Sounds = string.Join(Environment.NewLine, settings.Sounds),
AnimationColors = string.Join(Environment.NewLine, settings.AnimationColors)
};
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.");
}
var parsedSounds = vm.Sounds.Split(
new[] {"\r\n", "\r", "\n"},
StringSplitOptions.None
).Select(s => s.Trim()).ToArray();
if (vm.SoundsEnabled && !parsedSounds.Any())
{
ModelState.AddModelError(nameof(vm.Sounds), "You must have at least one sound if you enable sounds");
}
var parsedAnimationColors = vm.AnimationColors.Split(
new[] { "\r\n", "\r", "\n" },
StringSplitOptions.None
).Select(s => s.Trim()).ToArray();
if (vm.AnimationsEnabled && !parsedAnimationColors.Any())
{
ModelState.AddModelError(nameof(vm.AnimationColors), "You must have at least one animation color if you enable animations");
}
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,
NotificationEmail = vm.NotificationEmail,
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,
Sounds = parsedSounds,
AnimationColors = parsedAnimationColors
};
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

@ -1,22 +1,15 @@
using System;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Models;
using BTCPayServer.Models.AppViewModels;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using NBitcoin.DataEncoders;
using NBitcoin;
using BTCPayServer.Services.Apps;
using Newtonsoft.Json;
using YamlDotNet.RepresentationModel;
using System.IO;
using BTCPayServer.Services.Rates;
using System.Globalization;
using BTCPayServer.Services.Mails;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace BTCPayServer.Controllers
{
@ -26,28 +19,68 @@ namespace BTCPayServer.Controllers
{
public PointOfSaleSettings()
{
Title = "My awesome Point of Sale";
Title = "Tea shop";
Currency = "USD";
Template =
"tea:\n" +
" price: 0.02\n" +
" title: Green Tea # title is optional, defaults to the keys\n\n" +
"coffee:\n" +
" price: 1\n\n" +
"bamba:\n" +
" price: 3\n\n" +
"beer:\n" +
" price: 7\n\n" +
"hat:\n" +
" price: 15\n\n" +
"tshirt:\n" +
" price: 25";
"green tea:\n" +
" price: 1\n" +
" title: Green Tea\n" +
" description: Lovely, fresh and tender, Meng Ding Gan Lu ('sweet dew') is grown in the lush Meng Ding Mountains of the southwestern province of Sichuan where it has been cultivated for over a thousand years.\n" +
" image: https://cdn.pixabay.com/photo/2015/03/26/11/03/green-tea-692339__480.jpg\n\n" +
"black tea:\n" +
" price: 1\n" +
" title: Black Tea\n" +
" description: Tian Jian Tian Jian means 'heavenly tippy tea' in Chinese, and it describes the finest grade of dark tea. Our Tian Jian dark tea is from Hunan province which is famous for making some of the best dark teas available.\n" +
" image: https://cdn.pixabay.com/photo/2016/11/29/13/04/beverage-1869716__480.jpg\n\n" +
"rooibos:\n" +
" price: 1.2\n" +
" title: Rooibos\n" +
" description: Rooibos is a dramatic red tea made from a South African herb that contains polyphenols and flavonoids. Often called 'African redbush tea', Rooibos herbal tea delights the senses and delivers potential health benefits with each caffeine-free sip.\n" +
" image: https://cdn.pixabay.com/photo/2017/01/08/08/14/water-1962388__480.jpg\n\n" +
"pu erh:\n" +
" price: 2\n" +
" title: Pu Erh\n" +
" description: This loose pur-erh tea is produced in Yunnan Province, China. The process in a relatively high humidity environment has mellowed the elemental character of the tea when compared to young Pu-erh.\n" +
" image: https://cdn.pixabay.com/photo/2018/07/21/16/56/tea-cup-3552917__480.jpg\n\n" +
"herbal tea:\n" +
" price: 1.8\n" +
" title: Herbal Tea\n" +
" description: Chamomile tea is made from the flower heads of the chamomile plant. The medicinal use of chamomile dates back to the ancient Egyptians, Romans and Greeks. Pay us what you want!\n" +
" image: https://cdn.pixabay.com/photo/2015/07/02/20/57/chamomile-829538__480.jpg\n" +
" custom: true\n\n" +
"fruit tea:\n" +
" price: 1.5\n" +
" title: Fruit Tea\n" +
" description: The Tibetan Himalayas, the land is majestic and beautiful—a spiritual place where, despite the perilous environment, many journey seeking enlightenment. Pay us what you want!\n" +
" image: https://cdn.pixabay.com/photo/2016/09/16/11/24/darts-1673812__480.jpg\n" +
" custom: true";
EnableShoppingCart = false;
ShowCustomAmount = true;
ShowDiscount = true;
EnableTips = true;
}
public string Title { get; set; }
public string Currency { get; set; }
public string Template { get; set; }
public bool EnableShoppingCart { get; set; }
public bool ShowCustomAmount { get; set; }
public bool ShowDiscount { get; set; }
public bool EnableTips { get; set; }
public const string BUTTON_TEXT_DEF = "Buy for {0}";
public string ButtonText { get; set; } = BUTTON_TEXT_DEF;
public const string CUSTOM_BUTTON_TEXT_DEF = "Pay";
public string CustomButtonText { get; set; } = CUSTOM_BUTTON_TEXT_DEF;
public const string CUSTOM_TIP_TEXT_DEF = "Do you want to leave a tip?";
public string CustomTipText { get; set; } = CUSTOM_TIP_TEXT_DEF;
public static readonly int[] CUSTOM_TIP_PERCENTAGES_DEF = new int[] { 15, 18, 20 };
public int[] CustomTipPercentages { get; set; } = CUSTOM_TIP_PERCENTAGES_DEF;
public string CustomCSSLink { get; set; }
public string NotificationEmail { get; set; }
public string NotificationUrl { get; set; }
public bool? RedirectAutomatically { get; set; }
}
[HttpGet]
@ -57,19 +90,74 @@ namespace BTCPayServer.Controllers
var app = await GetOwnedApp(appId, AppType.PointOfSale);
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
return View(new UpdatePointOfSaleViewModel() { Title = settings.Title, ShowCustomAmount = settings.ShowCustomAmount, Currency = settings.Currency, Template = settings.Template });
var vm = new UpdatePointOfSaleViewModel()
{
NotificationEmailWarning = !await IsEmailConfigured(app.StoreDataId),
Id = appId,
Title = settings.Title,
EnableShoppingCart = settings.EnableShoppingCart,
ShowCustomAmount = settings.ShowCustomAmount,
ShowDiscount = settings.ShowDiscount,
EnableTips = settings.EnableTips,
Currency = settings.Currency,
Template = settings.Template,
ButtonText = settings.ButtonText ?? PointOfSaleSettings.BUTTON_TEXT_DEF,
CustomButtonText = settings.CustomButtonText ?? PointOfSaleSettings.CUSTOM_BUTTON_TEXT_DEF,
CustomTipText = settings.CustomTipText ?? PointOfSaleSettings.CUSTOM_TIP_TEXT_DEF,
CustomTipPercentages = settings.CustomTipPercentages != null ? string.Join(",", settings.CustomTipPercentages) : string.Join(",", PointOfSaleSettings.CUSTOM_TIP_PERCENTAGES_DEF),
CustomCSSLink = settings.CustomCSSLink,
NotificationEmail = settings.NotificationEmail,
NotificationUrl = settings.NotificationUrl,
RedirectAutomatically = settings.RedirectAutomatically.HasValue? settings.RedirectAutomatically.Value? "true": "false" : ""
};
if (HttpContext?.Request != null)
{
var appUrl = HttpContext.Request.GetAbsoluteRoot().WithTrailingSlash() + $"apps/{appId}/pos";
var encoder = HtmlEncoder.Default;
if (settings.ShowCustomAmount)
{
StringBuilder builder = new StringBuilder();
builder.AppendLine($"<form method=\"POST\" action=\"{encoder.Encode(appUrl)}\">");
builder.AppendLine($" <input type=\"hidden\" name=\"amount\" value=\"100\" />");
builder.AppendLine($" <input type=\"hidden\" name=\"email\" value=\"customer@example.com\" />");
builder.AppendLine($" <input type=\"hidden\" name=\"orderId\" value=\"CustomOrderId\" />");
builder.AppendLine($" <input type=\"hidden\" name=\"notificationUrl\" value=\"https://example.com/callbacks\" />");
builder.AppendLine($" <input type=\"hidden\" name=\"redirectUrl\" value=\"https://example.com/thanksyou\" />");
builder.AppendLine($" <button type=\"submit\">Buy now</button>");
builder.AppendLine($"</form>");
vm.Example1 = builder.ToString();
}
try
{
var items = _AppService.Parse(settings.Template, settings.Currency);
var builder = new StringBuilder();
builder.AppendLine($"<form method=\"POST\" action=\"{encoder.Encode(appUrl)}\">");
builder.AppendLine($" <input type=\"hidden\" name=\"email\" value=\"customer@example.com\" />");
builder.AppendLine($" <input type=\"hidden\" name=\"orderId\" value=\"CustomOrderId\" />");
builder.AppendLine($" <input type=\"hidden\" name=\"notificationUrl\" value=\"https://example.com/callbacks\" />");
builder.AppendLine($" <input type=\"hidden\" name=\"redirectUrl\" value=\"https://example.com/thanksyou\" />");
builder.AppendLine($" <button type=\"submit\" name=\"choiceKey\" value=\"{items[0].Id}\">Buy now</button>");
builder.AppendLine($"</form>");
vm.Example2 = builder.ToString();
}
catch { }
vm.InvoiceUrl = appUrl + "invoices/SkdsDghkdP3D3qkj7bLq3";
}
vm.ExampleCallback = "{\n \"id\":\"SkdsDghkdP3D3qkj7bLq3\",\n \"url\":\"https://btcpay.example.com/invoice?id=SkdsDghkdP3D3qkj7bLq3\",\n \"status\":\"paid\",\n \"price\":10,\n \"currency\":\"EUR\",\n \"invoiceTime\":1520373130312,\n \"expirationTime\":1520374030312,\n \"currentTime\":1520373179327,\n \"exceptionStatus\":false,\n \"buyerFields\":{\n \"buyerEmail\":\"customer@example.com\",\n \"buyerNotify\":false\n },\n \"paymentSubtotals\": {\n \"BTC\":114700\n },\n \"paymentTotals\": {\n \"BTC\":118400\n },\n \"transactionCurrency\": \"BTC\",\n \"amountPaid\": \"1025900\",\n \"exchangeRates\": {\n \"BTC\": {\n \"EUR\": 8721.690715789999,\n \"USD\": 10817.99\n }\n }\n}";
return View(vm);
}
[HttpPost]
[Route("{appId}/settings/pos")]
public async Task<IActionResult> UpdatePointOfSale(string appId, UpdatePointOfSaleViewModel vm)
{
if (_Currencies.GetCurrencyData(vm.Currency) == null)
if (_currencies.GetCurrencyData(vm.Currency, false) == null)
ModelState.AddModelError(nameof(vm.Currency), "Invalid currency");
try
{
Parse(vm.Template, vm.Currency);
_AppService.Parse(vm.Template, vm.Currency);
}
catch
{
@ -85,130 +173,25 @@ namespace BTCPayServer.Controllers
app.SetSettings(new PointOfSaleSettings()
{
Title = vm.Title,
EnableShoppingCart = vm.EnableShoppingCart,
ShowCustomAmount = vm.ShowCustomAmount,
ShowDiscount = vm.ShowDiscount,
EnableTips = vm.EnableTips,
Currency = vm.Currency.ToUpperInvariant(),
Template = vm.Template
Template = vm.Template,
ButtonText = vm.ButtonText,
CustomButtonText = vm.CustomButtonText,
CustomTipText = vm.CustomTipText,
CustomTipPercentages = ListSplit(vm.CustomTipPercentages),
CustomCSSLink = vm.CustomCSSLink,
NotificationUrl = vm.NotificationUrl,
NotificationEmail = vm.NotificationEmail,
RedirectAutomatically = string.IsNullOrEmpty(vm.RedirectAutomatically)? (bool?) null: bool.Parse(vm.RedirectAutomatically)
});
await UpdateAppSettings(app);
StatusMessage = "App updated";
return RedirectToAction(nameof(UpdatePointOfSale));
}
[HttpGet]
[Route("{appId}/pos")]
public async Task<IActionResult> ViewPointOfSale(string appId)
{
var app = await GetApp(appId, AppType.PointOfSale);
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
var currency = _Currencies.GetCurrencyData(settings.Currency);
double step = currency == null ? 1 : Math.Pow(10, -(currency.Divisibility));
return View(new ViewPointOfSaleViewModel()
{
Title = settings.Title,
Step = step.ToString(CultureInfo.InvariantCulture),
ShowCustomAmount = settings.ShowCustomAmount,
Items = Parse(settings.Template, settings.Currency)
});
}
private 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();
}
}
private ViewPointOfSaleViewModel.Item[] Parse(string template, string currency)
{
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 { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlMappingNode })
.Where(kv => kv.Value != null)
.Select(c => new ViewPointOfSaleViewModel.Item()
{
Id = c.Key,
Title = c.Value.Children
.Select(kv => new { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlScalarNode })
.Where(kv => kv.Value != null)
.Where(cc => cc.Key == "title")
.FirstOrDefault()?.Value?.Value ?? c.Key,
Price = c.Value.Children
.Select(kv => new { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlScalarNode })
.Where(kv => kv.Value != null)
.Where(cc => cc.Key == "price")
.Select(cc => new ViewPointOfSaleViewModel.Item.ItemPrice()
{
Value = decimal.Parse(cc.Value.Value, CultureInfo.InvariantCulture),
Formatted = FormatCurrency(cc.Value.Value, currency)
})
.Single()
})
.ToArray();
}
string FormatCurrency(string price, string currency)
{
return decimal.Parse(price, CultureInfo.InvariantCulture).ToString("C", _Currencies.GetCurrencyProvider(currency));
}
[HttpPost]
[Route("{appId}/pos")]
public async Task<IActionResult> ViewPointOfSale(string appId, double amount, string choiceKey)
{
var app = await GetApp(appId, AppType.PointOfSale);
if (string.IsNullOrEmpty(choiceKey) && amount <= 0)
{
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
}
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
if(string.IsNullOrEmpty(choiceKey) && !settings.ShowCustomAmount)
{
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
}
string title = null;
double price = 0.0;
if (!string.IsNullOrEmpty(choiceKey))
{
var choices = Parse(settings.Template, settings.Currency);
var choice = choices.FirstOrDefault(c => c.Id == choiceKey);
if (choice == null)
return NotFound();
title = choice.Title;
price = (double)choice.Price.Value;
}
else
{
price = amount;
title = settings.Title;
}
var store = await GetStore(app);
var invoice = await _InvoiceController.CreateInvoiceCore(new NBitpayClient.Invoice()
{
ItemDesc = title,
Currency = settings.Currency,
Price = price,
}, store, HttpContext.Request.GetAbsoluteRoot());
return Redirect(invoice.Data.Url);
}
private async Task<StoreData> GetStore(AppData app)
{
using (var ctx = _ContextFactory.CreateContext())
{
return await ctx.Stores.FirstOrDefaultAsync(s => s.Id == app.StoreDataId);
}
return RedirectToAction(nameof(ListApps));
}
private async Task UpdateAppSettings(AppData app)
@ -218,8 +201,25 @@ 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();
}
}
private int[] ListSplit(string list, string separator = ",")
{
if (string.IsNullOrEmpty(list))
{
return Array.Empty<int>();
}
else
{
// Remove all characters except numeric and comma
Regex charsToDestroy = new Regex(@"[^\d|\" + separator + "]");
list = charsToDestroy.Replace(list, "");
return list.Split(separator, System.StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToArray();
}
}
}
}

View File

@ -1,47 +1,65 @@
using System;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Models;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Security;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Mails;
using BTCPayServer.Services.Rates;
using Ganss.XSS;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using NBitcoin.DataEncoders;
using Microsoft.EntityFrameworkCore;
using NBitcoin;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Rates;
using NBitcoin.DataEncoders;
namespace BTCPayServer.Controllers
{
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
[AutoValidateAntiforgeryToken]
[Route("apps")]
public partial class AppsController : Controller
{
ApplicationDbContextFactory _ContextFactory;
UserManager<ApplicationUser> _UserManager;
CurrencyNameTable _Currencies;
InvoiceController _InvoiceController;
[TempData]
public string StatusMessage { get; set; }
public AppsController(
UserManager<ApplicationUser> userManager,
ApplicationDbContextFactory contextFactory,
EventAggregator eventAggregator,
BTCPayNetworkProvider networkProvider,
CurrencyNameTable currencies,
InvoiceController invoiceController)
HtmlSanitizer htmlSanitizer,
EmailSenderFactory emailSenderFactory,
AppService AppService)
{
_InvoiceController = invoiceController;
_UserManager = userManager;
_ContextFactory = contextFactory;
_Currencies = currencies;
_EventAggregator = eventAggregator;
_NetworkProvider = networkProvider;
_currencies = currencies;
_htmlSanitizer = htmlSanitizer;
_emailSenderFactory = emailSenderFactory;
_AppService = AppService;
}
private UserManager<ApplicationUser> _UserManager;
private ApplicationDbContextFactory _ContextFactory;
private readonly EventAggregator _EventAggregator;
private BTCPayNetworkProvider _NetworkProvider;
private readonly CurrencyNameTable _currencies;
private readonly HtmlSanitizer _htmlSanitizer;
private readonly EmailSenderFactory _emailSenderFactory;
private AppService _AppService;
[TempData]
public string StatusMessage { get; set; }
public string CreatedAppId { get; set; }
public async Task<IActionResult> ListApps()
{
var apps = await GetAllApps();
var apps = await _AppService.GetAllApps(GetUserId());
return View(new ListAppsViewModel()
{
Apps = apps
@ -55,7 +73,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));
}
@ -64,10 +82,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 need to create 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();
@ -79,10 +102,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 need to create 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;
@ -102,9 +130,9 @@ namespace BTCPayServer.Controllers
StatusMessage = "Error: You are not owner of this store";
return RedirectToAction(nameof(ListApps));
}
var id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(20));
using (var ctx = _ContextFactory.CreateContext())
{
var id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(32));
var appData = new AppData() { Id = id };
appData.StoreDataId = selectedStore;
appData.Name = vm.Name;
@ -113,7 +141,18 @@ namespace BTCPayServer.Controllers
await ctx.SaveChangesAsync();
}
StatusMessage = "App successfully created";
return RedirectToAction(nameof(ListApps));
CreatedAppId = id;
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]
@ -131,75 +170,20 @@ namespace BTCPayServer.Controllers
});
}
private async Task<AppData> GetOwnedApp(string appId, AppType? type = null)
private Task<AppData> GetOwnedApp(string appId, AppType? type = null)
{
var userId = GetUserId();
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;
}
}
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)
.Select(us => new
{
IsOwner = us.Role == StoreRoles.Owner,
StoreId = us.StoreDataId,
StoreName = us.StoreData.StoreName,
Apps = us.StoreData.Apps
})
.SelectMany(us => us.Apps.Select(app => new ListAppsViewModel.ListAppViewModel()
{
IsOwner = us.IsOwner,
AppName = app.Name,
AppType = app.AppType,
Id = app.Id,
StoreId = us.StoreId,
StoreName = us.StoreName
}))
.ToArrayAsync();
}
return _AppService.GetAppDataIfOwner(GetUserId(), appId, type);
}
private string GetUserId()
{
return _UserManager.GetUserId(User);
}
private async Task<bool> IsEmailConfigured(string storeId)
{
return (await (_emailSenderFactory.GetEmailSender(storeId) as EmailSender)?.GetEmailSettings())?.IsComplete() is true;
}
}
}

View File

@ -0,0 +1,295 @@
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;
using System.Threading.Tasks;
using BTCPayServer.Configuration;
using BTCPayServer.Data;
using BTCPayServer.Filters;
using BTCPayServer.ModelBinders;
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.Controllers
{
public class AppsPublicController : Controller
{
public AppsPublicController(AppService AppService,
BTCPayServerOptions btcPayServerOptions,
InvoiceController invoiceController,
UserManager<ApplicationUser> userManager)
{
_AppService = AppService;
_BtcPayServerOptions = btcPayServerOptions;
_InvoiceController = invoiceController;
_UserManager = userManager;
}
private AppService _AppService;
private readonly BTCPayServerOptions _BtcPayServerOptions;
private InvoiceController _InvoiceController;
private readonly UserManager<ApplicationUser> _UserManager;
[HttpGet]
[Route("/apps/{appId}/pos")]
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
public async Task<IActionResult> ViewPointOfSale(string appId)
{
var app = await _AppService.GetApp(appId, AppType.PointOfSale);
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
var numberFormatInfo = _AppService.Currencies.GetNumberFormatInfo(settings.Currency) ?? _AppService.Currencies.GetNumberFormatInfo("USD");
double step = Math.Pow(10, -(numberFormatInfo.CurrencyDecimalDigits));
return View(new ViewPointOfSaleViewModel()
{
Title = settings.Title,
Step = step.ToString(CultureInfo.InvariantCulture),
EnableShoppingCart = settings.EnableShoppingCart,
ShowCustomAmount = settings.ShowCustomAmount,
ShowDiscount = settings.ShowDiscount,
EnableTips = settings.EnableTips,
CurrencyCode = settings.Currency,
CurrencySymbol = numberFormatInfo.CurrencySymbol,
CurrencyInfo = new ViewPointOfSaleViewModel.CurrencyInfoData()
{
CurrencySymbol = string.IsNullOrEmpty(numberFormatInfo.CurrencySymbol) ? settings.Currency : numberFormatInfo.CurrencySymbol,
Divisibility = numberFormatInfo.CurrencyDecimalDigits,
DecimalSeparator = numberFormatInfo.CurrencyDecimalSeparator,
ThousandSeparator = numberFormatInfo.NumberGroupSeparator,
Prefixed = new[] { 0, 2 }.Contains(numberFormatInfo.CurrencyPositivePattern),
SymbolSpace = new[] { 2, 3 }.Contains(numberFormatInfo.CurrencyPositivePattern)
},
Items = _AppService.Parse(settings.Template, settings.Currency),
ButtonText = settings.ButtonText,
CustomButtonText = settings.CustomButtonText,
CustomTipText = settings.CustomTipText,
CustomTipPercentages = settings.CustomTipPercentages,
CustomCSSLink = settings.CustomCSSLink,
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.");
}
var appInfo = (ViewCrowdfundViewModel)(await _AppService.GetAppInfo(appId));
appInfo.HubPath = AppHub.GetHubPath(this.Request);
if (settings.Enabled)
return View(appInfo);
if (!isAdmin)
return NotFound();
return View(appInfo);
}
[HttpPost]
[Route("/apps/{appId}/crowdfund")]
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
[IgnoreAntiforgeryToken]
[EnableCors(CorsPolicies.All)]
public async Task<IActionResult> ContributeToCrowdfund(string appId, ContributeToCrowdfund request, CancellationToken cancellationToken)
{
var app = await _AppService.GetApp(appId, AppType.Crowdfund, true);
if (app == null)
return NotFound();
var settings = app.GetSettings<CrowdfundSettings>();
var isAdmin = await _AppService.GetAppDataIfOwner(GetUserId(), appId, AppType.Crowdfund) != null;
if (!settings.Enabled && !isAdmin)
{
return NotFound("Crowdfund is not currently active");
}
var info = (ViewCrowdfundViewModel)await _AppService.GetAppInfo(appId);
info.HubPath = AppHub.GetHubPath(this.Request);
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,
NotificationEmail = settings.NotificationEmail,
FullNotifications = true,
ExtendedNotifications = true,
RedirectURL = request.RedirectUrl ??
new Uri(new Uri( new Uri(HttpContext.Request.GetAbsoluteRoot()), _BtcPayServerOptions.RootPath), $"apps/{appId}/crowdfund").ToString()
}, store, HttpContext.Request.GetAbsoluteRoot(),
new List<string> {AppService.GetAppInternalTag(appId)},
cancellationToken: cancellationToken);
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,
[ModelBinder(typeof(InvariantDecimalModelBinder))] decimal amount,
string email,
string orderId,
string notificationUrl,
string redirectUrl,
string choiceKey,
string posData = null, CancellationToken cancellationToken = default)
{
var app = await _AppService.GetApp(appId, AppType.PointOfSale);
if (string.IsNullOrEmpty(choiceKey) && amount <= 0)
{
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
}
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
if (string.IsNullOrEmpty(choiceKey) && !settings.ShowCustomAmount && !settings.EnableShoppingCart)
{
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
}
string title = null;
var price = 0.0m;
ViewPointOfSaleViewModel.Item choice = null;
if (!string.IsNullOrEmpty(choiceKey))
{
var choices = _AppService.Parse(settings.Template, settings.Currency);
choice = choices.FirstOrDefault(c => c.Id == choiceKey);
if (choice == null)
return NotFound();
title = choice.Title;
price = choice.Price.Value;
if (amount > price)
price = amount;
}
else
{
if (!settings.ShowCustomAmount && !settings.EnableShoppingCart)
return NotFound();
price = amount;
title = settings.Title;
}
var store = await _AppService.GetStore(app);
store.AdditionalClaims.Add(new Claim(Policies.CanCreateInvoice.Key, store.Id));
var invoice = await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest()
{
ItemCode = choice?.Id,
ItemDesc = title,
Currency = settings.Currency,
Price = price,
BuyerEmail = email,
OrderId = orderId,
NotificationURL =
string.IsNullOrEmpty(notificationUrl) ? settings.NotificationUrl : notificationUrl,
NotificationEmail = settings.NotificationEmail,
RedirectURL = redirectUrl ?? Request.GetDisplayUrl(),
FullNotifications = true,
ExtendedNotifications = true,
PosData = string.IsNullOrEmpty(posData) ? null : posData,
RedirectAutomatically = settings.RedirectAutomatically,
}, store, HttpContext.Request.GetAbsoluteRoot(),
new List<string>() { AppService.GetAppInternalTag(appId) },
cancellationToken);
return RedirectToAction(nameof(InvoiceController.Checkout), "Invoice", new { invoiceId = invoice.Data.Id });
}
private string GetUserId()
{
return _UserManager.GetUserId(User);
}
}
}

View File

@ -0,0 +1,120 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Models;
using BTCPayServer.Payments.Changelly;
using BTCPayServer.Rating;
using BTCPayServer.Services.Rates;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers
{
[Route("[controller]/{storeId}")]
public class ChangellyController : Controller
{
private readonly ChangellyClientProvider _changellyClientProvider;
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly RateFetcher _RateProviderFactory;
public ChangellyController(ChangellyClientProvider changellyClientProvider,
BTCPayNetworkProvider btcPayNetworkProvider,
RateFetcher rateProviderFactory)
{
_RateProviderFactory = rateProviderFactory ?? throw new ArgumentNullException(nameof(rateProviderFactory));
_changellyClientProvider = changellyClientProvider;
_btcPayNetworkProvider = btcPayNetworkProvider;
}
[HttpGet]
[Route("currencies")]
public async Task<IActionResult> GetCurrencyList(string storeId)
{
try
{
var client = await TryGetChangellyClient(storeId);
return Ok(await client.GetCurrenciesFull());
}
catch (Exception e)
{
return BadRequest(new BitpayErrorModel()
{
Error = e.Message
});
}
}
[HttpGet]
[Route("calculate")]
public async Task<IActionResult> CalculateAmount(string storeId, string fromCurrency, string toCurrency,
decimal toCurrencyAmount, CancellationToken cancellationToken)
{
try
{
var client = await TryGetChangellyClient(storeId);
if (fromCurrency.Equals("usd", StringComparison.InvariantCultureIgnoreCase)
|| fromCurrency.Equals("eur", StringComparison.InvariantCultureIgnoreCase))
{
return await HandleCalculateFiatAmount(fromCurrency, toCurrency, toCurrencyAmount, cancellationToken);
}
var callCounter = 0;
var baseRate = await client.GetExchangeAmount(fromCurrency, toCurrency, 1);
var currentAmount = ChangellyCalculationHelper.ComputeBaseAmount(baseRate, toCurrencyAmount);
while (true)
{
if (callCounter > 10)
{
BadRequest();
}
var computedAmount = await client.GetExchangeAmount(fromCurrency, toCurrency, currentAmount);
callCounter++;
if (computedAmount < toCurrencyAmount)
{
currentAmount =
ChangellyCalculationHelper.ComputeCorrectAmount(currentAmount, computedAmount,
toCurrencyAmount);
}
else
{
return Ok(currentAmount);
}
}
}
catch (Exception e)
{
return BadRequest(new BitpayErrorModel()
{
Error = e.Message
});
}
}
private async Task<Changelly> TryGetChangellyClient(string storeId)
{
var store = IsTest? null: HttpContext.GetStoreData();
storeId = storeId ?? store?.Id;
return await _changellyClientProvider.TryGetChangellyClient(storeId, store);
}
private async Task<IActionResult> HandleCalculateFiatAmount(string fromCurrency, string toCurrency,
decimal toCurrencyAmount, CancellationToken cancellationToken)
{
var store = HttpContext.GetStoreData();
var rules = store.GetStoreBlob().GetRateRules(_btcPayNetworkProvider);
var rate = await _RateProviderFactory.FetchRate(new CurrencyPair(toCurrency, fromCurrency), rules, cancellationToken);
if (rate.BidAsk == null) return BadRequest();
var flatRate = rate.BidAsk.Center;
return Ok(flatRate * toCurrencyAmount);
}
public bool IsTest { get; set; } = false;
}
}

View File

@ -1,20 +1,107 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using BTCPayServer.Models;
using NBitcoin.Payment;
using System.Net.Http;
using Newtonsoft.Json.Linq;
using NBitcoin;
using Newtonsoft.Json;
using BTCPayServer.Services;
using BTCPayServer.HostedServices;
namespace BTCPayServer.Controllers
{
public class HomeController : Controller
{
public IActionResult Index()
private readonly CssThemeManager _cachedServerSettings;
public IHttpClientFactory HttpClientFactory { get; }
public HomeController(IHttpClientFactory httpClientFactory, CssThemeManager cachedServerSettings)
{
HttpClientFactory = httpClientFactory;
_cachedServerSettings = cachedServerSettings;
}
public async Task<IActionResult> Index()
{
if (_cachedServerSettings.RootAppType is Services.Apps.AppType.Crowdfund)
{
var serviceProvider = HttpContext.RequestServices;
var controller = (AppsPublicController)serviceProvider.GetService(typeof(AppsPublicController));
controller.Url = Url;
controller.ControllerContext = ControllerContext;
var res = await controller.ViewCrowdfund(_cachedServerSettings.RootAppId, null) as ViewResult;
if (res != null)
{
res.ViewName = "/Views/AppsPublic/ViewCrowdfund.cshtml";
return res; // return
}
}
return View("Home");
}
[Route("translate")]
public IActionResult BitpayTranslator()
{
return View(new BitpayTranslatorViewModel());
}
[HttpPost]
[Route("translate")]
public async Task<IActionResult> BitpayTranslator(BitpayTranslatorViewModel vm)
{
if (!ModelState.IsValid)
return View(vm);
vm.BitpayLink = vm.BitpayLink ?? string.Empty;
vm.BitpayLink = vm.BitpayLink.Trim();
if (!vm.BitpayLink.StartsWith("bitcoin:", StringComparison.OrdinalIgnoreCase))
{
var invoiceId = vm.BitpayLink.Substring(vm.BitpayLink.LastIndexOf("=", StringComparison.OrdinalIgnoreCase) + 1);
vm.BitpayLink = $"bitcoin:?r=https://bitpay.com/i/{invoiceId}";
}
try
{
BitcoinUrlBuilder urlBuilder = new BitcoinUrlBuilder(vm.BitpayLink);
#pragma warning disable CS0618 // Type or member is obsolete
if (!urlBuilder.PaymentRequestUrl.DnsSafeHost.EndsWith("bitpay.com", StringComparison.OrdinalIgnoreCase))
{
throw new Exception("This tool only work with bitpay");
}
var client = HttpClientFactory.CreateClient();
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, urlBuilder.PaymentRequestUrl);
#pragma warning restore CS0618 // Type or member is obsolete
request.Headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/payment-request"));
var result = await client.SendAsync(request);
// {"network":"main","currency":"BTC","requiredFeeRate":29.834,"outputs":[{"amount":255900,"address":"1PgPo5d4swD6pKfCgoXtoW61zqTfX9H7tj"}],"time":"2018-12-03T14:39:47.162Z","expires":"2018-12-03T14:54:47.162Z","memo":"Payment request for BitPay invoice HHfG8cprRMzZG6MErCqbjv for merchant VULTR Holdings LLC","paymentUrl":"https://bitpay.com/i/HHfG8cprRMzZG6MErCqbjv","paymentId":"HHfG8cprRMzZG6MErCqbjv"}
var str = await result.Content.ReadAsStringAsync();
try
{
var jobj = JObject.Parse(str);
vm.Address = ((JArray)jobj["outputs"])[0]["address"].Value<string>();
var amount = Money.Satoshis(((JArray)jobj["outputs"])[0]["amount"].Value<long>());
vm.Amount = amount.ToString();
vm.BitcoinUri = $"bitcoin:{vm.Address}?amount={amount.ToString()}";
}
catch (JsonReaderException)
{
ModelState.AddModelError(nameof(vm.BitpayLink), $"Invalid or expired bitpay invoice");
return View(vm);
}
}
catch (Exception ex)
{
ModelState.AddModelError(nameof(vm.BitpayLink), $"Error while requesting {ex.Message}");
return View(vm);
}
return View(vm);
}
public IActionResult About()
{
ViewData["Message"] = "Your application description page.";

View File

@ -1,23 +1,20 @@
using BTCPayServer.Authentication;
using Microsoft.Extensions.Logging;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Filters;
using BTCPayServer.Logging;
using BTCPayServer.Models;
using BTCPayServer.Security;
using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using NBitpayClient;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Cors;
using BTCPayServer.Services.Stores;
namespace BTCPayServer.Controllers
{
[EnableCors("BitpayAPI")]
[BitpayAPIConstraint]
[Authorize(Policies.CanCreateInvoice.Key, AuthenticationSchemes = Policies.BitpayAuthentication)]
public class InvoiceControllerAPI : Controller
{
private InvoiceController _InvoiceController;
@ -36,22 +33,27 @@ 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, CancellationToken cancellationToken)
{
return await _InvoiceController.CreateInvoiceCore(invoice, HttpContext.GetStoreData(), HttpContext.Request.GetAbsoluteRoot());
if (invoice == null)
throw new BitpayHttpException(400, "Invalid invoice");
return await _InvoiceController.CreateInvoiceCore(invoice, HttpContext.GetStoreData(), HttpContext.Request.GetAbsoluteRoot(), cancellationToken: cancellationToken);
}
[HttpGet]
[Route("invoices/{id}")]
public async Task<DataWrapper<InvoiceResponse>> GetInvoice(string id, string token)
public async Task<DataWrapper<InvoiceResponse>> GetInvoice(string id)
{
var invoice = await _InvoiceRepository.GetInvoice(HttpContext.GetStoreData().Id, id);
var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery()
{
InvoiceId = id,
StoreId = new[] { HttpContext.GetStoreData().Id }
})).FirstOrDefault();
if (invoice == null)
throw new BitpayHttpException(404, "Object not found");
var resp = invoice.EntityToDTO(_NetworkProvider);
return new DataWrapper<InvoiceResponse>(resp);
}
[HttpGet]
[Route("invoices")]
public async Task<DataWrapper<InvoiceResponse[]>> GetInvoices(
@ -66,15 +68,15 @@ namespace BTCPayServer.Controllers
{
if (dateEnd != null)
dateEnd = dateEnd.Value + TimeSpan.FromDays(1); //Should include the end day
var query = new InvoiceQuery()
{
Count = limit,
Skip = offset,
EndDate = dateEnd,
StartDate = dateStart,
OrderId = orderId,
ItemCode = itemCode,
OrderId = orderId == null ? null : new[] { orderId },
ItemCode = itemCode == null ? null : new[] { itemCode },
Status = status == null ? null : new[] { status },
StoreId = new[] { this.HttpContext.GetStoreData().Id }
};

View File

@ -1,87 +0,0 @@
using BTCPayServer.Filters;
using Microsoft.Extensions.Logging;
using BTCPayServer.Logging;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBitcoin.Payment;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Payments;
namespace BTCPayServer.Controllers
{
public partial class InvoiceController
{
[HttpGet]
[Route("i/{invoiceId}/{cryptoCode?}")]
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest")]
public async Task<IActionResult> GetInvoiceRequest(string invoiceId, string cryptoCode = null)
{
if (cryptoCode == null)
cryptoCode = "BTC";
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
var network = _NetworkProvider.GetNetwork(cryptoCode);
var paymentMethodId = new PaymentMethodId(cryptoCode, Payments.PaymentTypes.BTCLike);
if (invoice == null || invoice.IsExpired() || network == null || !invoice.Support(paymentMethodId))
return NotFound();
var dto = invoice.EntityToDTO(_NetworkProvider);
var paymentMethod = dto.CryptoInfo.First(c => c.GetpaymentMethodId() == paymentMethodId);
PaymentRequest request = new PaymentRequest
{
DetailsVersion = 1
};
request.Details.Expires = invoice.ExpirationTime;
request.Details.Memo = invoice.ProductInformation.ItemDesc;
request.Details.Network = network.NBitcoinNetwork;
request.Details.Outputs.Add(new PaymentOutput() { Amount = paymentMethod.Due, Script = BitcoinAddress.Create(paymentMethod.Address, network.NBitcoinNetwork).ScriptPubKey });
request.Details.MerchantData = Encoding.UTF8.GetBytes(invoice.Id);
request.Details.Time = DateTimeOffset.UtcNow;
request.Details.PaymentUrl = new Uri(invoice.ServerUrl.WithTrailingSlash() + ($"i/{invoice.Id}"), UriKind.Absolute);
var store = await _StoreRepository.FindStore(invoice.StoreId);
if (store == null)
throw new BitpayHttpException(401, "Unknown store");
if (store.StoreCertificate != null)
{
try
{
request.Sign(store.StoreCertificate, PKIType.X509SHA256);
}
catch (Exception ex)
{
Logs.PayServer.LogWarning(ex, "Error while signing payment request");
}
}
return new PaymentRequestActionResult(request);
}
[HttpPost]
[Route("i/{invoiceId}", Order = 99)]
[Route("i/{invoiceId}/{cryptoCode}", Order = 99)]
[MediaTypeConstraint("application/bitcoin-payment")]
public async Task<IActionResult> PostPayment(string invoiceId, string cryptoCode = null)
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
if (cryptoCode == null)
cryptoCode = "BTC";
var network = _NetworkProvider.GetNetwork(cryptoCode);
if (network == null || invoice == null || invoice.IsExpired() || !invoice.Support(new PaymentMethodId(cryptoCode, Payments.PaymentTypes.BTCLike)))
return NotFound();
var wallet = _WalletProvider.GetWallet(network);
if (wallet == null)
return NotFound();
var payment = PaymentMessage.Load(Request.Body);
var unused = wallet.BroadcastTransactionsAsync(payment.Transactions);
await _InvoiceRepository.AddRefundsAsync(invoiceId, payment.RefundTo.Select(p => new TxOut(p.Amount, p.Script)).ToArray(), network.NBitcoinNetwork);
return new PaymentAckActionResult(payment.CreateACK(invoiceId + " is currently processing, thanks for your purchase..."));
}
}
}

View File

@ -1,28 +1,32 @@
using BTCPayServer.Data;
using BTCPayServer.Filters;
using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using NBitcoin;
using NBitpayClient;
using System;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
using System.Net.Mime;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Events;
using NBXplorer;
using BTCPayServer.Filters;
using BTCPayServer.Models;
using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Changelly;
using BTCPayServer.Payments.CoinSwitch;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Security;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Invoices.Export;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore.Internal;
using NBitcoin;
using NBitpayClient;
using NBXplorer;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Controllers
{
@ -30,127 +34,149 @@ namespace BTCPayServer.Controllers
{
[HttpGet]
[Route("invoices/{invoiceId}")]
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
public async Task<IActionResult> Invoice(string invoiceId)
{
var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery()
{
UserId = GetUserId(),
InvoiceId = invoiceId,
UserId = GetUserId(),
IncludeAddresses = true,
IncludeEvents = true
})).FirstOrDefault();
if (invoice == null)
return NotFound();
var dto = invoice.EntityToDTO(_NetworkProvider);
var prodInfo = invoice.ProductInformation;
var store = await _StoreRepository.FindStore(invoice.StoreId);
InvoiceDetailsModel model = new InvoiceDetailsModel()
var model = new InvoiceDetailsModel()
{
StoreName = store.StoreName,
StoreLink = Url.Action(nameof(StoresController.UpdateStore), "Stores", new { storeId = store.Id }),
Id = invoice.Id,
Status = invoice.Status,
TransactionSpeed = invoice.SpeedPolicy == SpeedPolicy.HighSpeed ? "high" : invoice.SpeedPolicy == SpeedPolicy.MediumSpeed ? "medium" : "low",
State = invoice.GetInvoiceState().ToString(),
TransactionSpeed = invoice.SpeedPolicy == SpeedPolicy.HighSpeed ? "high" :
invoice.SpeedPolicy == SpeedPolicy.MediumSpeed ? "medium" :
invoice.SpeedPolicy == SpeedPolicy.LowMediumSpeed ? "low-medium" :
"low",
RefundEmail = invoice.RefundMail,
CreatedDate = invoice.InvoiceTime,
ExpirationDate = invoice.ExpirationTime,
MonitoringDate = invoice.MonitoringExpiration,
OrderId = invoice.OrderId,
BuyerInformation = invoice.BuyerInformation,
Fiat = FormatCurrency((decimal)dto.Price, dto.Currency),
Fiat = _CurrencyNameTable.DisplayFormatCurrency(prodInfo.Price, prodInfo.Currency),
TaxIncluded = _CurrencyNameTable.DisplayFormatCurrency(prodInfo.TaxIncluded, prodInfo.Currency),
NotificationEmail = invoice.NotificationEmail,
NotificationUrl = invoice.NotificationURL,
RedirectUrl = invoice.RedirectURL,
ProductInformation = invoice.ProductInformation,
StatusException = invoice.ExceptionStatus,
Events = invoice.Events
Events = invoice.Events,
PosData = PosDataParser.ParsePosData(invoice.PosData),
StatusMessage = StatusMessage
};
foreach (var data in invoice.GetPaymentMethods(null))
{
var cryptoInfo = dto.CryptoInfo.First(o => o.GetpaymentMethodId() == data.GetId());
var accounting = data.Calculate();
var paymentMethodId = data.GetId();
var cryptoPayment = new InvoiceDetailsModel.CryptoPayment();
cryptoPayment.PaymentMethod = ToString(paymentMethodId);
cryptoPayment.Due = accounting.Due.ToString() + $" {paymentMethodId.CryptoCode}";
cryptoPayment.Paid = accounting.CryptoPaid.ToString() + $" {paymentMethodId.CryptoCode}";
cryptoPayment.Overpaid = (accounting.DueUncapped > Money.Zero ? Money.Zero : -accounting.DueUncapped).ToString() + $" {paymentMethodId.CryptoCode}";
var onchainMethod = data.GetPaymentMethodDetails() as Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod;
if (onchainMethod != null)
{
cryptoPayment.Address = onchainMethod.DepositAddress;
}
cryptoPayment.Rate = FormatCurrency(data);
cryptoPayment.PaymentUrl = cryptoInfo.PaymentUrls.BIP21;
model.CryptoPayments.Add(cryptoPayment);
}
var onChainPayments = invoice
.GetPayments()
.Select<PaymentEntity, Task<object>>(async payment =>
{
var paymentNetwork = _NetworkProvider.GetNetwork(payment.GetCryptoCode());
var paymentData = payment.GetCryptoPaymentData();
if (paymentData is Payments.Bitcoin.BitcoinLikePaymentData onChainPaymentData)
{
var m = new InvoiceDetailsModel.Payment();
m.Crypto = payment.GetPaymentMethodId().CryptoCode;
m.DepositAddress = onChainPaymentData.Output.ScriptPubKey.GetDestinationAddress(paymentNetwork.NBitcoinNetwork);
int confirmationCount = 0;
if ((onChainPaymentData.ConfirmationCount < paymentNetwork.MaxTrackedConfirmation && payment.Accounted)
&& (onChainPaymentData.Legacy || invoice.MonitoringExpiration < DateTimeOffset.UtcNow)) // The confirmation count in the paymentData is not up to date
{
confirmationCount = (await ((ExplorerClientProvider)_ServiceProvider.GetService(typeof(ExplorerClientProvider))).GetExplorerClient(payment.GetCryptoCode())?.GetTransactionAsync(onChainPaymentData.Outpoint.Hash))?.Confirmations ?? 0;
onChainPaymentData.ConfirmationCount = confirmationCount;
payment.SetCryptoPaymentData(onChainPaymentData);
await _InvoiceRepository.UpdatePayments(new List<PaymentEntity> { payment });
}
else
{
confirmationCount = onChainPaymentData.ConfirmationCount;
}
if (confirmationCount >= paymentNetwork.MaxTrackedConfirmation)
{
m.Confirmations = "At least " + (paymentNetwork.MaxTrackedConfirmation);
}
else
{
m.Confirmations = confirmationCount.ToString(CultureInfo.InvariantCulture);
}
m.TransactionId = onChainPaymentData.Outpoint.Hash.ToString();
m.ReceivedTime = payment.ReceivedTime;
m.TransactionLink = string.Format(CultureInfo.InvariantCulture, paymentNetwork.BlockExplorerLink, m.TransactionId);
m.Replaced = !payment.Accounted;
return m;
}
else
{
var lightningPaymentData = (Payments.Lightning.LightningLikePaymentData)paymentData;
return new InvoiceDetailsModel.OffChainPayment()
{
Crypto = paymentNetwork.CryptoCode,
BOLT11 = lightningPaymentData.BOLT11
};
}
})
.ToArray();
await Task.WhenAll(onChainPayments);
model.Addresses = invoice.HistoricalAddresses.Select(h => new InvoiceDetailsModel.AddressModel
{
Destination = h.GetAddress(),
PaymentMethod = ToString(h.GetPaymentMethodId()),
Current = !h.UnAssigned.HasValue
}).ToArray();
model.OnChainPayments = onChainPayments.Select(p => p.GetAwaiter().GetResult()).OfType<InvoiceDetailsModel.Payment>().ToList();
model.OffChainPayments = onChainPayments.Select(p => p.GetAwaiter().GetResult()).OfType<InvoiceDetailsModel.OffChainPayment>().ToList();
model.StatusMessage = StatusMessage;
var updateConfirmationCountIfNeeded = invoice
.GetPayments()
.Select<PaymentEntity, Task>(async payment =>
{
var paymentNetwork = _NetworkProvider.GetNetwork(payment.GetCryptoCode());
var paymentData = payment.GetCryptoPaymentData();
if (paymentData is Payments.Bitcoin.BitcoinLikePaymentData onChainPaymentData)
{
int confirmationCount = 0;
if ((onChainPaymentData.ConfirmationCount < paymentNetwork.MaxTrackedConfirmation && payment.Accounted)
&& (onChainPaymentData.Legacy || invoice.MonitoringExpiration < DateTimeOffset.UtcNow))
// The confirmation count in the paymentData is not up to date
{
confirmationCount = (await ((ExplorerClientProvider)_ServiceProvider.GetService(typeof(ExplorerClientProvider))).GetExplorerClient(payment.GetCryptoCode())?.GetTransactionAsync(onChainPaymentData.Outpoint.Hash))?.Confirmations ?? 0;
onChainPaymentData.ConfirmationCount = confirmationCount;
payment.SetCryptoPaymentData(onChainPaymentData);
await _InvoiceRepository.UpdatePayments(new List<PaymentEntity> { payment });
}
}
})
.ToArray();
await Task.WhenAll(updateConfirmationCountIfNeeded);
var details = InvoicePopulatePayments(invoice);
model.CryptoPayments = details.CryptoPayments;
model.OnChainPayments = details.OnChainPayments;
model.OffChainPayments = details.OffChainPayments;
return View(model);
}
private InvoiceDetailsModel InvoicePopulatePayments(InvoiceEntity invoice)
{
var model = new InvoiceDetailsModel();
foreach (var data in invoice.GetPaymentMethods(null))
{
var accounting = data.Calculate();
var paymentMethodId = data.GetId();
var cryptoPayment = new InvoiceDetailsModel.CryptoPayment();
cryptoPayment.PaymentMethod = ToString(paymentMethodId);
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)
{
cryptoPayment.Address = onchainMethod.DepositAddress;
}
cryptoPayment.Rate = ExchangeRate(data);
model.CryptoPayments.Add(cryptoPayment);
}
foreach (var payment in invoice.GetPayments())
{
var paymentNetwork = _NetworkProvider.GetNetwork(payment.GetCryptoCode());
var paymentData = payment.GetCryptoPaymentData();
if (paymentData is Payments.Bitcoin.BitcoinLikePaymentData onChainPaymentData)
{
var m = new InvoiceDetailsModel.Payment();
m.Crypto = payment.GetPaymentMethodId().CryptoCode;
m.DepositAddress = onChainPaymentData.GetDestination(paymentNetwork);
int confirmationCount = onChainPaymentData.ConfirmationCount;
if (confirmationCount >= paymentNetwork.MaxTrackedConfirmation)
{
m.Confirmations = "At least " + (paymentNetwork.MaxTrackedConfirmation);
}
else
{
m.Confirmations = confirmationCount.ToString(CultureInfo.InvariantCulture);
}
m.TransactionId = onChainPaymentData.Outpoint.Hash.ToString();
m.ReceivedTime = payment.ReceivedTime;
m.TransactionLink = string.Format(CultureInfo.InvariantCulture, paymentNetwork.BlockExplorerLink, m.TransactionId);
m.Replaced = !payment.Accounted;
model.OnChainPayments.Add(m);
}
else
{
var lightningPaymentData = (LightningLikePaymentData)paymentData;
model.OffChainPayments.Add(new InvoiceDetailsModel.OffChainPayment()
{
Crypto = paymentNetwork.CryptoCode,
BOLT11 = lightningPaymentData.BOLT11
});
}
}
return model;
}
private string ToString(PaymentMethodId paymentMethodId)
{
var type = paymentMethodId.PaymentType.ToString();
@ -172,42 +198,84 @@ namespace BTCPayServer.Controllers
[Route("invoice")]
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest", false)]
[XFrameOptionsAttribute(null)]
public async Task<IActionResult> Checkout(string invoiceId, string id = null, string paymentMethodId = null)
[ReferrerPolicyAttribute("origin")]
public async Task<IActionResult> Checkout(string invoiceId, string id = null, string paymentMethodId = null,
[FromQuery]string view = null)
{
//Keep compatibility with Bitpay
invoiceId = invoiceId ?? id;
id = invoiceId;
////
//
var model = await GetInvoiceModel(invoiceId, paymentMethodId);
var model = await GetInvoiceModel(invoiceId, paymentMethodId == null ? null : PaymentMethodId.Parse(paymentMethodId));
if (model == null)
return NotFound();
if (view == "modal")
model.IsModal = true;
_CSP.Add(new ConsentSecurityPolicy("script-src", "'unsafe-eval'")); // Needed by Vue
if (!string.IsNullOrEmpty(model.CustomCSSLink) &&
Uri.TryCreate(model.CustomCSSLink, UriKind.Absolute, out var uri))
{
_CSP.Clear();
}
if (!string.IsNullOrEmpty(model.CustomLogoLink) &&
Uri.TryCreate(model.CustomLogoLink, UriKind.Absolute, out uri))
{
_CSP.Clear();
}
return View(nameof(Checkout), model);
}
private async Task<PaymentModel> GetInvoiceModel(string invoiceId, string paymentMethodIdStr)
[HttpGet]
[Route("invoice-noscript")]
public async Task<IActionResult> CheckoutNoScript(string invoiceId, string id = null, string paymentMethodId = null)
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
//Keep compatibility with Bitpay
invoiceId = invoiceId ?? id;
id = invoiceId;
//
var model = await GetInvoiceModel(invoiceId, paymentMethodId == null ? null : PaymentMethodId.Parse(paymentMethodId));
if (model == null)
return NotFound();
return View(model);
}
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();
isDefaultCrypto = true;
paymentMethodId = store.GetDefaultPaymentId(_NetworkProvider);
isDefaultPaymentId = true;
}
var paymentMethodId = PaymentMethodId.Parse(paymentMethodIdStr);
var network = _NetworkProvider.GetNetwork(paymentMethodId.CryptoCode);
if (network == null && isDefaultPaymentId)
{
network = _NetworkProvider.GetAll().FirstOrDefault();
paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
}
if (invoice == null || network == null)
return null;
if (!invoice.Support(paymentMethodId))
{
if (!isDefaultCrypto)
if (!isDefaultPaymentId)
return null;
var paymentMethodTemp = invoice.GetPaymentMethods(_NetworkProvider).First();
var paymentMethodTemp = invoice.GetPaymentMethods(_NetworkProvider)
.Where(c => paymentMethodId.CryptoCode == c.GetId().CryptoCode)
.FirstOrDefault();
if (paymentMethodTemp == null)
paymentMethodTemp = invoice.GetPaymentMethods(_NetworkProvider).First();
network = paymentMethodTemp.Network;
paymentMethodId = paymentMethodTemp.GetId();
}
@ -219,29 +287,51 @@ namespace BTCPayServer.Controllers
var storeBlob = store.GetStoreBlob();
var currency = invoice.ProductInformation.Currency;
var accounting = paymentMethod.Calculate();
ChangellySettings changelly = (storeBlob.ChangellySettings != null && storeBlob.ChangellySettings.Enabled &&
storeBlob.ChangellySettings.IsConfigured())
? storeBlob.ChangellySettings
: null;
CoinSwitchSettings coinswitch = (storeBlob.CoinSwitchSettings != null && storeBlob.CoinSwitchSettings.Enabled &&
storeBlob.CoinSwitchSettings.IsConfigured())
? storeBlob.CoinSwitchSettings
: null;
var changellyAmountDue = changelly != null
? (accounting.Due.ToDecimal(MoneyUnit.BTC) *
(1m + (changelly.AmountMarkupPercentage / 100m)))
: (decimal?)null;
var model = new PaymentModel()
{
CryptoCode = network.CryptoCode,
RootPath = this.Request.PathBase.Value.WithTrailingSlash(),
PaymentMethodId = paymentMethodId.ToString(),
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-US",
DefaultLang = storeBlob.DefaultLang ?? "en",
HtmlTitle = storeBlob.HtmlTitle ?? "BTCPay Invoice",
CustomCSSLink = storeBlob.CustomCSS?.AbsoluteUri,
CustomLogoLink = storeBlob.CustomLogo?.AbsoluteUri,
LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi,
BtcAddress = paymentMethodDetails.GetPaymentDestination(),
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ToString(),
BtcDue = accounting.Due.ToString(),
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ToString(),
OrderAmountFiat = OrderAmountFromInvoice(network.CryptoCode, invoice.ProductInformation),
CustomerEmail = invoice.RefundMail,
RequiresRefundEmail = storeBlob.RequiresRefundEmail,
ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds),
MaxTimeSeconds = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalSeconds,
MaxTimeMinutes = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalMinutes,
ItemDesc = invoice.ProductInformation.ItemDesc,
Rate = FormatCurrency(paymentMethod),
Rate = ExchangeRate(paymentMethod),
MerchantRefLink = invoice.RedirectURL ?? "/",
RedirectAutomatically = invoice.RedirectAutomatically,
StoreName = store.StoreName,
InvoiceBitcoinUrl = paymentMethodId.PaymentType == PaymentTypes.BTCLike ? cryptoInfo.PaymentUrls.BIP21 :
paymentMethodId.PaymentType == PaymentTypes.LightningLike ? cryptoInfo.PaymentUrls.BOLT11 :
@ -252,20 +342,32 @@ namespace BTCPayServer.Controllers
throw new NotSupportedException(),
TxCount = accounting.TxRequired,
BtcPaid = accounting.Paid.ToString(),
Status = invoice.Status,
CryptoImage = "/" + GetImage(paymentMethodId, network),
NetworkFee = paymentMethodDetails.GetTxFee(),
#pragma warning disable CS0618 // Type or member is obsolete
Status = invoice.StatusString,
#pragma warning restore CS0618 // Type or member is obsolete
NetworkFee = paymentMethodDetails.GetNextNetworkFee(),
IsMultiCurrency = invoice.GetPayments().Select(p => p.GetPaymentMethodId()).Concat(new[] { paymentMethod.GetId() }).Distinct().Count() > 1,
AllowCoinConversion = storeBlob.AllowCoinConversion,
ChangellyEnabled = changelly != null,
ChangellyMerchantId = changelly?.ChangellyMerchantId,
ChangellyAmountDue = changellyAmountDue,
CoinSwitchEnabled = coinswitch != null,
CoinSwitchAmountMarkupPercentage = coinswitch?.AmountMarkupPercentage ?? 0,
CoinSwitchMerchantId = coinswitch?.MerchantId,
CoinSwitchMode = coinswitch?.Mode,
StoreId = store.Id,
AvailableCryptos = invoice.GetPaymentMethods(_NetworkProvider)
.Where(i => i.Network != null)
.Select(kv => new PaymentModel.AvailableCrypto()
{
PaymentMethodId = kv.GetId().ToString(),
CryptoImage = "/" + GetImage(kv.GetId(), kv.Network),
CryptoCode = kv.GetId().CryptoCode,
PaymentMethodName = GetDisplayName(kv.GetId(), kv.Network),
IsLightning = kv.GetId().PaymentType == PaymentTypes.LightningLike,
CryptoImage = GetImage(kv.GetId(), kv.Network),
Link = Url.Action(nameof(Checkout), new { invoiceId = invoiceId, paymentMethodId = kv.GetId().ToString() })
}).Where(c => c.CryptoImage != "/")
.ToList()
.OrderByDescending(a => a.CryptoCode == "BTC").ThenBy(a => a.PaymentMethodName).ThenBy(a => a.IsLightning ? 1 : 0)
.ToList()
};
var expiration = TimeSpan.FromSeconds(model.ExpirationSeconds);
@ -273,27 +375,41 @@ namespace BTCPayServer.Controllers
return model;
}
private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network)
private string GetDisplayName(PaymentMethodId paymentMethodId, BTCPayNetwork network)
{
return (paymentMethodId.PaymentType == PaymentTypes.BTCLike ? Url.Content(network.CryptoImagePath) : Url.Content(network.LightningImagePath));
return paymentMethodId.PaymentType == PaymentTypes.BTCLike ?
network.DisplayName : network.DisplayName + " (Lightning)";
}
private string FormatCurrency(PaymentMethod paymentMethod)
private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network)
{
return paymentMethodId.PaymentType == PaymentTypes.BTCLike ?
this.Request.GetRelativePathOrAbsolute(network.CryptoImagePath) : this.Request.GetRelativePathOrAbsolute(network.LightningImagePath);
}
private string OrderAmountFromInvoice(string cryptoCode, ProductInformation productInformation)
{
// if invoice source currency is the same as currently display currency, no need for "order amount from invoice"
if (cryptoCode == productInformation.Currency)
return null;
return _CurrencyNameTable.DisplayFormatCurrency(productInformation.Price, productInformation.Currency);
}
private string ExchangeRate(PaymentMethod paymentMethod)
{
string currency = paymentMethod.ParentEntity.ProductInformation.Currency;
return FormatCurrency(paymentMethod.Rate, currency);
}
public string FormatCurrency(decimal price, string currency)
{
return price.ToString("C", _CurrencyNameTable.GetCurrencyProvider(currency)) + $" ({currency})";
return _CurrencyNameTable.DisplayFormatCurrency(paymentMethod.Rate, currency);
}
[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);
@ -301,12 +417,16 @@ 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)
return NotFound();
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
if (invoice == null || invoice.Status == "complete" || invoice.Status == "invalid" || invoice.Status == "expired")
var invoice = await _InvoiceRepository.GetInvoice(invoiceId);
if (invoice == null || invoice.Status == InvoiceStatus.Complete || invoice.Status == InvoiceStatus.Invalid || invoice.Status == InvoiceStatus.Expired)
return NotFound();
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
CompositeDisposable leases = new CompositeDisposable();
@ -314,7 +434,7 @@ namespace BTCPayServer.Controllers
{
leases.Add(_EventAggregator.Subscribe<Events.InvoiceDataChangedEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
leases.Add(_EventAggregator.Subscribe<Events.InvoiceNewAddressEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
leases.Add(_EventAggregator.Subscribe<Events.InvoiceEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
leases.Add(_EventAggregator.Subscribe<Events.InvoiceEvent>(async o => await NotifySocket(webSocket, o.Invoice.Id, invoiceId)));
while (true)
{
var message = await webSocket.ReceiveAsync(DummyBuffer, default(CancellationToken));
@ -346,6 +466,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)
@ -353,49 +474,95 @@ namespace BTCPayServer.Controllers
return BadRequest(ModelState);
}
await _InvoiceRepository.UpdateInvoice(invoiceId, data).ConfigureAwait(false);
return Ok();
return Ok("{}");
}
[HttpGet]
[Route("invoices")]
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
[BitpayAPIConstraint(false)]
public async Task<IActionResult> ListInvoices(string searchTerm = null, int skip = 0, int count = 50)
public async Task<IActionResult> ListInvoices(string searchTerm = null, int skip = 0, int count = 50, int timezoneOffset = 0)
{
var model = new InvoicesModel();
var filterString = new SearchString(searchTerm);
foreach (var invoice in await _InvoiceRepository.GetInvoices(new InvoiceQuery()
var model = new InvoicesModel
{
TextSearch = filterString.TextSearch,
Count = count,
SearchTerm = searchTerm,
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
}))
Count = count,
StatusMessage = StatusMessage,
TimezoneOffset = timezoneOffset
};
InvoiceQuery invoiceQuery = GetInvoiceQuery(searchTerm, timezoneOffset);
var counting = _InvoiceRepository.GetInvoicesTotal(invoiceQuery);
invoiceQuery.Count = count;
invoiceQuery.Skip = skip;
var list = await _InvoiceRepository.GetInvoices(invoiceQuery);
foreach (var invoice in list)
{
model.SearchTerm = searchTerm;
var state = invoice.GetInvoiceState();
model.Invoices.Add(new InvoiceModel()
{
Status = invoice.Status + (invoice.ExceptionStatus == null ? string.Empty : $" ({invoice.ExceptionStatus})"),
ShowCheckout = invoice.Status == "new",
Date = (DateTimeOffset.UtcNow - invoice.InvoiceTime).Prettify() + " ago",
Status = invoice.Status,
StatusString = state.ToString(),
ShowCheckout = invoice.Status == InvoiceStatus.New,
Date = invoice.InvoiceTime,
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(),
Details = InvoicePopulatePayments(invoice)
});
}
model.Skip = skip;
model.Count = count;
model.StatusMessage = StatusMessage;
model.Total = await counting;
return View(model);
}
private InvoiceQuery GetInvoiceQuery(string searchTerm = null, int timezoneOffset = 0)
{
var fs = new SearchString(searchTerm);
var invoiceQuery = new InvoiceQuery()
{
TextSearch = fs.TextSearch,
UserId = GetUserId(),
Unusual = fs.GetFilterBool("unusual"),
Status = fs.GetFilterArray("status"),
ExceptionStatus = fs.GetFilterArray("exceptionstatus"),
StoreId = fs.GetFilterArray("storeid"),
ItemCode = fs.GetFilterArray("itemcode"),
OrderId = fs.GetFilterArray("orderid"),
StartDate = fs.GetFilterDate("startdate", timezoneOffset),
EndDate = fs.GetFilterDate("enddate", timezoneOffset)
};
return invoiceQuery;
}
[HttpGet]
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
[BitpayAPIConstraint(false)]
public async Task<IActionResult> Export(string format, string searchTerm = null, int timezoneOffset = 0)
{
var model = new InvoiceExport(_NetworkProvider, _CurrencyNameTable);
InvoiceQuery invoiceQuery = GetInvoiceQuery(searchTerm, timezoneOffset);
invoiceQuery.Skip = 0;
invoiceQuery.Count = int.MaxValue;
var invoices = await _InvoiceRepository.GetInvoices(invoiceQuery);
var res = model.Process(invoices, format);
var cd = new ContentDisposition
{
FileName = $"btcpay-export-{DateTime.UtcNow.ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture)}.{format}",
Inline = true
};
Response.Headers.Add("Content-Disposition", cd.ToString());
Response.Headers.Add("X-Content-Type-Options", "nosniff");
return Content(res, "application/" + format);
}
[HttpGet]
[Route("invoices/create")]
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
@ -408,19 +575,38 @@ namespace BTCPayServer.Controllers
StatusMessage = "Error: You need to create at least one store before creating a transaction";
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
}
return View(new CreateInvoiceModel() { Stores = stores });
var paymentMethods = new SelectList(_NetworkProvider.GetAll().SelectMany(network => new[]
{
new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike),
new PaymentMethodId(network.CryptoCode, PaymentTypes.LightningLike)
}).Select(id => new SelectListItem(id.ToString(true), id.ToString(false))),
nameof(SelectListItem.Value),
nameof(SelectListItem.Text));
return View(new CreateInvoiceModel() { Stores = stores, AvailablePaymentMethods = paymentMethods });
}
[HttpPost]
[Route("invoices/create")]
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
[BitpayAPIConstraint(false)]
public async Task<IActionResult> CreateInvoice(CreateInvoiceModel model)
public async Task<IActionResult> CreateInvoice(CreateInvoiceModel model, CancellationToken cancellationToken)
{
var stores = await _StoreRepository.GetStoresByUserId(GetUserId());
model.Stores = new SelectList(stores, nameof(StoreData.Id), nameof(StoreData.StoreName), model.StoreId);
var paymentMethods = new SelectList(_NetworkProvider.GetAll().SelectMany(network => new[]
{
new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike),
new PaymentMethodId(network.CryptoCode, PaymentTypes.LightningLike)
}).Select(id => new SelectListItem(id.ToString(true), id.ToString(false))),
nameof(SelectListItem.Value),
nameof(SelectListItem.Text));
model.AvailablePaymentMethods = paymentMethods;
var store = stores.FirstOrDefault(s => s.Id == model.StoreId);
if(store == null)
if (store == null)
{
ModelState.AddModelError(nameof(model.StoreId), "Store not found");
}
@ -429,7 +615,7 @@ namespace BTCPayServer.Controllers
return View(model);
}
StatusMessage = null;
if (!store.HasClaim(Policies.CanModifyStoreSettings.Key))
if (!store.HasClaim(Policies.CanCreateInvoice.Key))
{
ModelState.AddModelError(nameof(model.StoreId), "You need to be owner of this store to create an invoice");
return View(model);
@ -441,6 +627,7 @@ namespace BTCPayServer.Controllers
return View(model);
}
if (StatusMessage != null)
{
return RedirectToAction(nameof(StoresController.UpdateStore), "Stores", new
@ -451,18 +638,23 @@ namespace BTCPayServer.Controllers
try
{
var result = await CreateInvoiceCore(new Invoice()
var result = await CreateInvoiceCore(new CreateInvoiceRequest()
{
Price = model.Amount.Value,
Currency = model.Currency,
PosData = model.PosData,
OrderId = model.OrderId,
//RedirectURL = redirect + "redirect",
NotificationEmail = model.NotificationEmail,
NotificationURL = model.NotificationUrl,
ItemDesc = model.ItemDesc,
FullNotifications = true,
BuyerEmail = model.BuyerEmail,
}, store, HttpContext.Request.GetAbsoluteRoot());
SupportedTransactionCurrencies = model.SupportedTransactionCurrencies?.ToDictionary(s => s, s => new InvoiceSupportedTransactionCurrency()
{
Enabled = true
})
}, store, HttpContext.Request.GetAbsoluteRoot(), cancellationToken: cancellationToken);
StatusMessage = $"Invoice {result.Data.Id} just created!";
return RedirectToAction(nameof(ListInvoices));
@ -475,27 +667,45 @@ namespace BTCPayServer.Controllers
}
[HttpPost]
[Route("invoices/{invoiceId}/changestate/{newState}")]
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
[BitpayAPIConstraint(false)]
public IActionResult SearchInvoice(InvoicesModel invoices)
public async Task<IActionResult> ChangeInvoiceState(string invoiceId, string newState)
{
return RedirectToAction(nameof(ListInvoices), new
var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery()
{
searchTerm = invoices.SearchTerm,
skip = invoices.Skip,
count = invoices.Count,
});
InvoiceId = invoiceId,
UserId = GetUserId()
})).FirstOrDefault();
var model = new InvoiceStateChangeModel();
if (invoice == null)
{
model.NotFound = true;
return NotFound(model);
}
if (newState == "invalid")
{
await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoiceId);
_EventAggregator.Publish(new InvoiceEvent(invoice, 1008, InvoiceEvent.MarkedInvalid));
model.StatusString = new InvoiceState("invalid", "marked").ToString();
}
else if (newState == "complete")
{
await _InvoiceRepository.UpdatePaidInvoiceToComplete(invoiceId);
_EventAggregator.Publish(new InvoiceEvent(invoice, 2008, InvoiceEvent.MarkedCompleted));
model.StatusString = new InvoiceState("complete", "marked").ToString();
}
return Json(model);
}
[HttpPost]
[Route("invoices/invalidatepaid")]
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
[BitpayAPIConstraint(false)]
public async Task<IActionResult> InvalidatePaidInvoice(string invoiceId)
public class InvoiceStateChangeModel
{
await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoiceId);
_EventAggregator.Publish(new InvoiceEvent(invoiceId, 1008, "invoice_markedInvalid"));
return RedirectToAction(nameof(ListInvoices));
public bool NotFound { get; set; }
public string StatusString { get; set; }
}
[TempData]
@ -509,5 +719,49 @@ namespace BTCPayServer.Controllers
{
return _UserManager.GetUserId(User);
}
public class PosDataParser
{
public static Dictionary<string, object> ParsePosData(string posData)
{
var result = new Dictionary<string, object>();
if (string.IsNullOrEmpty(posData))
{
return result;
}
try
{
var jObject = JObject.Parse(posData);
foreach (var item in jObject)
{
switch (item.Value.Type)
{
case JTokenType.Array:
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());
break;
}
}
}
catch
{
result.Add(string.Empty, posData);
}
return result;
}
}
}
}

View File

@ -1,53 +1,36 @@
using BTCPayServer.Authentication;
using System.Reflection;
using System.Linq;
using Microsoft.Extensions.Logging;
using BTCPayServer.Logging;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NBitpayClient;
using System;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Models;
using Newtonsoft.Json;
using System.Globalization;
using NBitcoin;
using NBitcoin.DataEncoders;
using BTCPayServer.Filters;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using System.Net;
using Microsoft.AspNetCore.Identity;
using Newtonsoft.Json.Linq;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using NBitcoin.Payment;
using BTCPayServer.Data;
using BTCPayServer.Models.InvoicingModels;
using System.Security.Claims;
using BTCPayServer.Services;
using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Wallets;
using BTCPayServer.Validations;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Mvc.Routing;
using NBXplorer.DerivationStrategy;
using NBXplorer;
using BTCPayServer.HostedServices;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Logging;
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;
using BTCPayServer.Services.Wallets;
using BTCPayServer.Validation;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBitpayClient;
using Newtonsoft.Json;
namespace BTCPayServer.Controllers
{
public partial class InvoiceController : Controller
{
InvoiceRepository _InvoiceRepository;
BTCPayRateProviderFactory _RateProvider;
ContentSecurityPolicies _CSP;
RateFetcher _RateProvider;
StoreRepository _StoreRepository;
UserManager<ApplicationUser> _UserManager;
private CurrencyNameTable _CurrencyNameTable;
@ -60,10 +43,11 @@ namespace BTCPayServer.Controllers
InvoiceRepository invoiceRepository,
CurrencyNameTable currencyNameTable,
UserManager<ApplicationUser> userManager,
BTCPayRateProviderFactory rateProvider,
RateFetcher rateProvider,
StoreRepository storeRepository,
EventAggregator eventAggregator,
BTCPayWalletProvider walletProvider,
ContentSecurityPolicies csp,
BTCPayNetworkProvider networkProvider)
{
_ServiceProvider = serviceProvider;
@ -75,20 +59,24 @@ namespace BTCPayServer.Controllers
_EventAggregator = eventAggregator;
_NetworkProvider = networkProvider;
_WalletProvider = walletProvider;
_CSP = csp;
}
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, CancellationToken cancellationToken = default)
{
if (!store.HasClaim(Policies.CanCreateInvoice.Key))
throw new UnauthorizedAccessException();
InvoiceLogs logs = new InvoiceLogs();
logs.Write("Creation of invoice starting");
var entity = new InvoiceEntity
{
Version = InvoiceEntity.Lastest_Version,
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);
@ -96,9 +84,18 @@ namespace BTCPayServer.Controllers
entity.ServerUrl = serverUrl;
entity.FullNotifications = invoice.FullNotifications || invoice.ExtendedNotifications;
entity.ExtendedNotifications = invoice.ExtendedNotifications;
entity.NotificationURL = notificationUri?.AbsoluteUri;
entity.BuyerInformation = Map<Invoice, BuyerInformation>(invoice);
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<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)
@ -107,17 +104,51 @@ namespace BTCPayServer.Controllers
throw new BitpayHttpException(400, "Invalid email");
entity.RefundMail = entity.BuyerInformation.BuyerEmail;
}
entity.ProductInformation = Map<Invoice, ProductInformation>(invoice);
entity.RedirectURL = invoice.RedirectURL ?? store.StoreWebsite;
entity.Status = "new";
entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy);
var taxIncluded = invoice.TaxIncluded.HasValue ? invoice.TaxIncluded.Value : 0m;
var currencyInfo = _CurrencyNameTable.GetNumberFormatInfo(invoice.Currency, false);
if (currencyInfo != null)
{
int divisibility = currencyInfo.CurrencyDecimalDigits;
invoice.Price = invoice.Price.RoundToSignificant(ref divisibility);
divisibility = currencyInfo.CurrencyDecimalDigits;
invoice.TaxIncluded = taxIncluded.RoundToSignificant(ref divisibility);
}
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;
entity.RedirectAutomatically =
invoice.RedirectAutomatically.GetValueOrDefault(storeBlob.RedirectAutomatically);
entity.Status = InvoiceStatus.New;
entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy);
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)
.Select(c => _NetworkProvider.GetNetwork(c.PaymentId.CryptoCode))
.Where(s => !excludeFilter.Match(s.PaymentId))
.Select(c => _NetworkProvider.GetNetwork(c.PaymentId.CryptoCode))
.Where(c => c != null))
{
currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, invoice.Currency));
@ -128,9 +159,10 @@ namespace BTCPayServer.Controllers
}
var rateRules = storeBlob.GetRateRules(_NetworkProvider);
var fetchingByCurrencyPair = _RateProvider.FetchRates(currencyPairsToFetch, rateRules);
var fetchingByCurrencyPair = _RateProvider.FetchRates(currencyPairsToFetch, rateRules, cancellationToken);
var fetchingAll = WhenAllFetched(logs, fetchingByCurrencyPair);
var supportedPaymentMethods = store.GetSupportedPaymentMethods(_NetworkProvider)
.Where(s => !excludeFilter.Match(s.PaymentId))
.Select(c =>
(Handler: (IPaymentMethodHandler)_ServiceProvider.GetService(typeof(IPaymentMethodHandler<>).MakeGenericType(c.GetType())),
SupportedPaymentMethod: c,
@ -138,65 +170,26 @@ namespace BTCPayServer.Controllers
.Where(c => c.Network != null)
.Select(o =>
(SupportedPaymentMethod: o.SupportedPaymentMethod,
PaymentMethod: CreatePaymentMethodAsync(fetchingByCurrencyPair, o.Handler, o.SupportedPaymentMethod, o.Network, entity, store)))
PaymentMethod: CreatePaymentMethodAsync(fetchingByCurrencyPair, o.Handler, o.SupportedPaymentMethod, o.Network, entity, store, logs)))
.ToList();
List<string> paymentMethodErrors = new List<string>();
List<ISupportedPaymentMethod> supported = new List<ISupportedPaymentMethod>();
var paymentMethods = new PaymentMethodDictionary();
foreach(var pair in fetchingByCurrencyPair)
{
var rateResult = await pair.Value;
bool hasError = false;
if(rateResult.Errors.Count != 0)
{
var allRateRuleErrors = string.Join(", ", rateResult.Errors.ToArray());
paymentMethodErrors.Add($"{pair.Key}: Rate rule error ({allRateRuleErrors})");
hasError = true;
}
if(rateResult.ExchangeExceptions.Count != 0)
{
foreach(var ex in rateResult.ExchangeExceptions)
{
paymentMethodErrors.Add($"{pair.Key}: Exception reaching exchange {ex.ExchangeName} ({ex.Exception.Message})");
}
hasError = true;
}
if(hasError)
{
paymentMethodErrors.Add($"{pair.Key}: The rule is {rateResult.Rule}");
paymentMethodErrors.Add($"{pair.Key}: Evaluated rule is {rateResult.EvaluatedRule}");
}
}
foreach (var o in supportedPaymentMethods)
{
try
{
var paymentMethod = await o.PaymentMethod;
if (paymentMethod == null)
throw new PaymentMethodUnavailableException("Payment method unavailable");
supported.Add(o.SupportedPaymentMethod);
paymentMethods.Add(paymentMethod);
}
catch (PaymentMethodUnavailableException ex)
{
paymentMethodErrors.Add($"{o.SupportedPaymentMethod.PaymentId.CryptoCode} ({o.SupportedPaymentMethod.PaymentId.PaymentType}): Payment method unavailable ({ex.Message})");
}
catch (Exception ex)
{
paymentMethodErrors.Add($"{o.SupportedPaymentMethod.PaymentId.CryptoCode} ({o.SupportedPaymentMethod.PaymentId.PaymentType}): Unexpected exception ({ex.ToString()})");
}
var paymentMethod = await o.PaymentMethod;
if (paymentMethod == null)
continue;
supported.Add(o.SupportedPaymentMethod);
paymentMethods.Add(paymentMethod);
}
if (supported.Count == 0)
{
StringBuilder errors = new StringBuilder();
errors.AppendLine("No payment method available for this store");
foreach (var error in paymentMethodErrors)
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);
errors.AppendLine(error.ToString());
}
throw new BitpayHttpException(400, errors.ToString());
}
@ -204,71 +197,130 @@ namespace BTCPayServer.Controllers
entity.SetSupportedPaymentMethods(supported);
entity.SetPaymentMethods(paymentMethods);
entity.PosData = invoice.PosData;
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, paymentMethodErrors, _NetworkProvider);
_EventAggregator.Publish(new Events.InvoiceEvent(entity, 1001, "invoice_created"));
foreach (var app in await getAppsTaggingStore)
{
entity.InternalTags.Add(AppService.GetAppInternalTag(app.Id));
}
using (logs.Measure("Saving invoice"))
{
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, _NetworkProvider);
}
_ = Task.Run(async () =>
{
try
{
await fetchingAll;
}
catch (AggregateException ex)
{
ex.Handle(e => { logs.Write($"Error while fetching rates {ex}"); return true; });
}
await _InvoiceRepository.AddInvoiceLogs(entity.Id, logs);
});
_EventAggregator.Publish(new Events.InvoiceEvent(entity, 1001, InvoiceEvent.Created));
var resp = entity.EntityToDTO(_NetworkProvider);
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
}
private async Task<PaymentMethod> CreatePaymentMethodAsync(Dictionary<CurrencyPair, Task<RateResult>> fetchingByCurrencyPair, IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network, InvoiceEntity entity, StoreData store)
private Task WhenAllFetched(InvoiceLogs logs, Dictionary<CurrencyPair, Task<RateResult>> fetchingByCurrencyPair)
{
var storeBlob = store.GetStoreBlob();
var rate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, entity.ProductInformation.Currency)];
if (rate.Value == null)
return null;
PaymentMethod paymentMethod = new PaymentMethod();
paymentMethod.ParentEntity = entity;
paymentMethod.Network = network;
paymentMethod.SetId(supportedPaymentMethod.PaymentId);
paymentMethod.Rate = rate.Value.Value;
var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, store, network);
if (storeBlob.NetworkFeeDisabled)
paymentDetails.SetNoTxFee();
paymentMethod.SetPaymentMethodDetails(paymentDetails);
Func<Money, Money, bool> compare = null;
CurrencyValue limitValue = null;
string errorMessage = null;
if (supportedPaymentMethod.PaymentId.PaymentType == PaymentTypes.LightningLike &&
storeBlob.LightningMaxValue != null)
return Task.WhenAll(fetchingByCurrencyPair.Select(async pair =>
{
compare = (a, b) => a > b;
limitValue = storeBlob.LightningMaxValue;
errorMessage = "The amount of the invoice is too high to be paid with lightning";
}
else if (supportedPaymentMethod.PaymentId.PaymentType == PaymentTypes.BTCLike &&
storeBlob.OnChainMinValue != null)
{
compare = (a, b) => a < b;
limitValue = storeBlob.OnChainMinValue;
errorMessage = "The amount of the invoice is too low to be paid on chain";
}
if (compare != null)
{
var limitValueRate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, limitValue.Currency)];
if (limitValueRate.Value.HasValue)
var rateResult = await pair.Value;
logs.Write($"{pair.Key}: The rating rule is {rateResult.Rule}");
logs.Write($"{pair.Key}: The evaluated rating rule is {rateResult.EvaluatedRule}");
if (rateResult.Errors.Count != 0)
{
var limitValueCrypto = Money.Coins(limitValue.Value / limitValueRate.Value.Value);
if (compare(paymentMethod.Calculate().Due, limitValueCrypto))
var allRateRuleErrors = string.Join(", ", rateResult.Errors.ToArray());
logs.Write($"{pair.Key}: Rate rule error ({allRateRuleErrors})");
}
foreach (var ex in rateResult.ExchangeExceptions)
{
logs.Write($"{pair.Key}: Exception reaching exchange {ex.ExchangeName} ({ex.Exception.Message})");
}
}).ToArray());
}
private async Task<PaymentMethod> CreatePaymentMethodAsync(Dictionary<CurrencyPair, Task<RateResult>> fetchingByCurrencyPair, IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network, InvoiceEntity entity, StoreData store, InvoiceLogs logs)
{
try
{
var logPrefix = $"{supportedPaymentMethod.PaymentId.ToString(true)}:";
var storeBlob = store.GetStoreBlob();
var preparePayment = handler.PreparePayment(supportedPaymentMethod, store, network);
var rate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, entity.ProductInformation.Currency)];
if (rate.BidAsk == null)
{
return null;
}
PaymentMethod paymentMethod = new PaymentMethod();
paymentMethod.ParentEntity = entity;
paymentMethod.Network = network;
paymentMethod.SetId(supportedPaymentMethod.PaymentId);
paymentMethod.Rate = rate.BidAsk.Bid;
paymentMethod.PreferOnion = this.Request.IsOnion();
using (logs.Measure($"{logPrefix} Payment method details creation"))
{
var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, store, network, preparePayment);
paymentMethod.SetPaymentMethodDetails(paymentDetails);
}
Func<Money, Money, bool> compare = null;
CurrencyValue limitValue = null;
string errorMessage = null;
if (supportedPaymentMethod.PaymentId.PaymentType == PaymentTypes.LightningLike &&
storeBlob.LightningMaxValue != null)
{
compare = (a, b) => a > b;
limitValue = storeBlob.LightningMaxValue;
errorMessage = "The amount of the invoice is too high to be paid with lightning";
}
else if (supportedPaymentMethod.PaymentId.PaymentType == PaymentTypes.BTCLike &&
storeBlob.OnChainMinValue != null)
{
compare = (a, b) => a < b;
limitValue = storeBlob.OnChainMinValue;
errorMessage = "The amount of the invoice is too low to be paid on chain";
}
if (compare != null)
{
var limitValueRate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, limitValue.Currency)];
if (limitValueRate.BidAsk != null)
{
throw new PaymentMethodUnavailableException(errorMessage);
var limitValueCrypto = Money.Coins(limitValue.Value / limitValueRate.BidAsk.Bid);
if (compare(paymentMethod.Calculate().Due, limitValueCrypto))
{
logs.Write($"{logPrefix} {errorMessage}");
return null;
}
}
}
}
///////////////
///////////////
#pragma warning disable CS0618
if (paymentMethod.GetId().IsBTCOnChain)
{
entity.TxFee = paymentMethod.TxFee;
entity.Rate = paymentMethod.Rate;
entity.DepositAddress = paymentMethod.DepositAddress;
}
if (paymentMethod.GetId().IsBTCOnChain)
{
entity.TxFee = paymentMethod.NextNetworkFee;
entity.Rate = paymentMethod.Rate;
entity.DepositAddress = paymentMethod.DepositAddress;
}
#pragma warning restore CS0618
return paymentMethod;
return paymentMethod;
}
catch (PaymentMethodUnavailableException ex)
{
logs.Write($"{supportedPaymentMethod.PaymentId.CryptoCode}: Payment method unavailable ({ex.Message})");
}
catch (Exception ex)
{
logs.Write($"{supportedPaymentMethod.PaymentId.CryptoCode}: Unexpected exception ({ex.ToString()})");
}
return null;
}
private SpeedPolicy ParseSpeedPolicy(string transactionSpeed, SpeedPolicy defaultPolicy)
@ -277,6 +329,7 @@ namespace BTCPayServer.Controllers
return defaultPolicy;
var mappings = new Dictionary<string, SpeedPolicy>();
mappings.Add("low", SpeedPolicy.LowSpeed);
mappings.Add("low-medium", SpeedPolicy.LowMediumSpeed);
mappings.Add("medium", SpeedPolicy.MediumSpeed);
mappings.Add("high", SpeedPolicy.HighSpeed);
if (!mappings.TryGetValue(transactionSpeed, out SpeedPolicy policy))

View File

@ -0,0 +1,68 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Controllers
{
public class Macaroons
{
public class Macaroon
{
public Macaroon(byte[] bytes)
{
Bytes = bytes;
Hex = NBitcoin.DataEncoders.Encoders.Hex.EncodeData(bytes);
}
public string Hex { get; set; }
public byte[] Bytes { get; set; }
}
public static async Task<Macaroons> GetFromDirectoryAsync(string directoryPath)
{
if (directoryPath == null)
throw new ArgumentNullException(nameof(directoryPath));
Macaroons macaroons = new Macaroons();
if (!Directory.Exists(directoryPath))
throw new DirectoryNotFoundException("Macaroons directory not found");
foreach(var file in Directory.GetFiles(directoryPath, "*.macaroon"))
{
try
{
switch (Path.GetFileName(file))
{
case "admin.macaroon":
macaroons.AdminMacaroon = new Macaroon(await File.ReadAllBytesAsync(file));
break;
case "readonly.macaroon":
macaroons.ReadonlyMacaroon = new Macaroon(await File.ReadAllBytesAsync(file));
break;
case "invoice.macaroon":
macaroons.InvoiceMacaroon = new Macaroon(await File.ReadAllBytesAsync(file));
break;
default:
break;
}
}
catch { }
}
return macaroons;
}
public Macaroons Clone()
{
return new Macaroons()
{
AdminMacaroon = AdminMacaroon,
InvoiceMacaroon = InvoiceMacaroon,
ReadonlyMacaroon = ReadonlyMacaroon
};
}
public Macaroon ReadonlyMacaroon { get; set; }
public Macaroon InvoiceMacaroon { get; set; }
public Macaroon AdminMacaroon { get; set; }
}
}

View File

@ -0,0 +1,205 @@
using System;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Models.ManageViewModels;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace BTCPayServer.Controllers
{
public partial class ManageController
{
private const string AuthenicatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6";
[HttpGet]
public async Task<IActionResult> TwoFactorAuthentication()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var model = new TwoFactorAuthenticationViewModel
{
HasAuthenticator = await _userManager.GetAuthenticatorKeyAsync(user) != null,
Is2faEnabled = user.TwoFactorEnabled,
RecoveryCodesLeft = await _userManager.CountRecoveryCodesAsync(user),
};
return View(model);
}
[HttpGet]
public async Task<IActionResult> Disable2faWarning()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
if (!user.TwoFactorEnabled)
{
throw new ApplicationException(
$"Unexpected error occurred disabling 2FA for user with ID '{user.Id}'.");
}
return View(nameof(Disable2fa));
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Disable2fa()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var disable2faResult = await _userManager.SetTwoFactorEnabledAsync(user, false);
if (!disable2faResult.Succeeded)
{
throw new ApplicationException(
$"Unexpected error occurred disabling 2FA for user with ID '{user.Id}'.");
}
_logger.LogInformation("User with ID {UserId} has disabled 2fa.", user.Id);
return RedirectToAction(nameof(TwoFactorAuthentication));
}
[HttpGet]
public async Task<IActionResult> EnableAuthenticator()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
if (string.IsNullOrEmpty(unformattedKey))
{
await _userManager.ResetAuthenticatorKeyAsync(user);
unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
}
var model = new EnableAuthenticatorViewModel
{
SharedKey = FormatKey(unformattedKey),
AuthenticatorUri = GenerateQrCodeUri(user.Email, unformattedKey)
};
return View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> EnableAuthenticator(EnableAuthenticatorViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
// Strip spaces and hypens
var verificationCode = model.Code.Replace(" ", string.Empty, StringComparison.InvariantCulture)
.Replace("-", string.Empty, StringComparison.InvariantCulture);
var is2faTokenValid = await _userManager.VerifyTwoFactorTokenAsync(
user, _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode);
if (!is2faTokenValid)
{
ModelState.AddModelError(nameof(model.Code), "Verification code is invalid.");
return View(model);
}
await _userManager.SetTwoFactorEnabledAsync(user, true);
_logger.LogInformation("User with ID {UserId} has enabled 2FA with an authenticator app.", user.Id);
return RedirectToAction(nameof(GenerateRecoveryCodes));
}
[HttpGet]
public IActionResult ResetAuthenticatorWarning()
{
return View(nameof(ResetAuthenticator));
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ResetAuthenticator()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
await _userManager.SetTwoFactorEnabledAsync(user, false);
await _userManager.ResetAuthenticatorKeyAsync(user);
_logger.LogInformation("User with id '{UserId}' has reset their authentication app key.", user.Id);
return RedirectToAction(nameof(EnableAuthenticator));
}
[HttpGet]
public async Task<IActionResult> GenerateRecoveryCodes()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
if (!user.TwoFactorEnabled)
{
throw new ApplicationException(
$"Cannot generate recovery codes for user with ID '{user.Id}' as they do not have 2FA enabled.");
}
var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10);
var model = new GenerateRecoveryCodesViewModel {RecoveryCodes = recoveryCodes.ToArray()};
_logger.LogInformation("User with ID {UserId} has generated new 2FA recovery codes.", user.Id);
return View(model);
}
private string GenerateQrCodeUri(string email, string unformattedKey)
{
return string.Format(CultureInfo.InvariantCulture,
AuthenicatorUriFormat,
_urlEncoder.Encode("BTCPayServer"),
_urlEncoder.Encode(email),
unformattedKey);
}
private string FormatKey(string unformattedKey)
{
var result = new StringBuilder();
int currentPosition = 0;
while (currentPosition + 4 < unformattedKey.Length)
{
result.Append(unformattedKey.Substring(currentPosition, 4)).Append(" ");
currentPosition += 4;
}
if (currentPosition < unformattedKey.Length)
{
result.Append(unformattedKey.Substring(currentPosition));
}
return result.ToString().ToLowerInvariant();
}
}
}

View File

@ -0,0 +1,87 @@
using System;
using System.Threading.Tasks;
using BTCPayServer.Models;
using BTCPayServer.Services.U2F.Models;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers
{
public partial class ManageController
{
[HttpGet]
public async Task<IActionResult> U2FAuthentication(string statusMessage = null)
{
return View(new U2FAuthenticationViewModel()
{
StatusMessage = statusMessage,
Devices = await _u2FService.GetDevices(_userManager.GetUserId(User))
});
}
[HttpGet]
public async Task<IActionResult> RemoveU2FDevice(string id)
{
await _u2FService.RemoveDevice(id, _userManager.GetUserId(User));
return RedirectToAction("U2FAuthentication", new
{
StatusMessage = "Device removed"
});
}
[HttpGet]
public IActionResult AddU2FDevice(string name)
{
if (!_btcPayServerEnvironment.IsSecure)
{
return RedirectToAction("U2FAuthentication", new
{
StatusMessage = new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Error,
Message = "Cannot register U2F device while not on https or tor"
}
});
}
var serverRegisterResponse = _u2FService.StartDeviceRegistration(_userManager.GetUserId(User),
Request.GetAbsoluteUriNoPathBase().ToString().TrimEnd('/'));
return View(new AddU2FDeviceViewModel()
{
AppId = serverRegisterResponse.AppId,
Challenge = serverRegisterResponse.Challenge,
Version = serverRegisterResponse.Version,
Name = name
});
}
[HttpPost]
public async Task<IActionResult> AddU2FDevice(AddU2FDeviceViewModel viewModel)
{
try
{
if (await _u2FService.CompleteRegistration(_userManager.GetUserId(User), viewModel.DeviceResponse,
string.IsNullOrEmpty(viewModel.Name) ? "Unlabelled U2F Device" : viewModel.Name))
{
return RedirectToAction("U2FAuthentication", new
{
StatusMessage = "Device added!"
});
}
throw new Exception("Could not add device.");
}
catch (Exception e)
{
return RedirectToAction("U2FAuthentication", new
{
StatusMessage = new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Error,
Message = e.Message
}
});
}
}
}
}

View File

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Encodings.Web;
@ -9,56 +8,59 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using BTCPayServer.Models;
using BTCPayServer.Models.ManageViewModels;
using BTCPayServer.Services;
using BTCPayServer.Authentication;
using Microsoft.AspNetCore.Hosting;
using NBitpayClient;
using NBitcoin;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
using BTCPayServer.Services.Mails;
using System.Globalization;
using BTCPayServer.Security;
using BTCPayServer.Services.U2F;
namespace BTCPayServer.Controllers
{
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
[Route("[controller]/[action]")]
public class ManageController : Controller
public partial class ManageController : Controller
{
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;
IHostingEnvironment _Env;
private readonly U2FService _u2FService;
private readonly BTCPayServerEnvironment _btcPayServerEnvironment;
StoreRepository _StoreRepository;
private const string AuthenicatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6";
public ManageController(
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
IEmailSender emailSender,
EmailSenderFactory emailSenderFactory,
ILogger<ManageController> logger,
UrlEncoder urlEncoder,
TokenRepository tokenRepository,
BTCPayWalletProvider walletProvider,
StoreRepository storeRepository,
IHostingEnvironment env)
IHostingEnvironment env,
U2FService u2FService,
BTCPayServerEnvironment btcPayServerEnvironment)
{
_userManager = userManager;
_signInManager = signInManager;
_emailSender = emailSender;
_EmailSenderFactory = emailSenderFactory;
_logger = logger;
_urlEncoder = urlEncoder;
_TokenRepository = tokenRepository;
_Env = env;
_u2FService = u2FService;
_btcPayServerEnvironment = btcPayServerEnvironment;
_StoreRepository = storeRepository;
}
@ -113,6 +115,7 @@ namespace BTCPayServer.Controllers
{
throw new ApplicationException($"Unexpected error occurred setting email for user with ID '{user.Id}'.");
}
await _userManager.SetUserNameAsync(user, model.Username);
}
var phoneNumber = user.PhoneNumber;
@ -156,8 +159,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));
}
@ -339,163 +341,6 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(ExternalLogins));
}
[HttpGet]
public async Task<IActionResult> TwoFactorAuthentication()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var model = new TwoFactorAuthenticationViewModel
{
HasAuthenticator = await _userManager.GetAuthenticatorKeyAsync(user) != null,
Is2faEnabled = user.TwoFactorEnabled,
RecoveryCodesLeft = await _userManager.CountRecoveryCodesAsync(user),
};
return View(model);
}
[HttpGet]
public async Task<IActionResult> Disable2faWarning()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
if (!user.TwoFactorEnabled)
{
throw new ApplicationException($"Unexpected error occurred disabling 2FA for user with ID '{user.Id}'.");
}
return View(nameof(Disable2fa));
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Disable2fa()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var disable2faResult = await _userManager.SetTwoFactorEnabledAsync(user, false);
if (!disable2faResult.Succeeded)
{
throw new ApplicationException($"Unexpected error occurred disabling 2FA for user with ID '{user.Id}'.");
}
_logger.LogInformation("User with ID {UserId} has disabled 2fa.", user.Id);
return RedirectToAction(nameof(TwoFactorAuthentication));
}
[HttpGet]
public async Task<IActionResult> EnableAuthenticator()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
if (string.IsNullOrEmpty(unformattedKey))
{
await _userManager.ResetAuthenticatorKeyAsync(user);
unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
}
var model = new EnableAuthenticatorViewModel
{
SharedKey = FormatKey(unformattedKey),
AuthenticatorUri = GenerateQrCodeUri(user.Email, unformattedKey)
};
return View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> EnableAuthenticator(EnableAuthenticatorViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
// Strip spaces and hypens
var verificationCode = model.Code.Replace(" ", string.Empty, StringComparison.InvariantCulture).Replace("-", string.Empty, StringComparison.InvariantCulture);
var is2faTokenValid = await _userManager.VerifyTwoFactorTokenAsync(
user, _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode);
if (!is2faTokenValid)
{
ModelState.AddModelError("model.Code", "Verification code is invalid.");
return View(model);
}
await _userManager.SetTwoFactorEnabledAsync(user, true);
_logger.LogInformation("User with ID {UserId} has enabled 2FA with an authenticator app.", user.Id);
return RedirectToAction(nameof(GenerateRecoveryCodes));
}
[HttpGet]
public IActionResult ResetAuthenticatorWarning()
{
return View(nameof(ResetAuthenticator));
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ResetAuthenticator()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
await _userManager.SetTwoFactorEnabledAsync(user, false);
await _userManager.ResetAuthenticatorKeyAsync(user);
_logger.LogInformation("User with id '{UserId}' has reset their authentication app key.", user.Id);
return RedirectToAction(nameof(EnableAuthenticator));
}
[HttpGet]
public async Task<IActionResult> GenerateRecoveryCodes()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
if (!user.TwoFactorEnabled)
{
throw new ApplicationException($"Cannot generate recovery codes for user with ID '{user.Id}' as they do not have 2FA enabled.");
}
var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10);
var model = new GenerateRecoveryCodesViewModel { RecoveryCodes = recoveryCodes.ToArray() };
_logger.LogInformation("User with ID {UserId} has generated new 2FA recovery codes.", user.Id);
return View(model);
}
#region Helpers
@ -506,33 +351,6 @@ namespace BTCPayServer.Controllers
ModelState.AddModelError(string.Empty, error.Description);
}
}
private string FormatKey(string unformattedKey)
{
var result = new StringBuilder();
int currentPosition = 0;
while (currentPosition + 4 < unformattedKey.Length)
{
result.Append(unformattedKey.Substring(currentPosition, 4)).Append(" ");
currentPosition += 4;
}
if (currentPosition < unformattedKey.Length)
{
result.Append(unformattedKey.Substring(currentPosition));
}
return result.ToString().ToLowerInvariant();
}
private string GenerateQrCodeUri(string email, string unformattedKey)
{
return string.Format(CultureInfo.InvariantCulture,
AuthenicatorUriFormat,
_urlEncoder.Encode("BTCPayServer"),
_urlEncoder.Encode(email),
unformattedKey);
}
#endregion
}
}

View File

@ -1,40 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using NBitcoin.Payment;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Controllers
{
public class PaymentRequestActionResult : IActionResult
{
PaymentRequest req;
public PaymentRequestActionResult(PaymentRequest req)
{
this.req = req;
}
public Task ExecuteResultAsync(ActionContext context)
{
context.HttpContext.Response.Headers["Content-Transfer-Encoding"] = "binary";
context.HttpContext.Response.ContentType = "application/bitcoin-paymentrequest";
req.WriteTo(context.HttpContext.Response.Body);
return Task.CompletedTask;
}
}
public class PaymentAckActionResult : IActionResult
{
PaymentACK req;
public PaymentAckActionResult(PaymentACK req)
{
this.req = req;
}
public Task ExecuteResultAsync(ActionContext context)
{
context.HttpContext.Response.Headers["Content-Transfer-Encoding"] = "binary";
context.HttpContext.Response.ContentType = "application/bitcoin-paymentack";
req.WriteTo(context.HttpContext.Response.Body);
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,378 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Filters;
using BTCPayServer.Models;
using BTCPayServer.Models.PaymentRequestViewModels;
using BTCPayServer.PaymentRequest;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Rating;
using BTCPayServer.Security;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.PaymentRequests;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using Ganss.XSS;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using NBitpayClient;
namespace BTCPayServer.Controllers
{
[Route("payment-requests")]
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
public class PaymentRequestController : Controller
{
private readonly InvoiceController _InvoiceController;
private readonly UserManager<ApplicationUser> _UserManager;
private readonly StoreRepository _StoreRepository;
private readonly PaymentRequestRepository _PaymentRequestRepository;
private readonly PaymentRequestService _PaymentRequestService;
private readonly EventAggregator _EventAggregator;
private readonly CurrencyNameTable _Currencies;
private readonly HtmlSanitizer _htmlSanitizer;
private readonly InvoiceRepository _InvoiceRepository;
public PaymentRequestController(
InvoiceController invoiceController,
UserManager<ApplicationUser> userManager,
StoreRepository storeRepository,
PaymentRequestRepository paymentRequestRepository,
PaymentRequestService paymentRequestService,
EventAggregator eventAggregator,
CurrencyNameTable currencies,
HtmlSanitizer htmlSanitizer,
InvoiceRepository invoiceRepository)
{
_InvoiceController = invoiceController;
_UserManager = userManager;
_StoreRepository = storeRepository;
_PaymentRequestRepository = paymentRequestRepository;
_PaymentRequestService = paymentRequestService;
_EventAggregator = eventAggregator;
_Currencies = currencies;
_htmlSanitizer = htmlSanitizer;
_InvoiceRepository = invoiceRepository;
}
[HttpGet]
[Route("")]
[BitpayAPIConstraint(false)]
public async Task<IActionResult> GetPaymentRequests(int skip = 0, int count = 50, string statusMessage = null)
{
var result = await _PaymentRequestRepository.FindPaymentRequests(new PaymentRequestQuery()
{
UserId = GetUserId(), Skip = skip, Count = count
});
return View(new ListPaymentRequestsViewModel()
{
Skip = skip,
StatusMessage = statusMessage,
Count = count,
Total = result.Total,
Items = result.Items.Select(data => new ViewPaymentRequestViewModel(data)).ToList()
});
}
[HttpGet]
[Route("edit/{id?}")]
public async Task<IActionResult> EditPaymentRequest(string id, string statusMessage = null)
{
SelectList stores = null;
var data = await _PaymentRequestRepository.FindPaymentRequest(id, GetUserId());
if (data == null && !string.IsNullOrEmpty(id))
{
return NotFound();
}
stores = new SelectList(await _StoreRepository.GetStoresByUserId(GetUserId()), nameof(StoreData.Id),
nameof(StoreData.StoreName), data?.StoreDataId);
if (!stores.Any())
{
return RedirectToAction("GetPaymentRequests",
new
{
StatusMessage = new StatusMessageModel()
{
Html =
$"Error: You need to create at least one store. <a href='{Url.Action("CreateStore", "UserStores")}'>Create store</a>",
Severity = StatusMessageModel.StatusSeverity.Error
}
});
}
return View(new UpdatePaymentRequestViewModel(data)
{
Stores = stores,
StatusMessage = statusMessage
});
}
[HttpPost]
[Route("edit/{id?}")]
public async Task<IActionResult> EditPaymentRequest(string id, UpdatePaymentRequestViewModel viewModel)
{
if (string.IsNullOrEmpty(viewModel.Currency) ||
_Currencies.GetCurrencyData(viewModel.Currency, false) == null)
ModelState.AddModelError(nameof(viewModel.Currency), "Invalid currency");
var data = await _PaymentRequestRepository.FindPaymentRequest(id, GetUserId());
if (data == null && !string.IsNullOrEmpty(id))
{
return NotFound();
}
if (!ModelState.IsValid)
{
viewModel.Stores = new SelectList(await _StoreRepository.GetStoresByUserId(GetUserId()),
nameof(StoreData.Id),
nameof(StoreData.StoreName), data?.StoreDataId);
return View(viewModel);
}
if (data == null)
{
data = new PaymentRequestData();
}
data.StoreDataId = viewModel.StoreId;
var blob = data.GetBlob();
blob.Title = viewModel.Title;
blob.Email = viewModel.Email;
blob.Description = _htmlSanitizer.Sanitize(viewModel.Description);
blob.Amount = viewModel.Amount;
blob.ExpiryDate = viewModel.ExpiryDate;
blob.Currency = viewModel.Currency;
blob.EmbeddedCSS = viewModel.EmbeddedCSS;
blob.CustomCSSLink = viewModel.CustomCSSLink;
blob.AllowCustomPaymentAmounts = viewModel.AllowCustomPaymentAmounts;
data.SetBlob(blob);
data = await _PaymentRequestRepository.CreateOrUpdatePaymentRequest(data);
_EventAggregator.Publish(new PaymentRequestUpdated()
{
Data = data,
PaymentRequestId = data.Id
});
return RedirectToAction("EditPaymentRequest", new {id = data.Id, StatusMessage = "Saved"});
}
[HttpGet]
[Route("{id}/remove")]
[BitpayAPIConstraint(false)]
public async Task<IActionResult> RemovePaymentRequestPrompt(string id)
{
var data = await _PaymentRequestRepository.FindPaymentRequest(id, GetUserId());
if (data == null)
{
return NotFound();
}
var blob = data.GetBlob();
return View("Confirm", new ConfirmModel()
{
Title = $"Remove Payment Request",
Description = $"Are you sure you want to remove access to the payment request '{blob.Title}' ?",
Action = "Delete"
});
}
[HttpPost]
[Route("{id}/remove")]
[BitpayAPIConstraint(false)]
public async Task<IActionResult> RemovePaymentRequest(string id)
{
var result = await _PaymentRequestRepository.RemovePaymentRequest(id, GetUserId());
if (result)
{
return RedirectToAction("GetPaymentRequests",
new {StatusMessage = "Payment request successfully removed"});
}
else
{
return RedirectToAction("GetPaymentRequests",
new
{
StatusMessage =
"Error: Payment request could not be removed. Any request that has generated invoices cannot be removed."
});
}
}
[HttpGet]
[Route("{id}")]
[AllowAnonymous]
public async Task<IActionResult> ViewPaymentRequest(string id, string statusMessage = null)
{
var result = await _PaymentRequestService.GetPaymentRequest(id, GetUserId());
if (result == null)
{
return NotFound();
}
result.HubPath = PaymentRequestHub.GetHubPath(this.Request);
result.StatusMessage = statusMessage;
return View(result);
}
[HttpGet]
[Route("{id}/pay")]
[AllowAnonymous]
public async Task<IActionResult> PayPaymentRequest(string id, bool redirectToInvoice = true,
decimal? amount = null, CancellationToken cancellationToken = default)
{
var result = await _PaymentRequestService.GetPaymentRequest(id, GetUserId());
if (result == null)
{
return NotFound();
}
result.HubPath = PaymentRequestHub.GetHubPath(this.Request);
if (result.AmountDue <= 0)
{
if (redirectToInvoice)
{
return RedirectToAction("ViewPaymentRequest", new {Id = id});
}
return BadRequest("Payment Request has already been settled.");
}
if (result.ExpiryDate.HasValue && DateTime.Now >= result.ExpiryDate)
{
if (redirectToInvoice)
{
return RedirectToAction("ViewPaymentRequest", new {Id = id});
}
return BadRequest("Payment Request has expired");
}
var statusesAllowedToDisplay = new List<InvoiceStatus>()
{
InvoiceStatus.New
};
var validInvoice = result.Invoices.FirstOrDefault(invoice =>
Enum.TryParse<InvoiceStatus>(invoice.Status, true, out var status) &&
statusesAllowedToDisplay.Contains(status));
if (validInvoice != null)
{
if (redirectToInvoice)
{
return RedirectToAction("Checkout", "Invoice", new {Id = validInvoice.Id});
}
return Ok(validInvoice.Id);
}
if (result.AllowCustomPaymentAmounts && amount != null)
amount = Math.Min(result.AmountDue, amount.Value);
else
amount = result.AmountDue;
var pr = await _PaymentRequestRepository.FindPaymentRequest(id, null);
var blob = pr.GetBlob();
var store = pr.StoreData;
store.AdditionalClaims.Add(new Claim(Policies.CanCreateInvoice.Key, store.Id));
try
{
var redirectUrl = Request.GetDisplayUrl().TrimEnd("/pay", StringComparison.InvariantCulture)
.Replace("hub?id=", string.Empty, StringComparison.InvariantCultureIgnoreCase);
var newInvoiceId = (await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest()
{
OrderId = $"{PaymentRequestRepository.GetOrderIdForPaymentRequest(id)}",
Currency = blob.Currency,
Price = amount.Value,
FullNotifications = true,
BuyerEmail = result.Email,
RedirectURL = redirectUrl,
}, store, HttpContext.Request.GetAbsoluteRoot(),
new List<string>() {PaymentRequestRepository.GetInternalTag(id)},
cancellationToken: cancellationToken))
.Data.Id;
if (redirectToInvoice)
{
return RedirectToAction("Checkout", "Invoice", new {Id = newInvoiceId});
}
return Ok(newInvoiceId);
}
catch (BitpayHttpException e)
{
return BadRequest(e.Message);
}
}
[HttpGet]
[Route("{id}/cancel")]
public async Task<IActionResult> CancelUnpaidPendingInvoice(string id, bool redirect = true)
{
var result = await _PaymentRequestService.GetPaymentRequest(id, GetUserId());
if (result == null )
{
return NotFound();
}
var invoice = result.Invoices.SingleOrDefault(requestInvoice =>
requestInvoice.Status.Equals(InvoiceState.ToString(InvoiceStatus.New),StringComparison.InvariantCulture) && !requestInvoice.Payments.Any());
if (invoice == null )
{
return BadRequest("No unpaid pending invoice to cancel");
}
await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoice.Id);
_EventAggregator.Publish(new InvoiceEvent(await _InvoiceRepository.GetInvoice(invoice.Id), 1008, InvoiceEvent.MarkedInvalid));
if (redirect)
{
return RedirectToAction(nameof(ViewPaymentRequest), new
{
Id = id,
StatusMessage = "Payment cancelled"
});
}
return Ok("Payment cancelled");
}
private string GetUserId()
{
return _UserManager.GetUserId(User);
}
[HttpGet]
[Route("{id}/clone")]
public async Task<IActionResult> ClonePaymentRequest(string id)
{
var result = await EditPaymentRequest(id);
if (result is ViewResult viewResult)
{
var model = (UpdatePaymentRequestViewModel)viewResult.Model;
model.Id = null;
model.Title = $"Clone of {model.Title}";
return View("EditPaymentRequest", model);
}
return NotFound();
}
}
}

View File

@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Filters;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers
{
public class PublicController : Controller
{
public PublicController(InvoiceController invoiceController,
StoreRepository storeRepository)
{
_InvoiceController = invoiceController;
_StoreRepository = storeRepository;
}
private InvoiceController _InvoiceController;
private StoreRepository _StoreRepository;
[HttpPost]
[Route("api/v1/invoices")]
[MediaTypeAcceptConstraintAttribute("text/html")]
[IgnoreAntiforgeryToken]
[EnableCors(CorsPolicies.All)]
public async Task<IActionResult> PayButtonHandle([FromForm]PayButtonViewModel model, CancellationToken cancellationToken)
{
var store = await _StoreRepository.FindStore(model.StoreId);
if (store == null)
ModelState.AddModelError("Store", "Invalid store");
else
{
var storeBlob = store.GetStoreBlob();
if (!storeBlob.AnyoneCanInvoice)
ModelState.AddModelError("Store", "Store has not enabled Pay Button");
}
if (model == null || model.Price <= 0)
ModelState.AddModelError("Price", "Price must be greater than 0");
if (!ModelState.IsValid)
return View();
var invoice = await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest()
{
Price = model.Price,
Currency = model.Currency,
ItemDesc = model.CheckoutDesc,
OrderId = model.OrderId,
NotificationEmail = model.NotifyEmail,
NotificationURL = model.ServerIpn,
RedirectURL = model.BrowserRedirect,
FullNotifications = true
}, store, HttpContext.Request.GetAbsoluteRoot(), cancellationToken: cancellationToken);
return Redirect(invoice.Data.Url);
}
}
}

View File

@ -0,0 +1,89 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Filters;
using BTCPayServer.Lightning;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
namespace BTCPayServer.Controllers
{
[Route("embed/{storeId}/{cryptoCode}/ln")]
[AllowAnonymous]
public class PublicLightningNodeInfoController : Controller
{
private readonly BTCPayNetworkProvider _BtcPayNetworkProvider;
private readonly LightningLikePaymentHandler _LightningLikePaymentHandler;
private readonly StoreRepository _StoreRepository;
public PublicLightningNodeInfoController(BTCPayNetworkProvider btcPayNetworkProvider,
LightningLikePaymentHandler lightningLikePaymentHandler, StoreRepository storeRepository)
{
_BtcPayNetworkProvider = btcPayNetworkProvider;
_LightningLikePaymentHandler = lightningLikePaymentHandler;
_StoreRepository = storeRepository;
}
[HttpGet]
[XFrameOptions(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
public async Task<IActionResult> ShowLightningNodeInfo(string storeId, string cryptoCode)
{
var store = await _StoreRepository.FindStore(storeId);
if (store == null)
return NotFound();
try
{
var paymentMethodDetails = GetExistingLightningSupportedPaymentMethod(cryptoCode, store);
var network = _BtcPayNetworkProvider.GetNetwork(cryptoCode);
var nodeInfo =
await _LightningLikePaymentHandler.GetNodeInfo(this.Request.IsOnion(), paymentMethodDetails,
network);
return View(new ShowLightningNodeInfoViewModel()
{
Available = true,
NodeInfo = nodeInfo.ToString(),
CryptoCode = cryptoCode,
CryptoImage = GetImage(paymentMethodDetails.PaymentId, network)
});
}
catch (Exception)
{
return View(new ShowLightningNodeInfoViewModel() {Available = false, CryptoCode = cryptoCode});
}
}
private LightningSupportedPaymentMethod GetExistingLightningSupportedPaymentMethod(string cryptoCode, StoreData store)
{
var id = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
var existing = store.GetSupportedPaymentMethods(_BtcPayNetworkProvider)
.OfType<LightningSupportedPaymentMethod>()
.FirstOrDefault(d => d.PaymentId == id);
return existing;
}
private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network)
{
var res = paymentMethodId.PaymentType == PaymentTypes.BTCLike
? Url.Content(network.CryptoImagePath)
: Url.Content(network.LightningImagePath);
return "/" + res;
}
}
public class ShowLightningNodeInfoViewModel
{
public string NodeInfo { get; set; }
public bool Available { get; set; }
public string CryptoCode { get; set; }
public string CryptoImage { get; set; }
}
}

View File

@ -10,53 +10,125 @@ using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Rating;
using Newtonsoft.Json;
using Microsoft.AspNetCore.Authorization;
using BTCPayServer.Authentication;
using Microsoft.AspNetCore.Cors;
using System.Threading;
namespace BTCPayServer.Controllers
{
[Authorize(AuthenticationSchemes = Security.Policies.BitpayAuthentication)]
[AllowAnonymous]
[EnableCors(CorsPolicies.All)]
public class RateController : Controller
{
BTCPayRateProviderFactory _RateProviderFactory;
RateFetcher _RateProviderFactory;
BTCPayNetworkProvider _NetworkProvider;
CurrencyNameTable _CurrencyNameTable;
StoreRepository _StoreRepo;
public TokenRepository TokenRepository { get; }
public RateController(
BTCPayRateProviderFactory rateProviderFactory,
RateFetcher rateProviderFactory,
BTCPayNetworkProvider networkProvider,
TokenRepository tokenRepository,
StoreRepository storeRepo,
CurrencyNameTable currencyNameTable)
{
_RateProviderFactory = rateProviderFactory ?? throw new ArgumentNullException(nameof(rateProviderFactory));
_NetworkProvider = networkProvider;
TokenRepository = tokenRepository;
_StoreRepo = storeRepo;
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
}
[Route("rates")]
[Route("rates/{baseCurrency}")]
[HttpGet]
[BitpayAPIConstraint]
public async Task<IActionResult> GetRates(string currencyPairs, string storeId)
public async Task<IActionResult> GetBaseCurrencyRates(string baseCurrency, string storeId, CancellationToken cancellationToken)
{
storeId = storeId ?? this.HttpContext.GetStoreData()?.Id;
var result = await GetRates2(currencyPairs, storeId);
storeId = await GetStoreId(storeId);
var store = this.HttpContext.GetStoreData();
if (store == null || store.Id != storeId)
store = await _StoreRepo.FindStore(storeId);
if (store == null)
{
var err = Json(new BitpayErrorsModel() { Error = "Store not found" });
err.StatusCode = 404;
return err;
}
var supportedMethods = store.GetSupportedPaymentMethods(_NetworkProvider);
var currencyCodes = supportedMethods.Where(method => !string.IsNullOrEmpty(method.PaymentId.CryptoCode))
.Select(method => method.PaymentId.CryptoCode).Distinct();
var currencypairs = BuildCurrencyPairs(currencyCodes, baseCurrency);
var result = await GetRates2(currencypairs, store.Id, cancellationToken);
var rates = (result as JsonResult)?.Value as Rate[];
if (rates == null)
return result;
return Json(new DataWrapper<Rate[]>(rates));
}
[Route("rates/{baseCurrency}/{currency}")]
[HttpGet]
[BitpayAPIConstraint]
public async Task<IActionResult> GetCurrencyPairRate(string baseCurrency, string currency, string storeId, CancellationToken cancellationToken)
{
storeId = await GetStoreId(storeId);
var result = await GetRates2($"{baseCurrency}_{currency}", storeId, cancellationToken);
var rates = (result as JsonResult)?.Value as Rate[];
if (rates == null)
return result;
return Json(new DataWrapper<Rate>(rates.First()));
}
[Route("rates")]
[HttpGet]
[BitpayAPIConstraint]
public async Task<IActionResult> GetRates(string currencyPairs, string storeId, CancellationToken cancellationToken)
{
var result = await GetRates2(currencyPairs, storeId, cancellationToken);
var rates = (result as JsonResult)?.Value as Rate[];
if (rates == null)
return result;
return Json(new DataWrapper<Rate[]>(rates));
}
private async Task<string> GetStoreId(string storeId)
{
if (storeId != null && this.HttpContext.GetStoreData()?.Id == storeId)
return storeId;
if(storeId == null)
{
var tokens = await this.TokenRepository.GetTokens(this.User.GetSIN());
storeId = tokens.Select(s => s.StoreId).Where(s => s != null).FirstOrDefault();
}
if (storeId == null)
return null;
var store = await _StoreRepo.FindStore(storeId);
if (store == null)
return null;
this.HttpContext.SetStoreData(store);
return storeId;
}
[Route("api/rates")]
[HttpGet]
public async Task<IActionResult> GetRates2(string currencyPairs, string storeId)
public async Task<IActionResult> GetRates2(string currencyPairs, string storeId, CancellationToken cancellationToken)
{
if(storeId == null || currencyPairs == null)
storeId = await GetStoreId(storeId);
if (storeId == null)
{
var result = Json(new BitpayErrorsModel() { Error = "You need to specify storeId (in your store settings) and currencyPairs (eg. BTC_USD,LTC_CAD)" });
var result = Json(new BitpayErrorsModel() { Error = "You need to specify storeId (in your store settings)" });
result.StatusCode = 400;
return result;
}
var store = this.HttpContext.GetStoreData();
if(store == null || store.Id != storeId)
if (store == null || store.Id != storeId)
store = await _StoreRepo.FindStore(storeId);
if (store == null)
{
@ -64,12 +136,25 @@ namespace BTCPayServer.Controllers
result.StatusCode = 404;
return result;
}
if (currencyPairs == null)
{
currencyPairs = store.GetStoreBlob().GetDefaultCurrencyPairString();
if (string.IsNullOrEmpty(currencyPairs))
{
var result = Json(new BitpayErrorsModel() { Error = "You need to setup the default currency pairs in 'Store Settings / Rates' or specify 'currencyPairs' query parameter (eg. BTC_USD,LTC_CAD)." });
result.StatusCode = 400;
return result;
}
}
var rules = store.GetStoreBlob().GetRateRules(_NetworkProvider);
HashSet<CurrencyPair> pairs = new HashSet<CurrencyPair>();
foreach(var currency in currencyPairs.Split(','))
foreach (var currency in currencyPairs.Split(','))
{
if(!CurrencyPair.TryParse(currency, out var pair))
if (!CurrencyPair.TryParse(currency, out var pair))
{
var result = Json(new BitpayErrorsModel() { Error = $"Currency pair {currency} uncorrectly formatted" });
result.StatusCode = 400;
@ -78,10 +163,10 @@ namespace BTCPayServer.Controllers
pairs.Add(pair);
}
var fetching = _RateProviderFactory.FetchRates(pairs, rules);
var fetching = _RateProviderFactory.FetchRates(pairs, rules, cancellationToken);
await Task.WhenAll(fetching.Select(f => f.Value).ToArray());
return Json(pairs
.Select(r => (Pair: r, Value: fetching[r].GetAwaiter().GetResult().Value))
.Select(r => (Pair: r, Value: fetching[r].GetAwaiter().GetResult().BidAsk?.Bid))
.Where(r => r.Value.HasValue)
.Select(r =>
new Rate()
@ -89,11 +174,25 @@ namespace BTCPayServer.Controllers
CryptoCode = r.Pair.Left,
Code = r.Pair.Right,
CurrencyPair = r.Pair.ToString(),
Name = _CurrencyNameTable.GetCurrencyData(r.Pair.Right)?.Name,
Name = _CurrencyNameTable.GetCurrencyData(r.Pair.Right, true).Name,
Value = r.Value.Value
}).Where(n => n.Name != null).ToArray());
}
private static string BuildCurrencyPairs(IEnumerable<string> currencyCodes, string baseCrypto)
{
StringBuilder currencyPairsBuilder = new StringBuilder();
bool first = true;
foreach (var currencyCode in currencyCodes)
{
if(!first)
currencyPairsBuilder.Append(",");
first = false;
currencyPairsBuilder.Append($"{baseCrypto}_{currencyCode}");
}
return currencyPairsBuilder.ToString();
}
public class Rate
{

View File

@ -0,0 +1,289 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Models;
using BTCPayServer.Models.ServerViewModels;
using BTCPayServer.Storage.Models;
using BTCPayServer.Storage.Services.Providers.AmazonS3Storage;
using BTCPayServer.Storage.Services.Providers.AmazonS3Storage.Configuration;
using BTCPayServer.Storage.Services.Providers.AzureBlobStorage;
using BTCPayServer.Storage.Services.Providers.AzureBlobStorage.Configuration;
using BTCPayServer.Storage.Services.Providers.FileSystemStorage;
using BTCPayServer.Storage.Services.Providers.FileSystemStorage.Configuration;
using BTCPayServer.Storage.Services.Providers.GoogleCloudStorage;
using BTCPayServer.Storage.Services.Providers.GoogleCloudStorage.Configuration;
using BTCPayServer.Storage.Services.Providers.Models;
using BTCPayServer.Storage.ViewModels;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Controllers
{
public partial class ServerController
{
[HttpGet("server/files/{fileId?}")]
public async Task<IActionResult> Files(string fileId = null, string statusMessage = null)
{
TempData["StatusMessage"] = statusMessage;
var fileUrl = string.IsNullOrEmpty(fileId) ? null : await _FileService.GetFileUrl(fileId);
return View(new ViewFilesViewModel()
{
Files = await _StoredFileRepository.GetFiles(),
SelectedFileId = string.IsNullOrEmpty(fileUrl) ? null : fileId,
DirectFileUrl = fileUrl,
StorageConfigured = (await _SettingsRepository.GetSettingAsync<StorageSettings>()) != null
});
}
[HttpGet("server/files/{fileId}/delete")]
public async Task<IActionResult> DeleteFile(string fileId)
{
try
{
await _FileService.RemoveFile(fileId, null);
return RedirectToAction(nameof(Files), new
{
fileId = "",
statusMessage = "File removed"
});
}
catch (Exception e)
{
return RedirectToAction(nameof(Files), new
{
statusMessage = new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Error,
Message = e.Message
}
});
}
}
[HttpGet("server/files/{fileId}/tmp")]
public async Task<IActionResult> CreateTemporaryFileUrl(string fileId)
{
var file = await _StoredFileRepository.GetFile(fileId);
if (file == null)
{
return NotFound();
}
return View(new CreateTemporaryFileUrlViewModel());
}
[HttpPost("server/files/{fileId}/tmp")]
public async Task<IActionResult> CreateTemporaryFileUrl(string fileId,
CreateTemporaryFileUrlViewModel viewModel)
{
if (viewModel.TimeAmount <= 0)
{
ModelState.AddModelError(nameof(viewModel.TimeAmount), "Time must be at least 1");
}
if (!ModelState.IsValid)
{
return View(viewModel);
}
var file = await _StoredFileRepository.GetFile(fileId);
if (file == null)
{
return NotFound();
}
var expiry = DateTimeOffset.Now;
switch (viewModel.TimeType)
{
case CreateTemporaryFileUrlViewModel.TmpFileTimeType.Seconds:
expiry =expiry.AddSeconds(viewModel.TimeAmount);
break;
case CreateTemporaryFileUrlViewModel.TmpFileTimeType.Minutes:
expiry = expiry.AddMinutes(viewModel.TimeAmount);
break;
case CreateTemporaryFileUrlViewModel.TmpFileTimeType.Hours:
expiry = expiry.AddHours(viewModel.TimeAmount);
break;
case CreateTemporaryFileUrlViewModel.TmpFileTimeType.Days:
expiry = expiry.AddDays(viewModel.TimeAmount);
break;
default:
throw new ArgumentOutOfRangeException();
}
var url = await _FileService.GetTemporaryFileUrl(fileId, expiry, viewModel.IsDownload);
return RedirectToAction(nameof(Files), new
{
StatusMessage = new StatusMessageModel()
{
Html =
$"Generated Temporary Url for file {file.FileName} which expires at {expiry:G}. <a href='{url}' target='_blank'>{url}</a>"
}.ToString(),
fileId,
});
}
public class CreateTemporaryFileUrlViewModel
{
public enum TmpFileTimeType
{
Seconds,
Minutes,
Hours,
Days
}
public int TimeAmount { get; set; }
public TmpFileTimeType TimeType { get; set; }
public bool IsDownload { get; set; }
}
[HttpPost("server/files/upload")]
public async Task<IActionResult> CreateFile(IFormFile file)
{
var newFile = await _FileService.AddFile(file, GetUserId());
return RedirectToAction(nameof(Files), new
{
statusMessage = "File added!",
fileId = newFile.Id
});
}
private string GetUserId()
{
return _UserManager.GetUserId(ControllerContext.HttpContext.User);
}
[HttpGet("server/storage")]
public async Task<IActionResult> Storage(bool forceChoice = false, string statusMessage = null)
{
TempData["StatusMessage"] = statusMessage;
var savedSettings = await _SettingsRepository.GetSettingAsync<StorageSettings>();
if (forceChoice || savedSettings == null)
{
return View(new ChooseStorageViewModel()
{
ShowChangeWarning = savedSettings != null,
Provider = savedSettings?.Provider ?? BTCPayServer.Storage.Models.StorageProvider.FileSystem
});
}
return RedirectToAction(nameof(StorageProvider), new
{
provider = savedSettings.Provider
});
}
[HttpPost("server/storage")]
public IActionResult Storage(StorageSettings viewModel)
{
return RedirectToAction("StorageProvider", "Server", new
{
provider = viewModel.Provider.ToString()
});
}
[HttpGet("server/storage/{provider}")]
public async Task<IActionResult> StorageProvider(string provider)
{
if (!Enum.TryParse(typeof(StorageProvider), provider, out var storageProvider))
{
return RedirectToAction(nameof(Storage), new
{
StatusMessage = new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Error,
Message = $"{provider} provider is not supported"
}.ToString()
});
}
var data = (await _SettingsRepository.GetSettingAsync<StorageSettings>()) ?? new StorageSettings();
var storageProviderService =
_StorageProviderServices.SingleOrDefault(service => service.StorageProvider().Equals(storageProvider));
switch (storageProviderService)
{
case null:
return RedirectToAction(nameof(Storage), new
{
StatusMessage = new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Error,
Message = $"{storageProvider} is not supported"
}.ToString()
});
case AzureBlobStorageFileProviderService fileProviderService:
return View(nameof(EditAzureBlobStorageStorageProvider),
fileProviderService.GetProviderConfiguration(data));
case AmazonS3FileProviderService fileProviderService:
return View(nameof(EditAmazonS3StorageProvider),
fileProviderService.GetProviderConfiguration(data));
case GoogleCloudStorageFileProviderService fileProviderService:
return View(nameof(EditGoogleCloudStorageStorageProvider),
fileProviderService.GetProviderConfiguration(data));
case FileSystemFileProviderService fileProviderService:
return View(nameof(EditFileSystemStorageProvider),
fileProviderService.GetProviderConfiguration(data));
}
return NotFound();
}
[HttpPost("server/storage/AzureBlobStorage")]
public async Task<IActionResult> EditAzureBlobStorageStorageProvider(AzureBlobStorageConfiguration viewModel)
{
return await SaveStorageProvider(viewModel, BTCPayServer.Storage.Models.StorageProvider.AzureBlobStorage);
}
[HttpPost("server/storage/AmazonS3")]
public async Task<IActionResult> EditAmazonS3StorageProvider(AmazonS3StorageConfiguration viewModel)
{
return await SaveStorageProvider(viewModel, BTCPayServer.Storage.Models.StorageProvider.AmazonS3);
}
[HttpPost("server/storage/GoogleCloudStorage")]
public async Task<IActionResult> EditGoogleCloudStorageStorageProvider(
GoogleCloudStorageConfiguration viewModel)
{
return await SaveStorageProvider(viewModel, BTCPayServer.Storage.Models.StorageProvider.GoogleCloudStorage);
}
[HttpPost("server/storage/FileSystem")]
public async Task<IActionResult> EditFileSystemStorageProvider(FileSystemStorageConfiguration viewModel)
{
return await SaveStorageProvider(viewModel, BTCPayServer.Storage.Models.StorageProvider.FileSystem);
}
private async Task<IActionResult> SaveStorageProvider(IBaseStorageConfiguration viewModel,
StorageProvider storageProvider)
{
if (!ModelState.IsValid)
{
return View(viewModel);
}
var data = (await _SettingsRepository.GetSettingAsync<StorageSettings>()) ?? new StorageSettings();
data.Provider = storageProvider;
data.Configuration = JObject.FromObject(viewModel);
await _SettingsRepository.UpdateSetting(data);
TempData["StatusMessage"] = new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Success,
Message = "Storage settings updated successfully"
}.ToString();
return View(viewModel);
}
}
}

View File

@ -1,38 +1,84 @@
using BTCPayServer.HostedServices;
using BTCPayServer.Configuration;
using Microsoft.Extensions.Logging;
using BTCPayServer.HostedServices;
using BTCPayServer.Models;
using BTCPayServer.Models.ServerViewModels;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services;
using BTCPayServer.Services.Mails;
using BTCPayServer.Services.Rates;
using BTCPayServer.Validations;
using BTCPayServer.Services.Stores;
using BTCPayServer.Validation;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBitcoin.DataEncoders;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Mail;
using System.Threading.Tasks;
using Renci.SshNet;
using BTCPayServer.Logging;
using BTCPayServer.Lightning;
using System.Runtime.CompilerServices;
using BTCPayServer.Storage.Models;
using BTCPayServer.Storage.Services;
using BTCPayServer.Storage.Services.Providers;
using BTCPayServer.Services.Apps;
using Microsoft.AspNetCore.Mvc.Rendering;
using BTCPayServer.Data;
namespace BTCPayServer.Controllers
{
[Authorize(Policy = BTCPayServer.Security.Policies.CanModifyServerSettings.Key)]
public class ServerController : Controller
public partial class ServerController : Controller
{
private UserManager<ApplicationUser> _UserManager;
SettingsRepository _SettingsRepository;
private BTCPayRateProviderFactory _RateProviderFactory;
private readonly NBXplorerDashboard _dashBoard;
private RateFetcher _RateProviderFactory;
private StoreRepository _StoreRepository;
LightningConfigurationProvider _LnConfigProvider;
private readonly TorServices _torServices;
BTCPayServerOptions _Options;
ApplicationDbContextFactory _ContextFactory;
private readonly StoredFileRepository _StoredFileRepository;
private readonly FileService _FileService;
private readonly IEnumerable<IStorageProviderService> _StorageProviderServices;
public ServerController(UserManager<ApplicationUser> userManager,
BTCPayRateProviderFactory rateProviderFactory,
SettingsRepository settingsRepository)
StoredFileRepository storedFileRepository,
FileService fileService,
IEnumerable<IStorageProviderService> storageProviderServices,
BTCPayServerOptions options,
RateFetcher rateProviderFactory,
SettingsRepository settingsRepository,
NBXplorerDashboard dashBoard,
IHttpClientFactory httpClientFactory,
LightningConfigurationProvider lnConfigProvider,
TorServices torServices,
StoreRepository storeRepository,
ApplicationDbContextFactory contextFactory)
{
_Options = options;
_StoredFileRepository = storedFileRepository;
_FileService = fileService;
_StorageProviderServices = storageProviderServices;
_UserManager = userManager;
_SettingsRepository = settingsRepository;
_dashBoard = dashBoard;
HttpClientFactory = httpClientFactory;
_RateProviderFactory = rateProviderFactory;
_StoreRepository = storeRepository;
_LnConfigProvider = lnConfigProvider;
_torServices = torServices;
_ContextFactory = contextFactory;
}
[Route("server/rates")]
@ -74,7 +120,7 @@ namespace BTCPayServer.Controllers
try
{
var service = GetCoinaverageService(vm, true);
if(service != null)
if (service != null)
await service.TestAuthAsync();
}
catch
@ -134,6 +180,194 @@ namespace BTCPayServer.Controllers
return View(userVM);
}
[Route("server/maintenance")]
public IActionResult Maintenance()
{
MaintenanceViewModel vm = new MaintenanceViewModel();
vm.UserName = "btcpayserver";
vm.DNSDomain = this.Request.Host.Host;
vm.SetConfiguredSSH(_Options.SSHSettings);
if (IPAddress.TryParse(vm.DNSDomain, out var unused))
vm.DNSDomain = null;
return View(vm);
}
[Route("server/maintenance")]
[HttpPost]
public async Task<IActionResult> Maintenance(MaintenanceViewModel vm, string command)
{
if (!ModelState.IsValid)
return View(vm);
vm.SetConfiguredSSH(_Options.SSHSettings);
if (command == "changedomain")
{
if (string.IsNullOrWhiteSpace(vm.DNSDomain))
{
ModelState.AddModelError(nameof(vm.DNSDomain), $"Required field");
return View(vm);
}
vm.DNSDomain = vm.DNSDomain.Trim().ToLowerInvariant();
if (vm.DNSDomain.Equals(this.Request.Host.Host, StringComparison.OrdinalIgnoreCase))
return View(vm);
if (IPAddress.TryParse(vm.DNSDomain, out var unused))
{
ModelState.AddModelError(nameof(vm.DNSDomain), $"This should be a domain name");
return View(vm);
}
if (vm.DNSDomain.Equals(this.Request.Host.Host, StringComparison.InvariantCultureIgnoreCase))
{
ModelState.AddModelError(nameof(vm.DNSDomain), $"The server is already set to use this domain");
return View(vm);
}
var builder = new UriBuilder();
using (var client = new HttpClient(new HttpClientHandler()
{
ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
}))
{
try
{
builder.Scheme = this.Request.Scheme;
builder.Host = 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();
var hasCommonAddress = addresses2.GetAwaiter().GetResult().Select(c => c.ToString()).Any(s => addressesSet.Contains(s));
if (!hasCommonAddress)
{
ModelState.AddModelError(nameof(vm.DNSDomain), $"Invalid host ({vm.DNSDomain} is not pointing to this BTCPay instance)");
return View(vm);
}
}
catch (Exception ex)
{
var messages = new List<object>();
messages.Add(ex.Message);
if (ex.InnerException != null)
messages.Add(ex.InnerException.Message);
ModelState.AddModelError(nameof(vm.DNSDomain), $"Invalid domain ({string.Join(", ", messages.ToArray())})");
return View(vm);
}
}
var error = RunSSH(vm, $"changedomain.sh {vm.DNSDomain}");
if (error != null)
return error;
builder.Path = null;
builder.Query = null;
StatusMessage = $"Domain name changing... the server will restart, please use \"{builder.Uri.AbsoluteUri}\"";
}
else if (command == "update")
{
var error = RunSSH(vm, $"btcpay-update.sh");
if (error != null)
return error;
StatusMessage = $"The server might restart soon if an update is available...";
}
else if (command == "clean")
{
var error = RunSSH(vm, $"btcpay-clean.sh");
if (error != null)
return error;
StatusMessage = $"The old docker images will be cleaned soon...";
}
else
{
return NotFound();
}
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")]
[AllowAnonymous]
public IActionResult SeeRunId(string expected = null)
{
if (expected == RunId)
return Ok();
return BadRequest();
}
private IActionResult RunSSH(MaintenanceViewModel vm, string ssh)
{
ssh = $"sudo bash -c '. /etc/profile.d/btcpay-env.sh && nohup {ssh} > /dev/null 2>&1 & disown'";
var sshClient = _Options.SSHSettings == null ? vm.CreateSSHClient(this.Request.Host.Host)
: new SshClient(_Options.SSHSettings.CreateConnectionInfo());
if (_Options.TrustedFingerprints.Count != 0)
{
sshClient.HostKeyReceived += (object sender, Renci.SshNet.Common.HostKeyEventArgs e) =>
{
if (_Options.TrustedFingerprints.Count == 0)
{
Logs.Configuration.LogWarning($"SSH host fingerprint for {e.HostKeyName} is untrusted, start BTCPay with -sshtrustedfingerprints \"{Encoders.Hex.EncodeData(e.FingerPrint)}\"");
e.CanTrust = true; // Not a typo, we want the connection to succeed with a warning
}
else
{
e.CanTrust = _Options.IsTrustedFingerprint(e.FingerPrint, e.HostKey);
if (!e.CanTrust)
Logs.Configuration.LogError($"SSH host fingerprint for {e.HostKeyName} is untrusted, start BTCPay with -sshtrustedfingerprints \"{Encoders.Hex.EncodeData(e.FingerPrint)}\"");
}
};
}
else
{
}
try
{
sshClient.Connect();
}
catch (Renci.SshNet.Common.SshAuthenticationException)
{
ModelState.AddModelError(nameof(vm.Password), "Invalid credentials");
sshClient.Dispose();
return View(vm);
}
catch (Exception ex)
{
var message = ex.Message;
if (ex is AggregateException aggrEx && aggrEx.InnerException?.Message != null)
{
message = aggrEx.InnerException.Message;
}
ModelState.AddModelError(nameof(vm.UserName), $"Connection problem ({message})");
sshClient.Dispose();
return View(vm);
}
var sshCommand = sshClient.CreateCommand(ssh);
sshCommand.CommandTimeout = TimeSpan.FromMinutes(1.0);
sshCommand.BeginExecute(ar =>
{
try
{
Logs.PayServer.LogInformation("Running SSH command: " + ssh);
var result = sshCommand.EndExecute(ar);
Logs.PayServer.LogInformation("SSH command executed: " + result);
}
catch (Exception ex)
{
Logs.PayServer.LogWarning("Error while executing SSH command: " + ex.Message);
}
sshClient.Dispose();
});
return null;
}
private static bool IsAdmin(IList<string> roles)
{
return roles.Contains(Roles.ServerAdmin, StringComparer.Ordinal);
@ -146,22 +380,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);
}
@ -172,12 +411,28 @@ namespace BTCPayServer.Controllers
var user = userId == null ? null : await _UserManager.FindByIdAsync(userId);
if (user == null)
return NotFound();
return View("Confirm", new ConfirmModel()
var roles = await _UserManager.GetRolesAsync(user);
if (IsAdmin(roles))
{
Title = "Delete user " + user.Email,
Description = "This user will be permanently deleted",
Action = "Delete"
});
var admins = await _UserManager.GetUsersInRoleAsync(Roles.ServerAdmin);
if (admins.Count == 1)
{
// return
return View("Confirm", new ConfirmModel("Unable to Delete Last Admin",
"This is the last Admin, so it can't be removed"));
}
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")]
@ -188,6 +443,7 @@ namespace BTCPayServer.Controllers
if (user == null)
return NotFound();
await _UserManager.DeleteAsync(user);
await _StoreRepository.CleanUnreachableStores();
StatusMessage = "User deleted";
return RedirectToAction(nameof(ListUsers));
}
@ -197,27 +453,336 @@ namespace BTCPayServer.Controllers
{
get; set;
}
[Route("server/emails")]
public async Task<IActionResult> Emails()
{
var data = (await _SettingsRepository.GetSettingAsync<EmailSettings>()) ?? new EmailSettings();
return View(new EmailsViewModel() { Settings = data });
}
public IHttpClientFactory HttpClientFactory { get; }
[Route("server/policies")]
public async Task<IActionResult> Policies()
{
var data = (await _SettingsRepository.GetSettingAsync<PoliciesSettings>()) ?? new PoliciesSettings();
// load display app dropdown
using (var ctx = _ContextFactory.CreateContext())
{
var userId = _UserManager.GetUserId(base.User);
var selectList = ctx.Users.Where(user => user.Id == userId)
.SelectMany(s => s.UserStores)
.Select(s => s.StoreData)
.SelectMany(s => s.Apps)
.Select(a => new SelectListItem($"{a.AppType} - {a.Name}", a.Id)).ToList();
selectList.Insert(0, new SelectListItem("(None)", null));
ViewBag.AppsList = new SelectList(selectList, "Value", "Text", data.RootAppId);
}
return View(data);
}
[Route("server/policies")]
[HttpPost]
public async Task<IActionResult> Policies(PoliciesSettings settings)
{
if (!String.IsNullOrEmpty(settings.RootAppId))
{
using (var ctx = _ContextFactory.CreateContext())
{
var app = ctx.Apps.SingleOrDefault(a => a.Id == settings.RootAppId);
if (app != null)
settings.RootAppType = Enum.Parse<AppType>(app.AppType);
else
settings.RootAppType = null;
}
}
else
{
// not preserved on client side, but clearing it just in case
settings.RootAppType = null;
}
await _SettingsRepository.UpdateSetting(settings);
TempData["StatusMessage"] = "Policies updated successfully";
return View(settings);
return RedirectToAction(nameof(Policies));
}
[Route("server/services")]
public async Task<IActionResult> Services()
{
var result = new ServicesViewModel();
result.ExternalServices = _Options.ExternalServices.ToList();
foreach (var externalService in _Options.OtherExternalServices)
{
result.OtherExternalServices.Add(new ServicesViewModel.OtherExternalService()
{
Name = externalService.Key,
Link = this.Request.GetAbsoluteUriNoPathBase(externalService.Value).AbsoluteUri
});
}
if (_Options.SSHSettings != null)
{
result.OtherExternalServices.Add(new ServicesViewModel.OtherExternalService()
{
Name = "SSH",
Link = this.Url.Action(nameof(SSHService))
});
}
foreach (var torService in _torServices.Services)
{
if (torService.VirtualPort == 80)
{
result.TorHttpServices.Add(new ServicesViewModel.OtherExternalService()
{
Name = torService.Name,
Link = $"http://{torService.OnionHost}"
});
}
else if (TryParseAsExternalService(torService, out var externalService))
{
result.ExternalServices.Add(externalService);
}
else
{
result.TorOtherServices.Add(new ServicesViewModel.OtherExternalService()
{
Name = torService.Name,
Link = $"{torService.OnionHost}:{torService.VirtualPort}"
});
}
}
var storageSettings = await _SettingsRepository.GetSettingAsync<StorageSettings>();
result.ExternalStorageServices.Add(new ServicesViewModel.OtherExternalService()
{
Name = storageSettings == null? "Not set": storageSettings.Provider.ToString(),
Link = Url.Action("Storage")
});
return View(result);
}
private static bool TryParseAsExternalService(TorService torService, out ExternalService externalService)
{
externalService = null;
if (torService.ServiceType == TorServiceType.P2P)
{
externalService = new ExternalService()
{
CryptoCode = torService.Network.CryptoCode,
DisplayName = "Full node P2P",
Type = ExternalServiceTypes.P2P,
ConnectionString = new ExternalConnectionString(new Uri($"bitcoin-p2p://{torService.OnionHost}:{torService.VirtualPort}", UriKind.Absolute)),
ServiceName = torService.Name,
};
}
return externalService != null;
}
private ExternalService GetService(string serviceName, string cryptoCode)
{
var result = _Options.ExternalServices.GetService(serviceName, cryptoCode);
if (result != null)
return result;
_torServices.Services.FirstOrDefault(s => TryParseAsExternalService(s, out result));
return result;
}
[Route("server/services/{serviceName}/{cryptoCode}")]
public async Task<IActionResult> Service(string serviceName, string cryptoCode, bool showQR = false, uint? nonce = null)
{
if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
{
StatusMessage = $"Error: {cryptoCode} is not fully synched";
return RedirectToAction(nameof(Services));
}
var service = GetService(serviceName, cryptoCode);
if (service == null)
return NotFound();
try
{
if (service.Type == ExternalServiceTypes.P2P)
{
return View("P2PService", new LightningWalletServices()
{
ShowQR = showQR,
WalletName = service.ServiceName,
ServiceLink = service.ConnectionString.Server.AbsoluteUri.WithoutEndingSlash()
});
}
var connectionString = await service.ConnectionString.Expand(this.Request.GetAbsoluteUriNoPathBase(), service.Type);
switch (service.Type)
{
case ExternalServiceTypes.Charge:
return LightningChargeServices(service, connectionString, showQR);
case ExternalServiceTypes.RTL:
case ExternalServiceTypes.Spark:
if (connectionString.AccessKey == null)
{
StatusMessage = $"Error: The access key of the service is not set";
return RedirectToAction(nameof(Services));
}
LightningWalletServices vm = new LightningWalletServices();
vm.ShowQR = showQR;
vm.WalletName = service.DisplayName;
vm.ServiceLink = $"{connectionString.Server}?access-key={connectionString.AccessKey}";
return View("LightningWalletServices", vm);
case ExternalServiceTypes.LNDGRPC:
case ExternalServiceTypes.LNDRest:
return LndServices(service, connectionString, nonce);
default:
throw new NotSupportedException(service.Type.ToString());
}
}
catch (Exception ex)
{
StatusMessage = $"Error: {ex.Message}";
return RedirectToAction(nameof(Services));
}
}
private IActionResult LightningChargeServices(ExternalService service, ExternalConnectionString connectionString, bool showQR = false)
{
ChargeServiceViewModel vm = new ChargeServiceViewModel();
vm.Uri = connectionString.Server.AbsoluteUri;
vm.APIToken = connectionString.APIToken;
var builder = new UriBuilder(connectionString.Server);
builder.UserName = "api-token";
builder.Password = vm.APIToken;
vm.AuthenticatedUri = builder.ToString();
return View(nameof(LightningChargeServices), vm);
}
private IActionResult LndServices(ExternalService service, ExternalConnectionString connectionString, uint? nonce)
{
var model = new LndGrpcServicesViewModel();
if (service.Type == ExternalServiceTypes.LNDGRPC)
{
model.Host = $"{connectionString.Server.DnsSafeHost}:{connectionString.Server.Port}";
model.SSL = connectionString.Server.Scheme == "https";
model.ConnectionType = "GRPC";
model.GRPCSSLCipherSuites = "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256";
}
else if (service.Type == ExternalServiceTypes.LNDRest)
{
model.Uri = connectionString.Server.AbsoluteUri;
model.ConnectionType = "REST";
}
if (connectionString.CertificateThumbprint != null)
{
model.CertificateThumbprint = connectionString.CertificateThumbprint;
}
if (connectionString.Macaroon != null)
{
model.Macaroon = Encoders.Hex.EncodeData(connectionString.Macaroon);
}
model.AdminMacaroon = connectionString.Macaroons?.AdminMacaroon?.Hex;
model.InvoiceMacaroon = connectionString.Macaroons?.InvoiceMacaroon?.Hex;
model.ReadonlyMacaroon = connectionString.Macaroons?.ReadonlyMacaroon?.Hex;
if (nonce != null)
{
var configKey = GetConfigKey("lnd", service.ServiceName, service.CryptoCode, nonce.Value);
var lnConfig = _LnConfigProvider.GetConfig(configKey);
if (lnConfig != null)
{
model.QRCodeLink = Request.GetAbsoluteUri(Url.Action(nameof(GetLNDConfig), new { configKey = configKey }));
model.QRCode = $"config={model.QRCodeLink}";
}
}
return View(nameof(LndServices), model);
}
private static uint GetConfigKey(string type, string serviceName, string cryptoCode, uint nonce)
{
return (uint)HashCode.Combine(type, serviceName, cryptoCode, nonce);
}
[Route("lnd-config/{configKey}/lnd.config")]
[AllowAnonymous]
public IActionResult GetLNDConfig(uint configKey)
{
var conf = _LnConfigProvider.GetConfig(configKey);
if (conf == null)
return NotFound();
return Json(conf);
}
[Route("server/services/{serviceName}/{cryptoCode}")]
[HttpPost]
public async Task<IActionResult> ServicePost(string serviceName, string cryptoCode)
{
if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
{
StatusMessage = $"Error: {cryptoCode} is not fully synched";
return RedirectToAction(nameof(Services));
}
var service = GetService(serviceName, cryptoCode);
if (service == null)
return NotFound();
ExternalConnectionString connectionString = null;
try
{
connectionString = await service.ConnectionString.Expand(this.Request.GetAbsoluteUriNoPathBase(), service.Type);
}
catch (Exception ex)
{
StatusMessage = $"Error: {ex.Message}";
return RedirectToAction(nameof(Services));
}
LightningConfigurations confs = new LightningConfigurations();
if (service.Type == ExternalServiceTypes.LNDGRPC)
{
LightningConfiguration grpcConf = new LightningConfiguration();
grpcConf.Type = "grpc";
grpcConf.Host = connectionString.Server.DnsSafeHost;
grpcConf.Port = connectionString.Server.Port;
grpcConf.SSL = connectionString.Server.Scheme == "https";
confs.Configurations.Add(grpcConf);
}
else if (service.Type == ExternalServiceTypes.LNDRest)
{
var restconf = new LNDRestConfiguration();
restconf.Type = "lnd-rest";
restconf.Uri = connectionString.Server.AbsoluteUri;
confs.Configurations.Add(restconf);
}
else
throw new NotSupportedException(service.Type.ToString());
var commonConf = (LNDConfiguration)confs.Configurations[confs.Configurations.Count - 1];
commonConf.ChainType = _Options.NetworkType.ToString();
commonConf.CryptoCode = cryptoCode;
commonConf.Macaroon = connectionString.Macaroon == null ? null : Encoders.Hex.EncodeData(connectionString.Macaroon);
commonConf.CertificateThumbprint = connectionString.CertificateThumbprint == null ? null : connectionString.CertificateThumbprint;
commonConf.AdminMacaroon = connectionString.Macaroons?.AdminMacaroon?.Hex;
commonConf.ReadonlyMacaroon = connectionString.Macaroons?.ReadonlyMacaroon?.Hex;
commonConf.InvoiceMacaroon = connectionString.Macaroons?.InvoiceMacaroon?.Hex;
var nonce = RandomUtils.GetUInt32();
var configKey = GetConfigKey("lnd", serviceName, cryptoCode, nonce);
_LnConfigProvider.KeepConfig(configKey, confs);
return RedirectToAction(nameof(Service), new { cryptoCode = cryptoCode, serviceName = serviceName, nonce = nonce });
}
[Route("server/services/ssh")]
public IActionResult SSHService(bool downloadKeyFile = false)
{
var settings = _Options.SSHSettings;
if (settings == null)
return NotFound();
if (downloadKeyFile)
{
if (!System.IO.File.Exists(settings.KeyFile))
return NotFound();
return File(System.IO.File.ReadAllBytes(settings.KeyFile), "application/octet-stream", "id_rsa");
}
var server = Extensions.IsLocalNetwork(settings.Server) ? this.Request.Host.Host : settings.Server;
SSHServiceViewModel vm = new SSHServiceViewModel();
string port = settings.Port == 22 ? "" : $" -p {settings.Port}";
vm.CommandLine = $"ssh {settings.Username}@{server}{port}";
vm.Password = settings.Password;
vm.KeyFilePassword = settings.KeyFilePassword;
vm.HasKeyFile = !string.IsNullOrEmpty(settings.KeyFile);
return View(vm);
}
[Route("server/theme")]
@ -235,19 +800,28 @@ namespace BTCPayServer.Controllers
return View(settings);
}
[Route("server/emails")]
public async Task<IActionResult> Emails()
{
var data = (await _SettingsRepository.GetSettingAsync<EmailSettings>()) ?? new EmailSettings();
return View(new EmailsViewModel() { Settings = data });
}
[Route("server/emails")]
[HttpPost]
public async Task<IActionResult> Emails(EmailsViewModel model, string command)
{
if (!model.Settings.IsComplete())
{
model.StatusMessage = "Error: Required fields missing";
return View(model);
}
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";
@ -265,5 +839,67 @@ namespace BTCPayServer.Controllers
return View(model);
}
}
[Route("server/logs/{file?}")]
public async Task<IActionResult> LogsView(string file = null, int offset = 0)
{
if (offset < 0)
{
offset = 0;
}
var vm = new LogsViewModel();
if (string.IsNullOrEmpty(_Options.LogFile))
{
vm.StatusMessage = "Error: File Logging Option not specified. " +
"You need to set debuglog and optionally " +
"debugloglevel in the configuration or through runtime arguments";
}
else
{
var di = Directory.GetParent(_Options.LogFile);
if (di == null)
{
vm.StatusMessage = "Error: Could not load log files";
}
var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(_Options.LogFile);
var fileExtension = Path.GetExtension(_Options.LogFile) ?? string.Empty;
var logFiles = di.GetFiles($"{fileNameWithoutExtension}*{fileExtension}");
vm.LogFileCount = logFiles.Length;
vm.LogFiles = logFiles
.OrderBy(info => info.LastWriteTime)
.Skip(offset)
.Take(5)
.ToList();
vm.LogFileOffset = offset;
if (string.IsNullOrEmpty(file))
return View("Logs", vm);
vm.Log = "";
var path = Path.Combine(di.FullName, file);
try
{
using (var fileStream = new FileStream(
path,
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite))
{
using (var reader = new StreamReader(fileStream))
{
vm.Log = await reader.ReadToEndAsync();
}
}
}
catch
{
return NotFound();
}
}
return View("Logs", vm);
}
}
}

View File

@ -0,0 +1,24 @@
using System.Threading.Tasks;
using BTCPayServer.Storage.Services;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Storage
{
[Route("Storage")]
public class StorageController
{
private readonly FileService _FileService;
public StorageController(FileService fileService)
{
_FileService = fileService;
}
[HttpGet("{fileId}")]
public async Task<IActionResult> GetFile(string fileId)
{
var url = await _FileService.GetFileUrl(fileId);
return new RedirectResult(url);
}
}
}

View File

@ -10,6 +10,7 @@ using BTCPayServer.Data;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Services;
using LedgerWallet;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBXplorer.DerivationStrategy;
@ -33,16 +34,73 @@ namespace BTCPayServer.Controllers
}
DerivationSchemeViewModel vm = new DerivationSchemeViewModel();
vm.ServerUrl = GetStoreUrl(storeId);
vm.CryptoCode = cryptoCode;
vm.RootKeyPath = network.GetRootKeyPath();
SetExistingValues(store, vm);
return View(vm);
}
[HttpGet]
[Route("{storeId}/derivations/{cryptoCode}/ledger/ws")]
public async Task<IActionResult> AddDerivationSchemeLedger(
string storeId,
string cryptoCode,
string command,
string keyPath = "")
{
if (!HttpContext.WebSockets.IsWebSocketRequest)
return NotFound();
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
var hw = new HardwareWalletService(webSocket);
object result = null;
var network = _NetworkProvider.GetNetwork(cryptoCode);
using (var normalOperationTimeout = new CancellationTokenSource())
{
normalOperationTimeout.CancelAfter(TimeSpan.FromMinutes(30));
try
{
if (command == "test")
{
result = await hw.Test(normalOperationTimeout.Token);
}
if (command == "getxpub")
{
var k = KeyPath.Parse(keyPath);
if (k.Indexes.Length == 0)
throw new FormatException("Invalid key path");
var getxpubResult = await hw.GetExtPubKey(network, k, normalOperationTimeout.Token);
result = getxpubResult;
}
}
catch (OperationCanceledException)
{ result = new LedgerTestResult() { Success = false, Error = "Timeout" }; }
catch (Exception ex)
{ result = new LedgerTestResult() { Success = false, Error = ex.Message }; }
finally { hw.Dispose(); }
try
{
if (result != null)
{
UTF8Encoding UTF8NOBOM = new UTF8Encoding(false);
var bytes = UTF8NOBOM.GetBytes(JsonConvert.SerializeObject(result, MvcJsonOptions.Value.SerializerSettings));
await webSocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, new CancellationTokenSource(2000).Token);
}
}
catch { }
finally
{
await webSocket.CloseSocket();
}
}
return new EmptyResult();
}
private void SetExistingValues(StoreData store, DerivationSchemeViewModel vm)
{
vm.DerivationScheme = GetExistingDerivationStrategy(vm.CryptoCode, store)?.DerivationStrategyBase.ToString();
vm.Enabled = !store.GetStoreBlob().IsExcluded(new PaymentMethodId(vm.CryptoCode, PaymentTypes.BTCLike));
}
private DerivationStrategy GetExistingDerivationStrategy(string cryptoCode, StoreData store)
@ -58,7 +116,6 @@ namespace BTCPayServer.Controllers
[Route("{storeId}/derivations/{cryptoCode}")]
public async Task<IActionResult> AddDerivationScheme(string storeId, DerivationSchemeViewModel vm, string cryptoCode)
{
vm.ServerUrl = GetStoreUrl(storeId);
vm.CryptoCode = cryptoCode;
var store = HttpContext.GetStoreData();
if (store == null)
@ -77,6 +134,11 @@ namespace BTCPayServer.Controllers
}
PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
var exisingStrategy = store.GetSupportedPaymentMethods(_NetworkProvider)
.Where(c => c.PaymentId == paymentMethodId)
.OfType<DerivationStrategy>()
.Select(c => c.DerivationStrategyBase.ToString())
.FirstOrDefault();
DerivationStrategy strategy = null;
try
{
@ -92,11 +154,41 @@ namespace BTCPayServer.Controllers
vm.Confirmation = false;
return View(vm);
}
var storeBlob = store.GetStoreBlob();
var wasExcluded = storeBlob.GetExcludedPaymentMethods().Match(paymentMethodId);
var willBeExcluded = !vm.Enabled;
if (!vm.Confirmation && strategy != null)
return ShowAddresses(vm, strategy);
var showAddress = // Show addresses if:
// - If the user is testing the hint address in confirmation screen
(vm.Confirmation && !string.IsNullOrWhiteSpace(vm.HintAddress)) ||
// - The user is setting a new derivation scheme
(!vm.Confirmation && strategy != null && exisingStrategy != strategy.DerivationStrategyBase.ToString()) ||
// - The user is clicking on continue without changing anything
(!vm.Confirmation && willBeExcluded == wasExcluded);
if (vm.Confirmation && !string.IsNullOrWhiteSpace(vm.HintAddress))
showAddress = showAddress && strategy != null;
if (!showAddress)
{
try
{
if (strategy != null)
await wallet.TrackAsync(strategy.DerivationStrategyBase);
store.SetSupportedPaymentMethod(paymentMethodId, strategy);
storeBlob.SetExcluded(paymentMethodId, willBeExcluded);
storeBlob.SetWalletKeyPathRoot(paymentMethodId, vm.KeyPath == null ? null : KeyPath.Parse(vm.KeyPath));
store.SetStoreBlob(storeBlob);
}
catch
{
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme");
return View(vm);
}
await _Repo.UpdateStore(store);
StatusMessage = $"Derivation scheme for {network.CryptoCode} has been modified.";
return RedirectToAction(nameof(UpdateStore), new { storeId = storeId });
}
else if (!string.IsNullOrEmpty(vm.HintAddress))
{
BitcoinAddress address = null;
try
@ -122,26 +214,8 @@ namespace BTCPayServer.Controllers
vm.StatusMessage = "Address successfully found, please verify that the rest is correct and click on \"Confirm\"";
ModelState.Remove(nameof(vm.HintAddress));
ModelState.Remove(nameof(vm.DerivationScheme));
return ShowAddresses(vm, strategy);
}
else
{
try
{
if (strategy != null)
await wallet.TrackAsync(strategy.DerivationStrategyBase);
store.SetSupportedPaymentMethod(paymentMethodId, strategy);
}
catch
{
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme");
return View(vm);
}
await _Repo.UpdateStore(store);
StatusMessage = $"Derivation scheme for {network.CryptoCode} has been modified.";
return RedirectToAction(nameof(UpdateStore), new { storeId = storeId });
}
return ShowAddresses(vm, strategy);
}
private IActionResult ShowAddresses(DerivationSchemeViewModel vm, DerivationStrategy strategy)
@ -160,199 +234,5 @@ namespace BTCPayServer.Controllers
vm.Confirmation = true;
return View(vm);
}
public class GetInfoResult
{
public int RecommendedSatoshiPerByte { get; set; }
public double Balance { get; set; }
}
public class SendToAddressResult
{
public string TransactionId { get; set; }
}
[HttpGet]
[Route("{storeId}/ws/ledger")]
public async Task<IActionResult> LedgerConnection(
string storeId,
string command,
// getinfo
string cryptoCode = null,
// getxpub
int account = 0,
// sendtoaddress
string destination = null, string amount = null, string feeRate = null, string substractFees = null
)
{
if (!HttpContext.WebSockets.IsWebSocketRequest)
return NotFound();
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
var hw = new HardwareWalletService(webSocket);
object result = null;
try
{
BTCPayNetwork network = null;
if (cryptoCode != null)
{
network = _NetworkProvider.GetNetwork(cryptoCode);
if (network == null)
throw new FormatException("Invalid value for crypto code");
}
BitcoinAddress destinationAddress = null;
if (destination != null)
{
try
{
destinationAddress = BitcoinAddress.Create(destination, network.NBitcoinNetwork);
}
catch { }
if (destinationAddress == null)
throw new FormatException("Invalid value for destination");
}
FeeRate feeRateValue = null;
if (feeRate != null)
{
try
{
feeRateValue = new FeeRate(Money.Satoshis(int.Parse(feeRate, CultureInfo.InvariantCulture)), 1);
}
catch { }
if (feeRateValue == null || feeRateValue.FeePerK <= Money.Zero)
throw new FormatException("Invalid value for fee rate");
}
Money amountBTC = null;
if (amount != null)
{
try
{
amountBTC = Money.Parse(amount);
}
catch { }
if (amountBTC == null || amountBTC <= Money.Zero)
throw new FormatException("Invalid value for amount");
}
bool subsctractFeesValue = false;
if (substractFees != null)
{
try
{
subsctractFeesValue = bool.Parse(substractFees);
}
catch { throw new FormatException("Invalid value for subtract fees"); }
}
if (command == "test")
{
result = await hw.Test();
}
if (command == "getxpub")
{
var getxpubResult = await hw.GetExtPubKey(network, account);
result = getxpubResult;
}
if (command == "getinfo")
{
var strategy = GetDirectDerivationStrategy(store, network);
var strategyBase = GetDerivationStrategy(store, network);
if (strategy == null || !await hw.SupportDerivation(network, strategy))
{
throw new Exception($"This store is not configured to use this ledger");
}
var feeProvider = _FeeRateProvider.CreateFeeProvider(network);
var recommendedFees = feeProvider.GetFeeRateAsync();
var balance = _WalletProvider.GetWallet(network).GetBalance(strategyBase);
result = new GetInfoResult() { Balance = (double)(await balance).ToDecimal(MoneyUnit.BTC), RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi };
}
if (command == "sendtoaddress")
{
if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
throw new Exception($"{network.CryptoCode}: not started or fully synched");
var strategy = GetDirectDerivationStrategy(store, network);
var strategyBase = GetDerivationStrategy(store, network);
var wallet = _WalletProvider.GetWallet(network);
var change = wallet.GetChangeAddressAsync(strategyBase);
var unspentCoins = await wallet.GetUnspentCoins(strategyBase);
var changeAddress = await change;
var transaction = await hw.SendToAddress(strategy, unspentCoins, network,
new[] { (destinationAddress as IDestination, amountBTC, subsctractFeesValue) },
feeRateValue,
changeAddress.Item1,
changeAddress.Item2, summary.Status.BitcoinStatus.MinRelayTxFee);
try
{
var broadcastResult = await wallet.BroadcastTransactionsAsync(new List<Transaction>() { transaction });
if (!broadcastResult[0].Success)
{
throw new Exception($"RPC Error while broadcasting: {broadcastResult[0].RPCCode} {broadcastResult[0].RPCCodeMessage} {broadcastResult[0].RPCMessage}");
}
}
catch (Exception ex)
{
throw new Exception("Error while broadcasting: " + ex.Message);
}
wallet.InvalidateCache(strategyBase);
result = new SendToAddressResult() { TransactionId = transaction.GetHash().ToString() };
}
}
catch (OperationCanceledException)
{ result = new LedgerTestResult() { Success = false, Error = "Timeout" }; }
catch (Exception ex)
{ result = new LedgerTestResult() { Success = false, Error = ex.Message }; }
try
{
if (result != null)
{
UTF8Encoding UTF8NOBOM = new UTF8Encoding(false);
var bytes = UTF8NOBOM.GetBytes(JsonConvert.SerializeObject(result, _MvcJsonOptions.SerializerSettings));
await webSocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, new CancellationTokenSource(2000).Token);
}
}
catch { }
finally
{
await webSocket.CloseSocket();
}
return new EmptyResult();
}
private DirectDerivationStrategy GetDirectDerivationStrategy(StoreData store, BTCPayNetwork network)
{
var strategy = GetDerivationStrategy(store, network);
var directStrategy = strategy as DirectDerivationStrategy;
if (directStrategy == null)
directStrategy = (strategy as P2SHDerivationStrategy).Inner as DirectDerivationStrategy;
if (!directStrategy.Segwit)
return null;
return directStrategy;
}
private DerivationStrategyBase GetDerivationStrategy(StoreData store, BTCPayNetwork network)
{
var strategy = store
.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<DerivationStrategy>()
.FirstOrDefault(s => s.Network.NBitcoinNetwork == network.NBitcoinNetwork);
if (strategy == null)
{
throw new Exception($"Derivation strategy for {network.CryptoCode} is not set");
}
return strategy.DerivationStrategyBase;
}
}
}

View File

@ -0,0 +1,97 @@
using System;
using System.Net.Http;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments.Changelly;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers
{
public partial class StoresController
{
[HttpGet]
[Route("{storeId}/changelly")]
public IActionResult UpdateChangellySettings(string storeId)
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
UpdateChangellySettingsViewModel vm = new UpdateChangellySettingsViewModel();
SetExistingValues(store, vm);
return View(vm);
}
private void SetExistingValues(StoreData store, UpdateChangellySettingsViewModel vm)
{
var existing = store.GetStoreBlob().ChangellySettings;
if (existing == null) return;
vm.ApiKey = existing.ApiKey;
vm.ApiSecret = existing.ApiSecret;
vm.ApiUrl = existing.ApiUrl;
vm.ChangellyMerchantId = existing.ChangellyMerchantId;
vm.Enabled = existing.Enabled;
vm.AmountMarkupPercentage = existing.AmountMarkupPercentage;
vm.ShowFiat = existing.ShowFiat;
}
[HttpPost]
[Route("{storeId}/changelly")]
public async Task<IActionResult> UpdateChangellySettings(string storeId, UpdateChangellySettingsViewModel vm,
string command)
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
if (vm.Enabled)
{
if (!ModelState.IsValid)
{
return View(vm);
}
}
var changellySettings = new ChangellySettings()
{
ApiKey = vm.ApiKey,
ApiSecret = vm.ApiSecret,
ApiUrl = vm.ApiUrl,
ChangellyMerchantId = vm.ChangellyMerchantId,
Enabled = vm.Enabled,
AmountMarkupPercentage = vm.AmountMarkupPercentage,
ShowFiat = vm.ShowFiat
};
switch (command)
{
case "save":
var storeBlob = store.GetStoreBlob();
storeBlob.ChangellySettings = changellySettings;
store.SetStoreBlob(storeBlob);
await _Repo.UpdateStore(store);
StatusMessage = "Changelly settings modified";
_changellyClientProvider.InvalidateClient(storeId);
return RedirectToAction(nameof(UpdateStore), new {
storeId});
case "test":
try
{
var client = new Changelly(_httpClientFactory.CreateClient(), changellySettings.ApiKey, changellySettings.ApiSecret,
changellySettings.ApiUrl);
var result = await client.GetCurrenciesFull();
vm.StatusMessage = "Test Successful";
return View(vm);
}
catch (Exception ex)
{
vm.StatusMessage = $"Error: {ex.Message}";
return View(vm);
}
default:
return View(vm);
}
}
}
}

View File

@ -0,0 +1,74 @@
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments.CoinSwitch;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers
{
public partial class StoresController
{
[HttpGet]
[Route("{storeId}/coinswitch")]
public IActionResult UpdateCoinSwitchSettings(string storeId)
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
UpdateCoinSwitchSettingsViewModel vm = new UpdateCoinSwitchSettingsViewModel();
SetExistingValues(store, vm);
return View(vm);
}
private void SetExistingValues(StoreData store, UpdateCoinSwitchSettingsViewModel vm)
{
var existing = store.GetStoreBlob().CoinSwitchSettings;
if (existing == null) return;
vm.MerchantId = existing.MerchantId;
vm.Enabled = existing.Enabled;
vm.Mode = existing.Mode;
vm.AmountMarkupPercentage = existing.AmountMarkupPercentage;
}
[HttpPost]
[Route("{storeId}/coinswitch")]
public async Task<IActionResult> UpdateCoinSwitchSettings(string storeId, UpdateCoinSwitchSettingsViewModel vm,
string command)
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
if (vm.Enabled)
{
if (!ModelState.IsValid)
{
return View(vm);
}
}
var coinSwitchSettings = new CoinSwitchSettings()
{
MerchantId = vm.MerchantId,
Enabled = vm.Enabled,
Mode = vm.Mode,
AmountMarkupPercentage = vm.AmountMarkupPercentage
};
switch (command)
{
case "save":
var storeBlob = store.GetStoreBlob();
storeBlob.CoinSwitchSettings = coinSwitchSettings;
store.SetStoreBlob(storeBlob);
await _Repo.UpdateStore(store);
StatusMessage = "CoinSwitch settings modified";
return RedirectToAction(nameof(UpdateStore), new {
storeId});
default:
return View(vm);
}
}
}
}

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

@ -5,12 +5,12 @@ using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning.CLightning;
using Microsoft.AspNetCore.Mvc;
using BTCPayServer.Payments.Lightning;
using System.Net;
using BTCPayServer.Data;
using System.Threading;
using BTCPayServer.Lightning;
namespace BTCPayServer.Controllers
{
@ -24,18 +24,21 @@ namespace BTCPayServer.Controllers
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
LightningNodeViewModel vm = new LightningNodeViewModel();
vm.CryptoCode = cryptoCode;
vm.InternalLightningNode = GetInternalLighningNode(cryptoCode)?.ToUri(true)?.AbsoluteUri;
LightningNodeViewModel vm = new LightningNodeViewModel
{
CryptoCode = cryptoCode,
InternalLightningNode = GetInternalLighningNode(cryptoCode)?.ToString(),
StoreId = storeId
};
SetExistingValues(store, vm);
return View(vm);
}
private void SetExistingValues(StoreData store, LightningNodeViewModel vm)
{
vm.Url = GetExistingLightningSupportedPaymentMethod(vm.CryptoCode, store)?.GetLightningUrl()?.ToString();
vm.ConnectionString = GetExistingLightningSupportedPaymentMethod(vm.CryptoCode, store)?.GetLightningUrl()?.ToString();
vm.Enabled = !store.GetStoreBlob().IsExcluded(new PaymentMethodId(vm.CryptoCode, PaymentTypes.LightningLike));
}
private LightningSupportedPaymentMethod GetExistingLightningSupportedPaymentMethod(string cryptoCode, StoreData store)
{
var id = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
@ -65,7 +68,7 @@ namespace BTCPayServer.Controllers
var network = vm.CryptoCode == null ? null : _ExplorerProvider.GetNetwork(vm.CryptoCode);
var internalLightning = GetInternalLighningNode(network.CryptoCode);
vm.InternalLightningNode = internalLightning?.ToUri(true)?.AbsoluteUri;
vm.InternalLightningNode = internalLightning?.ToString();
if (network == null)
{
ModelState.AddModelError(nameof(vm.CryptoCode), "Invalid network");
@ -74,33 +77,57 @@ namespace BTCPayServer.Controllers
PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.LightningLike);
Payments.Lightning.LightningSupportedPaymentMethod paymentMethod = null;
if (!string.IsNullOrEmpty(vm.Url))
if (!string.IsNullOrEmpty(vm.ConnectionString))
{
if (!LightningConnectionString.TryParse(vm.Url, out var connectionString, out var error))
if (!LightningConnectionString.TryParse(vm.ConnectionString, false, out var connectionString, out var error))
{
ModelState.AddModelError(nameof(vm.Url), $"Invalid URL ({error})");
ModelState.AddModelError(nameof(vm.ConnectionString), $"Invalid URL ({error})");
return View(vm);
}
var internalDomain = internalLightning?.ToUri(false)?.DnsSafeHost;
bool isLocal = (internalDomain == "127.0.0.1" || internalDomain == "localhost");
if(connectionString.ConnectionType == LightningConnectionType.LndGRPC)
{
ModelState.AddModelError(nameof(vm.ConnectionString), $"BTCPay does not support gRPC connections");
return View(vm);
}
var internalDomain = internalLightning?.BaseUri?.DnsSafeHost;
bool isInternalNode = connectionString.ConnectionType == LightningConnectionType.CLightning ||
connectionString.BaseUri.DnsSafeHost == internalDomain ||
isLocal;
(internalDomain == "127.0.0.1" || internalDomain == "localhost");
if (connectionString.BaseUri.Scheme == "http" && !isLocal)
if (connectionString.BaseUri.Scheme == "http")
{
if (!isInternalNode || (isInternalNode && !CanUseInternalLightning()))
if (!isInternalNode)
{
ModelState.AddModelError(nameof(vm.Url), "The url must be HTTPS");
ModelState.AddModelError(nameof(vm.ConnectionString), "The url must be HTTPS");
return View(vm);
}
}
if(connectionString.MacaroonFilePath != null)
{
if(!CanUseInternalLightning())
{
ModelState.AddModelError(nameof(vm.ConnectionString), "You are not authorized to use macaroonfilepath");
return View(vm);
}
if(!System.IO.File.Exists(connectionString.MacaroonFilePath))
{
ModelState.AddModelError(nameof(vm.ConnectionString), "The macaroonfilepath file does not exist");
return View(vm);
}
if(!System.IO.Path.IsPathRooted(connectionString.MacaroonFilePath))
{
ModelState.AddModelError(nameof(vm.ConnectionString), "The macaroonfilepath should be fully rooted");
return View(vm);
}
}
if (isInternalNode && !CanUseInternalLightning())
{
ModelState.AddModelError(nameof(vm.Url), "Unauthorized url");
ModelState.AddModelError(nameof(vm.ConnectionString), "Unauthorized url");
return View(vm);
}
@ -110,39 +137,42 @@ namespace BTCPayServer.Controllers
};
paymentMethod.SetLightningUrl(connectionString);
}
if (command == "save")
switch (command)
{
store.SetSupportedPaymentMethod(paymentMethodId, paymentMethod);
await _Repo.UpdateStore(store);
StatusMessage = $"Lightning node modified ({network.CryptoCode})";
return RedirectToAction(nameof(UpdateStore), new { storeId = storeId });
}
else // if(command == "test")
{
if (paymentMethod == null)
{
ModelState.AddModelError(nameof(vm.Url), "Missing url parameter");
case "save":
var storeBlob = store.GetStoreBlob();
storeBlob.SetExcluded(paymentMethodId, !vm.Enabled);
store.SetStoreBlob(storeBlob);
store.SetSupportedPaymentMethod(paymentMethodId, paymentMethod);
await _Repo.UpdateStore(store);
StatusMessage = $"Lightning node modified ({network.CryptoCode})";
return RedirectToAction(nameof(UpdateStore), new { storeId = storeId });
case "test" when paymentMethod == null:
ModelState.AddModelError(nameof(vm.ConnectionString), "Missing url parameter");
return View(vm);
}
var handler = (LightningLikePaymentHandler)_ServiceProvider.GetRequiredService<IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>>();
try
{
var info = await handler.Test(paymentMethod, network);
if (!vm.SkipPortTest)
case "test":
var handler = (LightningLikePaymentHandler)_ServiceProvider.GetRequiredService<IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>>();
try
{
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)))
var info = await handler.GetNodeInfo(this.Request.IsOnion(), paymentMethod, network);
if (!vm.SkipPortTest)
{
await handler.TestConnection(info, cts.Token);
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)))
{
await handler.TestConnection(info, cts.Token);
}
}
vm.StatusMessage = $"Connection to the lightning node succeeded ({info})";
}
catch (Exception ex)
{
vm.StatusMessage = $"Error: {ex.Message}";
return View(vm);
}
vm.StatusMessage = $"Connection to the lightning node succeed ({info})";
}
catch (Exception ex)
{
vm.StatusMessage = $"Error: {ex.Message}";
return View(vm);
}
return View(vm);
default:
return View(vm);
}
}

View File

@ -1,9 +1,19 @@
using BTCPayServer.Authentication;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Authentication;
using BTCPayServer.Configuration;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.Models;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Changelly;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Rating;
using BTCPayServer.Security;
using BTCPayServer.Services;
@ -11,6 +21,7 @@ using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
@ -18,14 +29,6 @@ using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.Extensions.Options;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBXplorer.DerivationStrategy;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace BTCPayServer.Controllers
{
@ -35,53 +38,49 @@ namespace BTCPayServer.Controllers
[AutoValidateAntiforgeryToken]
public partial class StoresController : Controller
{
BTCPayRateProviderFactory _RateFactory;
RateFetcher _RateFactory;
public string CreatedStoreId { get; set; }
public StoresController(
NBXplorerDashboard dashboard,
IServiceProvider serviceProvider,
BTCPayServerOptions btcpayServerOptions,
BTCPayServerEnvironment btcpayEnv,
IOptions<MvcJsonOptions> mvcJsonOptions,
StoreRepository repo,
TokenRepository tokenRepo,
UserManager<ApplicationUser> userManager,
AccessTokenController tokenController,
BTCPayWalletProvider walletProvider,
BTCPayNetworkProvider networkProvider,
BTCPayRateProviderFactory rateFactory,
RateFetcher rateFactory,
ExplorerClientProvider explorerProvider,
IFeeProviderFactory feeRateProvider,
LanguageService langService,
IHostingEnvironment env,
CoinAverageSettings coinAverage)
ChangellyClientProvider changellyClientProvider,
IOptions<MvcJsonOptions> mvcJsonOptions,
IHostingEnvironment env, IHttpClientFactory httpClientFactory)
{
_RateFactory = rateFactory;
_Dashboard = dashboard;
_Repo = repo;
_TokenRepository = tokenRepo;
_UserManager = userManager;
_LangService = langService;
_changellyClientProvider = changellyClientProvider;
MvcJsonOptions = mvcJsonOptions;
_TokenController = tokenController;
_WalletProvider = walletProvider;
_Env = env;
_httpClientFactory = httpClientFactory;
_NetworkProvider = networkProvider;
_ExplorerProvider = explorerProvider;
_MvcJsonOptions = mvcJsonOptions.Value;
_FeeRateProvider = feeRateProvider;
_ServiceProvider = serviceProvider;
_BtcpayServerOptions = btcpayServerOptions;
_BTCPayEnv = btcpayEnv;
_CoinAverage = coinAverage;
}
CoinAverageSettings _CoinAverage;
NBXplorerDashboard _Dashboard;
BTCPayServerOptions _BtcpayServerOptions;
BTCPayServerEnvironment _BTCPayEnv;
IServiceProvider _ServiceProvider;
BTCPayNetworkProvider _NetworkProvider;
private ExplorerClientProvider _ExplorerProvider;
private MvcJsonOptions _MvcJsonOptions;
private IFeeProviderFactory _FeeRateProvider;
BTCPayWalletProvider _WalletProvider;
AccessTokenController _TokenController;
@ -89,27 +88,19 @@ namespace BTCPayServer.Controllers
TokenRepository _TokenRepository;
UserManager<ApplicationUser> _UserManager;
private LanguageService _LangService;
private readonly ChangellyClientProvider _changellyClientProvider;
IHostingEnvironment _Env;
private IHttpClientFactory _httpClientFactory;
[TempData]
public string StatusMessage
{
get; set;
}
[HttpGet]
[Route("{storeId}/wallet/{cryptoCode}")]
public IActionResult Wallet(string cryptoCode)
[TempData]
public bool StoreNotConfigured
{
WalletModel model = new WalletModel();
model.ServerUrl = GetStoreUrl(StoreData.Id);
model.CryptoCurrency = cryptoCode;
return View(model);
}
private string GetStoreUrl(string storeId)
{
return HttpContext.Request.GetAbsoluteRoot() + "/stores/" + storeId + "/";
get; set;
}
[HttpGet]
@ -182,7 +173,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"
});
}
@ -198,24 +189,39 @@ namespace BTCPayServer.Controllers
[HttpGet]
[Route("{storeId}/rates")]
public IActionResult Rates()
public IActionResult Rates(string storeId)
{
var storeBlob = StoreData.GetStoreBlob();
var vm = new RatesViewModel();
vm.SetExchangeRates(GetSupportedExchanges(), storeBlob.PreferredExchange ?? CoinAverageRateProvider.CoinAverageName);
vm.RateMultiplier = (double)storeBlob.GetRateMultiplier();
vm.Spread = (double)(storeBlob.Spread * 100m);
vm.StoreId = storeId;
vm.Script = storeBlob.GetRateRules(_NetworkProvider).ToString();
vm.DefaultScript = storeBlob.GetDefaultRateRules(_NetworkProvider).ToString();
vm.AvailableExchanges = GetSupportedExchanges();
vm.DefaultCurrencyPairs = storeBlob.GetDefaultCurrencyPairString();
vm.ShowScripting = storeBlob.RateScripting;
return View(vm);
}
[HttpPost]
[Route("{storeId}/rates")]
public async Task<IActionResult> Rates(RatesViewModel model, string command = null)
public async Task<IActionResult> Rates(RatesViewModel model, string command = null, string storeId = null, CancellationToken cancellationToken = default)
{
model.SetExchangeRates(GetSupportedExchanges(), model.PreferredExchange);
model.StoreId = storeId ?? model.StoreId;
CurrencyPair[] currencyPairs = null;
try
{
currencyPairs = model.DefaultCurrencyPairs?
.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(p => CurrencyPair.Parse(p))
.ToArray();
}
catch
{
ModelState.AddModelError(nameof(model.DefaultCurrencyPairs), "Invalid currency pairs (should be for example: BTC_USD,BTC_CAD,BTC_JPY)");
}
if (!ModelState.IsValid)
{
return View(model);
@ -228,8 +234,8 @@ namespace BTCPayServer.Controllers
model.AvailableExchanges = GetSupportedExchanges();
blob.PreferredExchange = model.PreferredExchange;
blob.SetRateMultiplier(model.RateMultiplier);
blob.Spread = (decimal)model.Spread / 100.0m;
blob.DefaultCurrencyPairs = currencyPairs;
if (!model.ShowScripting)
{
if (!GetSupportedExchanges().Select(c => c.Name).Contains(blob.PreferredExchange, StringComparer.OrdinalIgnoreCase))
@ -277,7 +283,7 @@ namespace BTCPayServer.Controllers
pairs.Add(currencyPair);
}
var fetchs = _RateFactory.FetchRates(pairs.ToHashSet(), rules);
var fetchs = _RateFactory.FetchRates(pairs.ToHashSet(), rules, cancellationToken);
var testResults = new List<RatesViewModel.TestResultViewModel>();
foreach (var fetch in fetchs)
{
@ -286,7 +292,7 @@ namespace BTCPayServer.Controllers
{
CurrencyPair = fetch.Key.ToString(),
Error = testResult.Errors.Count != 0,
Rule = testResult.Errors.Count == 0 ? testResult.Rule + " = " + testResult.Value.Value.ToString(CultureInfo.InvariantCulture)
Rule = testResult.Errors.Count == 0 ? testResult.Rule + " = " + testResult.BidAsk.Bid.ToString(CultureInfo.InvariantCulture)
: testResult.EvaluatedRule
});
}
@ -316,7 +322,7 @@ namespace BTCPayServer.Controllers
Action = "Continue",
Title = "Rate rule scripting",
Description = scripting ?
"This action will mofify your current rate sources. Are you sure to turn on rate rules scripting? (Advanced users)"
"This action will modify your current rate sources. Are you sure to turn on rate rules scripting? (Advanced users)"
: "This action will delete your rate script. Are you sure to turn off rate rules scripting?",
ButtonClass = "btn-primary"
});
@ -341,17 +347,35 @@ namespace BTCPayServer.Controllers
{
var storeBlob = StoreData.GetStoreBlob();
var vm = new CheckoutExperienceViewModel();
vm.SetCryptoCurrencies(_ExplorerProvider, StoreData.GetDefaultCrypto());
vm.SetLanguages(_LangService, storeBlob.DefaultLang);
vm.LightningMaxValue = storeBlob.LightningMaxValue?.ToString() ?? "";
vm.OnChainMinValue = storeBlob.OnChainMinValue?.ToString() ?? "";
vm.AllowCoinConversion = storeBlob.AllowCoinConversion;
vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail;
SetCryptoCurrencies(vm, StoreData);
vm.CustomCSS = storeBlob.CustomCSS?.AbsoluteUri;
vm.CustomLogo = storeBlob.CustomLogo?.AbsoluteUri;
vm.HtmlTitle = storeBlob.HtmlTitle;
vm.SetLanguages(_LangService, storeBlob.DefaultLang);
vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail;
vm.OnChainMinValue = storeBlob.OnChainMinValue?.ToString() ?? "";
vm.LightningMaxValue = storeBlob.LightningMaxValue?.ToString() ?? "";
vm.LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi;
vm.RedirectAutomatically = storeBlob.RedirectAutomatically;
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")]
@ -376,26 +400,28 @@ namespace BTCPayServer.Controllers
}
bool needUpdate = false;
var blob = StoreData.GetStoreBlob();
if (StoreData.GetDefaultCrypto() != 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)
{
return View(model);
}
blob.DefaultLang = model.DefaultLang;
blob.AllowCoinConversion = model.AllowCoinConversion;
blob.RequiresRefundEmail = model.RequiresRefundEmail;
blob.LightningMaxValue = lightningMaxValue;
blob.OnChainMinValue = onchainMinValue;
blob.CustomLogo = string.IsNullOrWhiteSpace(model.CustomLogo) ? null : new Uri(model.CustomLogo, UriKind.Absolute);
blob.CustomCSS = string.IsNullOrWhiteSpace(model.CustomCSS) ? null : new Uri(model.CustomCSS, UriKind.Absolute);
blob.HtmlTitle = string.IsNullOrWhiteSpace(model.HtmlTitle) ? null : model.HtmlTitle;
blob.DefaultLang = model.DefaultLang;
blob.RequiresRefundEmail = model.RequiresRefundEmail;
blob.OnChainMinValue = onchainMinValue;
blob.LightningMaxValue = lightningMaxValue;
blob.LightningAmountInSatoshi = model.LightningAmountInSatoshi;
blob.RedirectAutomatically = model.RedirectAutomatically;
if (StoreData.SetStoreBlob(blob))
{
needUpdate = true;
@ -425,9 +451,11 @@ namespace BTCPayServer.Controllers
vm.Id = store.Id;
vm.StoreName = store.StoreName;
vm.StoreWebsite = store.StoreWebsite;
vm.NetworkFee = !storeBlob.NetworkFeeDisabled;
vm.NetworkFeeMode = storeBlob.NetworkFeeMode;
vm.AnyoneCanCreateInvoice = storeBlob.AnyoneCanInvoice;
vm.SpeedPolicy = store.SpeedPolicy;
AddPaymentMethods(store, vm);
vm.CanDelete = _Repo.CanDeleteStores();
AddPaymentMethods(store, storeBlob, vm);
vm.MonitoringExpiration = storeBlob.MonitoringExpiration;
vm.InvoiceExpiration = storeBlob.InvoiceExpiration;
vm.LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate;
@ -436,8 +464,9 @@ namespace BTCPayServer.Controllers
}
private void AddPaymentMethods(StoreData store, StoreViewModel vm)
private void AddPaymentMethods(StoreData store, StoreBlob storeBlob, StoreViewModel vm)
{
var excludeFilters = storeBlob.GetExcludedPaymentMethods();
var derivationByCryptoCode =
store
.GetSupportedPaymentMethods(_NetworkProvider)
@ -449,7 +478,9 @@ namespace BTCPayServer.Controllers
vm.DerivationSchemes.Add(new StoreViewModel.DerivationScheme()
{
Crypto = network.CryptoCode,
Value = strategy?.DerivationStrategyBase?.ToString() ?? string.Empty
Value = strategy?.DerivationStrategyBase?.ToString() ?? string.Empty,
WalletId = new WalletId(store.Id, network.CryptoCode),
Enabled = !excludeFilters.Match(new Payments.PaymentMethodId(network.CryptoCode, Payments.PaymentTypes.BTCLike))
});
}
@ -461,20 +492,37 @@ namespace BTCPayServer.Controllers
foreach (var network in _NetworkProvider.GetAll())
{
var lightning = lightningByCryptoCode.TryGet(network.CryptoCode);
var paymentId = new Payments.PaymentMethodId(network.CryptoCode, Payments.PaymentTypes.LightningLike);
vm.LightningNodes.Add(new StoreViewModel.LightningNode()
{
CryptoCode = network.CryptoCode,
Address = lightning?.GetLightningUrl()?.BaseUri.AbsoluteUri ?? string.Empty
Address = lightning?.GetLightningUrl()?.BaseUri.AbsoluteUri ?? string.Empty,
Enabled = !excludeFilters.Match(paymentId)
});
}
var changellyEnabled = storeBlob.ChangellySettings != null && storeBlob.ChangellySettings.Enabled;
vm.ThirdPartyPaymentMethods.Add(new StoreViewModel.ThirdPartyPaymentMethod()
{
Enabled = changellyEnabled,
Action = nameof(UpdateChangellySettings),
Provider = "Changelly"
});
var coinSwitchEnabled = storeBlob.CoinSwitchSettings != null && storeBlob.CoinSwitchSettings.Enabled;
vm.ThirdPartyPaymentMethods.Add(new StoreViewModel.ThirdPartyPaymentMethod()
{
Enabled = coinSwitchEnabled,
Action = nameof(UpdateCoinSwitchSettings),
Provider = "CoinSwitch"
});
}
[HttpPost]
[Route("{storeId}")]
public async Task<IActionResult> UpdateStore(StoreViewModel model)
public async Task<IActionResult> UpdateStore(StoreViewModel model, string command = null)
{
AddPaymentMethods(StoreData, model);
bool needUpdate = false;
if (StoreData.SpeedPolicy != model.SpeedPolicy)
{
@ -493,7 +541,8 @@ namespace BTCPayServer.Controllers
}
var blob = StoreData.GetStoreBlob();
blob.NetworkFeeDisabled = !model.NetworkFee;
blob.AnyoneCanInvoice = model.AnyoneCanCreateInvoice;
blob.NetworkFeeMode = model.NetworkFeeMode;
blob.MonitoringExpiration = model.MonitoringExpiration;
blob.InvoiceExpiration = model.InvoiceExpiration;
blob.LightningDescriptionTemplate = model.LightningDescriptionTemplate ?? string.Empty;
@ -514,11 +563,34 @@ namespace BTCPayServer.Controllers
{
storeId = StoreData.Id
});
}
[HttpGet]
[Route("{storeId}/delete")]
public IActionResult DeleteStore(string storeId)
{
return View("Confirm", new ConfirmModel()
{
Action = "Delete this store",
Title = "Delete this store",
Description = "This action is irreversible and will remove all information related to this store. (Invoices, Apps etc...)",
ButtonClass = "btn-danger"
});
}
[HttpPost]
[Route("{storeId}/delete")]
public async Task<IActionResult> DeleteStorePost(string storeId)
{
await _Repo.DeleteStore(StoreData.Id);
StatusMessage = "Success: Store successfully deleted";
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
}
private CoinAverageExchange[] GetSupportedExchanges()
{
return _CoinAverage.AvailableExchanges
return _RateFactory.RateProviderFactory.GetSupportedExchanges()
.Select(c => c.Value)
.OrderBy(s => s.Name, StringComparer.OrdinalIgnoreCase)
.ToArray();
@ -538,9 +610,9 @@ 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,
Label = t.Label,
SIN = t.SIN,
Id = t.Value
@ -554,6 +626,45 @@ namespace BTCPayServer.Controllers
return View(model);
}
[HttpGet]
[Route("{storeId}/tokens/{tokenId}/revoke")]
public async Task<IActionResult> RevokeToken(string tokenId)
{
var token = await _TokenRepository.GetToken(tokenId);
if (token == null || token.StoreId != StoreData.Id)
return NotFound();
return View("Confirm", new ConfirmModel()
{
Action = "Revoke the token",
Title = "Revoke the token",
Description = $"The access token with the label \"{token.Label}\" will be revoked, do you wish to continue?",
ButtonClass = "btn-danger"
});
}
[HttpPost]
[Route("{storeId}/tokens/{tokenId}/revoke")]
public async Task<IActionResult> RevokeTokenConfirm(string tokenId)
{
var token = await _TokenRepository.GetToken(tokenId);
if (token == null ||
token.StoreId != StoreData.Id ||
!await _TokenRepository.DeleteToken(tokenId))
StatusMessage = "Failure to revoke this token";
else
StatusMessage = "Token revoked";
return RedirectToAction(nameof(ListTokens));
}
[HttpGet]
[Route("{storeId}/tokens/{tokenId}")]
public async Task<IActionResult> ShowToken(string tokenId)
{
var token = await _TokenRepository.GetToken(tokenId);
if (token == null || token.StoreId != StoreData.Id)
return NotFound();
return View(token);
}
[HttpPost]
[Route("/api-tokens")]
[Route("{storeId}/Tokens/Create")]
@ -586,7 +697,6 @@ namespace BTCPayServer.Controllers
var tokenRequest = new TokenRequest()
{
Facade = model.Facade,
Label = model.Label,
Id = model.PublicKey == null ? null : NBitpayClient.Extensions.BitIdExtensions.GetBitIDSIN(new PubKey(model.PublicKey))
};
@ -598,7 +708,6 @@ namespace BTCPayServer.Controllers
await _TokenRepository.UpdatePairingCode(new PairingCodeEntity()
{
Id = tokenRequest.PairingCode,
Facade = model.Facade,
Label = model.Label,
});
await _TokenRepository.PairWithStoreAsync(tokenRequest.PairingCode, storeId);
@ -618,6 +727,7 @@ namespace BTCPayServer.Controllers
}
public string GeneratedPairingCode { get; set; }
public IOptions<MvcJsonOptions> MvcJsonOptions { get; }
[HttpGet]
[Route("/api-tokens")]
@ -637,7 +747,6 @@ namespace BTCPayServer.Controllers
}
}
var model = new CreateTokenViewModel();
model.Facade = "merchant";
ViewBag.HidePublicKey = storeId == null;
ViewBag.ShowStores = storeId == null;
ViewBag.ShowMenu = storeId != null;
@ -655,21 +764,6 @@ namespace BTCPayServer.Controllers
return View(model);
}
[HttpPost]
[Route("{storeId}/Tokens/Delete")]
public async Task<IActionResult> DeleteToken(string tokenId)
{
var token = await _TokenRepository.GetToken(tokenId);
if (token == null ||
token.StoreId != StoreData.Id ||
!await _TokenRepository.DeleteToken(tokenId))
StatusMessage = "Failure to revoke this token";
else
StatusMessage = "Token revoked";
return RedirectToAction(nameof(ListTokens));
}
[HttpPost]
[Route("{storeId}/tokens/apikey")]
public async Task<IActionResult> GenerateAPIKey()
@ -704,7 +798,6 @@ namespace BTCPayServer.Controllers
return View(new PairingModel()
{
Id = pairing.Id,
Facade = pairing.Facade,
Label = pairing.Label,
SIN = pairing.SIN ?? "Server-Initiated Pairing",
SelectedStore = selectedStore ?? stores.FirstOrDefault()?.Id,
@ -740,12 +833,17 @@ 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;
return RedirectToAction(nameof(ListTokens), new
{
storeId = store.Id
storeId = store.Id,
pairingCode = pairingCode
});
}
else
@ -764,5 +862,58 @@ namespace BTCPayServer.Controllers
return null;
return _UserManager.GetUserId(User);
}
// TODO: Need to have talk about how architect default currency implementation
// For now we have also hardcoded USD for Store creation and then Invoice creation
const string DEFAULT_CURRENCY = "USD";
[Route("{storeId}/paybutton")]
public IActionResult PayButton()
{
var store = StoreData;
var storeBlob = store.GetStoreBlob();
if (!storeBlob.AnyoneCanInvoice)
{
return View("PayButtonEnable", null);
}
var appUrl = HttpContext.Request.GetAbsoluteRoot().WithTrailingSlash();
var model = new PayButtonViewModel
{
Price = 10,
Currency = DEFAULT_CURRENCY,
ButtonSize = 2,
UrlRoot = appUrl,
PayButtonImageUrl = appUrl + "img/paybutton/pay.png",
StoreId = store.Id,
ButtonType = 0,
Min = 1,
Max = 20,
Step = 1
};
return View(model);
}
[HttpPost]
[Route("{storeId}/paybutton")]
public async Task<IActionResult> PayButton(bool enableStore)
{
var blob = StoreData.GetStoreBlob();
blob.AnyoneCanInvoice = enableStore;
if (StoreData.SetStoreBlob(blob))
{
await _Repo.UpdateStore(StoreData);
StatusMessage = "Store successfully updated";
}
return RedirectToAction(nameof(PayButton), new
{
storeId = StoreData.Id
});
}
}
}

View File

@ -23,33 +23,16 @@ namespace BTCPayServer.Controllers
private StoreRepository _Repo;
private BTCPayNetworkProvider _NetworkProvider;
private UserManager<ApplicationUser> _UserManager;
private BTCPayWalletProvider _WalletProvider;
public UserStoresController(
UserManager<ApplicationUser> userManager,
BTCPayNetworkProvider networkProvider,
BTCPayWalletProvider walletProvider,
StoreRepository storeRepository)
{
_Repo = storeRepository;
_NetworkProvider = networkProvider;
_UserManager = userManager;
_WalletProvider = walletProvider;
}
[HttpGet]
[Route("{storeId}/delete")]
public IActionResult DeleteStore(string storeId)
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
return View("Confirm", new ConfirmModel()
{
Title = "Delete store " + store.StoreName,
Description = "This store will still be accessible to users sharing it",
Action = "Delete"
});
}
}
[HttpGet]
[Route("create")]
@ -63,8 +46,23 @@ namespace BTCPayServer.Controllers
get; set;
}
[HttpGet]
[Route("{storeId}/me/delete")]
public IActionResult DeleteStore(string storeId)
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
return View("Confirm", new ConfirmModel()
{
Title = "Delete store " + store.StoreName,
Description = "This store will still be accessible to users sharing it",
Action = "Delete"
});
}
[HttpPost]
[Route("{storeId}/delete")]
[Route("{storeId}/me/delete")]
public async Task<IActionResult> DeleteStorePost(string storeId)
{
var userId = GetUserId();
@ -84,17 +82,6 @@ namespace BTCPayServer.Controllers
{
StoresViewModel result = new StoresViewModel();
var stores = await _Repo.GetStoresByUserId(GetUserId());
var balances = stores
.Select(s => s.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<DerivationStrategy>()
.Select(d => ((Wallet: _WalletProvider.GetWallet(d.Network),
DerivationStrategy: d.DerivationStrategyBase)))
.Where(_ => _.Wallet != null)
.Select(async _ => (await GetBalanceString(_)) + " " + _.Wallet.Network.CryptoCode))
.ToArray();
await Task.WhenAll(balances.SelectMany(_ => _));
for (int i = 0; i < stores.Length; i++)
{
var store = stores[i];
@ -103,8 +90,7 @@ namespace BTCPayServer.Controllers
Id = store.Id,
Name = store.StoreName,
WebSite = store.StoreWebsite,
IsOwner = store.HasClaim(Policies.CanModifyStoreSettings.Key),
Balances = store.HasClaim(Policies.CanModifyStoreSettings.Key) ? balances[i].Select(t => t.Result).ToArray() : Array.Empty<string>()
IsOwner = store.HasClaim(Policies.CanModifyStoreSettings.Key)
});
}
return View(result);
@ -121,25 +107,12 @@ namespace BTCPayServer.Controllers
var store = await _Repo.CreateStore(GetUserId(), vm.Name);
CreatedStoreId = store.Id;
StatusMessage = "Store successfully created";
return RedirectToAction(nameof(ListStores));
}
private static async Task<string> GetBalanceString((BTCPayWallet Wallet, DerivationStrategyBase DerivationStrategy) _)
{
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)))
return RedirectToAction(nameof(StoresController.UpdateStore), "Stores", new
{
try
{
return (await _.Wallet.GetBalance(_.DerivationStrategy, cts.Token)).ToString();
}
catch
{
return "--";
}
}
storeId = store.Id
});
}
private string GetUserId()
{
return _UserManager.GetUserId(User);

View File

@ -0,0 +1,623 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.ModelBinders;
using BTCPayServer.Models;
using BTCPayServer.Models.WalletViewModels;
using BTCPayServer.Security;
using BTCPayServer.Services;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
using LedgerWallet;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using NBitcoin;
using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
using Newtonsoft.Json;
using static BTCPayServer.Controllers.StoresController;
namespace BTCPayServer.Controllers
{
[Route("wallets")]
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
[AutoValidateAntiforgeryToken]
public partial class WalletsController : Controller
{
public StoreRepository Repository { get; }
public BTCPayNetworkProvider NetworkProvider { get; }
public ExplorerClientProvider ExplorerClientProvider { get; }
private readonly UserManager<ApplicationUser> _userManager;
private readonly IOptions<MvcJsonOptions> _mvcJsonOptions;
private readonly NBXplorerDashboard _dashboard;
private readonly IFeeProviderFactory _feeRateProvider;
private readonly BTCPayWalletProvider _walletProvider;
public RateFetcher RateFetcher { get; }
[TempData]
public string StatusMessage { get; set; }
CurrencyNameTable _currencyTable;
public WalletsController(StoreRepository repo,
CurrencyNameTable currencyTable,
BTCPayNetworkProvider networkProvider,
UserManager<ApplicationUser> userManager,
IOptions<MvcJsonOptions> mvcJsonOptions,
NBXplorerDashboard dashboard,
RateFetcher rateProvider,
ExplorerClientProvider explorerProvider,
IFeeProviderFactory feeRateProvider,
BTCPayWalletProvider walletProvider)
{
_currencyTable = currencyTable;
Repository = repo;
RateFetcher = rateProvider;
NetworkProvider = networkProvider;
_userManager = userManager;
_mvcJsonOptions = mvcJsonOptions;
_dashboard = dashboard;
ExplorerClientProvider = explorerProvider;
_feeRateProvider = feeRateProvider;
_walletProvider = walletProvider;
}
public async Task<IActionResult> ListWallets()
{
var wallets = new ListWalletsViewModel();
var stores = await Repository.GetStoresByUserId(GetUserId());
var onChainWallets = stores
.SelectMany(s => s.GetSupportedPaymentMethods(NetworkProvider)
.OfType<DerivationStrategy>()
.Select(d => ((Wallet: _walletProvider.GetWallet(d.Network),
DerivationStrategy: d.DerivationStrategyBase,
Network: d.Network)))
.Where(_ => _.Wallet != null)
.Select(_ => (Wallet: _.Wallet,
Store: s,
Balance: GetBalanceString(_.Wallet, _.DerivationStrategy),
DerivationStrategy: _.DerivationStrategy,
Network: _.Network)))
.ToList();
foreach (var wallet in onChainWallets)
{
ListWalletsViewModel.WalletViewModel walletVm = new ListWalletsViewModel.WalletViewModel();
wallets.Wallets.Add(walletVm);
walletVm.Balance = await wallet.Balance + " " + wallet.Wallet.Network.CryptoCode;
if (!wallet.Store.HasClaim(Policies.CanModifyStoreSettings.Key))
{
walletVm.Balance = "";
}
walletVm.CryptoCode = wallet.Network.CryptoCode;
walletVm.StoreId = wallet.Store.Id;
walletVm.Id = new WalletId(wallet.Store.Id, wallet.Network.CryptoCode);
walletVm.StoreName = wallet.Store.StoreName;
walletVm.IsOwner = wallet.Store.HasClaim(Policies.CanModifyStoreSettings.Key);
}
return View(wallets);
}
[HttpGet]
[Route("{walletId}")]
public async Task<IActionResult> WalletTransactions(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId)
{
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
if (paymentMethod == null)
return NotFound();
var wallet = _walletProvider.GetWallet(paymentMethod.Network);
var transactions = await wallet.FetchTransactions(paymentMethod.DerivationStrategyBase);
var model = new ListTransactionsViewModel();
foreach (var tx in transactions.UnconfirmedTransactions.Transactions.Concat(transactions.ConfirmedTransactions.Transactions))
{
var vm = new ListTransactionsViewModel.TransactionViewModel();
model.Transactions.Add(vm);
vm.Id = tx.TransactionId.ToString();
vm.Link = string.Format(CultureInfo.InvariantCulture, paymentMethod.Network.BlockExplorerLink, vm.Id);
vm.Timestamp = tx.Timestamp;
vm.Positive = tx.BalanceChange >= Money.Zero;
vm.Balance = tx.BalanceChange.ToString();
vm.IsConfirmed = tx.Confirmations != 0;
}
model.Transactions = model.Transactions.OrderByDescending(t => t.Timestamp).ToList();
return View(model);
}
[HttpGet]
[Route("{walletId}/send")]
public async Task<IActionResult> WalletSend(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, string defaultDestination = null, string defaultAmount = null)
{
if (walletId?.StoreId == null)
return NotFound();
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
if (paymentMethod == null)
return NotFound();
var network = this.NetworkProvider.GetNetwork(walletId?.CryptoCode);
if (network == null)
return NotFound();
var storeData = store.GetStoreBlob();
var rateRules = store.GetStoreBlob().GetRateRules(NetworkProvider);
rateRules.Spread = 0.0m;
var currencyPair = new Rating.CurrencyPair(paymentMethod.PaymentId.CryptoCode, GetCurrencyCode(storeData.DefaultLang) ?? "USD");
WalletSendModel model = new WalletSendModel()
{
Destination = defaultDestination,
CryptoCode = walletId.CryptoCode
};
if (double.TryParse(defaultAmount, out var amount))
model.Amount = (decimal)amount;
var feeProvider = _feeRateProvider.CreateFeeProvider(network);
var recommendedFees = feeProvider.GetFeeRateAsync();
var balance = _walletProvider.GetWallet(network).GetBalance(paymentMethod.DerivationStrategyBase);
model.CurrentBalance = (await balance).ToDecimal(MoneyUnit.BTC);
model.RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi;
model.FeeSatoshiPerByte = model.RecommendedSatoshiPerByte;
using (CancellationTokenSource cts = new CancellationTokenSource())
{
try
{
cts.CancelAfter(TimeSpan.FromSeconds(5));
var result = await RateFetcher.FetchRate(currencyPair, rateRules, cts.Token).WithCancellation(cts.Token);
if (result.BidAsk != null)
{
model.Rate = result.BidAsk.Center;
model.Divisibility = _currencyTable.GetNumberFormatInfo(currencyPair.Right, true).CurrencyDecimalDigits;
model.Fiat = currencyPair.Right;
}
else
{
model.RateError = $"{result.EvaluatedRule} ({string.Join(", ", result.Errors.OfType<object>().ToArray())})";
}
}
catch (Exception ex) { model.RateError = ex.Message; }
}
return View(model);
}
[HttpPost]
[Route("{walletId}/send")]
public async Task<IActionResult> WalletSend(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, WalletSendModel vm, string command = null, CancellationToken cancellation = default)
{
if (walletId?.StoreId == null)
return NotFound();
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
if (store == null)
return NotFound();
var network = this.NetworkProvider.GetNetwork(walletId?.CryptoCode);
if (network == null)
return NotFound();
var destination = ParseDestination(vm.Destination, network.NBitcoinNetwork);
if (destination == null)
ModelState.AddModelError(nameof(vm.Destination), "Invalid address");
if (vm.Amount.HasValue)
{
if (vm.CurrentBalance == vm.Amount.Value && !vm.SubstractFees)
ModelState.AddModelError(nameof(vm.Amount), "You are sending all your balance to the same destination, you should substract the fees");
if (vm.CurrentBalance < vm.Amount.Value)
ModelState.AddModelError(nameof(vm.Amount), "You are sending more than what you own");
}
if (!ModelState.IsValid)
return View(vm);
var sendModel = new WalletSendLedgerModel()
{
Destination = vm.Destination,
Amount = vm.Amount.Value,
SubstractFees = vm.SubstractFees,
FeeSatoshiPerByte = vm.FeeSatoshiPerByte,
NoChange = vm.NoChange
};
if (command == "ledger")
{
return RedirectToAction(nameof(WalletSendLedger), sendModel);
}
else
{
var storeData = (await Repository.FindStore(walletId.StoreId, GetUserId()));
var derivationScheme = GetPaymentMethod(walletId, storeData).DerivationStrategyBase;
var psbt = await CreatePSBT(network, derivationScheme, sendModel, cancellation);
return File(psbt.PSBT.ToBytes(), "application/octet-stream", $"Send-{vm.Amount.Value}-{network.CryptoCode}-to-{destination[0].ToString()}.psbt");
}
}
private async Task<CreatePSBTResponse> CreatePSBT(BTCPayNetwork network, DerivationStrategyBase derivationScheme, WalletSendLedgerModel sendModel, CancellationToken cancellationToken)
{
var nbx = ExplorerClientProvider.GetExplorerClient(network);
CreatePSBTRequest psbtRequest = new CreatePSBTRequest();
CreatePSBTDestination psbtDestination = new CreatePSBTDestination();
psbtRequest.Destinations.Add(psbtDestination);
psbtDestination.Destination = BitcoinAddress.Create(sendModel.Destination, network.NBitcoinNetwork);
psbtDestination.Amount = Money.Coins(sendModel.Amount);
psbtRequest.FeePreference = new FeePreference();
psbtRequest.FeePreference.ExplicitFeeRate = new FeeRate(Money.Satoshis(sendModel.FeeSatoshiPerByte), 1);
if (sendModel.NoChange)
{
psbtRequest.ExplicitChangeAddress = psbtDestination.Destination;
}
psbtDestination.SubstractFees = sendModel.SubstractFees;
var psbt = (await nbx.CreatePSBTAsync(derivationScheme, psbtRequest, cancellationToken));
if (psbt == null)
throw new NotSupportedException("You need to update your version of NBXplorer");
if (network.MinFee != null)
{
psbt.PSBT.TryGetFee(out var fee);
if (fee < network.MinFee)
{
psbtRequest.FeePreference = new FeePreference() { ExplicitFee = network.MinFee };
psbt = (await nbx.CreatePSBTAsync(derivationScheme, psbtRequest, cancellationToken));
}
}
return psbt;
}
[HttpGet]
[Route("{walletId}/send/ledger")]
public async Task<IActionResult> WalletSendLedger(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, WalletSendLedgerModel vm)
{
if (walletId?.StoreId == null)
return NotFound();
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
if (paymentMethod == null)
return NotFound();
var network = this.NetworkProvider.GetNetwork(walletId?.CryptoCode);
if (network == null)
return NotFound();
return View(vm);
}
private IDestination[] ParseDestination(string destination, Network network)
{
try
{
destination = destination?.Trim();
return new IDestination[] { BitcoinAddress.Create(destination, network) };
}
catch
{
return null;
}
}
[HttpGet]
[Route("{walletId}/rescan")]
public async Task<IActionResult> WalletRescan(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId)
{
if (walletId?.StoreId == null)
return NotFound();
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
if (paymentMethod == null)
return NotFound();
var vm = new RescanWalletModel();
vm.IsFullySync = _dashboard.IsFullySynched(walletId.CryptoCode, out var unused);
vm.IsServerAdmin = User.Claims.Any(c => c.Type == Policies.CanModifyServerSettings.Key && c.Value == "true");
vm.IsSupportedByCurrency = _dashboard.Get(walletId.CryptoCode)?.Status?.BitcoinStatus?.Capabilities?.CanScanTxoutSet == true;
var explorer = ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode);
var scanProgress = await explorer.GetScanUTXOSetInformationAsync(paymentMethod.DerivationStrategyBase);
if (scanProgress != null)
{
vm.PreviousError = scanProgress.Error;
if (scanProgress.Status == ScanUTXOStatus.Queued || scanProgress.Status == ScanUTXOStatus.Pending)
{
if (scanProgress.Progress == null)
{
vm.Progress = 0;
}
else
{
vm.Progress = scanProgress.Progress.OverallProgress;
vm.RemainingTime = TimeSpan.FromSeconds(scanProgress.Progress.RemainingSeconds).PrettyPrint();
}
}
if (scanProgress.Status == ScanUTXOStatus.Complete)
{
vm.LastSuccess = scanProgress.Progress;
vm.TimeOfScan = (scanProgress.Progress.CompletedAt.Value - scanProgress.Progress.StartedAt).PrettyPrint();
}
}
return View(vm);
}
[HttpPost]
[Route("{walletId}/rescan")]
[Authorize(Policy = Policies.CanModifyServerSettings.Key)]
public async Task<IActionResult> WalletRescan(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, RescanWalletModel vm)
{
if (walletId?.StoreId == null)
return NotFound();
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
if (paymentMethod == null)
return NotFound();
var explorer = ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode);
try
{
await explorer.ScanUTXOSetAsync(paymentMethod.DerivationStrategyBase, vm.BatchSize, vm.GapLimit, vm.StartingIndex);
}
catch (NBXplorerException ex) when (ex.Error.Code == "scanutxoset-in-progress")
{
}
return RedirectToAction();
}
private string GetCurrencyCode(string defaultLang)
{
if (defaultLang == null)
return null;
try
{
var ri = new RegionInfo(defaultLang);
return ri.ISOCurrencySymbol;
}
catch (ArgumentException) { }
return null;
}
private DerivationStrategy GetPaymentMethod(WalletId walletId, StoreData store)
{
if (store == null || !store.HasClaim(Policies.CanModifyStoreSettings.Key))
return null;
var paymentMethod = store
.GetSupportedPaymentMethods(NetworkProvider)
.OfType<DerivationStrategy>()
.FirstOrDefault(p => p.PaymentId.PaymentType == Payments.PaymentTypes.BTCLike && p.PaymentId.CryptoCode == walletId.CryptoCode);
return paymentMethod;
}
private static async Task<string> GetBalanceString(BTCPayWallet wallet, DerivationStrategyBase derivationStrategy)
{
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)))
{
try
{
return (await wallet.GetBalance(derivationStrategy, cts.Token)).ToString();
}
catch
{
return "--";
}
}
}
private string GetUserId()
{
return _userManager.GetUserId(User);
}
[HttpGet]
[Route("{walletId}/send/ledger/success")]
public IActionResult WalletSendLedgerSuccess(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId,
string txid)
{
StatusMessage = $"Transaction broadcasted ({txid})";
return RedirectToAction(nameof(this.WalletTransactions), new { walletId = walletId.ToString() });
}
[HttpGet]
[Route("{walletId}/send/ledger/ws")]
public async Task<IActionResult> LedgerConnection(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId,
string command,
// getinfo
// getxpub
int account = 0,
// sendtoaddress
bool noChange = false,
string destination = null, string amount = null, string feeRate = null, bool substractFees = false
)
{
if (!HttpContext.WebSockets.IsWebSocketRequest)
return NotFound();
var cryptoCode = walletId.CryptoCode;
var storeData = (await Repository.FindStore(walletId.StoreId, GetUserId()));
var derivationScheme = GetPaymentMethod(walletId, storeData).DerivationStrategyBase;
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
using (var normalOperationTimeout = new CancellationTokenSource())
using (var signTimeout = new CancellationTokenSource())
{
normalOperationTimeout.CancelAfter(TimeSpan.FromMinutes(30));
var hw = new HardwareWalletService(webSocket);
var model = new WalletSendLedgerModel();
object result = null;
try
{
BTCPayNetwork network = null;
if (cryptoCode != null)
{
network = NetworkProvider.GetNetwork(cryptoCode);
if (network == null)
throw new FormatException("Invalid value for crypto code");
}
if (destination != null)
{
try
{
BitcoinAddress.Create(destination.Trim(), network.NBitcoinNetwork);
model.Destination = destination.Trim();
}
catch { }
}
if (feeRate != null)
{
try
{
model.FeeSatoshiPerByte = int.Parse(feeRate, CultureInfo.InvariantCulture);
}
catch { }
if (model.FeeSatoshiPerByte <= 0)
throw new FormatException("Invalid value for fee rate");
}
if (amount != null)
{
try
{
model.Amount = Money.Parse(amount).ToDecimal(MoneyUnit.BTC);
}
catch { }
if (model.Amount <= 0m)
throw new FormatException("Invalid value for amount");
}
model.SubstractFees = substractFees;
model.NoChange = noChange;
if (command == "test")
{
result = await hw.Test(normalOperationTimeout.Token);
}
if (command == "sendtoaddress")
{
if (!_dashboard.IsFullySynched(network.CryptoCode, out var summary))
throw new Exception($"{network.CryptoCode}: not started or fully synched");
var psbt = await CreatePSBT(network, derivationScheme, model, normalOperationTimeout.Token);
var strategy = GetDirectDerivationStrategy(derivationScheme);
var storeBlob = storeData.GetStoreBlob();
var paymentId = new Payments.PaymentMethodId(cryptoCode, Payments.PaymentTypes.BTCLike);
var foundKeyPath = storeBlob.GetWalletKeyPathRoot(paymentId);
// Some deployment have the wallet root key path saved in the store blob
// If it does, we only have to make 1 call to the hw to check if it can sign the given strategy,
if (foundKeyPath == null || !await hw.CanSign(network, strategy, foundKeyPath, normalOperationTimeout.Token))
{
// If the saved wallet key path is not present or incorrect, let's scan the wallet to see if it can sign strategy
foundKeyPath = await hw.FindKeyPath(network, strategy, normalOperationTimeout.Token);
if (foundKeyPath == null)
throw new HardwareWalletException($"This store is not configured to use this ledger");
storeBlob.SetWalletKeyPathRoot(paymentId, foundKeyPath);
storeData.SetStoreBlob(storeBlob);
await Repository.UpdateStore(storeData);
}
// NBX only know the path relative to the account xpub.
// Here we rebase the hd_keys in the PSBT to have a keypath relative to the root HD so the wallet can sign
// Note that the fingerprint of the hd keys are now 0, which is wrong
// However, hardware wallets does not give a damn, and sometimes does not even allow us to get this fingerprint anyway.
foreach (var o in psbt.PSBT.Inputs.OfType<PSBTCoin>().Concat(psbt.PSBT.Outputs))
{
foreach (var keypath in o.HDKeyPaths.ToList())
{
var newKeyPath = foundKeyPath.Derive(keypath.Value.Item2);
o.HDKeyPaths.Remove(keypath.Key);
o.HDKeyPaths.Add(keypath.Key, Tuple.Create(default(HDFingerprint), newKeyPath));
}
}
signTimeout.CancelAfter(TimeSpan.FromMinutes(5));
psbt.PSBT = await hw.SignTransactionAsync(psbt.PSBT, psbt.ChangeAddress?.ScriptPubKey, signTimeout.Token);
if(!psbt.PSBT.TryFinalize(out var errors))
{
throw new Exception($"Error while finalizing the transaction ({new PSBTException(errors).ToString()})");
}
var transaction = psbt.PSBT.ExtractTransaction();
try
{
var broadcastResult = await ExplorerClientProvider.GetExplorerClient(network).BroadcastAsync(transaction);
if (!broadcastResult.Success)
{
throw new Exception($"RPC Error while broadcasting: {broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}");
}
}
catch (Exception ex)
{
throw new Exception("Error while broadcasting: " + ex.Message);
}
var wallet = _walletProvider.GetWallet(network);
wallet.InvalidateCache(derivationScheme);
result = new SendToAddressResult() { TransactionId = transaction.GetHash().ToString() };
}
}
catch (OperationCanceledException)
{ result = new LedgerTestResult() { Success = false, Error = "Timeout" }; }
catch (Exception ex)
{ result = new LedgerTestResult() { Success = false, Error = ex.Message }; }
finally { hw.Dispose(); }
try
{
if (result != null)
{
UTF8Encoding UTF8NOBOM = new UTF8Encoding(false);
var bytes = UTF8NOBOM.GetBytes(JsonConvert.SerializeObject(result, _mvcJsonOptions.Value.SerializerSettings));
await webSocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, new CancellationTokenSource(2000).Token);
}
}
catch { }
finally
{
await webSocket.CloseSocket();
}
}
return new EmptyResult();
}
private DirectDerivationStrategy GetDirectDerivationStrategy(DerivationStrategyBase strategy)
{
if (strategy == null)
throw new Exception("The derivation scheme is not provided");
var directStrategy = strategy as DirectDerivationStrategy;
if (directStrategy == null)
directStrategy = (strategy as P2SHDerivationStrategy).Inner as DirectDerivationStrategy;
return directStrategy;
}
}
public class GetInfoResult
{
}
public class SendToAddressResult
{
public string TransactionId { get; set; }
}
}

View File

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer
{
public static class CorsPolicies
{
public const string All = "BTCPAY_ALL";
}
}

View File

@ -21,7 +21,7 @@ namespace BTCPayServer
return false;
var currency = match.Groups.Last().Value.ToUpperInvariant();
var currencyData = _CurrencyTable.GetCurrencyData(currency);
var currencyData = _CurrencyTable.GetCurrencyData(currency, false);
if (currencyData == null)
return false;
v = Math.Round(v, currencyData.Divisibility);

View File

@ -19,5 +19,7 @@ namespace BTCPayServer.Data
{
get; set;
}
public StoreData StoreData { get; set; }
}
}

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

@ -1,11 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Linq;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using BTCPayServer.Models;
using Microsoft.EntityFrameworkCore.Infrastructure.Internal;
using BTCPayServer.Services.PaymentRequests;
using BTCPayServer.Services.U2F.Models;
using BTCPayServer.Storage.Models;
using Microsoft.EntityFrameworkCore.Infrastructure;
namespace BTCPayServer.Data
@ -54,6 +53,11 @@ namespace BTCPayServer.Data
{
get; set;
}
public DbSet<PaymentRequestData> PaymentRequests
{
get; set;
}
public DbSet<StoreData> Stores
{
@ -87,10 +91,18 @@ namespace BTCPayServer.Data
}
public DbSet<APIKeyData> ApiKeys
{
get; set;
}
public DbSet<StoredFile> Files
{
get; set;
}
public DbSet<U2FDevice> U2FDevices { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var isConfigured = optionsBuilder.Options.Extensions.OfType<RelationalOptionsExtension>().Any();
@ -102,14 +114,27 @@ namespace BTCPayServer.Data
{
base.OnModelCreating(builder);
builder.Entity<InvoiceData>()
.HasIndex(o => o.StoreDataId);
.HasOne(o => o.StoreData)
.WithMany(a => a.Invoices).OnDelete(DeleteBehavior.Cascade);
builder.Entity<InvoiceData>().HasIndex(o => o.StoreDataId);
builder.Entity<PaymentData>()
.HasIndex(o => o.InvoiceDataId);
.HasOne(o => o.InvoiceData)
.WithMany(i => i.Payments).OnDelete(DeleteBehavior.Cascade);
builder.Entity<PaymentData>()
.HasIndex(o => o.InvoiceDataId);
builder.Entity<RefundAddressesData>()
.HasOne(o => o.InvoiceData)
.WithMany(i => i.RefundAddresses).OnDelete(DeleteBehavior.Cascade);
builder.Entity<RefundAddressesData>()
.HasIndex(o => o.InvoiceDataId);
builder.Entity<UserStore>()
.HasOne(o => o.StoreData)
.WithMany(i => i.UserStores).OnDelete(DeleteBehavior.Cascade);
builder.Entity<UserStore>()
.HasKey(t => new
{
@ -117,9 +142,16 @@ namespace BTCPayServer.Data
t.StoreDataId
});
builder.Entity<APIKeyData>()
.HasOne(o => o.StoreData)
.WithMany(i => i.APIKeys)
.HasForeignKey(i => i.StoreId).OnDelete(DeleteBehavior.Cascade);
builder.Entity<APIKeyData>()
.HasIndex(o => o.StoreId);
builder.Entity<AppData>()
.HasOne(o => o.StoreData)
.WithMany(i => i.Apps).OnDelete(DeleteBehavior.Cascade);
builder.Entity<AppData>()
.HasOne(a => a.StoreData);
@ -133,6 +165,10 @@ namespace BTCPayServer.Data
.WithMany(t => t.UserStores)
.HasForeignKey(pt => pt.StoreDataId);
builder.Entity<AddressInvoiceData>()
.HasOne(o => o.InvoiceData)
.WithMany(i => i.AddressInvoices).OnDelete(DeleteBehavior.Cascade);
builder.Entity<AddressInvoiceData>()
#pragma warning disable CS0618
.HasKey(o => o.Address);
@ -141,12 +177,24 @@ namespace BTCPayServer.Data
builder.Entity<PairingCodeData>()
.HasKey(o => o.Id);
builder.Entity<PendingInvoiceData>()
.HasOne(o => o.InvoiceData)
.WithMany(o => o.PendingInvoices)
.HasForeignKey(o => o.Id).OnDelete(DeleteBehavior.Cascade);
builder.Entity<PairedSINData>()
.HasOne(o => o.StoreData)
.WithMany(i => i.PairedSINs).OnDelete(DeleteBehavior.Cascade);
builder.Entity<PairedSINData>(b =>
{
b.HasIndex(o => o.SIN);
b.HasIndex(o => o.StoreDataId);
});
builder.Entity<HistoricalAddressInvoiceData>()
.HasOne(o => o.InvoiceData)
.WithMany(i => i.HistoricalAddressInvoices).OnDelete(DeleteBehavior.Cascade);
builder.Entity<HistoricalAddressInvoiceData>()
.HasKey(o => new
{
@ -156,6 +204,10 @@ namespace BTCPayServer.Data
#pragma warning restore CS0618
});
builder.Entity<InvoiceEventData>()
.HasOne(o => o.InvoiceData)
.WithMany(i => i.Events).OnDelete(DeleteBehavior.Cascade);
builder.Entity<InvoiceEventData>()
.HasKey(o => new
{
@ -164,6 +216,16 @@ namespace BTCPayServer.Data
o.UniqueId
#pragma warning restore CS0618
});
builder.Entity<PaymentRequestData>()
.HasOne(o => o.StoreData)
.WithMany(i => i.PaymentRequests)
.OnDelete(DeleteBehavior.Cascade);
builder.Entity<PaymentRequestData>()
.HasIndex(o => o.Status);
}
}
}

View File

@ -3,16 +3,19 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Hangfire;
using Hangfire.MemoryStorage;
using Hangfire.PostgreSql;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations;
using JetBrains.Annotations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations.Operations;
using Microsoft.EntityFrameworkCore.Metadata;
namespace BTCPayServer.Data
{
public enum DatabaseType
{
Sqlite,
Postgres
Postgres,
MySQL,
}
public class ApplicationDbContextFactory
{
@ -24,6 +27,15 @@ namespace BTCPayServer.Data
_Type = type;
}
public DatabaseType Type
{
get
{
return _Type;
}
}
public ApplicationDbContext CreateContext()
{
var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
@ -31,20 +43,58 @@ namespace BTCPayServer.Data
return new ApplicationDbContext(builder.Options);
}
class CustomNpgsqlMigrationsSqlGenerator : NpgsqlMigrationsSqlGenerator
{
public CustomNpgsqlMigrationsSqlGenerator(MigrationsSqlGeneratorDependencies dependencies) : base(dependencies)
{
}
protected override void Generate(NpgsqlCreateDatabaseOperation operation, IModel model, MigrationCommandListBuilder builder)
{
builder
.Append("CREATE DATABASE ")
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name));
// POSTGRES gotcha: Indexed Text column (even if PK) are not used if we are not using C locale
builder
.Append(" TEMPLATE ")
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier("template0"));
builder
.Append(" LC_CTYPE ")
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier("C"));
builder
.Append(" LC_COLLATE ")
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier("C"));
builder
.Append(" ENCODING ")
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier("UTF8"));
if (operation.Tablespace != null)
{
builder
.Append(" TABLESPACE ")
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Tablespace));
}
builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator);
EndStatement(builder, suppressTransaction: true);
}
}
public void ConfigureBuilder(DbContextOptionsBuilder builder)
{
if (_Type == DatabaseType.Sqlite)
builder.UseSqlite(_ConnectionString);
else if (_Type == DatabaseType.Postgres)
builder.UseNpgsql(_ConnectionString);
}
public void ConfigureHangfireBuilder(IGlobalConfiguration builder)
{
if (_Type == DatabaseType.Sqlite)
builder.UseMemoryStorage(); //Sql provider does not support multiple workers
else if (_Type == DatabaseType.Postgres)
builder.UsePostgreSqlStorage(_ConnectionString);
builder
.UseNpgsql(_ConnectionString)
.ReplaceService<IMigrationsSqlGenerator, CustomNpgsqlMigrationsSqlGenerator>();
else if (_Type == DatabaseType.MySQL)
builder.UseMySql(_ConnectionString);
}
}
}

View File

@ -12,6 +12,11 @@ namespace BTCPayServer.Data
get; set;
}
public InvoiceData InvoiceData
{
get; set;
}
/// <summary>
/// Some crypto currencies share same address prefix
/// For not having exceptions thrown by two address on different network, we suffix by "#CRYPTOCODE"

View File

@ -80,5 +80,11 @@ namespace BTCPayServer.Data
{
get; set;
}
public List<PendingInvoiceData> PendingInvoices { get; set; }
public Services.Invoices.InvoiceState GetInvoiceState()
{
return new Services.Invoices.InvoiceState(Status, ExceptionStatus);
}
}
}

View File

@ -11,6 +11,10 @@ namespace BTCPayServer.Data
{
get; set;
}
public InvoiceData InvoiceData
{
get; set;
}
public string UniqueId { get; internal set; }
public DateTimeOffset Timestamp
{

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