Compare commits

...

936 Commits

Author SHA1 Message Date
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
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
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
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
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
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
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
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
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
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
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
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
efe666b284 Fix call to Rates via bitpay API 2018-05-06 22:41:38 +09:00
ca8af5047a Changed DefaultRateRules 2018-05-06 14:59:49 +03:00
cdc0b0d628 Fix crash when creating a token 2018-05-06 19:03:30 +09:00
87e28b70fd cap MinimumTotalDue to 1 satoshi 2018-05-06 13:55:03 +09:00
b96f464e39 Add "unusual:" filter to invoice list 2018-05-06 13:16:39 +09:00
bca68986f3 Added BTG Support 2018-05-05 23:06:09 +03:00
272ac49872 try to better respect event ordering 2018-05-06 02:06:07 +09:00
5f05ca5ac6 bump 2018-05-06 00:43:05 +09:00
7872b3ec55 Add a new invoice event: expiredPaidPartial and fix some corner case for tolerance 2018-05-06 00:40:44 +09:00
27a0aebd12 Merge pull request #155 from Kukks/feature/order-tolerance
Payment Tolerance
2018-05-06 00:06:39 +09:00
366490516e Can filter with "exceptionstatus:", show the exception status on invoice list page 2018-05-05 23:25:09 +09:00
9a92646d4d add test and refactor for PR 2018-05-05 16:07:22 +02:00
b002c49dac Merge remote-tracking branch 'btcpayserver/master' into feature/order-tolerance 2018-05-05 16:04:59 +02:00
3f4ec9ba80 simplify currency parsing if _ is forgotten and there is 6 letters 2018-05-05 22:59:53 +09:00
0290a5eacd update clightning 2018-05-05 22:46:07 +09:00
744734a6a1 Returns fallback feerate for coins not supporting fee rate query in NBXplorer 2018-05-05 22:19:36 +09:00
29f662f87c bump NBXplorer 2018-05-05 22:05:22 +09:00
af21f9f10c Merge remote-tracking branch 'btcpayserver/master' into feature/order-tolerance 2018-05-05 08:49:16 +02:00
efdc99b9d1 Do not spam the logs about failed mail 2018-05-05 01:42:42 +09:00
4458e63c1a Break default DOGE rules in two, add some documentation about inverses 2018-05-05 01:34:08 +09:00
3225745115 bump 2018-05-05 01:01:39 +09:00
a325592106 Can match exact reverse 2018-05-05 01:00:19 +09:00
01069ed583 Remove unnecessary branching 2018-05-04 17:50:05 +02:00
0fc770bbb1 extract logic of accounting to accounting and remove bitpay breaking changes 2018-05-04 17:47:33 +02:00
dfb79ef96e Merge remote-tracking branch 'btcpayserver/master' into feature/order-tolerance 2018-05-04 17:46:39 +02:00
4ebffc8d43 fix BIP70 bug 2018-05-05 00:44:02 +09:00
c2dad08fef Can solve inverse of currency pair 2018-05-05 00:40:54 +09:00
7c3ddf904c Merge remote-tracking branch 'upstream/master' 2018-05-04 09:47:03 -05:00
c3d73236e0 start work on payment tolerance feature 2018-05-04 16:15:34 +02:00
8a4da361fd Fix bug about invoice URL 2018-05-04 22:05:40 +09:00
57effe318b Fix missing URL for invoice 2018-05-04 21:41:50 +09:00
9325441693 fix typo 2018-05-04 16:09:43 +09:00
180341576b bump 2018-05-04 15:55:09 +09:00
e2533a93e3 Fix set email screen 2018-05-04 15:54:12 +09:00
14360bde78 Use rate directly from some exchanges, fix bug in ServerSettings 2018-05-04 15:36:10 +09:00
d793265bed Merge pull request #154 from rockstardev/master
Addressing several fixes that were assigned to me
2018-05-04 12:37:02 +09:00
0a449e1e8e Allowing custom HtmlTitle
Fix #96
2018-05-03 22:35:06 -05:00
74ccc34c9c Small enhancement on Rates page 2018-05-04 11:58:21 +09:00
674cd1486d Showing btcPaid once invoice is paid
Fix #144
2018-05-03 16:38:40 -05:00
ce12e87b70 Restoring QR Code for 2Fact authentication, fix #147 2018-05-03 16:13:50 -05:00
8f1324fdf3 Can clear email settings Fix #150 2018-05-04 02:16:12 +09:00
3ab69046b0 Add overpaid column Fix #149 2018-05-04 02:01:43 +09:00
6dc4bfaefe Make rate calculation scriptable 2018-05-04 01:46:52 +09:00
f460837f96 Make sure RateRules do not remove comments 2018-05-03 04:33:21 +09:00
34d0d3e011 make sure we can calculate the rate of default currencies 2018-05-03 03:40:10 +09:00
e57a488371 Refactor the RateProvider 2018-05-03 03:32:42 +09:00
43be1e191f Create the RateRules class for parsing rate calculation rules 2018-05-02 18:37:53 +09:00
25208915eb Merge remote-tracking branch 'upstream/master' 2018-04-30 10:29:30 -05:00
eb975bf8fc Isolate Bitpay's code outside of middleware inside BitpayClaimsFilter 2018-04-30 22:28:00 +09:00
21bbf49640 Rewrite authorization enforcement and simplify the code 2018-04-30 22:00:43 +09:00
9339c7dff2 Make sure btcpay does not wait all the invoces to be cleaned to start 2018-04-30 15:39:47 +09:00
af0eb831a2 Remove useless code and rename file 2018-04-30 02:37:32 +09:00
1fc9a1a54b Move to a Claim based security 2018-04-30 02:33:42 +09:00
3954ce2137 fix (again) the broken hr.js 2018-04-30 01:32:15 +09:00
271de362cb fix broken checkout 2018-04-30 00:29:34 +09:00
d41474ebc8 Bump 2018-04-29 20:52:51 +09:00
5b0b3e30f4 Small rewrite 2018-04-29 20:50:54 +09:00
48a95457b6 fix boolean 2018-04-29 20:49:38 +09:00
7c0b26174f Fix theme manager incorrectly applying default theme if rootPath is specified 2018-04-29 20:48:17 +09:00
f0145142a4 Make sure that we don't authenticate call with bitpay auth methods on non bitpay calls 2018-04-29 20:32:43 +09:00
2848caff2e Support Legacy API Key authentication to Bitpay Invoice API 2018-04-29 18:28:04 +09:00
9e05ad787f Merge pull request #146 from 2pac1/master
Croatian translation
2018-04-29 12:00:48 +09:00
de39fa0aea Update hr.js 2018-04-28 18:46:55 +02:00
94ff77f2b2 Update hr.js 2018-04-28 18:44:55 +02:00
bb7dc1ed4a Update hr.js 2018-04-28 18:34:47 +02:00
c5e833ee79 Update hr.js 2018-04-28 18:33:34 +02:00
4397591134 Create hr.js 2018-04-28 18:27:51 +02:00
986c7b94f4 Update Checkout.cshtml 2018-04-28 18:13:44 +02:00
a6ef7387cf Update LanguageService.cs 2018-04-28 18:12:11 +02:00
95bdeacd93 Order exchanges in the list 2018-04-28 10:58:14 +09:00
07c2f6b810 Remove TokenRepository dependency from InvoiceControllerAPI 2018-04-28 02:51:20 +09:00
8ff81f1648 Use claim based authentication 2018-04-28 02:09:24 +09:00
c3ee43c228 Add ExchangeSharp 2018-04-27 12:15:29 +09:00
d85da28ca7 Merge pull request #145 from lepipele/dev-asynctask
Refactoring async while loop functionality
2018-04-27 11:56:54 +09:00
042142396d Refactoring code to adhere to naming guidelines 2018-04-26 21:52:04 -05:00
fbc4ca89aa Enapsulating Token per code review discussions 2018-04-26 21:44:21 -05:00
2e5d29064b Removing CssThemeManager dependency on ServerController
Using newly created BaseAsyncService to listen for database changes of theme setting
2018-04-26 21:39:43 -05:00
ef0b8376d3 Abstracting hosted service that has listen loop tasks 2018-04-26 21:39:43 -05:00
1fa1b74261 Center the last row of the PoS screen 2018-04-27 00:00:32 +09:00
4f9e4116a2 Point of Sale support free entry 2018-04-26 22:09:18 +09:00
82d8fda05f update clightning in tests 2018-04-26 15:30:52 +09:00
d4935263da Update various packages 2018-04-26 11:45:09 +09:00
e158d909fb bump 2018-04-26 11:16:56 +09:00
de8147d5dd Can opt out required refund email from customer 2018-04-26 11:13:44 +09:00
16f1791a9a Invoice filter must work with duplicated filter 2018-04-26 11:03:02 +09:00
8745c3f8c6 Merge pull request #139 from lepipele/dev-timerfix
Recoding timer removing dependecy on browser's setInterval
2018-04-26 09:18:04 +09:00
ec5b45cff6 Recoding timer removing dependecy on browser's setInterval
Ref: https://github.com/btcpayserver/btcpayserver/issues/130
2018-04-25 13:30:00 -05:00
1348197295 Merge pull request #134 from lepipele/master
Ellipsis when there is lots of info, preserving responsive tables
2018-04-24 12:00:54 +09:00
f2516854d8 Fixing width to align first columns
Ref: https://github.com/btcpayserver/btcpayserver/pull/134#issuecomment-383785811
2018-04-23 21:58:22 -05:00
062ca6e743 Merge remote-tracking branch 'source/master' 2018-04-23 21:53:06 -05:00
44b6997bb5 Merge pull request #136 from Saevar2000/patch-1
Update is.js
2018-04-24 11:39:11 +09:00
78b544f9ca Update is.js 2018-04-23 23:08:35 +00:00
81926b4450 Ellipsis when there is lots of info, preserving responsive tables
Ref: https://github.com/btcpayserver/btcpayserver/issues/133
2018-04-23 16:00:03 -05:00
a7ad71d492 CoinAverage credentials are now correctly passed 2018-04-23 17:21:50 +09:00
18977f7265 Optimize number of requests sent to Quadrigacx 2018-04-23 17:06:22 +09:00
8a88b44e98 Add special rate provider for qudrigacx 2018-04-23 16:44:59 +09:00
c9e5fe42ba Set default AvailableExchanges inside CoinAverageSettings 2018-04-23 16:12:11 +09:00
56dffbf514 Set default exchange list 2018-04-23 16:09:18 +09:00
0e1fac3773 fix getting exchange rate of Coinaverage 2018-04-23 15:58:35 +09:00
e7c06880a8 Use API keys of bitcoinaverage for getting the exchange list 2018-04-23 15:48:18 +09:00
39463a3202 Merge pull request #132 from lepipele/master
Removing empty folder, fixing build warnings
2018-04-23 12:39:10 +09:00
36136f0f0f Removing empty folder, fixing build warnings 2018-04-22 22:30:37 -05:00
52e0845fc5 Merge remote-tracking branch 'upstream/master' 2018-04-21 09:59:46 -05:00
22e5b2869a bump 2018-04-20 12:28:58 +09:00
fc3f32a4e0 Merge branch 'dev-bootstrap' of https://github.com/lepipele/btcpayserver into lepipele-dev-bootstrap 2018-04-20 12:17:53 +09:00
3b0914e89e Migrating ManageNavPages to new navigation enum 2018-04-19 15:57:23 -05:00
76cd9a7b25 Abstracting navigation so it can use any enums 2018-04-19 15:42:12 -05:00
0934bebf7b Merge remote-tracking branch 'source/master' into dev-bootstrap 2018-04-19 11:45:30 -05:00
cd1a4c4749 Fixing modify user page and it's title 2018-04-19 11:44:24 -05:00
8075273ec8 Refactoring pills navigation 2018-04-19 11:40:12 -05:00
97b59be9bf Adding page for Theme settings 2018-04-19 11:39:51 -05:00
b87ec4f3d9 Primitive versioning of css files to ensure update on change 2018-04-19 11:15:45 -05:00
3822358096 Show more info about bitcoin average quota 2018-04-20 01:01:39 +09:00
ba7e8cfe78 Removing Merriweather as default body font, back to Arial
Ref: https://forkbitpay.slack.com/archives/C6PSCRFAM/p1524130675000104
2018-04-19 10:04:59 -05:00
41978f1c59 Remove useless line in invoice.cshtml 2018-04-19 18:39:39 +09:00
e75e691404 Merge branch 'dev-bootstrap' of https://github.com/lepipele/btcpayserver into lepipele-dev-bootstrap 2018-04-19 18:03:04 +09:00
a22216fd04 fix layout 2018-04-19 17:06:08 +09:00
6900e16aa4 bump 2018-04-19 16:54:47 +09:00
10c981b2a0 Update NBXplorer 2018-04-19 16:54:25 +09:00
5f940df1b4 Migrating Invoice styling 2018-04-18 23:44:01 -05:00
3f85918a0c Merge remote-tracking branch 'source/master' into dev-bootstrap
# Conflicts:
#	BTCPayServer/Controllers/ServerController.cs
#	BTCPayServer/Views/Invoice/Invoice.cshtml
2018-04-18 23:38:10 -05:00
e4299c09ea bump 2018-04-18 22:28:31 +09:00
e864cf35f7 bump NBitcoin 2018-04-18 22:28:04 +09:00
3652866660 View offchain payments in Invoice screen 2018-04-18 22:27:01 +09:00
0421004616 fix point of sale view on mobile 2018-04-18 21:52:13 +09:00
6936b034cb Add Bitcoin average quota 2018-04-18 18:23:39 +09:00
73ed4003a3 Use a drop down for preferred exchange list 2018-04-18 16:38:56 +09:00
5cb8cdd511 Refactoring: Do not query database when asking for Coinaverage rates, periodically get exchange list 2018-04-18 16:38:56 +09:00
195b5fdd1a Adding overriding of CreativeStartUri, refactoring PoliciesSettings 2018-04-17 17:24:00 -05:00
d19b78b6cc Moving Creative Start files to dedicated folder 2018-04-17 17:23:33 -05:00
9bbc05c3a7 Cleaning Invoice table, removing style attrs
Ref: https://github.com/btcpayserver/btcpayserver/issues/82

Co-authored-by: Esky33 <support@btcpayjungle.com>
2018-04-17 16:48:50 -05:00
7df3c86649 Tweaking primary color now that creative.css no longer overrides 2018-04-17 16:29:05 -05:00
c6e0a923bb Unifying bg-dark style, cleaning up references to extra colors 2018-04-17 16:22:20 -05:00
637fe1727b Adding missing font styles back in
These are referenced by Creative - Start Bootstrap theme
2018-04-17 16:12:17 -05:00
fd087bbeb8 Streamlining style for footer 2018-04-17 15:33:29 -05:00
2f515e1cc0 Removing unused classes 2018-04-17 15:20:27 -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
84cd9e570f Merge pull request #128 from pajasevi/cs-trancaction-count
Transaction count CS translation
2018-04-17 11:31:29 +09:00
ead97a24bd Update NBitcoin 2018-04-16 19:15:44 +09:00
415cde1629 Transaction count CS translation 2018-04-16 09:11:46 +02:00
b438312fde fix js 2018-04-16 11:38:10 +09:00
79ff2cb271 Merge pull request #127 from bitmario/master
Add Portuguese (Portugal) translation
2018-04-16 11:28:54 +09:00
ed1464c405 Merge pull request #125 from LinoxBE/dutch-translation
Dutch update txCount
2018-04-16 11:28:10 +09:00
f85631429b Merge pull request #126 from mutedstorm/patch-2
fix german translation
2018-04-16 11:27:41 +09:00
5ed56d1137 Update JA translations 2018-04-16 11:26:29 +09:00
d7719d25b4 Add Portuguese (Portugal) translation 2018-04-16 01:29:42 +01:00
6267cccc3f fix german translation
minor changes, thanks to (@raindogdance)
2018-04-15 22:47:08 +02:00
fd5c4021f7 Dutch update txCount 2018-04-15 20:00:11 +02:00
b8bf4d99ac Bump 2018-04-15 21:29:44 +09:00
0723eec508 Fix rate handling 2018-04-15 21:21:57 +09:00
7f01a12245 Merge pull request #124 from mutedstorm/patch-1
fix german translation
2018-04-15 21:20:06 +09:00
e1e3e5d953 fix german translation
fixed small errors and changed "Geldbörse / Brieftasche" back to Wallet because its never translated on German sites so its unnecessary.
2018-04-15 12:32:24 +02:00
18986faca8 Merge remote-tracking branch 'source/master' into dev-bootstrap
# Conflicts:
#	BTCPayServer/Controllers/ServerController.cs
2018-04-14 11:11:38 -05:00
2a68f8a90f Merge pull request #121 from lepipele/master
Adding German translations
2018-04-14 10:54:04 -05:00
659936577b Adding German translations
Again using Google Translate, we need native speaker to review them
2018-04-14 10:53:02 -05:00
85efc3b00c fix tests 2018-04-14 23:32:39 +09:00
5efac45d46 bump 2018-04-14 22:55:35 +09:00
c7dce280d7 fix js 2018-04-14 22:53:31 +09:00
04c6107196 Can configure rate caching and bitcoinaverage API keys 2018-04-14 22:52:57 +09:00
54ce9b5dab Merge pull request #120 from rsandrade/patch-2
Update pt_BR.js
2018-04-14 21:40:38 +09:00
cee955fb9d Update pt_BR.js 2018-04-14 07:48:31 -03:00
2e4b0daa48 add french translation, bump NBitcoin 2018-04-14 13:18:56 +09:00
e85ccfb47e Merge pull request #117 from lepipele/master
Improvements to i18n, invoice expiry bugfix
2018-04-14 13:06:45 +09:00
b099f93c78 Adjusting Policies form to look better on different screen sizes 2018-04-13 16:15:21 -05:00
81afe397be CssThemeManager that injects Bootstrap css uri from settings 2018-04-13 16:15:03 -05:00
f869c06aee Adding Bootstrap theme uri field to settings 2018-04-13 15:42:34 -05:00
75099b99d4 TxCount strings in Spanish 2018-04-13 14:44:42 -05:00
7b1b2a0f68 Bugfixing redirect link when invoice expires
Refactoring logic so that it's same for paid and expired
2018-04-13 14:39:45 -05:00
203c28df3d Extracting transaction string and supporting plural form 2018-04-13 14:10:06 -05:00
2e2c3cdec4 bump 2018-04-14 00:06:00 +09:00
6f827c86a4 Update images and bump 2018-04-13 14:34:29 +09:00
5aced90a3f Merge pull request #115 from iamvinny/master
Fix Portuguese translation
2018-04-13 10:47:37 +09:00
4646f88e3a Fix Portuguese translation 2018-04-12 18:45:05 -03:00
2b11cc1077 Simplify root key path calculation 2018-04-12 11:48:33 +09:00
77b42eb085 Do not forget to pass expiry to createinvoice on clightning 2018-04-11 18:42:19 +09:00
7de067cd7a remove unused 2018-04-10 19:12:37 +09:00
9da6df50b7 Add DOGECOIN 2018-04-10 19:07:57 +09:00
66b1623109 Merge pull request #109 from lepipele/master
Fixing ForgotPassword, updating BundleMinifier
2018-04-10 13:22:36 +09:00
2432834f3d Updating BundleMinifier, now supporting CSS variables 2018-04-09 23:13:14 -05:00
01fa483f95 Improving styling of Forgot password page
Fixes: https://github.com/btcpayserver/btcpayserver/issues/108
2018-04-09 23:12:03 -05:00
1ddf47256f Show more invoices on the invoice page, better search button 2018-04-09 17:53:43 +09:00
25fe32c3f8 Add border to table 2018-04-09 17:43:33 +09:00
ac9b8d03d7 Fix slow invoice creation 2018-04-09 16:25:31 +09:00
8fdfb2c4f6 Fix Back to Website path for Hangfire 2018-04-09 14:41:52 +09:00
b1da136f77 Update packages and remove hangfire hack 2018-04-09 14:31:39 +09:00
9a6f85fa21 Prevent full crash if lightning crash 2018-04-09 10:48:16 +09:00
7308453a74 Merge branch 'lepipele-dev-bootstrap4' 2018-04-08 14:57:54 +09:00
b798a17ef8 Updating Bootstrap4 path on POS 2018-04-08 00:28:39 -05:00
4b8899860e Migrating btn-info to btn-secondary 2018-04-08 00:25:00 -05:00
f46c8a0a0f Migrating btn-success to btn-primary 2018-04-08 00:08:15 -05:00
48832f9ac3 Updating tables not to have top border and margin as requested 2018-04-08 00:06:47 -05:00
9c798fc2e2 Bootstrap 4 custom theme to address issues from #106
Including SCSS variable changes in case we eventually want to
generate results CSS from Bootstrap SCSS
2018-04-08 00:05:00 -05:00
4704587f0a Removing unused Bootstrap 4 flavors and versions 2018-04-07 23:53:08 -05:00
58e6b63fd7 Removing legacy btn-default style 2018-04-07 23:50:34 -05:00
3c76dfb584 Migrating to btn-primary
btn-default has been removed in Bootstrap4:
https://github.com/twbs/bootstrap/issues/25029
2018-04-07 23:49:36 -05:00
10055d987d Merge remote-tracking branch 'source/master' into dev-bootstrap4 2018-04-07 23:22:11 -05:00
be49c60e83 Update lightning-charge 2018-04-08 12:29:20 +09:00
14016e2f84 Fix grammar 2018-04-07 21:34:24 +09:00
d7cb6f1cca Add a way to customize lightning invoice description 2018-04-07 16:27:46 +09:00
0f63162254 Merge branch 'dev-bootstrap4' of https://github.com/lepipele/btcpayserver into lepipele-dev-bootstrap4 2018-04-07 12:05:26 +09:00
21215dc537 Make sure a too high expiration do not trigger "The value needs to translate in milliseconds to -1 (signifying an infinite timeout)" 2018-04-07 11:53:33 +09:00
20e147edfc Fix #103 2018-04-07 02:49:26 +09:00
1048dd516b Pass itemDesc to lightning invoice (Fix #104) 2018-04-07 02:43:35 +09:00
42f44327f0 Update NBitcoin and NBXplorer 2018-04-07 02:23:12 +09:00
49200a4a9c Removed old Bootstrap, no longer needed 2018-04-06 00:16:27 -05:00
7d71757de3 Merge remote-tracking branch 'source/master' into dev-bootstrap4 2018-04-06 00:14:18 -05:00
0fb492a70f Migrating to FontAwesome
Glyphicons dropped from Bootstrap4:
https://getbootstrap.com/docs/4.0/migration/#components
New version of Glyphicons not readily available in CSS format
Using FA since it's already in solution
2018-04-06 00:14:07 -05:00
7ccc1abb95 Moving checkout CSS and JS to dedicated folder 2018-04-05 23:56:17 -05:00
d61858e260 Cleaning up CSS and JS files used for main theme 2018-04-05 23:51:55 -05:00
0ecd40f299 Removing legacy css files no longer used 2018-04-05 23:33:43 -05:00
d9d4e74126 Preserving btn-default style that's removed from Bootstrap4 2018-04-05 23:31:53 -05:00
42d04bff61 Migrating table styles 2018-04-05 23:20:12 -05:00
f9cc29f014 Removing CSS variables until NUglify is merged for bundling
Ref: https://github.com/madskristensen/BundlerMinifier/issues/306
2018-04-05 22:56:18 -05:00
992d359e79 Add a --rootpath option 2018-04-05 15:50:23 +09:00
1cc5427cbb Improve error message if you can't create an invoice in the UI 2018-04-05 15:44:27 +09:00
6270a626fb Fix checkout experience custom logo and css 2018-04-05 11:34:25 +09:00
40092b60fa Migrating navigation pills 2018-04-03 23:30:28 -05:00
5356b74490 Switching bundling to point to Bootstrap 4 2018-04-03 23:30:19 -05:00
e832ce5b4a Adding Bootstrap 4 to solution 2018-04-03 23:29:59 -05:00
a845ed88a7 bump 2018-04-03 18:01:47 +09:00
560c1c3dc0 do not use long cache provider 2018-04-03 17:56:55 +09:00
ecc5032bb2 Fix error message if invalid input lightning max value / min value. Increase cache of currency to 15 min 2018-04-03 17:54:50 +09:00
325b359ff6 Add OnChainMinValue 2018-04-03 17:39:28 +09:00
10fcc84379 Properly test PoS 2018-04-03 16:58:47 +09:00
149c29963d Add Point of Sale feature to BTCPay 2018-04-03 16:58:47 +09:00
546c39a98e Merge pull request #98 from Saevar2000/master
Update Icelandic
2018-04-02 22:17:09 +09:00
13223817a1 Update Icelandic 2018-04-02 13:05:55 +00:00
1b92314eb2 Merge pull request #97 from pajasevi/cs_lightning
Added CS translations for lightning payments
2018-04-02 22:02:54 +09:00
2b97808f1f add .vscode to .gitignore 2018-04-02 12:48:13 +00:00
8650446dcd Added CS translations for lightning payments 2018-04-02 11:57:12 +02:00
7f24b89a80 fix french mistake 2018-04-02 15:27:16 +09:00
e56ca73046 Merge pull request #95 from lepipele/dev-bugfixtrans
Bugfixing translations, they were breaking bundling
2018-04-01 14:45:54 -05:00
bf5062086c Bugfixing translations, they were breaking bundling 2018-04-01 14:43:25 -05:00
aa12167a6d Merge pull request #90 from LinoxBE/dutch-translation-update
Added Lightning related translations for Dutch
2018-03-31 22:50:56 +09:00
9fa9f62d02 Added Lightning related translations for Dutch (v2) 2018-03-31 15:27:16 +02:00
c9615b660e Merge branch 'master' of https://github.com/btcpayserver/btcpayserver into dutch-translation-update 2018-03-31 15:23:27 +02:00
0ac51f479f Merge pull request #92 from junderw/fixJA2
Update JA
2018-03-30 22:20:47 -05:00
37649fc77b Update JA 2018-03-31 10:12:24 +09:00
ca4585eee9 Merge pull request #91 from marcosrdz/patch-1
Update es.js
2018-03-30 13:45:47 -05:00
83a1492cd4 Update es.js 2018-03-30 12:52:33 -04:00
a1af694acb Added Lightning related translations for Dutch 2018-03-30 13:08:46 +02:00
6330c0f0d7 Merge pull request #88 from felipehuicochea/patch-1
Update es.js
2018-03-30 17:36:18 +09:00
224c569ed1 French translation 2018-03-30 17:35:41 +09:00
c608987526 Rename peer info to node info 2018-03-30 17:34:46 +09:00
0c8f37ca19 bump 2018-03-30 15:37:04 +09:00
aca67d6eae Update es.js
Minor typos and grammar fixes
2018-03-30 01:28:55 -05:00
5dea0312ac Plugging NodeInfo reference 2018-03-30 15:23:05 +09:00
f074007f67 Refactoring clipboard copy code 2018-03-30 15:23:05 +09:00
88818ece29 Both regular and lightning copy tabs with new simplified styles 2018-03-30 15:23:05 +09:00
fa0fa28949 Complete switch to new styles for regular copy tab 2018-03-30 15:23:05 +09:00
08e31f6fe8 Clearing up label styles and using new input for all textboxes 2018-03-30 15:23:04 +09:00
b976adeefe Refactoring styles, simplifying the hierarchy 2018-03-30 15:23:04 +09:00
53c53b98e6 Adding new translation strings 2018-03-30 15:23:04 +09:00
a171e00280 Adding PeerInfo textbox
We'll need to heavily refactor this HTML and CSS... way to many styles and complex structure
2018-03-30 15:23:03 +09:00
46f94d7175 Merge pull request #86 from Saevar2000/master
Add Icelandic
2018-03-29 23:21:27 +09:00
2e555cac22 Add Icelandic 2018-03-29 08:19:07 +00:00
0d91b3286a bump 2018-03-29 13:00:04 +09:00
396432b873 Remove ESSLint errors 2018-03-29 12:54:58 +09:00
15c58434e8 Renaming CreateInvoiceResponse to CLightningInvoice 2018-03-29 12:54:07 +09:00
daad1bdd25 Fix bug of lightning invoice notification spam at startup 2018-03-29 12:36:10 +09:00
c60966c725 Revert "Add temporary log for stufftech debug"
This reverts commit fb57d8c3ce9f4b6e5d1ced035cda4cd385e6c75a.
2018-03-29 12:25:26 +09:00
fb57d8c3ce Add temporary log for stufftech debug 2018-03-29 12:21:20 +09:00
799ce74f65 Add temporary log for stufftech debug 2018-03-29 12:20:06 +09:00
8e38d7ceb4 Revert "Add temporary log to debug stufftech"
This reverts commit a1c22e807146b46e90cf78dd542bd4e3a6f67bf7.
2018-03-29 12:17:03 +09:00
a1c22e8071 Add temporary log to debug stufftech 2018-03-29 12:14:51 +09:00
6d8acf54d6 Revert "Fix SQLite bug: New invoice repeating"
This reverts commit 9eb3aad072c0c28dc937e6317425b2a3a0e1ed94.
2018-03-29 12:10:03 +09:00
a500a89138 Revert "add hack sqlite specific"
This reverts commit c6d44e7a8936fe9ac7bb85f221ed176afd8b8540.
2018-03-29 12:09:57 +09:00
c6d44e7a89 add hack sqlite specific 2018-03-29 12:02:13 +09:00
9eb3aad072 Fix SQLite bug: New invoice repeating 2018-03-29 11:57:17 +09:00
9355454953 Merge pull request #85 from pajasevi/lang-cs-fix
Fixed cs translation
2018-03-28 14:53:40 -05:00
6467f06c54 Fixed cs translation 2018-03-28 21:45:23 +02:00
b9b4b5ea39 log invoice event if Lightning max value exceeded 2018-03-28 23:15:10 +09:00
e23243565f Refactor CreateInvoiceCore to better give feedback on payment method errors to the merchant, be faster, and give NodeInfo 2018-03-28 22:37:01 +09:00
d3420532ae bump 2018-03-28 15:14:35 +09:00
ade1b9d4eb Merge pull request #84 from lepipele/dev-bugfix
Bugfixing currency icon positioning on smaller screens
2018-03-28 15:11:56 +09:00
fc278d12fc Bugfixing currency icon positioning on smaller screens 2018-03-28 01:09:53 -05:00
8e5ec822dc Powered by BTCPay Server 2018-03-27 15:22:48 +09:00
26aac66a76 Allow merchant to customize their checkout page 2018-03-27 15:14:50 +09:00
a562e90bdb Separate Checkout Experience settings from General store settings 2018-03-27 14:48:32 +09:00
a0f3698701 bump 2018-03-27 11:21:06 +09:00
02163f9482 Rewrite CanParseDerivationScheme 2018-03-27 11:21:06 +09:00
b74fe171e2 Merge pull request #83 from lepipele/master
Bugfixing isLightning compare for Conversion tab
2018-03-27 10:39:56 +09:00
2785bb4d9b Bugfixing isLightning compare for Conversion tab 2018-03-26 15:02:53 -05:00
5eac84d3a3 Fix bug: bitcoinAddress field of Invoice was showing ligthning BOLT11 address 2018-03-26 12:38:14 +09:00
a0a2ab6fcd update publish-docker 2018-03-26 11:54:10 +09:00
7730ead8e4 bump 2018-03-26 09:49:03 +09:00
8eee0dd14c Merge pull request #81 from pajasevi/lang-CS
Czech language support
2018-03-26 09:46:59 +09:00
7dd88d8d8f Can send max invoice value for lightning payments 2018-03-26 01:57:44 +09:00
56d1d3e645 Czech language support 2018-03-25 17:17:38 +02:00
c2308675b2 Better doc on the StoreUsers page 2018-03-25 14:09:40 +09:00
cb866a1c05 Make JP a bit shorter 2018-03-24 23:55:23 +09:00
95290e8331 Disable convertir tab for all lightning payments 2018-03-24 23:43:02 +09:00
f5e62c775b Remove BTC mentions from ConversionTab_Lightning 2018-03-24 23:37:18 +09:00
f533309b49 plug japanese translation 2018-03-24 23:02:41 +09:00
d1c70a7cb3 Merge pull request #78 from junderw/fixJA
Fix Japanese
2018-03-24 22:58:26 +09:00
2f8590ca7a Fix Japanese 2018-03-24 22:06:03 +09:00
08badbde56 bump 2018-03-24 20:40:48 +09:00
8e38da80e0 Better UX to set the xpub correctly 2018-03-24 20:40:26 +09:00
cd2e3350b0 Japanese support WIP 2018-03-24 20:15:42 +09:00
a0d2790491 Activate spanish 2018-03-24 14:35:49 +09:00
8ca99e5635 Merge branch 'spanish-language' of https://github.com/marcosrdz/btcpayserver into marcosrdz-spanish-language 2018-03-24 14:35:05 +09:00
5a2563ca7f Spanish translation 2018-03-24 01:15:43 -04:00
a23cd28531 Merge pull request #76 from LinoxBE/french-translation-fix
Fix French translation
2018-03-24 14:12:18 +09:00
58a967b59e Fix French translation 2018-03-23 19:41:27 +01:00
9bf0c20198 bump 2018-03-24 02:40:05 +09:00
6b7ac0e000 Merge pull request #75 from lepipele/dev-i18n
Fixing problems on expiry page for different languages
2018-03-24 02:39:37 +09:00
188c0a9a86 Fixing third line in expiry translation for Dutch 2018-03-23 12:33:57 -05:00
c49479c8ad Styling changes to make expiry text fit in different languages 2018-03-23 12:32:00 -05:00
2072b6e136 Fix english selection when the store has not set default language 2018-03-24 01:58:11 +09:00
08d82390b0 Remove language not yet translated 2018-03-24 01:15:28 +09:00
b845a545e2 Plug Dutch to LanguageService 2018-03-24 01:10:19 +09:00
db958b2401 Add Dutch 2018-03-24 01:06:00 +09:00
7266420eec Plug portugeuse to language service 2018-03-24 01:04:05 +09:00
f36fbe7a76 Merge branch 'patch-1' of https://github.com/rsandrade/btcpayserver into rsandrade-patch-1 2018-03-24 00:59:38 +09:00
8e279b110c use full language code 2018-03-24 00:58:37 +09:00
d626870e46 Create pt.js 2018-03-23 10:48:03 -03:00
df49b094d5 French translation 2018-03-23 18:04:44 +09:00
7d17bf7f2a Can set store default language 2018-03-23 17:27:48 +09:00
e51f3dd1ae Merge branch 'lepipele-dev-i18n' 2018-03-23 16:44:07 +09:00
b810b88c6c Merge branch 'dev-i18n' of https://github.com/lepipele/btcpayserver into lepipele-dev-i18n 2018-03-23 16:39:44 +09:00
39b34ff4ed Can invite user to manage your store 2018-03-23 16:24:57 +09:00
f72fd63113 Merge remote-tracking branch 'source/master' into dev-i18n
# Conflicts:
#	BTCPayServer/Properties/launchSettings.json
2018-03-22 23:35:45 -05:00
97eedc2c9f German test translation completed 2018-03-22 23:33:47 -05:00
db222c53e3 Faster language selection on page load 2018-03-22 23:16:38 -05:00
61e919b88d Removing flicker on invoice load 2018-03-22 23:15:54 -05:00
d14040c142 Foundation for future translations of supported languages 2018-03-22 23:04:42 -05:00
13a3a581d8 Detecting language from querystring 2018-03-22 23:02:53 -05:00
f6dbae1cef Extracting translation strings from core.js 2018-03-22 22:48:16 -05:00
ccbcda86ac Binding translation for Your email placeholder 2018-03-22 13:26:10 -05:00
b74e8cf756 Translating Invoice expired state 2018-03-22 12:57:51 -05:00
8f8266f15d Extracting complex Checkout body structure for easier navigation 2018-03-22 12:08:49 -05:00
ab8d3f5813 Extracting strings for translation 2018-03-22 12:02:55 -05:00
08220dbea5 Reorganizing file structure for i18n support 2018-03-22 11:12:15 -05:00
3b2cf2f1de Can add administrator, fix #65 2018-03-22 19:55:14 +09:00
c3beca27be Bugfix: Pressing enter no longer reloads page when providing email 2018-03-22 00:21:26 -05:00
28b820241f Integrating dropdown for language selection
So hard to find good jquery alternative that drops up
2018-03-22 00:07:30 -05:00
e985224092 Bugfixing IExplorer bug
Doesn't allow i18n without key:value format
2018-03-22 00:06:56 -05:00
f1c467aa7d Fix nodeinfo 2018-03-22 12:26:38 +09:00
ae7cfe90ab Show the NodeInfo when testing connection 2018-03-22 12:02:38 +09:00
718a36ddd0 Remove dev time stuff 2018-03-22 01:10:14 +09:00
c0b903d79c Wallet page is now an action link in the store settings 2018-03-22 01:07:11 +09:00
48eaf906b0 Update text in AddLightningNode 2018-03-21 23:51:28 +09:00
f42fde970a bump 2018-03-21 15:07:26 +09:00
59afebaa57 Translating few items and testing how it works 2018-03-20 14:30:37 -05:00
3e06e45054 Cleanup, removing outdated classes and spinner 2018-03-20 14:19:10 -05:00
fe55acb268 Reorganizing Checkout page resources and adding i18n libs 2018-03-20 13:24:11 -05:00
710dbb51f4 Remove useless code 2018-03-21 03:11:03 +09:00
d426d66819 fill exisitng values in AddDerivationScheme and AddLightningNode 2018-03-21 03:05:51 +09:00
265cddc38b Change the UX to set lightning node or derivation schemes 2018-03-21 02:48:11 +09:00
21b91ac8f7 Add btc.lightning and ltc.lightning 2018-03-21 02:09:25 +09:00
e656813844 Html cleanup, removing comments and extracting bp-spinner 2018-03-20 11:51:52 -05:00
e8730f74be update docker-compose 2018-03-21 01:10:10 +09:00
6d611d7d05 Can connect directly to CLightning via TCP or UNIX socket 2018-03-21 00:31:19 +09:00
392f3a16f1 Introduce LightningClientFactory 2018-03-20 12:10:35 +09:00
2b2e12b290 Abstract ChargeClient to prepare for support of other lightning implementation 2018-03-20 11:59:43 +09:00
73cc75fe66 Fixing display bug when invoice is paid 2018-03-19 11:16:57 +09:00
cc186fc8b3 Fix bug: Incorrect confirmation count in Invoice screen under some circumstances. 2018-03-19 09:45:54 +09:00
632ad81b94 update .net core 2018-03-19 00:44:12 +09:00
1d243910ae bump 2018-03-18 15:29:21 +09:00
e624649cd8 Merge branch 'lepipele-dev-shapeshift' 2018-03-18 15:28:42 +09:00
e6ca07e9b5 Merge remote-tracking branch 'source/master' into dev-shapeshift 2018-03-18 00:42:55 -05:00
b3d6435772 Custom text in Conversion tab in case of BTC LN payment 2018-03-18 00:41:16 -05:00
806474c8c6 Allow account selection of the ledger 2018-03-18 14:15:23 +09:00
1524fb4499 Merge remote-tracking branch 'source/master' into dev-shapeshift 2018-03-17 23:50:11 -05:00
14b70ff35e Rendering of Conversion tab on Invoice if enabled in store settings 2018-03-17 23:49:09 -05:00
c36a900627 Store setting for allowing conversion through Shapeshift 2018-03-17 23:48:06 -05:00
d3befb5b86 Fixing slider style when there are only two tabs 2018-03-17 23:36:32 -05:00
7f0ce1f802 Changing text to clarify usage 2018-03-17 23:28:39 -05:00
8342ad9175 Remove reference to browser mode of ledger 2018-03-18 12:58:14 +09:00
acb2407654 Fix bug: Paying a lightning invoice might miss 1 satoshi due to rounding error 2018-03-18 02:26:33 +09:00
b8a4f0c012 fix tests 2018-03-18 01:59:16 +09:00
57bb3b231c Add linux script for manual testing 2018-03-17 19:40:23 +09:00
e5d626e0fd Remove useless stuff in command line for tests 2018-03-17 19:35:37 +09:00
e2c4c913ff Remove hard coded container names in test docker-compose 2018-03-17 19:33:36 +09:00
09f97915d6 Fix charge listener bug, and decouple charge from clightning in test docker compose 2018-03-17 19:26:30 +09:00
81328b2667 Update charge in tests and fix two build time warnings 2018-03-17 17:49:42 +09:00
0d8affc68d Remove dependency on Eclair for tests 2018-03-17 17:02:47 +09:00
da77d278fb Adding Shapeshift button in new Altcoins tab 2018-03-16 23:46:39 -05:00
5e9f6f3542 Refactoring jquery logic for tab switching
Need to be more general to incorporate third tab
2018-03-16 23:15:01 -05:00
f337470f09 Adding styles for third tab - altcoin payments 2018-03-16 23:14:13 -05:00
636224d0c8 Checkout html and js cleanup 2018-03-16 22:46:30 -05:00
b28b3ef4ff Fix: Invoice can't be paid in lightning anymore if lightning server sent error 2018-03-14 20:10:04 +09:00
9e2e102ec4 Fix bug making it impossible to remove LTC xpub 2018-03-14 19:32:24 +09:00
9e16b83202 Testing Shapeshift integration 2018-03-13 12:20:22 -05:00
cbd40d49c1 bump 2018-03-13 15:56:17 +09:00
1d051648b7 Merge pull request #60 from lepipele/dev-lepi
Bugfixing loading spinner when switching currency
2018-03-13 15:41:22 +09:00
49cf804914 bump 2018-03-13 15:39:52 +09:00
0f6ad75536 Remove internal exception thrown by NBitcoin 2018-03-13 15:28:39 +09:00
56eea18b2d Bugfixing loading spinner when switching currency
Moving it to buttons so it directly interacts with actions and doesn't break form states
2018-03-13 00:34:26 -05:00
b3698846c6 Improve UX of invoice list and invoice details 2018-03-13 09:13:16 +09:00
6806d96baa Listen to all derivation schemes 2018-03-12 19:02:03 +09:00
dc3b3077c2 Add text align for rate in invoice detail page 2018-03-12 11:02:02 +09:00
936ae64ca3 Allow connection via non https lightning charge node through localhost or 127.0.0.1 2018-03-11 15:14:05 +09:00
3a0a5dbd7f Accept all success HTTP code for invoice callbacks 2018-03-07 14:22:02 -05:00
ed4430ae7d bump 2018-03-07 07:49:46 -05:00
9a0e4e35d9 Merge pull request #56 from lepipele/dev-lepi
Tweaking Checkout page so that it works properly in IE
2018-03-07 07:48:52 -05:00
5715dd2058 Disabling AJAX caching that messes up checkout in IE
Ref: https://stackoverflow.com/questions/4303829/how-to-prevent-a-jquery-ajax-request-from-caching-in-internet-explorer
2018-03-06 22:04:03 -06:00
da4c132f9d Adding Vue.js binding attributes 2018-03-06 22:02:34 -06:00
303a617f9e Improve invoice.cshtml display if offchain payment is present 2018-03-06 16:37:25 -05:00
3116ec9cb8 Fix bitcoin logo for internet explorer, update nbxplorer 2018-03-06 10:41:41 -05:00
b3f4eab075 fix doc 2018-03-06 09:40:21 -05:00
c98593b47b add dependencies to README 2018-03-06 09:35:20 -05:00
2c49d61682 shebang and fix line ending 2018-03-06 09:23:08 -05:00
21f5c94cff update readme 2018-03-06 09:18:00 -05:00
834ba4afab chmod +x scripts 2018-03-06 09:15:09 -05:00
d690cd6b7b Add build and run scripts 2018-03-06 09:14:45 -05:00
937bd07daa add doc 2018-03-05 10:58:27 -05:00
559b822111 fix broken expiration screen on checkout 2018-03-03 20:33:52 -05:00
7afb4c6b11 bump 2018-03-03 15:08:11 -05:00
1c98a3a33d Rename currency selection with "Pay With" 2018-03-03 15:07:34 -05:00
3320eb284e Merge branch 'lepipele-dev-lepi' 2018-03-03 15:06:58 -05:00
919fb60558 Hiding currency selection when invoice paid 2018-03-03 00:52:49 -06:00
7796589105 Extracting public methods in core.js 2018-03-03 00:41:52 -06:00
de6d3198ff Bundling JS and CSS files for Checkout.cshtml
Now we'll finally have versioning so when those JS/CSS files update, clients will properly request new bundle
2018-03-03 00:32:51 -06:00
f1e971d047 Refactoring core.js in preparation for bundling
Moving Vue registration to body for quick update of page
Removing defer dependancy for core.js
2018-03-03 00:32:04 -06:00
acd98aad32 Showing loader for better UX when switching currencies 2018-03-03 00:11:08 -06:00
b0c810398c Moving currency selection to order details
This way state transitions of form are now properly preserved
2018-03-02 23:49:51 -06:00
03a0044745 Currency selection moved to top of the form 2018-03-02 23:42:17 -06:00
15684efdce Update README.md 2018-03-02 15:04:00 -05:00
b67a962d12 Make sure the txrelayfee is correctly set 2018-03-02 14:16:16 -05:00
339cedadf7 Save a call to nbxplorer.GetStatus, update NBXplorer 2018-03-02 14:03:47 -05:00
e19d730fb7 Merge pull request #54 from practicalswift/typos
Fix typos
2018-03-03 02:43:16 +09:00
649497e54f Fix typos 2018-03-01 15:11:30 +01:00
40eee0924a bump 2018-03-01 11:48:55 +09:00
bbf5fb3c30 show port if failing to connect to lightning node 2018-03-01 10:31:01 +09:00
346cdf2431 show block gap if lightning node is not synched 2018-02-28 23:12:09 +09:00
030cb09af8 Fix bug of address parsing for lightning 2018-02-28 22:56:12 +09:00
9f734349da Prettify date on the invoice list, and add orderid 2018-02-28 19:03:23 +09:00
2d5a861df0 Update to work with 0.16.0 2018-02-28 17:01:10 +09:00
061f428a54 fix bundling 2018-02-27 17:29:57 +09:00
9a539fd350 Remove default profile for launchSettings 2018-02-27 17:19:37 +09:00
4149fe10e9 Removing unused bundle.css & bundle.min.css 2018-02-27 17:03:51 +09:00
1014083160 Preserving bundles directory required for build 2018-02-27 17:03:49 +09:00
dfa3167c18 Removing generated bundles from source control 2018-02-27 17:03:45 +09:00
7e09efb9a3 Adding BundleJsCss as property on BtcPayServerOptions 2018-02-27 17:03:41 +09:00
fb736c0d0f Moving AddBundles to BtcPayServerServices 2018-02-27 17:03:39 +09:00
e4a7263e9b Turning JS/CSS bundling on 2018-02-27 17:03:38 +09:00
c52926f2b0 Using min versions of JS and CSS files 2018-02-27 17:03:37 +09:00
b6138b36be Restoring Unobtrusive Jquery validation 2018-02-27 17:03:29 +09:00
04bce3ae00 Bundling of CSS/JS files that's configurable in launchSettings.json
If you set BTCPAY_BUDNLEJSCSS to true it'll bundle all JS/CSS files into one

Ref: https://github.com/btcpayserver/btcpayserver/issues/47
2018-02-27 16:44:28 +09:00
68ca162dd3 Cleaning up JS/CSS references on Checkout page 2018-02-27 16:39:15 +09:00
100bb02cd5 fix logo size in copy part of checkout 2018-02-26 23:21:35 +09:00
856249d52c Update test sdk 2018-02-26 19:06:02 +09:00
309d6fdfe0 Can configure an internallightningnode to make things easier 2018-02-26 18:58:02 +09:00
f289420364 update image 2018-02-26 17:07:19 +09:00
f05e85de5f fix typo 2018-02-26 16:16:15 +09:00
4138849546 Better logo and warning 2018-02-26 16:13:10 +09:00
7052e4e1dc adjust layout of UpdateStore 2018-02-26 15:40:49 +09:00
ffb3e4f1fb Add logo for lightning 2018-02-26 15:33:03 +09:00
297834be66 Tell to users that using lightning is reckless 2018-02-26 15:16:17 +09:00
5924f1730c Poll for charge invoice 2018-02-26 14:52:08 +09:00
adc6bea4dc make tests bit more resilient 2018-02-26 13:36:00 +09:00
ef431f688f Make ChargeListener use only one websocket connection per url 2018-02-26 13:29:23 +09:00
c8923af573 Lightning Network support implementation 2018-02-26 00:48:12 +09:00
3d33ecf397 make IsAvailable async 2018-02-23 16:09:15 +09:00
82d38da18e FAQ 2018-02-23 15:31:19 +09:00
200e259b82 Add lightning dependencies to tests and docker-compose 2018-02-23 15:21:42 +09:00
2d1f4e5e0a update NBitcoin 2018-02-21 14:19:11 +09:00
7fe64612ad bump nbxplorer 2018-02-21 11:56:30 +09:00
5e452a679e simplify code 2018-02-20 14:23:50 +09:00
10be8aec82 bump 2018-02-20 12:46:40 +09:00
0e1a1fd2cd Remove dependencies in StoreController to on chain payment specific stuff 2018-02-20 12:45:04 +09:00
3f07010de8 Rename IPaymentMethodFactory to ISupportedPaymentMethod 2018-02-20 10:44:39 +09:00
2e45c8b190 Isolate PaymentMethodId in its own class, generalise DerivationStrategy 2018-02-19 23:13:23 +09:00
b4f4401cdc remove unused code, remove derivationscheme specific logic from InvoiceEntity 2018-02-19 22:41:47 +09:00
a6b92a0dd5 Fix build 2018-02-19 18:58:58 +09:00
2f3238c65e Use decimal for calculations instead of Money, and round due amount at ceil satoshi 2018-02-19 18:54:21 +09:00
65f5a38b4a bump 2018-02-19 15:14:07 +09:00
271cbf682f fix casing 2018-02-19 15:13:45 +09:00
a634593903 Big refactoring renaming cryptoData => PaymentMethod 2018-02-19 15:09:05 +09:00
af94de93d1 Add some comments 2018-02-19 11:31:34 +09:00
35f669aa15 Isolating code of on chain specific payment in its own folder 2018-02-19 11:06:08 +09:00
4795bd8108 Add some sanity check, make sure to use CrytpoDataId everywhere 2018-02-19 03:35:19 +09:00
29aed99fd1 prevent a crash if the new property DerivationStrategies is notset at invoice level 2018-02-19 02:56:44 +09:00
aa4519ac30 Big refactoring for supporting new type of payment 2018-02-19 02:38:03 +09:00
752133b01c fix bug 2018-02-18 20:37:42 +09:00
fe0c21ba08 Make sure that IPN sent for the send invoice are sent one at a time 2018-02-18 16:09:09 +09:00
90904a6b5e bump 2018-02-18 02:42:38 +09:00
8e3f7ea68d do not block next invoices if one invoice fail processing 2018-02-18 02:40:53 +09:00
a239104a28 fix uppercase 2018-02-18 02:36:11 +09:00
3bc232e1da Further isolate bitcoin related stuff inside BitcoinLikePaymentData 2018-02-18 02:35:02 +09:00
a1ee09cd85 Further abstract payment data by encapsulating bitcoin related logic into BitcoinLikePaymentData 2018-02-18 02:19:35 +09:00
b898cc030c general code cleanup + add analyzers 2018-02-17 13:18:16 +09:00
0602353dd2 fix bug happening if only btc is supported 2018-02-17 01:55:38 +09:00
9d406923ae make sure the waitingInvoices tasks are done 2018-02-17 01:43:43 +09:00
aa8565e3cc forgot remove dev time stuff 2018-02-17 01:35:30 +09:00
5de330b1f9 Refactoring to keep coin logic out of InvoiceWatcher 2018-02-17 01:34:40 +09:00
66597aed46 hide websocket exceptions 2018-02-15 16:17:27 +09:00
3071826f06 bump 2018-02-15 15:17:41 +09:00
c3684eb064 BTCPayWallet should be singleton per cryptcode 2018-02-15 15:17:12 +09:00
335dd9e66d bump 2018-02-15 14:44:35 +09:00
cd1611dbcd make sure to not spam too much NBXplorer 2018-02-15 14:44:08 +09:00
c17793aca9 do not freeze the stores page 2018-02-15 13:33:29 +09:00
01d898b618 Caching GetCoins 2018-02-15 13:02:12 +09:00
17069c311b Remove transaction cache 2018-02-15 12:42:48 +09:00
921d072942 fix electrum format for add derivation 2018-02-15 11:47:45 +09:00
6181e8b3e4 Refactor the code to prepare the group to support of another hardware wallet 2018-02-13 16:57:40 +09:00
93fc12bb2e fix typo 2018-02-13 15:28:22 +09:00
8e73c1a2f0 error message if not using segwit 2018-02-13 13:22:37 +09:00
e97c15578d bump nbxplorer 2018-02-13 12:42:03 +09:00
fd4f4e6aff better feedback if forgot to activate browser support 2018-02-13 12:20:56 +09:00
cedf8f75e8 Small UI adjustements 2018-02-13 11:41:21 +09:00
cd0a650df4 Ledger wallet support 2018-02-13 03:27:36 +09:00
6d6b9e2ba6 disable test not passing because of bitpay 2018-02-10 22:24:50 +09:00
fd915fdc5c fix test 2018-02-10 22:21:52 +09:00
59be813fe9 bump 2018-02-10 22:10:36 +09:00
465fbdd47f Fix bug which can happen if parsing of CoinAverage decimal is on another culture 2018-02-10 22:03:33 +09:00
f220abb716 Make the address verification step mandatory 2018-02-07 21:59:16 +09:00
db46ca87d7 do not share cache between long and short profile 2018-02-01 21:34:07 +01:00
d873a1a545 Set a longer timeout for the cache for /rates, update NBXPlorer, bump 2018-02-01 21:24:13 +01:00
a464a8702b Merge pull request #42 from Eskyee/patch-2
bootstrap.min.css deleted last line
2018-01-26 07:02:35 +01:00
63722b932a Merge pull request #41 from Eskyee/patch-1
bootstrap.css deleted last line
2018-01-26 07:02:22 +01:00
698b3c46cd bootstrap.min.css deleted last line
deleted this line
/*# sourceMappingURL=bootstrap.min.css.map */
because of a debug error in browser's
[Error] Failed to load resource: the server responded with a status of 404
(Not Found) (bootstrap.min.css.map, line 0)
https://btcpay-server-testnet.azurewebsites.net/vendor/bootstrap/css/bootstrap.min.css.map
2018-01-26 01:19:58 +00:00
df81051d07 bootstrap.css deleted last line
deleted this line 
/*# sourceMappingURL=bootstrap.css.map */
because of a debug error in browser's
[Error] Failed to load resource: the server responded with a status of 404 
(Not Found) (bootstrap.css.map, line 0)
https://btcpay-server-testnet.azurewebsites.net/vendor/bootstrap/css/bootstrap.css.map
2018-01-26 01:12:52 +00:00
ac70a77361 Fix #38 with paidOver + paidLate 2018-01-24 10:37:23 +01:00
59a2432af9 Better invoice loop, fix javascript 2018-01-20 14:09:57 +09:00
ea4fa8d5d4 Mock rate provider 2018-01-20 12:30:22 +09:00
ade3eff75c unwrap rates in api/rates 2018-01-20 12:11:24 +09:00
db2a2a2b6c Fix expiration message on checkout page 2018-01-20 00:33:37 +09:00
579dcb5af8 Fix confirmation message when changing altcoin derivation scheme 2018-01-20 00:29:20 +09:00
69247dee8a Fix api/rates allow to scope by cyrptoCode and storeId 2018-01-19 18:11:43 +09:00
7b9541b8e9 Do not crash if some of the altcoins are unavailable 2018-01-19 17:39:15 +09:00
a12e4d7f64 fix typo 2018-01-19 17:14:27 +09:00
897da9b07a Better explanation for the price source 2018-01-19 17:13:29 +09:00
293525d480 do not query the rate source if the preferred exchange did not changed 2018-01-19 16:19:13 +09:00
198e810355 Store can customize rate source 2018-01-19 16:00:20 +09:00
fe25e00c94 Fix https://github.com/btcpayserver/btcpayserver/issues/38 2018-01-19 10:52:44 +09:00
8b129ab2e5 Merge pull request #37 from lepipele/dev-lepi
Resolving problems with Vue console warnings
2018-01-19 01:41:01 +09:00
770bed54d1 bump 2018-01-19 00:52:38 +09:00
774817d4ac Add transaction speed on the invoice page 2018-01-19 00:52:17 +09:00
b8068b2ae8 Vue ignoring custom HTML5 elements
Ref: https://github.com/btcpayserver/btcpayserver/issues/34#issuecomment-358541767
2018-01-18 09:48:21 -06:00
3007a6bbc8 Upgrading Vue and linking production (min) version 2018-01-18 09:47:39 -06:00
1c0c8fece2 Change default speed to medium 2018-01-19 00:37:00 +09:00
c52eee47f0 bump 2018-01-19 00:13:40 +09:00
f88c98b9d9 fix block explorer link for mainnet 2018-01-18 23:57:41 +09:00
b0e9e10f7e Add extended notifications 2018-01-18 20:56:55 +09:00
39d47e33f6 Fix https://github.com/btcpayserver/btcpayserver/issues/31 2018-01-18 18:53:11 +09:00
4b7b6c6327 debug 2018-01-18 18:33:26 +09:00
a59edc5e8c bump 2018-01-18 18:12:44 +09:00
5ba322ea6a Add debug messages 2018-01-18 18:12:01 +09:00
b47b4b10cb should fix https://github.com/btcpayserver/btcpayserver/issues/31 2018-01-18 17:21:29 +09:00
c88f391935 Merge branch 'master' of https://github.com/btcpayserver/btcpayserver 2018-01-18 12:45:55 +09:00
26d3178e93 Fix expiration transitioning with a delay 2018-01-18 12:45:39 +09:00
1ad27c7827 Merge pull request #35 from thijstriemstra/patch-1
fix typos
2018-01-18 11:48:05 +09:00
4200a8eed5 add c# comment 2018-01-17 17:45:57 +01:00
daceb7af8e happy new year 2018-01-17 17:43:57 +01:00
f703b53bce fix typos 2018-01-17 17:42:06 +01:00
2762224f0f Fix parsing bug zpub LTC mainnet 2018-01-17 19:39:15 +09:00
86fc64d184 Bump 2018-01-17 16:34:24 +09:00
726cd6fd49 Add badge if not on mainnet in the top bar 2018-01-17 16:34:01 +09:00
be1c4666e0 resize videos 2018-01-17 16:28:09 +09:00
b75dfc4191 Merge branch 'lepipele-dev-lepi' 2018-01-17 16:23:21 +09:00
97815f8daf Merge branch 'dev-lepi' of https://github.com/lepipele/btcpayserver into lepipele-dev-lepi 2018-01-17 16:18:54 +09:00
af16e1db69 update docker test 2018-01-17 16:17:27 +09:00
5f6913b3a2 Can tweak the rate at store level 2018-01-17 15:59:31 +09:00
2b31af80cb Can configure invoice expiration 2018-01-17 15:14:53 +09:00
3f9889d374 Update docker, remove tx cache, use new nbxplorer 2018-01-17 15:02:53 +09:00
f8189c64a4 Non blocking modal that shows sync progress
Ref: https://forkbitpay.slack.com/archives/C7M093Z55/p1515557792000053
2018-01-16 10:37:06 -06:00
c9b5f89f17 Make sure BTCPayServer exit cleanly in case of crash during startup, see https://github.com/aspnet/Hosting/issues/1194 2018-01-15 14:42:51 +09:00
ecb82f2cc9 do not send IPN if not set 2018-01-15 00:22:40 +09:00
f340c6eb7f Add more events to invoice 2018-01-14 22:06:06 +09:00
ba0e080816 Invoices has events recorded 2018-01-14 21:48:23 +09:00
bb3d107309 Fix legacy rate not being correct for alt only payment 2018-01-14 15:26:14 +09:00
8517b222bf Add redirect url to the invoice page 2018-01-14 15:01:09 +09:00
aed32204b5 Fix crashing ManageController 2018-01-14 14:52:15 +09:00
6b4eeff3f1 add tests, do not returns bitcoinAddress field if not supported by the invoice 2018-01-13 22:01:09 +09:00
e3cc589ebb fix nbxplorer 2018-01-13 21:07:04 +09:00
4a152e8ffc fix build 2018-01-13 17:32:08 +09:00
d54a9474d1 Fixing exception thrown when invoice is paid and supporting only LTC 2018-01-13 17:23:09 +09:00
98472211fc bump 2018-01-13 12:55:05 +09:00
099c9fa1f9 Fix balance calculation when there is unconfirmed tx 2018-01-13 12:53:56 +09:00
5226b77ffc Fix bug happening when removing data of nbxplorer 2018-01-13 02:28:23 +09:00
290779ee39 bump 2018-01-13 01:48:43 +09:00
4f39a8060c Fixing bug of uncorrectly unassigned addresses 2018-01-13 01:48:19 +09:00
92caa98dfb bump 2018-01-13 01:06:18 +09:00
df7bb9e2f8 Add info about nbxplorer synching 2018-01-13 01:05:38 +09:00
02a039d695 Fixing bug when targetting testnet 2018-01-12 22:36:13 +09:00
b5e4c803aa fix bad network throwing nullreferenceexception 2018-01-12 22:08:20 +09:00
2b7c70622f bump 2018-01-12 21:55:13 +09:00
88779e7129 Make sure websockets does not throw, fix annying warning of emails 2018-01-12 18:32:46 +09:00
6beb7abfd2 fix test 2018-01-12 17:04:47 +09:00
a1ebedc0d1 Fix unit test 2018-01-12 16:54:57 +09:00
d5ad0cdb39 Fix a edge case "The instance of entity type 'HistoricalAddressInvoiceData'" 2018-01-12 16:42:10 +09:00
39fb8dbb6a better handle case when BTC is not supported by a store 2018-01-12 16:30:34 +09:00
58194cb060 Fix tests, clean code of Options 2018-01-12 16:00:31 +09:00
ef165e15bf Fix config discovery when datadir is set 2018-01-12 13:45:25 +09:00
bf871c46ec update nbxplorer 2018-01-12 13:32:27 +09:00
52331e057f update nbxplorer 2018-01-12 13:16:17 +09:00
b59021a0be Cleanup the design for multi currencies support, use a single nbxplorer instead of two 2018-01-12 11:54:57 +09:00
8596e16feb make default configuration just work with multi chains 2018-01-11 22:52:28 +09:00
223558c01d Adding transaction caching 2018-01-11 21:01:00 +09:00
3a91965187 Some refactoring improving performance, and better tests for multiple currencies 2018-01-11 17:29:48 +09:00
55d50af39d BTCWallet is single currency, introduce BTCWalletProvider 2018-01-11 14:36:12 +09:00
3ff293ab7f Fix #30 2018-01-10 21:14:09 +09:00
7bcf2b5472 fix slack link 2018-01-10 19:42:38 +09:00
983f34814f bump, add field to know if replaced 2018-01-10 18:40:53 +09:00
a33e20b46b fix invoice page 2018-01-10 18:38:49 +09:00
bafdcb04ed fix error if invoice is requested without existing 2018-01-10 18:33:05 +09:00
cb4468d3b3 Fixing payment in different crypto 2018-01-10 18:30:45 +09:00
de6f0008a6 bump 2018-01-10 15:43:39 +09:00
7618eacef1 Fix: altcoin payment not detected after another coin payment 2018-01-10 15:43:07 +09:00
842e083ebe bump nbxplorer 2018-01-10 12:18:34 +09:00
1c510df3fc bump 2018-01-10 02:41:33 +09:00
c80ffe396e forgot passing cancellation 2018-01-10 02:13:49 +09:00
401a31e5c2 Cleanup code 2018-01-10 02:07:42 +09:00
2df60bd121 bump 2018-01-09 23:55:37 +09:00
6d10c8a6c1 Can change crypto on checkout page 2018-01-09 23:55:08 +09:00
44898b5e23 Checkout page: Bind crypto image to client cycle view model, add logo on main QR code 2018-01-09 22:43:36 +09:00
c0f53db561 fix sync bar 2018-01-09 18:52:16 +09:00
133fb96d28 bump 2018-01-09 17:27:54 +09:00
98b7ad62af Fix order accounting 2018-01-09 17:27:26 +09:00
3069fe0dd9 BTCPayServer should work on HTTP even if externalurl is https 2018-01-09 16:54:40 +09:00
729555b96f Fix NBxplorerListener disconnecting itself every minute 2018-01-09 16:10:16 +09:00
b4040ba7ad Update NBXplorer, bump 2018-01-09 14:12:28 +09:00
863752a471 update nbxplorer (fix ltc testnet) 2018-01-09 12:53:58 +09:00
6ae9d13c43 Allow checkout with litecoin 2018-01-09 11:41:07 +09:00
0c735f4e29 Fix accounting calculation when multi crypto 2018-01-09 10:54:19 +09:00
76d50b018b Calculate rate properly per crypto 2018-01-09 02:57:06 +09:00
31672a2587 Add litecoin to docker-compose fix bugs when two networks generate same address 2018-01-09 01:56:37 +09:00
a048494f34 bump version 2018-01-08 23:12:28 +09:00
c513d6bd44 Fix litecoin registration 2018-01-08 23:05:41 +09:00
c3d37b1f78 Can set derivation scheme for a specific crypto currency 2018-01-08 22:45:09 +09:00
5910644cda Remove useless field 2018-01-08 20:57:11 +09:00
a16cd3e287 Improve invoice page with currencies information 2018-01-08 20:06:16 +09:00
e3a0122eb3 make sure to not crash whole process if nbxplorer unavailable 2018-01-08 18:18:34 +09:00
1cda0eff16 bump nbxplorer 2018-01-08 17:17:39 +09:00
6003aa4236 Add polling for connection through websocket 2018-01-08 16:50:56 +09:00
8753dd15de Remove BOM from IPN 2018-01-08 04:18:15 +09:00
6ae6335c6d Fix layout_cshtml 2018-01-08 04:14:35 +09:00
e3a1eed8b3 Use Websocket for blockchain notifications 2018-01-08 02:36:41 +09:00
eb44203475 Remove internal url 2018-01-07 21:58:46 +09:00
80e878c2f5 Removing http callback notification system 2018-01-07 21:48:00 +09:00
6cb1649fc2 fix leak 2018-01-07 21:07:06 +09:00
63fceed5f4 invoice watcher can watch several currencies 2018-01-07 02:16:42 +09:00
781b2885cc Refactoring to prepare multiple DerivationSchemes per store and invoices 2018-01-06 19:10:55 +09:00
2f9afda0ab bump 2018-01-06 11:38:54 +09:00
108146ca92 Fixing QR Code and Button to use BIP21 2018-01-06 11:38:24 +09:00
bb46294a6d Fix progress bar on synching 2018-01-06 01:24:04 +09:00
f8aad6ac80 bump 2018-01-05 00:41:21 +09:00
658d1f1df0 Merge pull request #25 from lepipele/dev-lepi
Indicator now faintly visible without hover
2018-01-05 00:34:26 +09:00
ee3144f34a Indicator now faintly visible without hover
Reference: https://github.com/btcpayserver/btcpayserver/pull/23#issuecomment-355302053
2018-01-04 08:59:44 -06:00
9a34fe46fb Fix #20 2018-01-04 22:56:49 +09:00
766d96c02d fix layout 2018-01-04 22:52:14 +09:00
7445c89773 Merge pull request #23 from lepipele/dev-lepi
Implementing indicator that shows total line row can be expanded
2018-01-04 22:44:00 +09:00
28ac5608a5 Update NBxplorer, bump version 2018-01-04 22:43:28 +09:00
44c925a4ba Fix #24 2018-01-04 22:21:47 +09:00
51cd89f177 Implementing indicator that shows total line row can be expanded 2018-01-03 17:17:47 -06:00
ab188ad54f use EmptyResult instead of custom NoResponse 2017-12-25 21:52:27 +09:00
513835ed0f remove eclair dependency 2017-12-21 18:06:21 +09:00
a863812f90 Refactor how invoice payments are computed 2017-12-21 18:01:26 +09:00
a37fdde214 Big refactorying for supporting multi currencies 2017-12-21 15:52:04 +09:00
d5ef36fe50 add deploy to azure button 2017-12-19 12:41:15 +09:00
7430ceb23d update doc 2017-12-19 12:19:08 +09:00
395b550c21 add more doc for contributing 2017-12-19 11:47:43 +09:00
774565b121 fix test README 2017-12-19 11:43:52 +09:00
72d1344002 add banner to README 2017-12-19 11:42:04 +09:00
b4ee5dcb0d Update README.md 2017-12-19 11:35:46 +09:00
a0f0ff0bf1 bump 2017-12-18 17:23:27 +09:00
db2cc8f951 show message when bitcoin core is starting 2017-12-18 16:35:16 +09:00
24007f1515 Fix: Forgot to pass in the Rate field to the invoice DTO 2017-12-18 08:56:27 +09:00
3d7445f359 Fix logs name for Events, try catch websocket closure 2017-12-17 22:57:27 +09:00
34760afe77 Do not show release in footer if compiled in release 2017-12-17 22:50:05 +09:00
417209b057 fix checkout page bug 2017-12-17 22:37:40 +09:00
9026378b86 bump version 2017-12-17 22:14:37 +09:00
9b3dca1683 Electrum v3.0 use xpub for testnet see https://github.com/spesmilo/electrum/issues/3539#issuecomment-352246947 2017-12-17 22:02:49 +09:00
cde593a935 bump 2017-12-17 20:30:43 +09:00
f0755260a6 don't crash if fail to connect to websockets 2017-12-17 20:28:18 +09:00
582e1eb4f8 version bump 2017-12-17 20:01:21 +09:00
aaadda3e0f Use websockets in checkout page to get notified of paid invoices 2017-12-17 19:58:55 +09:00
9d7f5b5b6e Fix bug: If electrum zpub is entered, the wrong value is saved into database 2017-12-17 19:41:46 +09:00
99040597dc BTCPrice should be bitcoin price of item 2017-12-17 19:40:42 +09:00
d9794216dd Send InvoicePaymentEvent 2017-12-17 14:33:38 +09:00
84bb6056d3 Use EventAggregator to decouple several classes 2017-12-17 14:17:42 +09:00
dfed2daa8e Fix synching information 2017-12-17 11:07:11 +09:00
1521ec8071 Fix nullreferenceexception 2017-12-17 02:38:04 +09:00
bf7ae178ef Fix #18, fix electrum format not recognizing standard p2pkh on testnet 2017-12-17 02:28:37 +09:00
dc7f96c6da Show a modal when node is synching 2017-12-17 02:07:11 +09:00
c6959bb0bc Can start without NBXplorer being ready 2017-12-17 01:04:20 +09:00
d4dd6c84bc Auto detect NGinx X-Forwarded 2017-12-15 19:11:48 +09:00
e59678360c Update background 2017-12-13 22:38:07 +09:00
1b6fa0c7d8 Prepare Eclair integration 2017-12-13 15:49:19 +09:00
95a5936daf Update youtube links 2017-12-11 18:03:06 +09:00
477d4117ce update slack invite site 2017-12-08 22:04:52 +09:00
444f119e50 Add twitter link 2017-12-08 17:02:10 +09:00
fa13a2874e Estimate rate with BTCPay if BitcoinAverage stops works 2017-12-08 15:04:47 +09:00
24ce325e31 Support electrum segwit xpub format 2017-12-06 18:08:21 +09:00
a52a1901c4 Can delete user 2017-12-04 14:39:02 +09:00
45aee607e3 Can lock down registrations 2017-12-04 00:55:39 +09:00
c263016939 fix help 2017-12-03 23:42:10 +09:00
741915b1f8 Allow filtering of invoices over storeid and status 2017-12-03 23:35:52 +09:00
6f2534ba82 Can set currency in the create invoice form fix #15 2017-12-03 22:36:04 +09:00
43635071d9 Show ISO code in checkout page 2017-12-03 22:14:08 +09:00
22f06ecd4e Can set store policy to define how much time to wait before passing a transaction from paid to invalid. 2017-12-03 14:43:52 +09:00
7efe83eba8 notify on invalid in fullnotification is true 2017-12-03 13:42:12 +09:00
a5b732e197 Update NBitcoin, NBxplorer and bump 2017-12-03 01:56:26 +09:00
f404aaf768 bump 2017-12-02 23:22:47 +09:00
e1f8177834 Can configure externalurl in case BTCPay is behind a reverse proxy 2017-12-02 23:22:23 +09:00
cff391a7a9 Put checkout title to BTCPay 2017-12-02 14:13:11 +09:00
9cd7608a53 Fixing bug caused by BTC being too high 2017-12-02 14:07:14 +09:00
6950a06532 break line in yaml for increased readability 2017-11-27 17:01:11 +09:00
0e6c2ec556 fix search button 2017-11-13 00:27:16 +09:00
479fc50d9a Add PendingInvoice inside CreateInvoice 2017-11-12 23:51:14 +09:00
a29a8f7ed9 Do not use AddAsync 2017-11-12 23:37:21 +09:00
83cf637f9d fetch dependencies when creating request simultaneously 2017-11-12 23:23:21 +09:00
5dbb4bf6be Merge branch 'master' of https://github.com/btcpayserver/btcpayserver 2017-11-12 23:18:45 +09:00
f1f227b746 Index invoice in a parallel thread 2017-11-12 23:03:33 +09:00
b96cac16c6 Merge pull request #11 from lepipele/dev-lepi
Allowing user to invalidate paid invoice
2017-11-06 07:37:06 -08:00
f58fdafdcd Simplifying check for invoiceData null and status 2017-11-06 07:43:24 -06:00
b7b39f8284 Merge remote-tracking branch 'source/master' into dev-lepi
# Conflicts:
#	BTCPayServer/Services/Invoices/InvoiceRepository.cs
2017-11-06 07:35:17 -06:00
7a173a6692 Update NBXplorer 2017-11-06 00:54:03 -08:00
b042f98f0f Correctly handle RBF 2017-11-06 00:31:02 -08:00
0bb260bec9 Allowing user to invalidate paid invoice 2017-11-05 21:15:52 -06:00
024ab8ff69 Update NBXplorer 2017-11-04 16:02:00 -07:00
50cb61f3bd Update nbxplorer 2017-11-02 15:38:52 -07:00
f2befb7c48 Add video link to homepage 2017-11-02 14:03:49 -07:00
dd80263490 update nbxplorer 2017-11-01 02:17:39 -07:00
73c44f8726 don't crash if the user unset his extpubkey 2017-11-01 01:06:59 -07:00
ca85095273 Move code into CurrencyNameTable 2017-10-27 18:58:43 +09:00
b5196aa541 update footer, version bump 2017-10-27 18:07:44 +09:00
b649ae0a1e fix linter issues 2017-10-27 17:54:56 +09:00
4deb7c3270 Adopt dotnet core editorconfig, big reformating 2017-10-27 17:53:04 +09:00
b10da976c7 Merge branch 'lepipele-dev-lepi' 2017-10-27 17:37:55 +09:00
27a2e614a5 fixing indent, fix checkout page opacity 2017-10-27 17:37:15 +09:00
e7a931e9ba Bugfixing footer so that it sticks to bottom
The issue happened because we have 3 different css templates that influence styles. Down the road we probably need to look more into merging them into single robust theme.
2017-10-27 00:36:09 -05:00
979bc31868 Merge remote-tracking branch 'source/master' into dev-lepi 2017-10-27 00:15:50 -05:00
33cf4066d8 Returning code for redirection on payment completed 2017-10-27 00:13:14 -05:00
009444951a Switching to VueQrcode, cleaner code and easier refresh 2017-10-27 00:06:25 -05:00
9e27e14692 Adding Vue.js and databinding checkout properties on clientside 2017-10-26 23:46:21 -05:00
bc00633135 fix footer 2017-10-27 12:49:04 +09:00
9a7aa181ed fix footer 2017-10-27 12:44:35 +09:00
6cc90cbeb4 Refactoring ToJson code 2017-10-26 22:27:15 -05:00
59de852484 remove warning 2017-10-27 12:11:08 +09:00
c9d5e9ac05 bump 2017-10-27 12:09:02 +09:00
59ce3b5fc0 Add footer to know what is the current running build 2017-10-27 12:08:18 +09:00
b71f9d0a08 Use CoinAverage as rate provider + add caching to avoid hitting limits 2017-10-27 11:39:11 +09:00
55a4c3c08d Change address if invoice get partially paid 2017-10-25 01:41:01 +09:00
c79db1dc99 bump version 2017-10-24 14:53:05 +09:00
a5bd27661b fix xxs vulns 2017-10-24 14:52:19 +09:00
9a0d0a7124 fix xss vulns 2017-10-24 14:20:05 +09:00
454939adad neutral default launchprofile 2017-10-24 00:52:58 +09:00
37c02d2539 Can delete a store 2017-10-23 22:55:46 +09:00
5f8407b4b1 Allow the merchant to disable network fees at store level 2017-10-23 19:27:22 +09:00
abeb10cc8c version bump 2017-10-23 18:17:22 +09:00
4dca81403b Better handle transition from paid to invalid 2017-10-23 18:07:50 +09:00
6ba6a34df2 Fix the states of invoice to match bitpay 2017-10-23 17:44:04 +09:00
6d14fe9c30 Do not crash if transactionSpeed not set 2017-10-23 17:05:08 +09:00
c0c4637c77 do not ignore transactionSpeed set at the invoice level 2017-10-23 14:51:21 +09:00
752d34f603 do not throw 500 if using a pairing code which does not exists 2017-10-23 14:12:54 +09:00
0009ed0921 Bug fix: orderId was ignored 2017-10-23 01:23:56 +09:00
953de22233 Add video to readme 2017-10-21 20:53:48 +09:00
1477aef39c remove outdated file 2017-10-21 20:34:30 +09:00
2cca8f82e1 Update README.md 2017-10-21 20:31:36 +09:00
9094af025d Update README.md 2017-10-21 20:30:59 +09:00
0ba608bd1e Update README.md 2017-10-21 20:30:49 +09:00
67f5d497d5 helper script for using cli on regtest 2017-10-21 20:20:06 +09:00
3cc614c024 Add command line so no need of bitcoin-cli in dev 2017-10-21 19:56:55 +09:00
7c20bab1c5 Merge branch 'lepipele-master' 2017-10-21 18:08:49 +09:00
a2a3f43fd0 Small cleanup bugfixes
-Default redirection to / if Url is null
-Removing old Javascript notice
-Passing whole model as function now requires
2017-10-20 23:25:27 -05:00
9f17e3e1f8 Temporary importing legacy graphics 2017-10-20 23:15:49 -05:00
c1a2fc22f4 Reverting back to 15 minutes waiting for invoice 2017-10-20 22:42:15 -05:00
f1f19369a3 Automatic conversion to lower Camel Case for JSON 2017-10-20 22:37:01 -05:00
e49f25af09 Returning whole invoice serialized as JSON on $ajax call 2017-10-20 22:24:28 -05:00
bc28571174 Merge remote-tracking branch 'source/master' 2017-10-20 22:06:54 -05:00
2beae1dcd3 Refactoring logic for referencing server model in js script
Will keep removing unnecessary boilerplate "assign variable" code and try to streamline it as much as possible
2017-10-20 22:06:42 -05:00
39a0b1e29e Merge pull request #4 from lepipele/master
Bunch of small fixes
2017-10-21 11:19:37 +09:00
0f603ffb0a Allowing customization of expiry time for easier debugging 2017-10-20 17:32:52 -05:00
8fe5835e09 Adding text overflow protection on td when invoice is displayed
Responsive layout better maintained this way
2017-10-20 17:15:25 -05:00
5fed7a3a0c Linking back to Invoices after expiry 2017-10-20 17:14:43 -05:00
28ea694791 Extracting my tests to separate class, adding test for generating ExtPubKey 2017-10-20 16:49:13 -05:00
45b0991841 Fixing typo in namespace 2017-10-20 14:06:37 -05:00
505d9904af Merge remote-tracking branch 'source/master'
# Conflicts:
#	BTCPayServer.Tests/UnitTest1.cs
2017-10-20 14:04:40 -05:00
da385f9295 Adding Unit Test for generating test Bitpay checkout page 2017-10-20 14:00:38 -05:00
214de77a41 Remove launch profile from docker 2017-10-19 17:45:07 +09:00
7b2db3755e fix bip urls formatting 2017-10-19 17:06:51 +09:00
44791cc9f3 bump 2017-10-19 16:47:24 +09:00
a14b94c96f Fix copy/paste BTC amount, add redirect store link to invoice 2017-10-19 16:37:07 +09:00
db1cf5c2ce format currency correctly 2017-10-19 16:08:41 +09:00
1a060a6c7b Fix checkout page 2017-10-19 01:33:55 +09:00
ff719fbe2d bump 2017-10-18 18:45:00 +09:00
94e9ab7f67 In server-initiated situation, the server can set the label 2017-10-18 18:44:24 +09:00
06a96e8b77 README, prevent a nullreferenceexception 2017-10-18 10:40:59 +09:00
d43c3dc968 generate 7 digit pairing code, notify parent windows of checkout 2017-10-17 17:04:33 +09:00
e3c5efa929 dump version 2017-10-17 13:56:17 +09:00
8db9d93d23 Add api-tokens page, do not hide qrcode if small 2017-10-17 13:52:30 +09:00
517bb94b8b Do not crash if user is not authorized to go on the store 2017-10-17 11:45:52 +09:00
c804f55d82 bump 2017-10-17 11:19:57 +09:00
c96a25c9b9 Checkout page was not working correctly on /invoice?id= 2017-10-17 11:16:34 +09:00
4c57726be7 bump 2017-10-15 15:33:15 +09:00
8f723d7131 Fix typos 2017-10-15 15:32:53 +09:00
a7e10c0fb9 Can't pair same SIN to different store 2017-10-13 18:06:46 +09:00
15e73e1cad Properly limit CORS to bitpay api 2017-10-13 17:46:19 +09:00
a17192ee99 Add Cors 2017-10-13 17:18:32 +09:00
27200d1fb0 X-Frame-Options 2017-10-13 17:13:21 +09:00
9ddceae824 Validate email in the api 2017-10-13 16:59:02 +09:00
d1961e0938 Support other way of passing buyer info 2017-10-13 16:44:55 +09:00
033432d6fb Keep compatible checkout page address 2017-10-13 16:07:57 +09:00
c98f0ba55b Fix watcher loop 2017-10-13 15:05:06 +09:00
76993d2532 prevent watcher loop to crash 2017-10-13 14:59:05 +09:00
62e3f2d8e1 Prevent null invoice to be added to InvoiceWatcher 2017-10-13 14:53:42 +09:00
9806cab090 Fix Bitpay api route detection 2017-10-13 14:41:28 +09:00
0d9fbe2d41 Fix expiration field in Invoice details page 2017-10-13 11:27:05 +09:00
016db76306 Add page for viewing the Invoice details 2017-10-13 00:25:45 +09:00
d469084596 Remove useless line from response 2017-10-12 19:16:01 +09:00
bb4decd522 Add AppInsight logs 2017-10-12 18:16:15 +09:00
caca8e81c2 Do not drop column for sqlite 2017-10-12 16:41:11 +09:00
bae08b6966 Use callback to update invoice state instead of long polling 2017-10-12 16:33:53 +09:00
472 changed files with 97485 additions and 42355 deletions

View File

@ -121,3 +121,4 @@ bower_components
output
.vs
**/launchSettings.json

149
.editorconfig Normal file
View File

@ -0,0 +1,149 @@
# editorconfig.org
# top-most EditorConfig file
root = true
# Default settings:
# A newline ending every file
# Use 4 spaces as indentation
[*]
insert_final_newline = true
indent_style = space
indent_size = 4
[project.json]
indent_size = 2
# C# files
[*.cs]
# New line preferences
csharp_new_line_before_open_brace = all
csharp_new_line_before_else = true
csharp_new_line_before_catch = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_within_query_expression_clauses = true
# Indentation preferences
csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents = true
csharp_indent_switch_labels = true
csharp_indent_labels = flush_left
# avoid this. unless absolutely necessary
dotnet_style_qualification_for_field = false:suggestion
dotnet_style_qualification_for_property = false:suggestion
dotnet_style_qualification_for_method = false:suggestion
dotnet_style_qualification_for_event = false:suggestion
# only use var when it's obvious what the variable type is
csharp_style_var_for_built_in_types = false:none
csharp_style_var_when_type_is_apparent = false:none
csharp_style_var_elsewhere = false:suggestion
# use language keywords instead of BCL types
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
dotnet_style_predefined_type_for_member_access = true:suggestion
# name all constant fields using PascalCase
dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields
dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style
dotnet_naming_symbols.constant_fields.applicable_kinds = field
dotnet_naming_symbols.constant_fields.required_modifiers = const
dotnet_naming_style.pascal_case_style.capitalization = pascal_case
# internal and private fields should be _camelCase
dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion
dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields
dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style
dotnet_naming_symbols.private_internal_fields.applicable_kinds = field
dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal
dotnet_naming_style.camel_case_underscore_style.required_prefix = _
dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case
# Code style defaults
dotnet_sort_system_directives_first = true
csharp_preserve_single_line_blocks = true
csharp_preserve_single_line_statements = false
# Expression-level preferences
dotnet_style_object_initializer = true:suggestion
dotnet_style_collection_initializer = true:suggestion
dotnet_style_explicit_tuple_names = true:suggestion
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_null_propagation = true:suggestion
# Expression-bodied members
csharp_style_expression_bodied_methods = false:none
csharp_style_expression_bodied_constructors = false:none
csharp_style_expression_bodied_operators = false:none
csharp_style_expression_bodied_properties = true:none
csharp_style_expression_bodied_indexers = true:none
csharp_style_expression_bodied_accessors = true:none
# Pattern matching
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
csharp_style_inlined_variable_declaration = true:suggestion
# Null checking preferences
csharp_style_throw_expression = true:suggestion
csharp_style_conditional_delegate_call = true:suggestion
# Space preferences
csharp_space_after_cast = false
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_after_comma = true
csharp_space_after_dot = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_after_semicolon_in_for_statement = true
csharp_space_around_binary_operators = before_and_after
csharp_space_around_declaration_statements = do_not_ignore
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_before_comma = false
csharp_space_before_dot = false
csharp_space_before_open_square_brackets = false
csharp_space_before_semicolon_in_for_statement = false
csharp_space_between_empty_square_brackets = false
csharp_space_between_method_call_empty_parameter_list_parentheses = false
csharp_space_between_method_call_name_and_opening_parenthesis = false
csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_method_declaration_name_and_open_parenthesis = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_between_square_brackets = false
# C++ Files
[*.{cpp,h,in}]
curly_bracket_next_line = true
indent_brace_style = Allman
# Xml project files
[*.{csproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}]
indent_size = 2
# Xml build files
[*.builds]
indent_size = 2
# Xml files
[*.{xml,stylecop,resx,ruleset}]
indent_size = 2
# Xml config files
[*.{props,targets,config,nuspec}]
indent_size = 2
# Shell scripts
[*.sh]
end_of_line = lf
[*.{cmd, bat}]
end_of_line = crlf

17
.gitattributes vendored Normal file
View File

@ -0,0 +1,17 @@
# Set the default behavior, in case people don't have core.autocrlf set.
* text=auto
# Explicitly declare text files you want to always be normalized and converted
# to native line endings on checkout.
*.c text
*.h text
# Declare files that will always have CRLF line endings on checkout.
*.sln text eol=crlf
# Declare files that will always have CRLF line endings on checkout.
*.sh text eol=lf
# Denote all files that are truly binary and should not be modified.
*.png binary
*.jpg binary

6
.gitignore vendored
View File

@ -287,3 +287,9 @@ __pycache__/
*.odx.cs
*.xsd.cs
/BTCPayServer/Build/dockerfiles
# Bundling JS/CSS
BTCPayServer/wwwroot/bundles/*
!BTCPayServer/wwwroot/bundles/.gitignore
.vscode

View File

@ -1,15 +1,16 @@
<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>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.3.0-preview-20170720-02" />
<PackageReference Include="xunit" Version="2.2.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.2" />
<PackageReference Include="xunit" Version="2.3.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
</ItemGroup>
<ItemGroup>

View File

@ -1,7 +1,12 @@
using BTCPayServer.Configuration;
using BTCPayServer.Hosting;
using BTCPayServer.Servcices.Invoices;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Rating;
using BTCPayServer.Security;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Tests.Logging;
using BTCPayServer.Tests.Mocks;
using Microsoft.AspNetCore.Hosting;
@ -30,117 +35,166 @@ using Xunit;
namespace BTCPayServer.Tests
{
public class BTCPayServerTester : IDisposable
{
private string _Directory;
public class BTCPayServerTester : IDisposable
{
private string _Directory;
public BTCPayServerTester(string scope)
{
this._Directory = scope ?? throw new ArgumentNullException(nameof(scope));
}
public BTCPayServerTester(string scope)
{
this._Directory = scope ?? throw new ArgumentNullException(nameof(scope));
}
public Uri NBXplorerUri
{
get; set;
}
public string CookieFile
{
get; set;
}
public Uri ServerUri
{
get;
set;
}
public Uri NBXplorerUri
{
get; set;
}
public ExtKey HDPrivateKey
{
get; set;
}
public Uri LTCNBXplorerUri { get; set; }
public string Postgres
{
get; set;
}
public Uri ServerUri
{
get;
set;
}
IWebHost _Host;
public void Start()
{
if(!Directory.Exists(_Directory))
Directory.CreateDirectory(_Directory);
public string Postgres
{
get; set;
}
HDPrivateKey = new ExtKey();
var port = Utils.FreeTcpPort();
StringBuilder config = new StringBuilder();
config.AppendLine($"regtest=1");
config.AppendLine($"port={port}");
config.AppendLine($"explorer.url={NBXplorerUri.AbsoluteUri}");
config.AppendLine($"explorer.cookiefile={CookieFile}");
config.AppendLine($"hdpubkey={HDPrivateKey.Neuter().ToString(Network.RegTest)}");
if(Postgres != null)
config.AppendLine($"postgres=" + Postgres);
File.WriteAllText(Path.Combine(_Directory, "settings.config"), config.ToString());
IWebHost _Host;
public int Port
{
get; set;
}
ServerUri = new Uri("http://127.0.0.1:" + port + "/");
var conf = new DefaultConfiguration() { Logger = Logs.LogProvider.CreateLogger("Console") }.CreateConfiguration(new[] { "--datadir", _Directory });
public bool MockRates { get; set; } = true;
_Host = new WebHostBuilder()
.UseConfiguration(conf)
.ConfigureServices(s =>
{
s.AddSingleton<IRateProvider>(new MockRateProvider(new Rate("USD", 5000m)));
s.AddLogging(l =>
{
l.SetMinimumLevel(LogLevel.Information)
.AddFilter("Microsoft", LogLevel.Error)
.AddProvider(Logs.LogProvider);
});
})
.UseKestrel()
.UseStartup<Startup>()
.Build();
_Host.Start();
Runtime = (BTCPayServerRuntime)_Host.Services.GetService(typeof(BTCPayServerRuntime));
var watcher = (InvoiceWatcher)_Host.Services.GetService(typeof(InvoiceWatcher));
watcher.PollInterval = TimeSpan.FromMilliseconds(50);
}
public void Start()
{
if (!Directory.Exists(_Directory))
Directory.CreateDirectory(_Directory);
string chain = NBXplorerDefaultSettings.GetFolderName(NetworkType.Regtest);
string chainDirectory = Path.Combine(_Directory, chain);
if (!Directory.Exists(chainDirectory))
Directory.CreateDirectory(chainDirectory);
public BTCPayServerRuntime Runtime
{
get; set;
}
public T GetController<T>(string userId = null) where T : Controller
{
var context = new DefaultHttpContext();
context.Request.Host = new HostString("127.0.0.1");
context.Request.Scheme = "http";
context.Request.Protocol = "http";
if(userId != null)
{
context.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, userId) }));
}
var scope = (IServiceScopeFactory)_Host.Services.GetService(typeof(IServiceScopeFactory));
var provider = scope.CreateScope().ServiceProvider;
context.RequestServices = provider;
StringBuilder config = new StringBuilder();
config.AppendLine($"{chain.ToLowerInvariant()}=1");
config.AppendLine($"port={Port}");
config.AppendLine($"chains=btc,ltc");
var httpAccessor = provider.GetRequiredService<IHttpContextAccessor>();
httpAccessor.HttpContext = context;
config.AppendLine($"btc.explorer.url={NBXplorerUri.AbsoluteUri}");
config.AppendLine($"btc.explorer.cookiefile=0");
var controller = (T)ActivatorUtilities.CreateInstance(provider, typeof(T));
controller.Url = new UrlHelperMock();
controller.ControllerContext = new ControllerContext()
{
HttpContext = context
};
return controller;
}
config.AppendLine($"ltc.explorer.url={LTCNBXplorerUri.AbsoluteUri}");
config.AppendLine($"ltc.explorer.cookiefile=0");
public void Dispose()
{
if(_Host != null)
_Host.Dispose();
}
}
config.AppendLine($"btc.lightning={IntegratedLightning.AbsoluteUri}");
if (Postgres != null)
config.AppendLine($"postgres=" + Postgres);
var confPath = Path.Combine(chainDirectory, "settings.config");
File.WriteAllText(confPath, config.ToString());
ServerUri = new Uri("http://" + HostName + ":" + Port + "/");
Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development");
var conf = new DefaultConfiguration() { Logger = Logs.LogProvider.CreateLogger("Console") }.CreateConfiguration(new[] { "--datadir", _Directory, "--conf", confPath });
_Host = new WebHostBuilder()
.UseConfiguration(conf)
.ConfigureServices(s =>
{
s.AddLogging(l =>
{
l.SetMinimumLevel(LogLevel.Information)
.AddFilter("Microsoft", LogLevel.Error)
.AddFilter("Hangfire", LogLevel.Error)
.AddProvider(Logs.LogProvider);
});
})
.UseKestrel()
.UseStartup<Startup>()
.Build();
_Host.Start();
InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository));
var rateProvider = (BTCPayRateProviderFactory)_Host.Services.GetService(typeof(BTCPayRateProviderFactory));
rateProvider.DirectProviders.Clear();
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_USD"),
BidAsk = new BidAsk(500m)
});
rateProvider.DirectProviders.Add("coinaverage", coinAverageMock);
}
public string HostName
{
get;
internal set;
}
public InvoiceRepository InvoiceRepository { get; private set; }
public Uri IntegratedLightning { get; internal set; }
public bool InContainer { get; internal set; }
public T GetService<T>()
{
return _Host.Services.GetRequiredService<T>();
}
public T GetController<T>(string userId = null, string storeId = null) where T : Controller
{
var context = new DefaultHttpContext();
context.Request.Host = new HostString("127.0.0.1");
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));
}
if(storeId != null)
{
context.SetStoreData(GetService<StoreRepository>().FindStore(storeId, userId).GetAwaiter().GetResult());
}
var scope = (IServiceScopeFactory)_Host.Services.GetService(typeof(IServiceScopeFactory));
var provider = scope.CreateScope().ServiceProvider;
context.RequestServices = provider;
var httpAccessor = provider.GetRequiredService<IHttpContextAccessor>();
httpAccessor.HttpContext = context;
var controller = (T)ActivatorUtilities.CreateInstance(provider, typeof(T));
controller.Url = new UrlHelperMock(new Uri($"http://{HostName}:{Port}/"));
controller.ControllerContext = new ControllerContext()
{
HttpContext = context
};
return controller;
}
public void Dispose()
{
if (_Host != null)
_Host.Dispose();
}
}
}

View File

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Text;
using BTCPayServer.Payments.Lightning.Charge;
using BTCPayServer.Payments.Lightning.CLightning;
using NBitcoin;
namespace BTCPayServer.Tests
{
public class ChargeTester
{
private ServerTester _Parent;
public ChargeTester(ServerTester serverTester, string environmentName, string defaultValue, string defaultHost, Network network)
{
this._Parent = serverTester;
var url = serverTester.GetEnvironment(environmentName, defaultValue);
Client = new ChargeClient(new Uri(url), network);
P2PHost = _Parent.GetEnvironment(environmentName + "_HOST", defaultHost);
}
public ChargeClient Client { get; set; }
public string P2PHost { get; }
}
}

View File

@ -11,65 +11,65 @@ using Microsoft.AspNetCore.Hosting.Server.Features;
namespace BTCPayServer.Tests
{
public class CustomServer : IDisposable
{
TaskCompletionSource<bool> _Evt = null;
IWebHost _Host = null;
CancellationTokenSource _Closed = new CancellationTokenSource();
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);
req.Response.StatusCode = 200;
return Task.CompletedTask;
});
})
.UseKestrel()
.UseUrls("http://127.0.0.1:" + port)
.Build();
_Host.Start();
}
public class CustomServer : IDisposable
{
TaskCompletionSource<bool> _Evt = null;
IWebHost _Host = null;
CancellationTokenSource _Closed = new CancellationTokenSource();
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);
req.Response.StatusCode = 200;
return Task.CompletedTask;
});
})
.UseKestrel()
.UseUrls("http://127.0.0.1:" + port)
.Build();
_Host.Start();
}
public Uri GetUri()
{
return new Uri(_Host.ServerFeatures.Get<IServerAddressesFeature>().Addresses.First());
}
public Uri GetUri()
{
return new Uri(_Host.ServerFeatures.Get<IServerAddressesFeature>().Addresses.First());
}
Action<HttpContext> _Act;
public void ProcessNextRequest(Action<HttpContext> act)
{
var source = new TaskCompletionSource<bool>();
CancellationTokenSource cancellation = new CancellationTokenSource(20000);
cancellation.Token.Register(() => source.TrySetCanceled());
source = new TaskCompletionSource<bool>();
_Evt = source;
_Act = act;
try
{
_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");
}
}
Action<HttpContext> _Act;
public void ProcessNextRequest(Action<HttpContext> act)
{
var source = new TaskCompletionSource<bool>();
CancellationTokenSource cancellation = new CancellationTokenSource(20000);
cancellation.Token.Register(() => source.TrySetCanceled());
source = new TaskCompletionSource<bool>();
_Evt = source;
_Act = act;
try
{
_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");
}
}
public void Dispose()
{
_Closed.Cancel();
_Host.Dispose();
}
}
public void Dispose()
{
_Closed.Cancel();
_Host.Dispose();
}
}
}

View File

@ -1,4 +1,4 @@
FROM microsoft/dotnet:2.0.0-sdk
FROM microsoft/dotnet:2.1.300-sdk-alpine3.7
WORKDIR /app
# caches restore result by copying csproj file separately
COPY BTCPayServer.Tests/BTCPayServer.Tests.csproj BTCPayServer.Tests/BTCPayServer.Tests.csproj
@ -9,4 +9,4 @@ RUN dotnet restore
# copies the rest of your code
COPY . ../.
ENTRYPOINT ["dotnet", "test"]
ENTRYPOINT ["dotnet", "test"]

View File

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Payments.Lightning.CLightning;
using NBitcoin;
namespace BTCPayServer.Tests
{
public class LightningDTester
{
ServerTester parent;
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);
}
public CLightningRPCClient RPC { get; }
public string P2PHost { get; }
}
}

View File

@ -6,84 +6,88 @@ using Xunit.Abstractions;
namespace BTCPayServer.Tests.Logging
{
public interface ILog
{
void LogInformation(string msg);
}
public interface ILog
{
void LogInformation(string msg);
}
public class XUnitLogProvider : ILoggerProvider
{
ITestOutputHelper _Helper;
public XUnitLogProvider(ITestOutputHelper helper)
{
_Helper = helper;
}
public ILogger CreateLogger(string categoryName)
{
return new XUnitLog(_Helper) { Name = categoryName };
}
public class XUnitLogProvider : ILoggerProvider
{
ITestOutputHelper _Helper;
public XUnitLogProvider(ITestOutputHelper helper)
{
_Helper = helper;
}
public ILogger CreateLogger(string categoryName)
{
return new XUnitLog(_Helper) { Name = categoryName };
}
public void Dispose()
{
}
}
public class XUnitLog : ILog, ILogger, IDisposable
{
ITestOutputHelper _Helper;
public XUnitLog(ITestOutputHelper helper)
{
_Helper = helper;
}
public void Dispose()
{
public string Name
{
get; set;
}
}
}
public class XUnitLog : ILog, ILogger, IDisposable
{
ITestOutputHelper _Helper;
public XUnitLog(ITestOutputHelper helper)
{
_Helper = helper;
}
public IDisposable BeginScope<TState>(TState state)
{
return this;
}
public string Name
{
get; set;
}
public void Dispose()
{
public IDisposable BeginScope<TState>(TState state)
{
return this;
}
}
public void Dispose()
{
public bool IsEnabled(LogLevel logLevel)
{
return true;
}
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
StringBuilder builder = new StringBuilder();
builder.Append(formatter(state, exception));
if(exception != null)
{
builder.AppendLine();
builder.Append(exception.ToString());
}
LogInformation(builder.ToString());
}
public bool IsEnabled(LogLevel logLevel)
{
return true;
}
public void LogInformation(string msg)
{
if(msg != null)
_Helper.WriteLine(Name + ": " + msg);
}
}
public class Logs
{
public static ILog Tester
{
get; set;
}
public static XUnitLogProvider LogProvider
{
get;
set;
}
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
StringBuilder builder = new StringBuilder();
builder.Append(formatter(state, exception));
if (exception != null)
{
builder.AppendLine();
builder.Append(exception.ToString());
}
LogInformation(builder.ToString());
}
public void LogInformation(string msg)
{
if (msg != null)
try
{
_Helper.WriteLine(DateTimeOffset.UtcNow + " :" + Name + ": " + msg);
}
catch { }
}
}
public class Logs
{
public static ILog Tester
{
get; set;
}
public static XUnitLogProvider LogProvider
{
get;
set;
}
}
}

View File

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

View File

@ -6,33 +6,38 @@ using Microsoft.AspNetCore.Mvc.Routing;
namespace BTCPayServer.Tests.Mocks
{
public class UrlHelperMock : IUrlHelper
{
public ActionContext ActionContext => throw new NotImplementedException();
public class UrlHelperMock : IUrlHelper
{
Uri _BaseUrl;
public UrlHelperMock(Uri baseUrl)
{
_BaseUrl = baseUrl;
}
public ActionContext ActionContext => throw new NotImplementedException();
public string Action(UrlActionContext actionContext)
{
return "http://127.0.0.1/mock";
}
public string Action(UrlActionContext actionContext)
{
return $"{_BaseUrl}mock";
}
public string Content(string contentPath)
{
return "http://127.0.0.1/mock";
}
public string Content(string contentPath)
{
return $"{_BaseUrl}{contentPath}";
}
public bool IsLocalUrl(string url)
{
return false;
}
public bool IsLocalUrl(string url)
{
return false;
}
public string Link(string routeName, object values)
{
return "http://127.0.0.1/mock";
}
public string Link(string routeName, object values)
{
return _BaseUrl.AbsoluteUri;
}
public string RouteUrl(UrlRouteContext routeContext)
{
return "http://127.0.0.1/mock";
}
}
public string RouteUrl(UrlRouteContext routeContext)
{
return _BaseUrl.AbsoluteUri;
}
}
}

View File

@ -8,105 +8,105 @@ using Xunit;
namespace BTCPayServer.Tests
{
public class ProcessLauncher
{
string _CurrentDirectory;
public string CurrentDirectory
{
get
{
return _CurrentDirectory;
}
}
public ProcessLauncher()
{
_CurrentDirectory = Directory.GetCurrentDirectory();
}
public bool GoTo(string[] directories, bool createIfNotExists = false)
{
var original = _CurrentDirectory;
foreach(var dir in directories)
{
var newDirectory = Path.Combine(_CurrentDirectory, dir);
if(!Directory.Exists(newDirectory))
{
if(createIfNotExists)
Directory.CreateDirectory(newDirectory);
else
{
_CurrentDirectory = original;
return false;
}
}
_CurrentDirectory = newDirectory;
}
return true;
}
public class ProcessLauncher
{
string _CurrentDirectory;
public string CurrentDirectory
{
get
{
return _CurrentDirectory;
}
}
public ProcessLauncher()
{
_CurrentDirectory = Directory.GetCurrentDirectory();
}
public bool GoTo(string[] directories, bool createIfNotExists = false)
{
var original = _CurrentDirectory;
foreach (var dir in directories)
{
var newDirectory = Path.Combine(_CurrentDirectory, dir);
if (!Directory.Exists(newDirectory))
{
if (createIfNotExists)
Directory.CreateDirectory(newDirectory);
else
{
_CurrentDirectory = original;
return false;
}
}
_CurrentDirectory = newDirectory;
}
return true;
}
Stack<string> _Directories = new Stack<string>();
public void PushDirectory()
{
_Directories.Push(_CurrentDirectory);
}
Stack<string> _Directories = new Stack<string>();
public void PushDirectory()
{
_Directories.Push(_CurrentDirectory);
}
public void PopDirectory()
{
_CurrentDirectory = _Directories.Pop();
}
public void PopDirectory()
{
_CurrentDirectory = _Directories.Pop();
}
public bool GoTo(string directory, bool createIfNotExists = false)
{
return GoTo(new[] { directory }, createIfNotExists);
}
public bool GoTo(string directory, bool createIfNotExists = false)
{
return GoTo(new[] { directory }, createIfNotExists);
}
public void Run(string processName, string args)
{
Start(processName, args).WaitForExit();
}
public void Run(string processName, string args)
{
Start(processName, args).WaitForExit();
}
public Process Start(string processName, string args)
{
Logs.Tester.LogInformation($"Running [{processName} {args}] from {_CurrentDirectory}");
StringBuilder builder = new StringBuilder();
var process = new Process()
{
StartInfo = new ProcessStartInfo
{
WorkingDirectory = _CurrentDirectory,
FileName = processName,
Arguments = args,
RedirectStandardOutput = true,
RedirectStandardError = true
}
};
try
{
process.OutputDataReceived += (s, e) =>
{
Logs.Tester.LogInformation(e.Data);
};
process.ErrorDataReceived += (s, e) =>
{
Logs.Tester.LogInformation(e.Data);
};
process.Start();
process.BeginErrorReadLine();
process.BeginOutputReadLine();
}
catch(Exception ex) { throw new Exception($"You need to install {processName} for this test (info : {ex.Message})"); }
return process;
}
public Process Start(string processName, string args)
{
Logs.Tester.LogInformation($"Running [{processName} {args}] from {_CurrentDirectory}");
StringBuilder builder = new StringBuilder();
var process = new Process()
{
StartInfo = new ProcessStartInfo
{
WorkingDirectory = _CurrentDirectory,
FileName = processName,
Arguments = args,
RedirectStandardOutput = true,
RedirectStandardError = true
}
};
try
{
process.OutputDataReceived += (s, e) =>
{
Logs.Tester.LogInformation(e.Data);
};
process.ErrorDataReceived += (s, e) =>
{
Logs.Tester.LogInformation(e.Data);
};
process.Start();
process.BeginErrorReadLine();
process.BeginOutputReadLine();
}
catch (Exception ex) { throw new Exception($"You need to install {processName} for this test (info : {ex.Message})"); }
return process;
}
public void AssertExists(string file)
{
var path = Path.Combine(_CurrentDirectory, file);
Assert.True(File.Exists(path), $"The file {path} should exist");
}
public void AssertExists(string file)
{
var path = Path.Combine(_CurrentDirectory, file);
Assert.True(File.Exists(path), $"The file {path} should exist");
}
public bool Exists(string file)
{
var path = Path.Combine(_CurrentDirectory, file);
return File.Exists(path);
}
}
public bool Exists(string file)
{
var path = Path.Combine(_CurrentDirectory, file);
return File.Exists(path);
}
}
}

View File

@ -0,0 +1,7 @@
{
"profiles": {
"BTCPayServer.Tests": {
"commandName": "Project"
}
}
}

View File

@ -1,10 +1,14 @@
# How to run the tests
# How to be started for development
The tests depends on having a proper environment running with Postgres, Bitcoind, NBxplorer configured.
BTCPay Server tests depend on having a proper environment running with Postgres, Bitcoind, NBxplorer configured.
You can however use the `docker-compose.yml` of this folder to get it running.
In addition, when you run a debug session of BTCPay (Hitting F5 on Visual Studio Code or Visual Studio 2017), it will run the launch profile called `Docker-Regtest`. This launch profile depends on this `docker-compose` running.
This is running a bitcoind instance on regtest, a private bitcoin blockchain for testing on which you can generate blocks yourself.
```
docker-compose up nbxplorer
docker-compose up dev
```
You can run the tests while it is running through your favorite IDE, or with
@ -19,14 +23,62 @@ Once you want to stop
docker-compose down
```
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
```
The Bitcoin RPC server is exposed to the host, for example, you can send 0.23111090 BTC to mohu16LH66ptoWGEL1GtP6KHTBJYXMWhEf.
## How to manually test payments
### Using the test bitcoin-cli
You can call bitcoin-cli inside the container with `docker exec`, for example, if you want to send `0.23111090` to `mohu16LH66ptoWGEL1GtP6KHTBJYXMWhEf`:
```
./docker-bitcoin-cli.sh sendtoaddress "mohu16LH66ptoWGEL1GtP6KHTBJYXMWhEf" 0.23111090
```
If you are using Powershell:
```
.\docker-bitcoin-cli.ps1 sendtoaddress "mohu16LH66ptoWGEL1GtP6KHTBJYXMWhEf" 0.23111090
```
### Using the test litecoin-cli
Same as bitcoin-cli, but with `.\docker-litecoin-cli.ps1` and `.\docker-litecoin-cli.sh` instead.
### Using the test lightning-cli
If you are using Linux:
```
./docker-customer-lightning-cli.sh pay lnbcrt100u1pd2e6uspp5ajnadvhazjrz55twd5k6yeg9u87wpw0q2fdr7g960yl5asv5fmnqdq9d3hkccqpxmedyrk0ehw5ueqx5e0r4qrrv74cewddfcvsxaawqz7634cmjj39sqwy5tvhz0hasktkk6t9pqfdh3edmf3z09zst5y7khv3rvxh8ctqqw6mwhh
```
If you are using Powershell:
```
.\docker-customer-lightning-cli.ps1 pay lnbcrt100u1pd2e6uspp5ajnadvhazjrz55twd5k6yeg9u87wpw0q2fdr7g960yl5asv5fmnqdq9d3hkccqpxmedyrk0ehw5ueqx5e0r4qrrv74cewddfcvsxaawqz7634cmjj39sqwy5tvhz0hasktkk6t9pqfdh3edmf3z09zst5y7khv3rvxh8ctqqw6mwhh
```
If you get this message:
```
bitcoin-cli -regtest -rpcport=43782 -rpcuser=ceiwHEbqWI83 -rpcpassword=DwubwWsoo3 sendtoaddress "mohu16LH66ptoWGEL1GtP6KHTBJYXMWhEf" 0.23111090
```
{ "code" : 205, "message" : "Could not find a route", "data" : { "getroute_tries" : 1, "sendpay_tries" : 0 } }
```
Please, run the test `CanSetLightningServer`, this will establish a channel between the customer and the merchant, then, retry.
## FAQ
`docker-compose up dev` failed or tests are not passing, what should I do?
1. Run `docker-compose down --v` (this will reset your test environment)
2. Run `docker-compose pull` (this will ensure you have the lastest images)
3. Run again with `docker-compose up dev`
If you still have issues, try to restart docker.

View File

@ -0,0 +1,179 @@
using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;
using BTCPayServer.Rating;
using Xunit;
using System.Globalization;
namespace BTCPayServer.Tests
{
public class RateRulesTest
{
[Fact]
public void CanParseRateRules()
{
// Check happy path
StringBuilder builder = new StringBuilder();
builder.AppendLine("// Some cool comments");
builder.AppendLine("DOGE_X = DOGE_BTC * BTC_X * 1.1");
builder.AppendLine("DOGE_BTC = Bittrex(DOGE_BTC)");
builder.AppendLine("// Some other cool comments");
builder.AppendLine("BTC_usd = GDax(BTC_USD)");
builder.AppendLine("BTC_X = Coinbase(BTC_X);");
builder.AppendLine("X_X = CoinAverage(X_X) * 1.02");
Assert.False(RateRules.TryParse("DPW*&W&#hdi&#&3JJD", out var rules));
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
Assert.Equal(
"// Some cool comments\n" +
"DOGE_X = DOGE_BTC * BTC_X * 1.1;\n" +
"DOGE_BTC = bittrex(DOGE_BTC);\n" +
"// Some other cool comments\n" +
"BTC_USD = gdax(BTC_USD);\n" +
"BTC_X = coinbase(BTC_X);\n" +
"X_X = coinaverage(X_X) * 1.02;",
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_CAD", Expected: "bittrex(DOGE_BTC) * coinbase(BTC_CAD) * 1.1"),
(Pair: "LTC_CAD", Expected: "coinaverage(LTC_CAD) * 1.02"),
};
foreach (var test in 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());
////////////////
// Check errors conditions
builder = new StringBuilder();
builder.AppendLine("DOGE_X = LTC_CAD * BTC_X * 1.1");
builder.AppendLine("DOGE_BTC = Bittrex(DOGE_BTC)");
builder.AppendLine("BTC_usd = GDax(BTC_USD)");
builder.AppendLine("LTC_CHF = LTC_CHF * 1.01");
builder.AppendLine("BTC_X = Coinbase(BTC_X)");
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
tests = new[]
{
(Pair: "LTC_CAD", Expected: "ERR_NO_RULE_MATCH(LTC_CAD)"),
(Pair: "DOGE_USD", Expected: "ERR_NO_RULE_MATCH(LTC_CAD) * gdax(BTC_USD) * 1.1"),
(Pair: "LTC_CHF", Expected: "ERR_TOO_MUCH_NESTED_CALLS(LTC_CHF) * 1.01"),
};
foreach (var test in tests)
{
Assert.Equal(test.Expected, rules.GetRuleFor(CurrencyPair.Parse(test.Pair)).ToString());
}
//////////////////
// Check if we can resolve exchange rates
builder = new StringBuilder();
builder.AppendLine("DOGE_X = DOGE_BTC * BTC_X * 1.1");
builder.AppendLine("DOGE_BTC = Bittrex(DOGE_BTC)");
builder.AppendLine("BTC_usd = GDax(BTC_USD)");
builder.AppendLine("BTC_X = Coinbase(BTC_X)");
builder.AppendLine("X_X = CoinAverage(X_X) * 1.02");
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
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_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)"),
};
foreach (var test in tests2)
{
var rule = rules.GetRuleFor(CurrencyPair.Parse(test.Pair));
Assert.Equal(test.Expected, rule.ToString());
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"), 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"), 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);
////////
// Make sure parenthesis are correctly calculated
builder = new StringBuilder();
builder.AppendLine("DOGE_X = DOGE_BTC * BTC_X");
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;
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"), 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);
// 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"), 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);
////////
// 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.Value.Value);
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.Value.Value);
// 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

@ -1,88 +1,199 @@
using BTCPayServer.Controllers;
using System.Linq;
using BTCPayServer.Models.AccountViewModels;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBitcoin.RPC;
using NBitpayClient;
using NBXplorer;
using NBXplorer.Models;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using System.Globalization;
using BTCPayServer.Payments.Lightning.CLightning;
using BTCPayServer.Payments.Lightning.Charge;
namespace BTCPayServer.Tests
{
public class ServerTester : IDisposable
{
public static ServerTester Create([CallerMemberNameAttribute]string scope = null)
{
return new ServerTester(scope);
}
public class ServerTester : IDisposable
{
public static ServerTester Create([CallerMemberNameAttribute]string scope = null)
{
return new ServerTester(scope);
}
string _Directory;
public ServerTester(string scope)
{
_Directory = scope;
}
string _Directory;
public ServerTester(string scope)
{
_Directory = scope;
if (Directory.Exists(_Directory))
Utils.DeleteDirectory(_Directory);
if (!Directory.Exists(_Directory))
Directory.CreateDirectory(_Directory);
public void Start()
{
if(Directory.Exists(_Directory))
Utils.DeleteDirectory(_Directory);
if(!Directory.Exists(_Directory))
Directory.CreateDirectory(_Directory);
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);
LTCExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_LTCRPCCONNECTION", "server=http://127.0.0.1:43783;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork("LTC").NBitcoinNetwork);
ExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_RPCCONNECTION", "server=http://127.0.0.1:43782;ceiwHEbqWI83:DwubwWsoo3")), Network);
ExplorerClient = new ExplorerClient(Network, new Uri(GetEnvironment("TESTS_NBXPLORERURL", "http://127.0.0.1:32838/")));
PayTester = new BTCPayServerTester(Path.Combine(_Directory, "pay"))
{
NBXplorerUri = ExplorerClient.Address,
Postgres = GetEnvironment("TESTS_POSTGRES", "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver")
};
PayTester.Start();
}
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/")));
private string GetEnvironment(string variable, string defaultValue)
{
var var = Environment.GetEnvironmentVariable(variable);
return String.IsNullOrEmpty(var) ? defaultValue : var;
}
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);
public TestAccount NewAccount()
{
return new TestAccount(this);
}
MerchantCharge = new ChargeTester(this, "TEST_MERCHANTCHARGE", "http://api-token:foiewnccewuify@127.0.0.1:54938/", "merchant_lightningd", btc);
public RPCClient ExplorerNode
{
get; set;
}
PayTester = new BTCPayServerTester(Path.Combine(_Directory, "pay"))
{
NBXplorerUri = ExplorerClient.Address,
LTCNBXplorerUri = LTCExplorerClient.Address,
Postgres = GetEnvironment("TESTS_POSTGRES", "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver"),
IntegratedLightning = MerchantCharge.Client.Uri
};
PayTester.Port = int.Parse(GetEnvironment("TESTS_PORT", Utils.FreeTcpPort().ToString(CultureInfo.InvariantCulture)), CultureInfo.InvariantCulture);
PayTester.HostName = GetEnvironment("TESTS_HOSTNAME", "127.0.0.1");
PayTester.InContainer = bool.Parse(GetEnvironment("TESTS_INCONTAINER", "false"));
}
public ExplorerClient ExplorerClient
{
get; set;
}
public bool Dockerized
{
get; set;
}
public void Start()
{
PayTester.Start();
}
public BTCPayServerTester PayTester
{
get; set;
}
public Network Network
{
get;
set;
} = Network.RegTest;
public void Dispose()
{
if(PayTester != null)
PayTester.Dispose();
}
}
/// <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()
{
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 ?? "");
}
}
}
private async Task<GetInfoResponse> WaitLNSynched()
{
while (true)
{
var merchantInfo = await MerchantCharge.Client.GetInfoAsync();
var blockCount = await ExplorerNode.GetBlockCountAsync();
if (merchantInfo.BlockHeight != blockCount)
{
await Task.Delay(1000);
}
else
{
return merchantInfo;
}
}
}
public void SendLightningPayment(Invoice invoice)
{
SendLightningPaymentAsync(invoice).GetAwaiter().GetResult();
}
public async Task SendLightningPaymentAsync(Invoice invoice)
{
var bolt11 = invoice.CryptoInfo.Where(o => o.PaymentUrls.BOLT11 != null).First().PaymentUrls.BOLT11;
bolt11 = bolt11.Replace("lightning:", "", StringComparison.OrdinalIgnoreCase);
await CustomerLightningD.SendAsync(bolt11);
}
public CLightningRPCClient CustomerLightningD { get; set; }
public CLightningRPCClient MerchantLightningD { get; private set; }
public ChargeTester MerchantCharge { get; private set; }
internal string GetEnvironment(string variable, string defaultValue)
{
var var = Environment.GetEnvironmentVariable(variable);
return String.IsNullOrEmpty(var) ? defaultValue : var;
}
public TestAccount NewAccount()
{
return new TestAccount(this);
}
public BTCPayNetworkProvider NetworkProvider { get; private set; }
public RPCClient ExplorerNode
{
get; set;
}
public RPCClient LTCExplorerNode
{
get; set;
}
public ExplorerClient ExplorerClient
{
get; set;
}
public ExplorerClient LTCExplorerClient { get; set; }
HttpClient _Http = new HttpClient();
public BTCPayServerTester PayTester
{
get; set;
}
public void Dispose()
{
if (PayTester != null)
PayTester.Dispose();
}
}
}

View File

@ -1,7 +1,8 @@
using BTCPayServer.Controllers;
using System.Linq;
using BTCPayServer.Models.AccountViewModels;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Servcices.Invoices;
using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBitpayClient;
@ -10,83 +11,130 @@ using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Xunit;
using NBXplorer.DerivationStrategy;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
namespace BTCPayServer.Tests
{
public class TestAccount
{
ServerTester parent;
public TestAccount(ServerTester parent)
{
this.parent = parent;
BitPay = new Bitpay(new Key(), parent.PayTester.ServerUri);
}
ServerTester parent;
public TestAccount(ServerTester parent)
{
this.parent = parent;
BitPay = new Bitpay(new Key(), parent.PayTester.ServerUri);
}
public void GrantAccess()
{
GrantAccessAsync().GetAwaiter().GetResult();
}
public void GrantAccess()
{
GrantAccessAsync().GetAwaiter().GetResult();
}
public void Register()
{
RegisterAsync().GetAwaiter().GetResult();
}
public void Register()
{
RegisterAsync().GetAwaiter().GetResult();
}
public BitcoinExtKey ExtKey
{
get; set;
}
public BitcoinExtKey ExtKey
{
get; set;
}
public async Task GrantAccessAsync()
{
await RegisterAsync();
var store = await CreateStoreAsync();
var pairingCode = BitPay.RequestClientAuthorization("test", Facade.Merchant);
Assert.IsType<ViewResult>(await store.RequestPairing(pairingCode.ToString()));
await store.Pair(pairingCode.ToString(), StoreId);
}
public StoresController CreateStore()
{
return CreateStoreAsync().GetAwaiter().GetResult();
}
public async Task<StoresController> CreateStoreAsync()
{
ExtKey = new ExtKey().GetWif(parent.Network);
var store = parent.PayTester.GetController<StoresController>(UserId);
await store.CreateStore(new CreateStoreViewModel() { Name = "Test Store" });
StoreId = store.CreatedStoreId;
await store.UpdateStore(StoreId, new StoreViewModel()
{
DerivationScheme = ExtKey.Neuter().ToString() + "-[legacy]",
SpeedPolicy = SpeedPolicy.MediumSpeed
}, "Save");
return store;
}
public async Task GrantAccessAsync()
{
await RegisterAsync();
await CreateStoreAsync();
var store = this.GetController<StoresController>();
var pairingCode = BitPay.RequestClientAuthorization("test", Facade.Merchant);
Assert.IsType<ViewResult>(await store.RequestPairing(pairingCode.ToString()));
await store.Pair(pairingCode.ToString(), StoreId);
}
public void CreateStore()
{
CreateStoreAsync().GetAwaiter().GetResult();
}
private async Task RegisterAsync()
{
var account = parent.PayTester.GetController<AccountController>();
await account.Register(new RegisterViewModel()
{
Email = Guid.NewGuid() + "@toto.com",
ConfirmPassword = "Kitten0@",
Password = "Kitten0@",
});
UserId = account.RegisteredUserId;
}
public T GetController<T>(bool setImplicitStore = true) where T : Controller
{
return parent.PayTester.GetController<T>(UserId, setImplicitStore ? StoreId : null);
}
public Bitpay BitPay
{
get; set;
}
public string UserId
{
get; set;
}
public async Task CreateStoreAsync()
{
var store = this.GetController<UserStoresController>();
await store.CreateStore(new CreateStoreViewModel() { Name = "Test Store" });
StoreId = store.CreatedStoreId;
}
public string StoreId
{
get; set;
}
}
public BTCPayNetwork SupportedNetwork { get; set; }
public void RegisterDerivationScheme(string crytoCode)
{
RegisterDerivationSchemeAsync(crytoCode).GetAwaiter().GetResult();
}
public async Task RegisterDerivationSchemeAsync(string cryptoCode)
{
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);
await store.AddDerivationScheme(StoreId, new DerivationSchemeViewModel()
{
DerivationScheme = DerivationScheme.ToString(),
Confirmation = true
}, cryptoCode);
}
public DerivationStrategyBase DerivationScheme { get; set; }
private async Task RegisterAsync()
{
var account = parent.PayTester.GetController<AccountController>();
await account.Register(new RegisterViewModel()
{
Email = Guid.NewGuid() + "@toto.com",
ConfirmPassword = "Kitten0@",
Password = "Kitten0@",
});
UserId = account.RegisteredUserId;
}
public Bitpay BitPay
{
get; set;
}
public string UserId
{
get; set;
}
public string StoreId
{
get; set;
}
public void RegisterLightningNode(string cryptoCode, LightningConnectionType connectionType)
{
RegisterLightningNodeAsync(cryptoCode, connectionType).GetAwaiter().GetResult();
}
public async Task RegisterLightningNodeAsync(string cryptoCode, LightningConnectionType connectionType)
{
var storeController = this.GetController<StoresController>();
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()),
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

@ -10,54 +10,54 @@ namespace BTCPayServer.Tests
{
public class Utils
{
public static int FreeTcpPort()
{
TcpListener l = new TcpListener(IPAddress.Loopback, 0);
l.Start();
int port = ((IPEndPoint)l.LocalEndpoint).Port;
l.Stop();
return port;
}
public static int FreeTcpPort()
{
TcpListener l = new TcpListener(IPAddress.Loopback, 0);
l.Start();
int port = ((IPEndPoint)l.LocalEndpoint).Port;
l.Stop();
return port;
}
// http://stackoverflow.com/a/14933880/2061103
public static void DeleteDirectory(string destinationDir)
{
const int magicDust = 10;
for(var gnomes = 1; gnomes <= magicDust; gnomes++)
{
try
{
Directory.Delete(destinationDir, true);
}
catch(DirectoryNotFoundException)
{
return; // good!
}
catch(IOException)
{
if(gnomes == magicDust)
throw;
// System.IO.IOException: The directory is not empty
System.Diagnostics.Debug.WriteLine("Gnomes prevent deletion of {0}! Applying magic dust, attempt #{1}.", destinationDir, gnomes);
// http://stackoverflow.com/a/14933880/2061103
public static void DeleteDirectory(string destinationDir)
{
const int magicDust = 10;
for (var gnomes = 1; gnomes <= magicDust; gnomes++)
{
try
{
Directory.Delete(destinationDir, true);
}
catch (DirectoryNotFoundException)
{
return; // good!
}
catch (IOException)
{
if (gnomes == magicDust)
throw;
// System.IO.IOException: The directory is not empty
System.Diagnostics.Debug.WriteLine("Gnomes prevent deletion of {0}! Applying magic dust, attempt #{1}.", destinationDir, gnomes);
// see http://stackoverflow.com/questions/329355/cannot-delete-directory-with-directory-deletepath-true for more magic
Thread.Sleep(100);
continue;
}
catch(UnauthorizedAccessException)
{
if(gnomes == magicDust)
throw;
// Wait, maybe another software make us authorized a little later
System.Diagnostics.Debug.WriteLine("Gnomes prevent deletion of {0}! Applying magic dust, attempt #{1}.", destinationDir, gnomes);
// see http://stackoverflow.com/questions/329355/cannot-delete-directory-with-directory-deletepath-true for more magic
Thread.Sleep(100);
continue;
}
catch (UnauthorizedAccessException)
{
if (gnomes == magicDust)
throw;
// Wait, maybe another software make us authorized a little later
System.Diagnostics.Debug.WriteLine("Gnomes prevent deletion of {0}! Applying magic dust, attempt #{1}.", destinationDir, gnomes);
// see http://stackoverflow.com/questions/329355/cannot-delete-directory-with-directory-deletepath-true for more magic
Thread.Sleep(100);
continue;
}
return;
}
// depending on your use case, consider throwing an exception here
}
}
// see http://stackoverflow.com/questions/329355/cannot-delete-directory-with-directory-deletepath-true for more magic
Thread.Sleep(100);
continue;
}
return;
}
// depending on your use case, consider throwing an exception here
}
}
}

View File

@ -0,0 +1 @@
docker exec -ti btcpayservertests_bitcoind_1 bitcoin-cli -datadir="/data" $args

View File

@ -0,0 +1,3 @@
#!/bin/bash
docker exec -ti btcpayservertests_bitcoind_1 bitcoin-cli -datadir="/data" "$@"

View File

@ -1,5 +1,8 @@
version: "3"
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.
services:
tests:
@ -7,44 +10,175 @@ services:
context: ..
dockerfile: BTCPayServer.Tests/Dockerfile
environment:
TESTS_RPCCONNECTION: server=http://bitcoind:43782;ceiwHEbqWI83:DwubwWsoo3
TESTS_NBXPLORERURL: http://nbxplorer:32838/
TESTS_BTCRPCCONNECTION: server=http://bitcoind:43782;ceiwHEbqWI83:DwubwWsoo3
TESTS_LTCRPCCONNECTION: server=http://litecoind:43782;ceiwHEbqWI83:DwubwWsoo3
TESTS_BTCNBXPLORERURL: http://nbxplorer:32838/
TESTS_LTCNBXPLORERURL: http://nbxplorer:32838/
TESTS_POSTGRES: User ID=postgres;Host=postgres;Port=5432;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_INCONTAINER: "true"
expose:
- "80"
links:
- dev
extra_hosts:
- "tests:127.0.0.1"
volumes:
- "customer_lightningd_datadir:/etc/customer_lightningd_datadir"
- "merchant_lightningd_datadir:/etc/merchant_lightningd_datadir"
# 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
environment:
BITCOIN_EXTRA_ARGS: |
regtest=1
connect=bitcoind:39388
links:
- nbxplorer
- postgres
- customer_lightningd
- merchant_lightningd
- lightning-charged
nbxplorer:
image: nicolasdorier/nbxplorer:1.0.0.16
image: nicolasdorier/nbxplorer:1.0.2.8
ports:
- "32838:32838"
expose:
- "32838"
environment:
NBXPLORER_NETWORK: regtest
NBXPLORER_RPCURL: http://bitcoind:43782/
NBXPLORER_RPCUSER: ceiwHEbqWI83
NBXPLORER_RPCPASSWORD: DwubwWsoo3
NBXPLORER_NODEENDPOINT: bitcoind:39388
NBXPLORER_CHAINS: "btc,ltc"
NBXPLORER_BTCRPCURL: http://bitcoind:43782/
NBXPLORER_BTCNODEENDPOINT: bitcoind:39388
NBXPLORER_BTCRPCUSER: ceiwHEbqWI83
NBXPLORER_BTCRPCPASSWORD: DwubwWsoo3
NBXPLORER_LTCRPCURL: http://litecoind:43782/
NBXPLORER_LTCNODEENDPOINT: litecoind:39388
NBXPLORER_LTCRPCUSER: ceiwHEbqWI83
NBXPLORER_LTCRPCPASSWORD: DwubwWsoo3
NBXPLORER_BIND: 0.0.0.0:32838
NBXPLORER_VERBOSE: 1
NBXPLORER_NOAUTH: 1
links:
- bitcoind
- postgres
- litecoind
bitcoind:
image: nicolasdorier/docker-bitcoin:0.15.0.1
ports:
- "43782:43782"
- "39388:39388"
image: nicolasdorier/docker-bitcoin:0.16.0
environment:
BITCOIN_EXTRA_ARGS: "regtest=1\nrpcport=43782\nport=39388\nwhitelist=0.0.0.0/0"
BITCOIN_RPC_USER: ceiwHEbqWI83
BITCOIN_RPC_PASSWORD: DwubwWsoo3
BITCOIN_EXTRA_ARGS: |
rpcuser=ceiwHEbqWI83
rpcpassword=DwubwWsoo3
regtest=1
server=1
rpcport=43782
port=39388
whitelist=0.0.0.0/0
ports:
- "43782:43782"
expose:
- "43782"
- "39388"
- "43782" # RPC
- "39388" # P2P
volumes:
- "bitcoin_datadir:/data"
customer_lightningd:
image: nicolasdorier/clightning:0.0.0.20-dev
environment:
EXPOSE_TCP: "true"
LIGHTNINGD_OPT: |
bitcoin-datadir=/etc/bitcoin
bitcoin-rpcconnect=bitcoind
network=regtest
bind-addr=0.0.0.0
announce-addr=customer_lightningd
log-level=debug
dev-broadcast-interval=1000
ports:
- "30992:9835" # api port
expose:
- "9735" # server port
- "9835" # api port
volumes:
- "bitcoin_datadir:/etc/bitcoin"
- "customer_lightningd_datadir:/root/.lightning"
links:
- bitcoind
lightning-charged:
image: shesek/lightning-charge:0.3.12
environment:
NETWORK: regtest
API_TOKEN: foiewnccewuify
BITCOIND_RPCCONNECT: bitcoind
volumes:
- "bitcoin_datadir:/etc/bitcoin"
- "lightning_charge_datadir:/data"
- "merchant_lightningd_datadir:/etc/lightning"
expose:
- "9112" # Charge
- "9735" # Lightning
ports:
- "54938:9112" # Charge
links:
- bitcoind
- merchant_lightningd
merchant_lightningd:
image: nicolasdorier/clightning:0.0.0.20-dev
environment:
EXPOSE_TCP: "true"
LIGHTNINGD_OPT: |
bitcoin-datadir=/etc/bitcoin
bitcoin-rpcconnect=bitcoind
bind-addr=0.0.0.0
announce-addr=merchant_lightningd
network=regtest
log-level=debug
dev-broadcast-interval=1000
ports:
- "30993:9835" # api port
expose:
- "9735" # server port
- "9835" # api port
volumes:
- "bitcoin_datadir:/etc/bitcoin"
- "merchant_lightningd_datadir:/root/.lightning"
links:
- bitcoind
litecoind:
image: nicolasdorier/docker-litecoin:0.15.1
environment:
BITCOIN_EXTRA_ARGS: |
rpcuser=ceiwHEbqWI83
rpcpassword=DwubwWsoo3
regtest=1
server=1
rpcport=43782
port=39388
whitelist=0.0.0.0/0
ports:
- "43783:43782"
expose:
- "43782" # RPC
- "39388" # P2P
postgres:
image: postgres:9.6.5
ports:
- "39372:5432"
- "39372:5432"
expose:
- "5432"
volumes:
bitcoin_datadir:
customer_lightningd_datadir:
merchant_lightningd_datadir:
lightning_charge_datadir:

View File

@ -0,0 +1 @@
docker exec -ti btcpayservertests_customer_lightningd_1 lightning-cli $args

View File

@ -0,0 +1,3 @@
#!/bin/bash
docker exec -ti btcpayservertests_customer_lightningd_1 lightning-cli "$@"

View File

@ -0,0 +1 @@
docker exec -ti btcpayservertests_litecoind_1 litecoin-cli -datadir="/data" $args

View File

@ -0,0 +1,3 @@
#!/bin/bash
docker exec -ti btcpayservertests_litecoind_1 litecoin-cli -datadir="/data" "$@"

View File

@ -0,0 +1 @@
docker exec -ti btcpayservertests_merchant_lightningd_1 lightning-cli $args

View File

@ -0,0 +1,3 @@
#!/bin/bash
docker exec -ti btcpayservertests_merchant_lightningd_1 lightning-cli "$@"

View File

@ -1,35 +0,0 @@
using NBitcoin;
using NBitcoin.DataEncoders;
using System;
using System.Collections.Generic;
using System.Security.Principal;
using System.Text;
namespace BTCPayServer.Authentication
{
public class BitIdentity : IIdentity
{
public BitIdentity(PubKey key)
{
PubKey = key;
_Name = Encoders.Base58Check.EncodeData(Encoders.Hex.DecodeData("0f02" + key.Hash.ToString()));
SIN = NBitpayClient.Extensions.BitIdExtensions.GetBitIDSIN(key);
}
string _Name;
public string SIN
{
get;
}
public PubKey PubKey
{
get;
}
public string AuthenticationType => "BitID";
public bool IsAuthenticated => true;
public string Name => _Name;
}
}

View File

@ -8,43 +8,43 @@ namespace BTCPayServer.Authentication
{
public class BitTokenEntity
{
public string Facade
{
get; set;
}
public string Value
{
get; set;
}
public string StoreId
{
get; set;
}
public string Label
{
get; set;
}
public DateTimeOffset PairingTime
{
get; set;
}
public string SIN
{
get;
set;
}
public string Facade
{
get; set;
}
public string Value
{
get; set;
}
public string StoreId
{
get; set;
}
public string Label
{
get; set;
}
public DateTimeOffset PairingTime
{
get; set;
}
public string SIN
{
get;
set;
}
public BitTokenEntity Clone(Facade facade)
{
return new BitTokenEntity()
{
Label = Label,
Facade = Facade,
StoreId = StoreId,
PairingTime = PairingTime,
SIN = SIN,
Value = Value
};
}
}
public BitTokenEntity Clone(Facade facade)
{
return new BitTokenEntity()
{
Label = Label,
Facade = Facade,
StoreId = StoreId,
PairingTime = PairingTime,
SIN = SIN,
Value = Value
};
}
}
}

View File

@ -4,47 +4,47 @@ using System.Text;
namespace BTCPayServer.Authentication
{
public class PairingCodeEntity
{
public string Id
{
get;
set;
}
public string Facade
{
get;
set;
}
public string Label
{
get;
set;
}
public string SIN
{
get;
set;
}
public DateTimeOffset CreatedTime
{
get;
set;
}
public DateTimeOffset Expiration
{
get;
set;
}
public string TokenValue
{
get;
set;
}
public class PairingCodeEntity
{
public string Id
{
get;
set;
}
public string Facade
{
get;
set;
}
public string Label
{
get;
set;
}
public string SIN
{
get;
set;
}
public DateTimeOffset CreatedTime
{
get;
set;
}
public DateTimeOffset Expiration
{
get;
set;
}
public string TokenValue
{
get;
set;
}
public bool IsExpired()
{
return DateTimeOffset.UtcNow > Expiration;
}
}
public bool IsExpired()
{
return DateTimeOffset.UtcNow > Expiration;
}
}
}

View File

@ -13,173 +13,238 @@ using System.Linq;
namespace BTCPayServer.Authentication
{
public class TokenRepository
{
ApplicationDbContextFactory _Factory;
public TokenRepository(ApplicationDbContextFactory dbFactory)
{
if(dbFactory == null)
throw new ArgumentNullException(nameof(dbFactory));
_Factory = dbFactory;
}
public enum PairingResult
{
Partial,
Complete,
ReusedKey,
Expired
}
public async Task<BitTokenEntity[]> GetTokens(string sin)
{
using(var ctx = _Factory.CreateContext())
{
return (await ctx.PairedSINData
.Where(p => p.SIN == sin)
.ToListAsync())
.Select(p => CreateTokenEntity(p))
.ToArray();
}
}
public class TokenRepository
{
ApplicationDbContextFactory _Factory;
public TokenRepository(ApplicationDbContextFactory dbFactory)
{
if (dbFactory == null)
throw new ArgumentNullException(nameof(dbFactory));
_Factory = dbFactory;
}
private BitTokenEntity CreateTokenEntity(PairedSINData data)
{
return new BitTokenEntity()
{
Label = data.Label,
Facade = data.Facade,
Value = data.Id,
SIN = data.SIN,
PairingTime = data.PairingTime,
StoreId = data.StoreDataId
};
}
public async Task<BitTokenEntity[]> GetTokens(string sin)
{
if (sin == null)
return Array.Empty<BitTokenEntity>();
using (var ctx = _Factory.CreateContext())
{
return (await ctx.PairedSINData
.Where(p => p.SIN == sin)
.ToListAsync())
.Select(p => CreateTokenEntity(p))
.ToArray();
}
}
public async Task<string> CreatePairingCodeAsync()
{
string pairingCodeId = Encoders.Base58.EncodeData(RandomUtils.GetBytes(6));
using(var ctx = _Factory.CreateContext())
{
var now = DateTime.UtcNow;
var expiration = DateTime.UtcNow + TimeSpan.FromMinutes(15);
await ctx.PairingCodes.AddAsync(new PairingCodeData()
{
Id = pairingCodeId,
DateCreated = now,
Expiration = expiration,
TokenValue = Encoders.Base58.EncodeData(RandomUtils.GetBytes(32))
});
await ctx.SaveChangesAsync();
}
return pairingCodeId;
}
public async Task<String> GetStoreIdFromAPIKey(string apiKey)
{
using (var ctx = _Factory.CreateContext())
{
return await ctx.ApiKeys.Where(o => o.Id == apiKey).Select(o => o.StoreId).FirstOrDefaultAsync();
}
}
public async Task<PairingCodeEntity> UpdatePairingCode(PairingCodeEntity pairingCodeEntity)
{
using(var ctx = _Factory.CreateContext())
{
var pairingCode = await ctx.PairingCodes.FindAsync(pairingCodeEntity.Id);
pairingCode.Label = pairingCodeEntity.Label;
pairingCode.Facade = pairingCodeEntity.Facade;
await ctx.SaveChangesAsync();
return CreatePairingCodeEntity(pairingCode);
}
}
public async Task GenerateLegacyAPIKey(string storeId)
{
// It is legacy support and Bitpay generate string of unknown format, trying to replicate them
// as good as possible. The string below got generated for me.
var chars = "ERo0vkBMOYhyU0ZHvirCplbLDIGWPdi1ok77VnW7QdE";
var rand = new Random(Math.Abs(RandomUtils.GetInt32()));
var generated = new char[chars.Length];
for (int i = 0; i < generated.Length; i++)
{
generated[i] = chars[rand.Next(0, generated.Length)];
}
public async Task<bool> PairWithStoreAsync(string pairingCodeId, string storeId)
{
using(var ctx = _Factory.CreateContext())
{
var pairingCode = await ctx.PairingCodes.FindAsync(pairingCodeId);
if(pairingCode == null || pairingCode.Expiration < DateTimeOffset.UtcNow)
return false;
pairingCode.StoreDataId = storeId;
await ActivateIfComplete(ctx, pairingCode);
await ctx.SaveChangesAsync();
}
return true;
}
using (var ctx = _Factory.CreateContext())
{
var existing = await ctx.ApiKeys.Where(o => o.StoreId == storeId).FirstOrDefaultAsync();
if (existing != null)
{
ctx.ApiKeys.Remove(existing);
}
ctx.ApiKeys.Add(new APIKeyData() { Id = new string(generated), StoreId = storeId });
await ctx.SaveChangesAsync().ConfigureAwait(false);
}
}
public async Task<bool> PairWithSINAsync(string pairingCodeId, string sin)
{
using(var ctx = _Factory.CreateContext())
{
var pairingCode = await ctx.PairingCodes.FindAsync(pairingCodeId);
if(pairingCode == null || pairingCode.Expiration < DateTimeOffset.UtcNow)
return false;
pairingCode.SIN = sin;
await ActivateIfComplete(ctx, pairingCode);
await ctx.SaveChangesAsync();
}
return true;
}
public async Task<string[]> GetLegacyAPIKeys(string storeId)
{
using (var ctx = _Factory.CreateContext())
{
return await ctx.ApiKeys.Where(o => o.StoreId == storeId).Select(c => c.Id).ToArrayAsync();
}
}
private BitTokenEntity CreateTokenEntity(PairedSINData data)
{
return new BitTokenEntity()
{
Label = data.Label,
Facade = data.Facade,
Value = data.Id,
SIN = data.SIN,
PairingTime = data.PairingTime,
StoreId = data.StoreDataId
};
}
public async Task<string> CreatePairingCodeAsync()
{
string pairingCodeId = null;
while (true)
{
pairingCodeId = Encoders.Base58.EncodeData(RandomUtils.GetBytes(6));
if (pairingCodeId.Length == 7) // woocommerce plugin check for exactly 7 digits
break;
}
using (var ctx = _Factory.CreateContext())
{
var now = DateTime.UtcNow;
var expiration = DateTime.UtcNow + TimeSpan.FromMinutes(15);
ctx.PairingCodes.Add(new PairingCodeData()
{
Id = pairingCodeId,
DateCreated = now,
Expiration = expiration,
TokenValue = Encoders.Base58.EncodeData(RandomUtils.GetBytes(32))
});
await ctx.SaveChangesAsync();
}
return pairingCodeId;
}
public async Task<PairingCodeEntity> UpdatePairingCode(PairingCodeEntity pairingCodeEntity)
{
using (var ctx = _Factory.CreateContext())
{
var pairingCode = await ctx.PairingCodes.FindAsync(pairingCodeEntity.Id);
pairingCode.Label = pairingCodeEntity.Label;
pairingCode.Facade = pairingCodeEntity.Facade;
await ctx.SaveChangesAsync();
return CreatePairingCodeEntity(pairingCode);
}
}
public async Task<PairingResult> PairWithStoreAsync(string pairingCodeId, string storeId)
{
using (var ctx = _Factory.CreateContext())
{
var pairingCode = await ctx.PairingCodes.FindAsync(pairingCodeId);
if (pairingCode == null || pairingCode.Expiration < DateTimeOffset.UtcNow)
return PairingResult.Expired;
pairingCode.StoreDataId = storeId;
var result = await ActivateIfComplete(ctx, pairingCode);
await ctx.SaveChangesAsync();
return result;
}
}
public async Task<PairingResult> PairWithSINAsync(string pairingCodeId, string sin)
{
using (var ctx = _Factory.CreateContext())
{
var pairingCode = await ctx.PairingCodes.FindAsync(pairingCodeId);
if (pairingCode == null || pairingCode.Expiration < DateTimeOffset.UtcNow)
return PairingResult.Expired;
pairingCode.SIN = sin;
var result = await ActivateIfComplete(ctx, pairingCode);
await ctx.SaveChangesAsync();
return result;
}
}
private async Task ActivateIfComplete(ApplicationDbContext ctx, PairingCodeData pairingCode)
{
if(!string.IsNullOrEmpty(pairingCode.SIN) && !string.IsNullOrEmpty(pairingCode.StoreDataId))
{
ctx.PairingCodes.Remove(pairingCode);
await ctx.PairedSINData.AddAsync(new PairedSINData()
{
Id = pairingCode.TokenValue,
PairingTime = DateTime.UtcNow,
Facade = pairingCode.Facade,
Label = pairingCode.Label,
StoreDataId = pairingCode.StoreDataId,
SIN = pairingCode.SIN
});
}
}
private async Task<PairingResult> ActivateIfComplete(ApplicationDbContext ctx, PairingCodeData pairingCode)
{
if (!string.IsNullOrEmpty(pairingCode.SIN) && !string.IsNullOrEmpty(pairingCode.StoreDataId))
{
ctx.PairingCodes.Remove(pairingCode);
// Can have concurrency issues... but no harm can be done
var alreadyUsed = await ctx.PairedSINData.Where(p => p.SIN == pairingCode.SIN && p.StoreDataId != pairingCode.StoreDataId).AnyAsync();
if (alreadyUsed)
return PairingResult.ReusedKey;
await ctx.PairedSINData.AddAsync(new PairedSINData()
{
Id = pairingCode.TokenValue,
PairingTime = DateTime.UtcNow,
Facade = pairingCode.Facade,
Label = pairingCode.Label,
StoreDataId = pairingCode.StoreDataId,
SIN = pairingCode.SIN
});
return PairingResult.Complete;
}
return PairingResult.Partial;
}
public async Task<BitTokenEntity[]> GetTokensByStoreIdAsync(string storeId)
{
using(var ctx = _Factory.CreateContext())
{
return (await ctx.PairedSINData.Where(p => p.StoreDataId == storeId).ToListAsync())
.Select(c => CreateTokenEntity(c))
.ToArray();
}
}
public async Task<BitTokenEntity[]> GetTokensByStoreIdAsync(string storeId)
{
using (var ctx = _Factory.CreateContext())
{
return (await ctx.PairedSINData.Where(p => p.StoreDataId == storeId).ToListAsync())
.Select(c => CreateTokenEntity(c))
.ToArray();
}
}
public async Task<PairingCodeEntity> GetPairingAsync(string pairingCode)
{
using(var ctx = _Factory.CreateContext())
{
return CreatePairingCodeEntity(await ctx.PairingCodes.FindAsync(pairingCode));
}
}
public async Task<PairingCodeEntity> GetPairingAsync(string pairingCode)
{
using (var ctx = _Factory.CreateContext())
{
return CreatePairingCodeEntity(await ctx.PairingCodes.FindAsync(pairingCode));
}
}
private PairingCodeEntity CreatePairingCodeEntity(PairingCodeData data)
{
return new PairingCodeEntity()
{
Facade = data.Facade,
Id = data.Id,
Label = data.Label,
Expiration = data.Expiration,
CreatedTime = data.DateCreated,
TokenValue = data.TokenValue,
SIN = data.SIN
};
}
private PairingCodeEntity CreatePairingCodeEntity(PairingCodeData data)
{
if (data == null)
return null;
return new PairingCodeEntity()
{
Facade = data.Facade,
Id = data.Id,
Label = data.Label,
Expiration = data.Expiration,
CreatedTime = data.DateCreated,
TokenValue = data.TokenValue,
SIN = data.SIN
};
}
public async Task<bool> DeleteToken(string tokenId)
{
using(var ctx = _Factory.CreateContext())
{
var token = await ctx.PairedSINData.FindAsync(tokenId);
if(token == null)
return false;
ctx.PairedSINData.Remove(token);
await ctx.SaveChangesAsync();
return true;
}
}
public async Task<bool> DeleteToken(string tokenId)
{
using (var ctx = _Factory.CreateContext())
{
var token = await ctx.PairedSINData.FindAsync(tokenId);
if (token == null)
return false;
ctx.PairedSINData.Remove(token);
await ctx.SaveChangesAsync();
return true;
}
}
public async Task<BitTokenEntity> GetToken(string tokenId)
{
using(var ctx = _Factory.CreateContext())
{
var token = await ctx.PairedSINData.FindAsync(tokenId);
return CreateTokenEntity(token);
}
}
public async Task<BitTokenEntity> GetToken(string tokenId)
{
using (var ctx = _Factory.CreateContext())
{
var token = await ctx.PairedSINData.FindAsync(tokenId);
return CreateTokenEntity(token);
}
}
}
}
}

View File

@ -0,0 +1,77 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using NBitcoin;
using NBXplorer;
namespace BTCPayServer
{
public class BTCPayDefaultSettings
{
static BTCPayDefaultSettings()
{
_Settings = new Dictionary<NetworkType, BTCPayDefaultSettings>();
foreach (var chainType in new[] { NetworkType.Mainnet, NetworkType.Testnet, NetworkType.Regtest })
{
var settings = new BTCPayDefaultSettings();
_Settings.Add(chainType, settings);
settings.DefaultDataDirectory = StandardConfiguration.DefaultDataDirectory.GetDirectory("BTCPayServer", NBXplorerDefaultSettings.GetFolderName(chainType));
settings.DefaultConfigurationFile = Path.Combine(settings.DefaultDataDirectory, "settings.config");
settings.DefaultPort = (chainType == NetworkType.Mainnet ? 23000 :
chainType == NetworkType.Regtest ? 23002 :
chainType == NetworkType.Testnet ? 23001 : throw new NotSupportedException(chainType.ToString()));
}
}
static Dictionary<NetworkType, BTCPayDefaultSettings> _Settings;
public static BTCPayDefaultSettings GetDefaultSettings(NetworkType chainType)
{
return _Settings[chainType];
}
public string DefaultDataDirectory { get; set; }
public string DefaultConfigurationFile { get; set; }
public int DefaultPort { get; set; }
}
public class BTCPayNetwork
{
public Network NBitcoinNetwork { get; set; }
public string CryptoCode { get; internal set; }
public string BlockExplorerLink { get; internal set; }
public string UriScheme { get; internal set; }
[Obsolete("Should not be needed")]
public bool IsBTC
{
get
{
return CryptoCode == "BTC";
}
}
public string CryptoImagePath { get; set; }
public string LightningImagePath { get; set; }
public NBXplorer.NBXplorerNetwork NBXplorerNetwork { get; set; }
public BTCPayDefaultSettings DefaultSettings { get; set; }
public KeyPath CoinType { get; internal set; }
public int MaxTrackedConfirmation { get; internal set; } = 6;
public string[] DefaultRateRules { get; internal set; } = Array.Empty<string>();
public override string ToString()
{
return CryptoCode;
}
internal KeyPath GetRootKeyPath()
{
return new KeyPath(NBitcoinNetwork.Consensus.SupportSegwit ? "49'" : "44'")
.Derive(CoinType);
}
}
}

View File

@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
using NBitcoin;
using NBitpayClient;
using NBXplorer;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
public void InitBitcoin()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("BTC");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
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",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("0'") : new KeyPath("1'")
});
}
}
}

View File

@ -0,0 +1,29 @@
using NBitcoin;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
public void InitBitcoinGold()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("BTG");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
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-symbol.svg",
LightningImagePath = "imlegacy/btg-symbol.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("156'") : new KeyPath("1'")
});
}
}
}

View File

@ -0,0 +1,34 @@
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 InitDogecoin()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("DOGE");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://dogechain.info/tx/{0}" : "https://dogechain.info/tx/{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "dogecoin",
DefaultRateRules = new[]
{
"DOGE_X = DOGE_BTC * BTC_X",
"DOGE_BTC = bittrex(DOGE_BTC)"
},
CryptoImagePath = "imlegacy/dogecoin.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("3'") : new KeyPath("1'")
});
}
}
}

View File

@ -0,0 +1,34 @@
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,
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,30 @@
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 InitLitecoin()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("LTC");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
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",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("2'") : 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 InitMonacoin()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("MONA");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
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 = zaif(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,34 @@
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,
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,101 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
using Microsoft.Extensions.Caching.Memory;
using NBitcoin;
using NBitpayClient;
using NBXplorer;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
Dictionary<string, BTCPayNetwork> _Networks = new Dictionary<string, BTCPayNetwork>();
private readonly NBXplorerNetworkProvider _NBXplorerNetworkProvider;
public NBXplorerNetworkProvider NBXplorerNetworkProvider
{
get
{
return _NBXplorerNetworkProvider;
}
}
BTCPayNetworkProvider(BTCPayNetworkProvider filtered, string[] cryptoCodes)
{
NetworkType = filtered.NetworkType;
_NBXplorerNetworkProvider = new NBXplorerNetworkProvider(filtered.NetworkType);
_Networks = new Dictionary<string, BTCPayNetwork>();
cryptoCodes = cryptoCodes.Select(c => c.ToUpperInvariant()).ToArray();
foreach (var network in filtered._Networks)
{
if(cryptoCodes.Contains(network.Key))
{
_Networks.Add(network.Key, network.Value);
}
}
}
public NetworkType NetworkType { get; private set; }
public BTCPayNetworkProvider(NetworkType networkType)
{
_NBXplorerNetworkProvider = new NBXplorerNetworkProvider(networkType);
NetworkType = networkType;
InitBitcoin();
InitLitecoin();
InitDogecoin();
InitBitcoinGold();
InitMonacoin();
InitPolis();
InitFeathercoin();
}
/// <summary>
/// Keep only the specified crypto
/// </summary>
/// <param name="cryptoCodes">Crypto to support</param>
/// <returns></returns>
public BTCPayNetworkProvider Filter(string[] cryptoCodes)
{
return new BTCPayNetworkProvider(this, cryptoCodes);
}
[Obsolete("To use only for legacy stuff")]
public BTCPayNetwork BTC
{
get
{
return GetNetwork("BTC");
}
}
public void Add(BTCPayNetwork network)
{
_Networks.Add(network.CryptoCode.ToUpperInvariant(), network);
}
public IEnumerable<BTCPayNetwork> GetAll()
{
return _Networks.Values.ToArray();
}
public bool Support(string cryptoCode)
{
return _Networks.ContainsKey(cryptoCode.ToUpperInvariant());
}
public BTCPayNetwork GetNetwork(string cryptoCode)
{
if(!_Networks.TryGetValue(cryptoCode.ToUpperInvariant(), out BTCPayNetwork network))
{
if (cryptoCode == "XBT")
return GetNetwork("BTC");
}
return network;
}
}
}

View File

@ -1,14 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<Version>1.0.0.7</Version>
<TargetFramework>netcoreapp2.1</TargetFramework>
<Version>1.0.2.32</Version>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Build\dockerfiles\**" />
<Compile Remove="wwwroot\bundles\jqueryvalidate\**" />
<Compile Remove="wwwroot\css\**" />
<Compile Remove="wwwroot\vendor\jquery-nice-select\**" />
<Content Remove="Build\dockerfiles\**" />
<Content Remove="wwwroot\bundles\jqueryvalidate\**" />
<Content Remove="wwwroot\css\**" />
<Content Remove="wwwroot\vendor\jquery-nice-select\**" />
<EmbeddedResource Remove="Build\dockerfiles\**" />
<EmbeddedResource Remove="wwwroot\bundles\jqueryvalidate\**" />
<EmbeddedResource Remove="wwwroot\css\**" />
<EmbeddedResource Remove="wwwroot\vendor\jquery-nice-select\**" />
<None Remove="Build\dockerfiles\**" />
<None Remove="wwwroot\bundles\jqueryvalidate\**" />
<None Remove="wwwroot\css\**" />
<None Remove="wwwroot\vendor\jquery-nice-select\**" />
</ItemGroup>
<ItemGroup>
<None Remove="Currencies.txt" />
@ -17,43 +30,41 @@
<EmbeddedResource Include="Currencies.txt" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Hangfire" Version="1.6.17" />
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.1" />
<PackageReference Include="Hangfire.PostgreSql" Version="1.4.8.1" />
<PackageReference Include="BuildBundlerMinifier" Version="2.7.385" />
<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.2" />
<PackageReference Include="LedgerWallet" Version="1.0.1.36" />
<PackageReference Include="Meziantou.AspNetCore.BundleTagHelpers" Version="2.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
<PackageReference Include="NBitcoin" Version="4.0.0.38" />
<PackageReference Include="NBitpayClient" Version="1.0.0.10" />
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="2.6.0" />
<PackageReference Include="NBitcoin" Version="4.1.1.10" />
<PackageReference Include="NBitpayClient" Version="1.0.0.27" />
<PackageReference Include="DBreeze" Version="1.87.0" />
<PackageReference Include="NBXplorer.Client" Version="1.0.0.16" />
<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.0" />
<PackageReference Include="System.ValueTuple" Version="4.4.0" />
<PackageReference Include="System.Xml.XmlSerializer" Version="4.0.11" />
<PackageReference Include="NBXplorer.Client" Version="1.0.2.10" />
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.2" />
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.3" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.14" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.1.0" />
<PackageReference Include="System.Xml.XmlSerializer" Version="4.3.0" />
<PackageReference Include="Text.Analyzers" Version="2.6.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.0.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version=" 2.1.0" PrivateAssets="All" />
<PackageReference Include="YamlDotNet" Version="4.3.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>
<ItemGroup>
<None Include="wwwroot\img\bitcoin-symbol.svg" />
<None Include="wwwroot\js\core.js" />
<None Include="wwwroot\js\creative.js" />
<None Include="wwwroot\js\creative.min.js" />
<None Include="wwwroot\js\site.js" />
<None Include="wwwroot\js\site.min.js" />
<None Include="wwwroot\vendor\bootstrap\js\bootstrap.js" />
<None Include="wwwroot\vendor\bootstrap\js\bootstrap.min.js" />
<None Include="wwwroot\checkout\js\core.js" />
<None Include="wwwroot\vendor\bootstrap4-creativestart\creative.js" />
<None Include="wwwroot\vendor\font-awesome\fonts\fontawesome-webfont.svg" />
<None Include="wwwroot\vendor\font-awesome\fonts\fontawesome-webfont.woff2" />
<None Include="wwwroot\vendor\font-awesome\less\animated.less" />
@ -99,5 +110,12 @@
<ItemGroup>
<Folder Include="Build\" />
<Folder Include="wwwroot\vendor\clipboard.js\" />
</ItemGroup>
</Project>
<ItemGroup>
<None Update="devtest.pfx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<RuleSet Name="Microsoft Managed Recommended Rules" Description="These rules focus on the most critical problems in your code, including potential security holes, application crashes, and other important logic and design errors. It is recommended to include this rule set in any custom rule set you create for your projects." ToolsVersion="10.0">
<Localization ResourceAssembly="Microsoft.VisualStudio.CodeAnalysis.RuleSets.Strings.dll" ResourceBaseName="Microsoft.VisualStudio.CodeAnalysis.RuleSets.Strings.Localized">
<Name Resource="MinimumRecommendedRules_Name" />
<Description Resource="MinimumRecommendedRules_Description" />
</Localization>
<Rules AnalyzerId="Microsoft.Analyzers.ManagedCodeAnalysis" RuleNamespace="Microsoft.Rules.Managed">
<Rule Id="CA1001" Action="Warning" />
<Rule Id="CA1009" Action="Warning" />
<Rule Id="CA1016" Action="Warning" />
<Rule Id="CA1033" Action="Warning" />
<Rule Id="CA1049" Action="Warning" />
<Rule Id="CA1060" Action="Warning" />
<Rule Id="CA1061" Action="Warning" />
<Rule Id="CA1063" Action="Warning" />
<Rule Id="CA1065" Action="Warning" />
<Rule Id="CA1301" Action="Warning" />
<Rule Id="CA1400" Action="Warning" />
<Rule Id="CA1401" Action="Warning" />
<Rule Id="CA1403" Action="Warning" />
<Rule Id="CA1404" Action="Warning" />
<Rule Id="CA1405" Action="Warning" />
<Rule Id="CA1410" Action="Warning" />
<Rule Id="CA1415" Action="Warning" />
<Rule Id="CA1821" Action="Warning" />
<Rule Id="CA1900" Action="Warning" />
<Rule Id="CA1901" Action="Warning" />
<Rule Id="CA2002" Action="Warning" />
<Rule Id="CA2100" Action="Warning" />
<Rule Id="CA2101" Action="Warning" />
<Rule Id="CA2108" Action="Warning" />
<Rule Id="CA2111" Action="Warning" />
<Rule Id="CA2112" Action="Warning" />
<Rule Id="CA2114" Action="Warning" />
<Rule Id="CA2116" Action="Warning" />
<Rule Id="CA2117" Action="Warning" />
<Rule Id="CA2122" Action="Warning" />
<Rule Id="CA2123" Action="Warning" />
<Rule Id="CA2124" Action="Warning" />
<Rule Id="CA2126" Action="Warning" />
<Rule Id="CA2131" Action="Warning" />
<Rule Id="CA2132" Action="Warning" />
<Rule Id="CA2133" Action="Warning" />
<Rule Id="CA2134" Action="Warning" />
<Rule Id="CA2137" Action="Warning" />
<Rule Id="CA2138" Action="Warning" />
<Rule Id="CA2140" Action="Warning" />
<Rule Id="CA2141" Action="Warning" />
<Rule Id="CA2146" Action="Warning" />
<Rule Id="CA2147" Action="Warning" />
<Rule Id="CA2149" Action="Warning" />
<Rule Id="CA2200" Action="Warning" />
<Rule Id="CA2202" Action="Warning" />
<Rule Id="CA2207" Action="Warning" />
<Rule Id="CA2212" Action="Warning" />
<Rule Id="CA2213" Action="Warning" />
<Rule Id="CA2214" Action="Warning" />
<Rule Id="CA2216" Action="Warning" />
<Rule Id="CA2220" Action="Warning" />
<Rule Id="CA2229" Action="Warning" />
<Rule Id="CA2231" Action="Warning" />
<Rule Id="CA2232" Action="Warning" />
<Rule Id="CA2235" Action="Warning" />
<Rule Id="CA2236" Action="Warning" />
<Rule Id="CA2237" Action="Warning" />
<Rule Id="CA2238" Action="Warning" />
<Rule Id="CA2240" Action="Warning" />
<Rule Id="CA2241" Action="Warning" />
<Rule Id="CA2242" Action="Warning" />
</Rules>
<Rules AnalyzerId="Microsoft.CodeAnalysis.CSharp.Analyzers" RuleNamespace="Microsoft.CodeAnalysis.CSharp.Analyzers" />
</RuleSet>

View File

@ -4,15 +4,15 @@ using System.Text;
namespace BTCPayServer
{
public class BitpayHttpException : Exception
{
public BitpayHttpException(int code, string message) : base(message)
{
StatusCode = code;
}
public int StatusCode
{
get; set;
}
}
public class BitpayHttpException : Exception
{
public BitpayHttpException(int code, string message) : base(message)
{
StatusCode = code;
}
public int StatusCode
{
get; set;
}
}
}

View File

@ -9,64 +9,124 @@ using System.Net;
using System.Text;
using StandardConfiguration;
using Microsoft.Extensions.Configuration;
using NBXplorer;
using BTCPayServer.Payments.Lightning;
namespace BTCPayServer.Configuration
{
public class BTCPayServerOptions
{
public Network Network
{
get; set;
}
public Uri Explorer
{
get; set;
}
public class NBXplorerConnectionSetting
{
public string CryptoCode { get; internal set; }
public Uri ExplorerUri { get; internal set; }
public string CookieFile { get; internal set; }
}
public string CookieFile
{
get; set;
}
public string ConfigurationFile
{
get;
private set;
}
public string DataDir
{
get;
private set;
}
public List<IPEndPoint> Listen
{
get;
set;
}
public class BTCPayServerOptions
{
public NetworkType NetworkType
{
get; set;
}
public string ConfigurationFile
{
get;
private set;
}
public string DataDir
{
get;
private set;
}
public List<IPEndPoint> Listen
{
get;
set;
}
public void LoadArgs(IConfiguration conf)
{
var networkInfo = DefaultConfiguration.GetNetwork(conf);
Network = networkInfo?.Network;
if(Network == null)
throw new ConfigException("Invalid network");
public List<NBXplorerConnectionSetting> NBXplorerConnectionSettings
{
get;
set;
} = new List<NBXplorerConnectionSetting>();
DataDir = conf.GetOrDefault<string>("datadir", networkInfo.DefaultDataDirectory);
Logs.Configuration.LogInformation("Network: " + Network);
public void LoadArgs(IConfiguration conf)
{
NetworkType = DefaultConfiguration.GetNetworkType(conf);
var defaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType);
DataDir = conf.GetOrDefault<string>("datadir", defaultSettings.DefaultDataDirectory);
Logs.Configuration.LogInformation("Network: " + NetworkType.ToString());
Explorer = conf.GetOrDefault<Uri>("explorer.url", networkInfo.DefaultExplorerUrl);
CookieFile = conf.GetOrDefault<string>("explorer.cookiefile", networkInfo.DefaultExplorerCookieFile);
RequireHttps = conf.GetOrDefault<bool>("requirehttps", false);
PostgresConnectionString = conf.GetOrDefault<string>("postgres", null);
}
var supportedChains = conf.GetOrDefault<string>("chains", "btc")
.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(t => t.ToUpperInvariant());
NetworkProvider = new BTCPayNetworkProvider(NetworkType).Filter(supportedChains.ToArray());
foreach (var chain in supportedChains)
{
if (NetworkProvider.GetNetwork(chain) == null)
throw new ConfigException($"Invalid chains \"{chain}\"");
}
public bool RequireHttps
{
get; set;
}
public string PostgresConnectionString
{
get;
set;
}
}
var validChains = new List<string>();
foreach (var net in NetworkProvider.GetAll())
{
NBXplorerConnectionSetting setting = new NBXplorerConnectionSetting();
setting.CryptoCode = net.CryptoCode;
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))
{
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/)");
}
InternalLightningByCryptoCode.Add(net.CryptoCode, connectionString);
}
}
Logs.Configuration.LogInformation("Supported chains: " + String.Join(',', supportedChains.ToArray()));
PostgresConnectionString = conf.GetOrDefault<string>("postgres", null);
BundleJsCss = conf.GetOrDefault<bool>("bundlejscss", true);
ExternalUrl = conf.GetOrDefault<Uri>("externalurl", null);
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");
}
public string RootPath { get; set; }
public Dictionary<string, LightningConnectionString> InternalLightningByCryptoCode { get; set; } = new Dictionary<string, LightningConnectionString>();
public BTCPayNetworkProvider NetworkProvider { get; set; }
public string PostgresConnectionString
{
get;
set;
}
public Uri ExternalUrl
{
get;
set;
}
public bool BundleJsCss
{
get;
set;
}
internal string GetRootUri()
{
if (ExternalUrl == null)
return null;
UriBuilder builder = new UriBuilder(ExternalUrl);
builder.Path = RootPath;
return builder.ToString();
}
}
}

View File

@ -1,113 +0,0 @@
using BTCPayServer.Authentication;
using Microsoft.Extensions.Logging;
using BTCPayServer.Logging;
using DBreeze;
using NBitcoin;
using NBXplorer;
using NBXplorer.DerivationStrategy;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using BTCPayServer.Data;
using BTCPayServer.Servcices.Invoices;
using BTCPayServer.Services.Wallets;
namespace BTCPayServer.Configuration
{
public class BTCPayServerRuntime : IDisposable
{
public ExplorerClient Explorer
{
get;
private set;
}
public void Configure(BTCPayServerOptions opts)
{
ConfigureAsync(opts).GetAwaiter().GetResult();
}
public async Task ConfigureAsync(BTCPayServerOptions opts)
{
Network = opts.Network;
Explorer = new ExplorerClient(opts.Network, opts.Explorer);
if(!Explorer.SetCookieAuth(opts.CookieFile))
Explorer.SetNoAuth();
CancellationTokenSource cts = new CancellationTokenSource(30000);
try
{
Logs.Configuration.LogInformation("Trying to connect to explorer " + Explorer.Address.AbsoluteUri);
await Explorer.WaitServerStartedAsync(cts.Token).ConfigureAwait(false);
Logs.Configuration.LogInformation("Connection successfull");
}
catch(Exception ex)
{
throw new ConfigException($"Could not connect to NBXplorer, {ex.Message}");
}
DBreezeEngine db = new DBreezeEngine(CreateDBPath(opts, "TokensDB"));
_Resources.Add(db);
db = new DBreezeEngine(CreateDBPath(opts, "InvoiceDB"));
_Resources.Add(db);
ApplicationDbContextFactory dbContext = null;
if(opts.PostgresConnectionString == null)
{
var connStr = "Data Source=" + Path.Combine(opts.DataDir, "sqllite.db");
Logs.Configuration.LogInformation($"SQLite DB used ({connStr})");
dbContext = new ApplicationDbContextFactory(DatabaseType.Sqlite, connStr);
}
else
{
Logs.Configuration.LogInformation($"Postgres DB used ({opts.PostgresConnectionString})");
dbContext = new ApplicationDbContextFactory(DatabaseType.Postgres, opts.PostgresConnectionString);
}
DBFactory = dbContext;
InvoiceRepository = new InvoiceRepository(dbContext, db, Network);
}
private static string CreateDBPath(BTCPayServerOptions opts, string name)
{
var dbpath = Path.Combine(opts.DataDir, name);
if(!Directory.Exists(dbpath))
Directory.CreateDirectory(dbpath);
return dbpath;
}
List<IDisposable> _Resources = new List<IDisposable>();
public void Dispose()
{
lock(_Resources)
{
foreach(var r in _Resources)
{
r.Dispose();
}
_Resources.Clear();
}
}
public Network Network
{
get;
private set;
}
public InvoiceRepository InvoiceRepository
{
get;
set;
}
public ApplicationDbContextFactory DBFactory
{
get;
set;
}
}
}

View File

@ -5,11 +5,11 @@ using System.Threading.Tasks;
namespace BTCPayServer.Configuration
{
public class ConfigException : Exception
{
public ConfigException(string message) : base(message)
{
public class ConfigException : Exception
{
public ConfigException(string message) : base(message)
{
}
}
}
}
}

View File

@ -9,44 +9,51 @@ using Microsoft.Extensions.Primitives;
namespace BTCPayServer.Configuration
{
public static class ConfigurationExtensions
{
public static T GetOrDefault<T>(this IConfiguration configuration, string key, T defaultValue)
{
var str = configuration[key] ?? configuration[key.Replace(".", string.Empty)];
if(str == null)
return defaultValue;
if(typeof(T) == typeof(bool))
{
var trueValues = new[] { "1", "true" };
var falseValues = new[] { "0", "false" };
if(trueValues.Contains(str, StringComparer.OrdinalIgnoreCase))
return (T)(object)true;
if(falseValues.Contains(str, StringComparer.OrdinalIgnoreCase))
return (T)(object)false;
throw new FormatException();
}
else if(typeof(T) == typeof(Uri))
return (T)(object)new Uri(str, UriKind.Absolute);
else if(typeof(T) == typeof(string))
return (T)(object)str;
else if(typeof(T) == typeof(IPEndPoint))
{
var separator = str.LastIndexOf(":");
if(separator == -1)
throw new FormatException();
var ip = str.Substring(0, separator);
var port = str.Substring(separator + 1);
return (T)(object)new IPEndPoint(IPAddress.Parse(ip), int.Parse(port));
}
else if(typeof(T) == typeof(int))
{
return (T)(object)int.Parse(str, CultureInfo.InvariantCulture);
}
else
{
throw new NotSupportedException("Configuration value does not support time " + typeof(T).Name);
}
}
}
public static class ConfigurationExtensions
{
public static T GetOrDefault<T>(this IConfiguration configuration, string key, T defaultValue)
{
var str = configuration[key] ?? configuration[key.Replace(".", string.Empty, StringComparison.InvariantCulture)];
if (str == null)
return defaultValue;
if (typeof(T) == typeof(bool))
{
var trueValues = new[] { "1", "true" };
var falseValues = new[] { "0", "false" };
if (trueValues.Contains(str, StringComparer.OrdinalIgnoreCase))
return (T)(object)true;
if (falseValues.Contains(str, StringComparer.OrdinalIgnoreCase))
return (T)(object)false;
throw new FormatException();
}
else if (typeof(T) == typeof(Uri))
if (string.IsNullOrEmpty(str))
{
return defaultValue;
}
else
{
return (T)(object)new Uri(str, UriKind.Absolute);
}
else if (typeof(T) == typeof(string))
return (T)(object)str;
else if (typeof(T) == typeof(IPEndPoint))
{
var separator = str.LastIndexOf(":", StringComparison.InvariantCulture);
if (separator == -1)
throw new FormatException();
var ip = str.Substring(0, separator);
var port = str.Substring(separator + 1);
return (T)(object)new IPEndPoint(IPAddress.Parse(ip), int.Parse(port, CultureInfo.InvariantCulture));
}
else if (typeof(T) == typeof(int))
{
return (T)(object)int.Parse(str, CultureInfo.InvariantCulture);
}
else
{
throw new NotSupportedException("Configuration value does not support time " + typeof(T).Name);
}
}
}
}

View File

@ -10,91 +10,114 @@ using System.Threading.Tasks;
using NBitcoin;
using System.Text;
using CommandLine;
using NBXplorer;
namespace BTCPayServer.Configuration
{
public class DefaultConfiguration : StandardConfiguration.DefaultConfiguration
{
protected override CommandLineApplication CreateCommandLineApplicationCore()
{
CommandLineApplication app = new CommandLineApplication(true)
{
FullName = "NBXplorer\r\nLightweight block explorer for tracking HD wallets",
Name = "NBXplorer"
};
app.HelpOption("-? | -h | --help");
app.Option("-n | --network", $"Set the network among ({NetworkInformation.ToStringAll()}) (default: {Network.Main.ToString()})", CommandOptionType.SingleValue);
app.Option("--testnet | -testnet", $"Use testnet", CommandOptionType.BoolValue);
app.Option("--regtest | -regtest", $"Use regtest", CommandOptionType.BoolValue);
app.Option("--requirehttps", $"Will redirect to https version of the website (default: false)", CommandOptionType.BoolValue);
app.Option("--postgres", $"Connection string to postgres database (default: sqlite is used)", CommandOptionType.SingleValue);
app.Option("--explorerurl", $"Url of the NBxplorer (default: : Default setting of NBXplorer for the network)", CommandOptionType.SingleValue);
app.Option("--explorercookiefile", $"Path to the cookie file (default: Default setting of NBXplorer for the network)", CommandOptionType.SingleValue);
public class DefaultConfiguration : StandardConfiguration.DefaultConfiguration
{
protected override CommandLineApplication CreateCommandLineApplicationCore()
{
var provider = new BTCPayNetworkProvider(NetworkType.Mainnet);
var chains = string.Join(",", provider.GetAll().Select(n => n.CryptoCode.ToLowerInvariant()).ToArray());
CommandLineApplication app = new CommandLineApplication(true)
{
FullName = "BTCPay\r\nOpen source, self-hosted payment processor.",
Name = "BTCPay"
};
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("--rootpath", "The root path in the URL to access BTCPay (default: /)", 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}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);
}
return app;
}
return app;
}
public override string EnvironmentVariablePrefix => "BTCPAY_";
public override string EnvironmentVariablePrefix => "BTCPAY_";
protected override string GetDefaultDataDir(IConfiguration conf)
{
return BTCPayDefaultSettings.GetDefaultSettings(GetNetworkType(conf)).DefaultDataDirectory;
}
protected override string GetDefaultDataDir(IConfiguration conf)
{
return GetNetwork(conf).DefaultDataDirectory;
}
protected override string GetDefaultConfigurationFile(IConfiguration conf)
{
var network = BTCPayDefaultSettings.GetDefaultSettings(GetNetworkType(conf));
var dataDir = conf["datadir"];
if (dataDir == null)
return network.DefaultConfigurationFile;
var fileName = Path.GetFileName(network.DefaultConfigurationFile);
var chainDir = Path.GetFileName(Path.GetDirectoryName(network.DefaultConfigurationFile));
chainDir = Path.Combine(dataDir, chainDir);
try
{
if (!Directory.Exists(chainDir))
Directory.CreateDirectory(chainDir);
}
catch { }
return Path.Combine(chainDir, fileName);
}
protected override string GetDefaultConfigurationFile(IConfiguration conf)
{
var network = GetNetwork(conf);
var dataDir = conf["datadir"];
if(dataDir == null)
return network.DefaultConfigurationFile;
var fileName = Path.GetFileName(network.DefaultConfigurationFile);
return Path.Combine(dataDir, fileName);
}
public static NetworkType GetNetworkType(IConfiguration conf)
{
var network = conf.GetOrDefault<string>("network", null);
if (network != null)
{
var n = Network.GetNetwork(network);
if (n == null)
{
throw new ConfigException($"Invalid network parameter '{network}'");
}
return n.NetworkType;
}
var net = conf.GetOrDefault<bool>("regtest", false) ? NetworkType.Regtest :
conf.GetOrDefault<bool>("testnet", false) ? NetworkType.Testnet : NetworkType.Mainnet;
public static NetworkInformation GetNetwork(IConfiguration conf)
{
var network = conf.GetOrDefault<string>("network", null);
if(network != null)
{
var info = NetworkInformation.GetNetworkByName(network);
if(info == null)
throw new ConfigException($"Invalid network name {network}");
return info;
}
return net;
}
var net = conf.GetOrDefault<bool>("regtest", false) ? Network.RegTest :
conf.GetOrDefault<bool>("testnet", false) ? Network.TestNet : Network.Main;
return NetworkInformation.GetNetworkByName(net.Name);
}
protected override string GetDefaultConfigurationFileTemplate(IConfiguration conf)
{
var network = GetNetwork(conf);
StringBuilder builder = new StringBuilder();
builder.AppendLine("### Global settings ###");
builder.AppendLine("#testnet=0");
builder.AppendLine("#regtest=0");
builder.AppendLine();
builder.AppendLine("### Server settings ###");
builder.AppendLine("#requirehttps=0");
builder.AppendLine("#port=" + network.DefaultPort);
builder.AppendLine("#bind=127.0.0.1");
builder.AppendLine();
builder.AppendLine("### Database ###");
builder.AppendLine("#postgres=User ID=root;Password=myPassword;Host=localhost;Port=5432;Database=myDataBase;");
builder.AppendLine();
builder.AppendLine("### NBXplorer settings ###");
builder.AppendLine("#explorer.url=" + network.DefaultExplorerUrl.AbsoluteUri);
builder.AppendLine("#explorer.cookiefile=" + network.DefaultExplorerCookieFile);
return builder.ToString();
}
protected override string GetDefaultConfigurationFileTemplate(IConfiguration conf)
{
var networkType = GetNetworkType(conf);
var defaultSettings = BTCPayDefaultSettings.GetDefaultSettings(networkType);
StringBuilder builder = new StringBuilder();
builder.AppendLine("### Global settings ###");
builder.AppendLine("#network=mainnet");
builder.AppendLine();
builder.AppendLine("### Server settings ###");
builder.AppendLine("#port=" + defaultSettings.DefaultPort);
builder.AppendLine("#bind=127.0.0.1");
builder.AppendLine();
builder.AppendLine("### Database ###");
builder.AppendLine("#postgres=User ID=root;Password=myPassword;Host=localhost;Port=5432;Database=myDataBase;");
builder.AppendLine();
builder.AppendLine("### NBXplorer settings ###");
foreach (var n in new BTCPayNetworkProvider(networkType).GetAll())
{
builder.AppendLine($"#{n.CryptoCode}.explorer.url={n.NBXplorerNetwork.DefaultSettings.DefaultUrl}");
builder.AppendLine($"#{n.CryptoCode}.explorer.cookiefile={ n.NBXplorerNetwork.DefaultSettings.DefaultCookieFile}");
builder.AppendLine($"#{n.CryptoCode}.lightning=/root/.lightning/lightning-rpc");
builder.AppendLine($"#{n.CryptoCode}.lightning=https://apitoken:API_TOKEN_SECRET@charge.example.com/");
}
return builder.ToString();
}
protected override IPEndPoint GetDefaultEndpoint(IConfiguration conf)
{
return new IPEndPoint(IPAddress.Parse("127.0.0.1"), GetNetwork(conf).DefaultPort);
}
}
protected override IPEndPoint GetDefaultEndpoint(IConfiguration conf)
{
return new IPEndPoint(IPAddress.Parse("127.0.0.1"), BTCPayDefaultSettings.GetDefaultSettings(GetNetworkType(conf)).DefaultPort);
}
}
}

View File

@ -1,103 +0,0 @@
using Microsoft.Extensions.Configuration;
using NBitcoin;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Configuration
{
public class NetworkInformation
{
static NetworkInformation()
{
_Networks = new Dictionary<string, NetworkInformation>();
foreach(var network in Network.GetNetworks())
{
NetworkInformation info = new NetworkInformation();
info.DefaultDataDirectory = StandardConfiguration.DefaultDataDirectory.GetDirectory("BTCPayServer", network.Name);
info.DefaultConfigurationFile = Path.Combine(info.DefaultDataDirectory, "settings.config");
info.DefaultExplorerCookieFile = Path.Combine(StandardConfiguration.DefaultDataDirectory.GetDirectory("NBXplorer", network.Name, false), ".cookie");
info.Network = network;
info.DefaultExplorerUrl = new Uri("http://127.0.0.1:24446", UriKind.Absolute);
info.DefaultPort = 23002;
_Networks.Add(network.Name, info);
if(network == Network.Main)
{
info.DefaultExplorerUrl = new Uri("http://127.0.0.1:24444", UriKind.Absolute);
Main = info;
info.DefaultPort = 23000;
}
if(network == Network.TestNet)
{
info.DefaultExplorerUrl = new Uri("http://127.0.0.1:24445", UriKind.Absolute);
info.DefaultPort = 23001;
}
}
}
static Dictionary<string, NetworkInformation> _Networks;
public static NetworkInformation GetNetworkByName(string name)
{
var value = _Networks.TryGet(name);
if(value != null)
return value;
//Maybe alias ?
var network = Network.GetNetwork(name);
if(network != null)
{
value = _Networks.TryGet(network.Name);
if(value != null)
return value;
}
return null;
}
public static NetworkInformation Main
{
get;
set;
}
public Network Network
{
get; set;
}
public string DefaultConfigurationFile
{
get;
set;
}
public string DefaultDataDirectory
{
get;
set;
}
public Uri DefaultExplorerUrl
{
get;
internal set;
}
public int DefaultPort
{
get;
private set;
}
public string DefaultExplorerCookieFile
{
get;
internal set;
}
public override string ToString()
{
return Network.ToString();
}
public static string ToStringAll()
{
return string.Join(", ", _Networks.Select(n => n.Key).ToArray());
}
}
}

View File

@ -12,69 +12,81 @@ using System.Threading.Tasks;
namespace BTCPayServer.Controllers
{
public class AccessTokenController : Controller
{
TokenRepository _TokenRepository;
public AccessTokenController(TokenRepository tokenRepository)
{
_TokenRepository = tokenRepository ?? throw new ArgumentNullException(nameof(tokenRepository));
}
[HttpGet]
[Route("tokens")]
public async Task<GetTokensResponse> Tokens()
{
var tokens = await _TokenRepository.GetTokens(this.GetBitIdentity().SIN);
return new GetTokensResponse(tokens);
}
[BitpayAPIConstraint]
public class AccessTokenController : Controller
{
TokenRepository _TokenRepository;
public AccessTokenController(TokenRepository tokenRepository)
{
_TokenRepository = tokenRepository ?? throw new ArgumentNullException(nameof(tokenRepository));
}
[HttpGet]
[Route("tokens")]
public async Task<GetTokensResponse> Tokens()
{
var tokens = await _TokenRepository.GetTokens(this.User.GetSIN());
return new GetTokensResponse(tokens);
}
[HttpPost]
[Route("tokens")]
public async Task<DataWrapper<List<PairingCodeResponse>>> Tokens([FromBody] TokenRequest request)
{
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");
[HttpPost]
[Route("tokens")]
public async Task<DataWrapper<List<PairingCodeResponse>>> Tokens([FromBody] TokenRequest request)
{
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
});
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
});
}
else
{
var sin = this.GetBitIdentity(false)?.SIN ?? request.Id;
if(string.IsNullOrEmpty(request.Id) || !NBitpayClient.Extensions.BitIdExtensions.ValidateSIN(request.Id))
throw new BitpayHttpException(400, "'id' property is required, alternatively, use BitId");
}
else
{
var sin = this.User.GetSIN() ?? request.Id;
if (string.IsNullOrEmpty(request.Id) || !NBitpayClient.Extensions.BitIdExtensions.ValidateSIN(request.Id))
throw new BitpayHttpException(400, "'id' property is required, alternatively, use BitId");
pairingEntity = await _TokenRepository.GetPairingAsync(request.PairingCode);
pairingEntity.SIN = sin;
if(!await _TokenRepository.PairWithSINAsync(request.PairingCode, sin))
throw new BitpayHttpException(400, "Unknown pairing code");
}
pairingEntity = await _TokenRepository.GetPairingAsync(request.PairingCode);
if (pairingEntity == null)
throw new BitpayHttpException(404, "The specified pairingCode is not found");
pairingEntity.SIN = sin;
var pairingCodes = new List<PairingCodeResponse>
{
new PairingCodeResponse()
{
PairingCode = pairingEntity.Id,
PairingExpiration = pairingEntity.Expiration,
DateCreated = pairingEntity.CreatedTime,
Facade = pairingEntity.Facade,
Token = pairingEntity.TokenValue,
Label = pairingEntity.Label
}
};
return DataWrapper.Create(pairingCodes);
}
}
if (string.IsNullOrEmpty(pairingEntity.Label) && !string.IsNullOrEmpty(request.Label))
{
pairingEntity.Label = request.Label;
await _TokenRepository.UpdatePairingCode(pairingEntity);
}
var result = await _TokenRepository.PairWithSINAsync(request.PairingCode, sin);
if (result != PairingResult.Complete && result != PairingResult.Partial)
throw new BitpayHttpException(400, $"Error while pairing ({result})");
}
var pairingCodes = new List<PairingCodeResponse>
{
new PairingCodeResponse()
{
Policies = new Newtonsoft.Json.Linq.JArray(),
PairingCode = pairingEntity.Id,
PairingExpiration = pairingEntity.Expiration,
DateCreated = pairingEntity.CreatedTime,
Facade = pairingEntity.Facade,
Token = pairingEntity.TokenValue,
Label = pairingEntity.Label
}
};
return DataWrapper.Create(pairingCodes);
}
}
}

View File

@ -15,506 +15,509 @@ using BTCPayServer.Models.AccountViewModels;
using BTCPayServer.Services;
using BTCPayServer.Services.Mails;
using BTCPayServer.Services.Stores;
using BTCPayServer.Logging;
using BTCPayServer.Security;
namespace BTCPayServer.Controllers
{
[Authorize]
[Route("[controller]/[action]")]
public class AccountController : Controller
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly IEmailSender _emailSender;
private readonly ILogger _logger;
StoreRepository storeRepository;
RoleManager<IdentityRole> _RoleManager;
SettingsRepository _SettingsRepository;
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
[Route("[controller]/[action]")]
public class AccountController : Controller
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly IEmailSender _emailSender;
StoreRepository storeRepository;
RoleManager<IdentityRole> _RoleManager;
SettingsRepository _SettingsRepository;
ILogger _logger;
public AccountController(
UserManager<ApplicationUser> userManager,
RoleManager<IdentityRole> roleManager,
StoreRepository storeRepository,
SignInManager<ApplicationUser> signInManager,
IEmailSender emailSender,
SettingsRepository settingsRepository,
ILogger<AccountController> logger)
{
this.storeRepository = storeRepository;
_userManager = userManager;
_signInManager = signInManager;
_emailSender = emailSender;
_logger = logger;
_RoleManager = roleManager;
_SettingsRepository = settingsRepository;
}
public AccountController(
UserManager<ApplicationUser> userManager,
RoleManager<IdentityRole> roleManager,
StoreRepository storeRepository,
SignInManager<ApplicationUser> signInManager,
IEmailSender emailSender,
SettingsRepository settingsRepository)
{
this.storeRepository = storeRepository;
_userManager = userManager;
_signInManager = signInManager;
_emailSender = emailSender;
_RoleManager = roleManager;
_SettingsRepository = settingsRepository;
_logger = Logs.PayServer;
}
[TempData]
public string ErrorMessage
{
get; set;
}
[TempData]
public string ErrorMessage
{
get; set;
}
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> Login(string returnUrl = null)
{
// Clear the existing external cookie to ensure a clean login process
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> Login(string returnUrl = null)
{
// Clear the existing external cookie to ensure a clean login process
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
ViewData["ReturnUrl"] = returnUrl;
return View();
}
ViewData["ReturnUrl"] = returnUrl;
return View();
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
{
ViewData["ReturnUrl"] = returnUrl;
if(ModelState.IsValid)
{
// Require the user to have a confirmed email before they can log on.
var user = await _userManager.FindByEmailAsync(model.Email);
if(user != null)
{
if(user.RequiresEmailConfirmation && !await _userManager.IsEmailConfirmedAsync(user))
{
ModelState.AddModelError(string.Empty,
"You must have a confirmed email to log in.");
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);
if(result.Succeeded)
{
_logger.LogInformation("User logged in.");
return RedirectToLocal(returnUrl);
}
if(result.RequiresTwoFactor)
{
return RedirectToAction(nameof(LoginWith2fa), new
{
returnUrl,
model.RememberMe
});
}
if(result.IsLockedOut)
{
_logger.LogWarning("User account locked out.");
return RedirectToAction(nameof(Lockout));
}
else
{
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return View(model);
}
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
{
ViewData["ReturnUrl"] = returnUrl;
if (ModelState.IsValid)
{
// Require the user to have a confirmed email before they can log on.
var user = await _userManager.FindByEmailAsync(model.Email);
if (user != null)
{
if (user.RequiresEmailConfirmation && !await _userManager.IsEmailConfirmedAsync(user))
{
ModelState.AddModelError(string.Empty,
"You must have a confirmed email to log in.");
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);
if (result.Succeeded)
{
_logger.LogInformation("User logged in.");
return RedirectToLocal(returnUrl);
}
if (result.RequiresTwoFactor)
{
return RedirectToAction(nameof(LoginWith2fa), new
{
returnUrl,
model.RememberMe
});
}
if (result.IsLockedOut)
{
_logger.LogWarning("User account locked out.");
return RedirectToAction(nameof(Lockout));
}
else
{
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return View(model);
}
}
// If we got this far, something failed, redisplay form
return View(model);
}
// If we got this far, something failed, redisplay form
return View(model);
}
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> LoginWith2fa(bool rememberMe, string returnUrl = null)
{
// Ensure the user has gone through the username & password screen first
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> LoginWith2fa(bool rememberMe, string returnUrl = null)
{
// Ensure the user has gone through the username & password screen first
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
if(user == null)
{
throw new ApplicationException($"Unable to load two-factor authentication user.");
}
if (user == null)
{
throw new ApplicationException($"Unable to load two-factor authentication user.");
}
var model = new LoginWith2faViewModel { RememberMe = rememberMe };
ViewData["ReturnUrl"] = returnUrl;
var model = new LoginWith2faViewModel { RememberMe = rememberMe };
ViewData["ReturnUrl"] = returnUrl;
return View(model);
}
return View(model);
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> LoginWith2fa(LoginWith2faViewModel model, bool rememberMe, string returnUrl = null)
{
if(!ModelState.IsValid)
{
return View(model);
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> LoginWith2fa(LoginWith2faViewModel model, bool rememberMe, string returnUrl = null)
{
if (!ModelState.IsValid)
{
return View(model);
}
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
if(user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var authenticatorCode = model.TwoFactorCode.Replace(" ", string.Empty).Replace("-", string.Empty);
var authenticatorCode = model.TwoFactorCode.Replace(" ", string.Empty, StringComparison.InvariantCulture).Replace("-", string.Empty, StringComparison.InvariantCulture);
var result = await _signInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, rememberMe, model.RememberMachine);
var result = await _signInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, rememberMe, model.RememberMachine);
if(result.Succeeded)
{
_logger.LogInformation("User with ID {UserId} logged in with 2fa.", user.Id);
return RedirectToLocal(returnUrl);
}
else if(result.IsLockedOut)
{
_logger.LogWarning("User with ID {UserId} account locked out.", user.Id);
return RedirectToAction(nameof(Lockout));
}
else
{
_logger.LogWarning("Invalid authenticator code entered for user with ID {UserId}.", user.Id);
ModelState.AddModelError(string.Empty, "Invalid authenticator code.");
return View();
}
}
if (result.Succeeded)
{
_logger.LogInformation("User with ID {UserId} logged in with 2fa.", user.Id);
return RedirectToLocal(returnUrl);
}
else if (result.IsLockedOut)
{
_logger.LogWarning("User with ID {UserId} account locked out.", user.Id);
return RedirectToAction(nameof(Lockout));
}
else
{
_logger.LogWarning("Invalid authenticator code entered for user with ID {UserId}.", user.Id);
ModelState.AddModelError(string.Empty, "Invalid authenticator code.");
return View();
}
}
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> LoginWithRecoveryCode(string returnUrl = null)
{
// Ensure the user has gone through the username & password screen first
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
if(user == null)
{
throw new ApplicationException($"Unable to load two-factor authentication user.");
}
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> LoginWithRecoveryCode(string returnUrl = null)
{
// Ensure the user has gone through the username & password screen first
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
if (user == null)
{
throw new ApplicationException($"Unable to load two-factor authentication user.");
}
ViewData["ReturnUrl"] = returnUrl;
ViewData["ReturnUrl"] = returnUrl;
return View();
}
return View();
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> LoginWithRecoveryCode(LoginWithRecoveryCodeViewModel model, string returnUrl = null)
{
if(!ModelState.IsValid)
{
return View(model);
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> LoginWithRecoveryCode(LoginWithRecoveryCodeViewModel model, string returnUrl = null)
{
if (!ModelState.IsValid)
{
return View(model);
}
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
if(user == null)
{
throw new ApplicationException($"Unable to load two-factor authentication user.");
}
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
if (user == null)
{
throw new ApplicationException($"Unable to load two-factor authentication user.");
}
var recoveryCode = model.RecoveryCode.Replace(" ", string.Empty);
var recoveryCode = model.RecoveryCode.Replace(" ", string.Empty, StringComparison.InvariantCulture);
var result = await _signInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode);
var result = await _signInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode);
if(result.Succeeded)
{
_logger.LogInformation("User with ID {UserId} logged in with a recovery code.", user.Id);
return RedirectToLocal(returnUrl);
}
if(result.IsLockedOut)
{
_logger.LogWarning("User with ID {UserId} account locked out.", user.Id);
return RedirectToAction(nameof(Lockout));
}
else
{
_logger.LogWarning("Invalid recovery code entered for user with ID {UserId}", user.Id);
ModelState.AddModelError(string.Empty, "Invalid recovery code entered.");
return View();
}
}
if (result.Succeeded)
{
_logger.LogInformation("User with ID {UserId} logged in with a recovery code.", user.Id);
return RedirectToLocal(returnUrl);
}
if (result.IsLockedOut)
{
_logger.LogWarning("User with ID {UserId} account locked out.", user.Id);
return RedirectToAction(nameof(Lockout));
}
else
{
_logger.LogWarning("Invalid recovery code entered for user with ID {UserId}", user.Id);
ModelState.AddModelError(string.Empty, "Invalid recovery code entered.");
return View();
}
}
[HttpGet]
[AllowAnonymous]
public IActionResult Lockout()
{
return View();
}
[HttpGet]
[AllowAnonymous]
public IActionResult Lockout()
{
return View();
}
[HttpGet]
[AllowAnonymous]
public IActionResult Register(string returnUrl = null)
{
ViewData["ReturnUrl"] = returnUrl;
return View();
}
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> Register(string returnUrl = null)
{
var policies = await _SettingsRepository.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
if (policies.LockSubscription)
return RedirectToAction(nameof(HomeController.Index), "Home");
ViewData["ReturnUrl"] = returnUrl;
return View();
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Register(RegisterViewModel model, string returnUrl = null)
{
ViewData["ReturnUrl"] = returnUrl;
if(ModelState.IsValid)
{
var policies = await _SettingsRepository.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
var user = new ApplicationUser { UserName = model.Email, Email = model.Email, RequiresEmailConfirmation = policies.RequiresConfirmedEmail };
var result = await _userManager.CreateAsync(user, model.Password);
if(result.Succeeded)
{
_logger.LogInformation("User created a new account with password.");
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Register(RegisterViewModel model, string returnUrl = null)
{
ViewData["ReturnUrl"] = returnUrl;
var policies = await _SettingsRepository.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
if (policies.LockSubscription)
return RedirectToAction(nameof(HomeController.Index), "Home");
if (ModelState.IsValid)
{
var user = new ApplicationUser { UserName = model.Email, Email = model.Email, RequiresEmailConfirmation = policies.RequiresConfirmedEmail };
var result = await _userManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
var admin = await _userManager.GetUsersInRoleAsync(Roles.ServerAdmin);
Logs.PayServer.LogInformation($"A new user just registered {user.Email} {(admin.Count == 0 ? "(admin)" : "")}");
if (admin.Count == 0)
{
await _RoleManager.CreateAsync(new IdentityRole(Roles.ServerAdmin));
await _userManager.AddToRoleAsync(user, Roles.ServerAdmin);
}
var admin = await _userManager.GetUsersInRoleAsync(Roles.ServerAdmin);
if(admin.Count == 0)
{
_logger.LogInformation("Admin created.");
await _RoleManager.CreateAsync(new IdentityRole(Roles.ServerAdmin));
await _userManager.AddToRoleAsync(user, Roles.ServerAdmin);
}
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme);
RegisteredUserId = user.Id;
await _emailSender.SendEmailConfirmationAsync(model.Email, callbackUrl);
if (!policies.RequiresConfirmedEmail)
{
await _signInManager.SignInAsync(user, isPersistent: false);
return RedirectToLocal(returnUrl);
}
else
{
TempData["StatusMessage"] = "Account created, please confirm your email";
return View();
}
}
AddErrors(result);
}
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme);
RegisteredUserId = user.Id;
await _emailSender.SendEmailConfirmationAsync(model.Email, callbackUrl);
_logger.LogInformation("User created a new account with password.");
if(!policies.RequiresConfirmedEmail)
{
await _signInManager.SignInAsync(user, isPersistent: false);
return RedirectToLocal(returnUrl);
}
else
{
TempData["StatusMessage"] = "Account created, please confirm your email";
return View();
}
}
AddErrors(result);
}
// If we got this far, something failed, redisplay form
return View(model);
}
// If we got this far, something failed, redisplay form
return View(model);
}
/// <summary>
/// Test property
/// </summary>
public string RegisteredUserId
{
get; set;
}
/// <summary>
/// Test property
/// </summary>
public string RegisteredUserId
{
get; set;
}
[HttpGet]
public async Task<IActionResult> Logout()
{
await _signInManager.SignOutAsync();
_logger.LogInformation("User logged out.");
return RedirectToAction(nameof(HomeController.Index), "Home");
}
[HttpGet]
public async Task<IActionResult> Logout()
{
await _signInManager.SignOutAsync();
_logger.LogInformation("User logged out.");
return RedirectToAction(nameof(HomeController.Index), "Home");
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public IActionResult ExternalLogin(string provider, string returnUrl = null)
{
// Request a redirect to the external login provider.
var redirectUrl = Url.Action(nameof(ExternalLoginCallback), "Account", new
{
returnUrl
});
var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
return Challenge(properties, provider);
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public IActionResult ExternalLogin(string provider, string returnUrl = null)
{
// Request a redirect to the external login provider.
var redirectUrl = Url.Action(nameof(ExternalLoginCallback), "Account", new
{
returnUrl
});
var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
return Challenge(properties, provider);
}
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, string remoteError = null)
{
if (remoteError != null)
{
ErrorMessage = $"Error from external provider: {remoteError}";
return RedirectToAction(nameof(Login));
}
var info = await _signInManager.GetExternalLoginInfoAsync();
if (info == null)
{
return RedirectToAction(nameof(Login));
}
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, string remoteError = null)
{
if(remoteError != null)
{
ErrorMessage = $"Error from external provider: {remoteError}";
return RedirectToAction(nameof(Login));
}
var info = await _signInManager.GetExternalLoginInfoAsync();
if(info == null)
{
return RedirectToAction(nameof(Login));
}
// Sign in the user with this external login provider if the user already has a login.
var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: true);
if (result.Succeeded)
{
_logger.LogInformation("User logged in with {Name} provider.", info.LoginProvider);
return RedirectToLocal(returnUrl);
}
if (result.IsLockedOut)
{
return RedirectToAction(nameof(Lockout));
}
else
{
// If the user does not have an account, then ask the user to create an account.
ViewData["ReturnUrl"] = returnUrl;
ViewData["LoginProvider"] = info.LoginProvider;
var email = info.Principal.FindFirstValue(ClaimTypes.Email);
return View("ExternalLogin", new ExternalLoginViewModel { Email = email });
}
}
// Sign in the user with this external login provider if the user already has a login.
var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: true);
if(result.Succeeded)
{
_logger.LogInformation("User logged in with {Name} provider.", info.LoginProvider);
return RedirectToLocal(returnUrl);
}
if(result.IsLockedOut)
{
return RedirectToAction(nameof(Lockout));
}
else
{
// If the user does not have an account, then ask the user to create an account.
ViewData["ReturnUrl"] = returnUrl;
ViewData["LoginProvider"] = info.LoginProvider;
var email = info.Principal.FindFirstValue(ClaimTypes.Email);
return View("ExternalLogin", new ExternalLoginViewModel { Email = email });
}
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ExternalLoginConfirmation(ExternalLoginViewModel model, string returnUrl = null)
{
if (ModelState.IsValid)
{
// Get the information about the user from the external login provider
var info = await _signInManager.GetExternalLoginInfoAsync();
if (info == null)
{
throw new ApplicationException("Error loading external login information during confirmation.");
}
var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
var result = await _userManager.CreateAsync(user);
if (result.Succeeded)
{
result = await _userManager.AddLoginAsync(user, info);
if (result.Succeeded)
{
await _signInManager.SignInAsync(user, isPersistent: false);
_logger.LogInformation("User created an account using {Name} provider.", info.LoginProvider);
return RedirectToLocal(returnUrl);
}
}
AddErrors(result);
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ExternalLoginConfirmation(ExternalLoginViewModel model, string returnUrl = null)
{
if(ModelState.IsValid)
{
// Get the information about the user from the external login provider
var info = await _signInManager.GetExternalLoginInfoAsync();
if(info == null)
{
throw new ApplicationException("Error loading external login information during confirmation.");
}
var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
var result = await _userManager.CreateAsync(user);
if(result.Succeeded)
{
result = await _userManager.AddLoginAsync(user, info);
if(result.Succeeded)
{
await _signInManager.SignInAsync(user, isPersistent: false);
_logger.LogInformation("User created an account using {Name} provider.", info.LoginProvider);
return RedirectToLocal(returnUrl);
}
}
AddErrors(result);
}
ViewData["ReturnUrl"] = returnUrl;
return View(nameof(ExternalLogin), model);
}
ViewData["ReturnUrl"] = returnUrl;
return View(nameof(ExternalLogin), model);
}
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> ConfirmEmail(string userId, string code)
{
if (userId == null || code == null)
{
return RedirectToAction(nameof(HomeController.Index), "Home");
}
var user = await _userManager.FindByIdAsync(userId);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{userId}'.");
}
var result = await _userManager.ConfirmEmailAsync(user, code);
return View(result.Succeeded ? "ConfirmEmail" : "Error");
}
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> ConfirmEmail(string userId, string code)
{
if(userId == null || code == null)
{
return RedirectToAction(nameof(HomeController.Index), "Home");
}
var user = await _userManager.FindByIdAsync(userId);
if(user == null)
{
throw new ApplicationException($"Unable to load user with ID '{userId}'.");
}
var result = await _userManager.ConfirmEmailAsync(user, code);
return View(result.Succeeded ? "ConfirmEmail" : "Error");
}
[HttpGet]
[AllowAnonymous]
public IActionResult ForgotPassword()
{
return View();
}
[HttpGet]
[AllowAnonymous]
public IActionResult ForgotPassword()
{
return View();
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ForgotPassword(ForgotPasswordViewModel model)
{
if (ModelState.IsValid)
{
var user = await _userManager.FindByEmailAsync(model.Email);
if (user == null || !(await _userManager.IsEmailConfirmedAsync(user)))
{
// Don't reveal that the user does not exist or is not confirmed
return RedirectToAction(nameof(ForgotPasswordConfirmation));
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ForgotPassword(ForgotPasswordViewModel model)
{
if(ModelState.IsValid)
{
var user = await _userManager.FindByEmailAsync(model.Email);
if(user == null || !(await _userManager.IsEmailConfirmedAsync(user)))
{
// Don't reveal that the user does not exist or is not confirmed
return RedirectToAction(nameof(ForgotPasswordConfirmation));
}
// For more information on how to enable account confirmation and password reset please
// 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>");
return RedirectToAction(nameof(ForgotPasswordConfirmation));
}
// For more information on how to enable account confirmation and password reset please
// 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>");
return RedirectToAction(nameof(ForgotPasswordConfirmation));
}
// If we got this far, something failed, redisplay form
return View(model);
}
// If we got this far, something failed, redisplay form
return View(model);
}
[HttpGet]
[AllowAnonymous]
public IActionResult ForgotPasswordConfirmation()
{
return View();
}
[HttpGet]
[AllowAnonymous]
public IActionResult ForgotPasswordConfirmation()
{
return View();
}
[HttpGet]
[AllowAnonymous]
public IActionResult ResetPassword(string code = null)
{
if (code == null)
{
throw new ApplicationException("A code must be supplied for password reset.");
}
var model = new ResetPasswordViewModel { Code = code };
return View(model);
}
[HttpGet]
[AllowAnonymous]
public IActionResult ResetPassword(string code = null)
{
if(code == null)
{
throw new ApplicationException("A code must be supplied for password reset.");
}
var model = new ResetPasswordViewModel { Code = code };
return View(model);
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ResetPassword(ResetPasswordViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
var user = await _userManager.FindByEmailAsync(model.Email);
if (user == null)
{
// Don't reveal that the user does not exist
return RedirectToAction(nameof(ResetPasswordConfirmation));
}
var result = await _userManager.ResetPasswordAsync(user, model.Code, model.Password);
if (result.Succeeded)
{
return RedirectToAction(nameof(ResetPasswordConfirmation));
}
AddErrors(result);
return View();
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ResetPassword(ResetPasswordViewModel model)
{
if(!ModelState.IsValid)
{
return View(model);
}
var user = await _userManager.FindByEmailAsync(model.Email);
if(user == null)
{
// Don't reveal that the user does not exist
return RedirectToAction(nameof(ResetPasswordConfirmation));
}
var result = await _userManager.ResetPasswordAsync(user, model.Code, model.Password);
if(result.Succeeded)
{
return RedirectToAction(nameof(ResetPasswordConfirmation));
}
AddErrors(result);
return View();
}
[HttpGet]
[AllowAnonymous]
public IActionResult ResetPasswordConfirmation()
{
return View();
}
[HttpGet]
[AllowAnonymous]
public IActionResult ResetPasswordConfirmation()
{
return View();
}
[HttpGet]
public IActionResult AccessDenied()
{
return View();
}
[HttpGet]
public IActionResult AccessDenied()
{
return View();
}
#region Helpers
#region Helpers
private void AddErrors(IdentityResult result)
{
foreach(var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
}
private void AddErrors(IdentityResult result)
{
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
}
private IActionResult RedirectToLocal(string returnUrl)
{
if(Url.IsLocalUrl(returnUrl))
{
return Redirect(returnUrl);
}
else
{
return RedirectToAction(nameof(HomeController.Index), "Home");
}
}
private IActionResult RedirectToLocal(string returnUrl)
{
if (Url.IsLocalUrl(returnUrl))
{
return Redirect(returnUrl);
}
else
{
return RedirectToAction(nameof(HomeController.Index), "Home");
}
}
#endregion
}
#endregion
}
}

View File

@ -0,0 +1,282 @@
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 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 System.Text;
using System.Text.Encodings.Web;
namespace BTCPayServer.Controllers
{
public partial class AppsController
{
public class PointOfSaleSettings
{
public PointOfSaleSettings()
{
Title = "My awesome Point of Sale";
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";
ShowCustomAmount = true;
}
public string Title { get; set; }
public string Currency { get; set; }
public string Template { get; set; }
public bool ShowCustomAmount { get; set; }
}
[HttpGet]
[Route("{appId}/settings/pos")]
public async Task<IActionResult> UpdatePointOfSale(string appId)
{
var app = await GetOwnedApp(appId, AppType.PointOfSale);
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
var vm = new UpdatePointOfSaleViewModel()
{
Title = settings.Title,
ShowCustomAmount = settings.ShowCustomAmount,
Currency = settings.Currency,
Template = settings.Template
};
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 = 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, false) == null)
ModelState.AddModelError(nameof(vm.Currency), "Invalid currency");
try
{
Parse(vm.Template, vm.Currency);
}
catch
{
ModelState.AddModelError(nameof(vm.Template), "Invalid template");
}
if (!ModelState.IsValid)
{
return View(vm);
}
var app = await GetOwnedApp(appId, AppType.PointOfSale);
if (app == null)
return NotFound();
app.SetSettings(new PointOfSaleSettings()
{
Title = vm.Title,
ShowCustomAmount = vm.ShowCustomAmount,
Currency = vm.Currency.ToUpperInvariant(),
Template = vm.Template
});
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, false);
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")]
[IgnoreAntiforgeryToken]
public async Task<IActionResult> ViewPointOfSale(string appId,
decimal amount,
string email,
string orderId,
string notificationUrl,
string redirectUrl,
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;
var price = 0.0m;
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 = choice.Price.Value;
}
else
{
if (!settings.ShowCustomAmount)
return NotFound();
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,
BuyerEmail = email,
OrderId = orderId,
NotificationURL = notificationUrl,
RedirectURL = redirectUrl,
FullNotifications = true
}, 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);
}
}
private async Task UpdateAppSettings(AppData app)
{
using (var ctx = _ContextFactory.CreateContext())
{
ctx.Apps.Add(app);
ctx.Entry<AppData>(app).State = EntityState.Modified;
ctx.Entry<AppData>(app).Property(a => a.Settings).IsModified = true;
await ctx.SaveChangesAsync();
}
}
}
}

View File

@ -0,0 +1,200 @@
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 Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using NBitcoin.DataEncoders;
using NBitcoin;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Rates;
namespace BTCPayServer.Controllers
{
[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,
CurrencyNameTable currencies,
InvoiceController invoiceController)
{
_InvoiceController = invoiceController;
_UserManager = userManager;
_ContextFactory = contextFactory;
_Currencies = currencies;
}
public async Task<IActionResult> ListApps()
{
var apps = await GetAllApps();
return View(new ListAppsViewModel()
{
Apps = apps
});
}
[HttpPost]
[Route("{appId}/delete")]
public async Task<IActionResult> DeleteAppPost(string appId)
{
var appData = await GetOwnedApp(appId);
if (appData == null)
return NotFound();
if (await DeleteApp(appData))
StatusMessage = "App removed successfully";
return RedirectToAction(nameof(ListApps));
}
[HttpGet]
[Route("create")]
public async Task<IActionResult> CreateApp()
{
var stores = await GetOwnedStores();
if (stores.Length == 0)
{
StatusMessage = "Error: You must have created at least one store";
return RedirectToAction(nameof(ListApps));
}
var vm = new CreateAppViewModel();
vm.SetStores(stores);
return View(vm);
}
[HttpPost]
[Route("create")]
public async Task<IActionResult> CreateApp(CreateAppViewModel vm)
{
var stores = await GetOwnedStores();
if (stores.Length == 0)
{
StatusMessage = "Error: You must own at least one store";
return RedirectToAction(nameof(ListApps));
}
var selectedStore = vm.SelectedStore;
vm.SetStores(stores);
vm.SelectedStore = selectedStore;
if (!Enum.TryParse<AppType>(vm.SelectedAppType, out AppType appType))
ModelState.AddModelError(nameof(vm.SelectedAppType), "Invalid App Type");
if (!ModelState.IsValid)
{
return View(vm);
}
if (!stores.Any(s => s.Id == selectedStore))
{
StatusMessage = "Error: You are not owner of this store";
return RedirectToAction(nameof(ListApps));
}
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;
appData.AppType = appType.ToString();
ctx.Apps.Add(appData);
await ctx.SaveChangesAsync();
}
StatusMessage = "App successfully created";
return RedirectToAction(nameof(ListApps));
}
[HttpGet]
[Route("{appId}/delete")]
public async Task<IActionResult> DeleteApp(string appId)
{
var appData = await GetOwnedApp(appId);
if (appData == null)
return NotFound();
return View("Confirm", new ConfirmModel()
{
Title = $"Delete app {appData.Name} ({appData.AppType})",
Description = "This app will be removed from this store",
Action = "Delete"
});
}
private async 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)
.Join(ctx.Apps, us => us.StoreDataId, app => app.StoreDataId,
(us, app) =>
new ListAppsViewModel.ListAppViewModel()
{
IsOwner = us.Role == StoreRoles.Owner,
StoreId = us.StoreDataId,
StoreName = us.StoreData.StoreName,
AppName = app.Name,
AppType = app.AppType,
Id = app.Id
})
.ToArrayAsync();
}
}
private string GetUserId()
{
return _UserManager.GetUserId(User);
}
}
}

View File

@ -10,112 +10,83 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Servcices.Invoices;
using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Cors;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using BTCPayServer.Security;
namespace BTCPayServer.Controllers
{
public partial class InvoiceController
[EnableCors("BitpayAPI")]
[BitpayAPIConstraint]
[Authorize(Policies.CanUseStore.Key, AuthenticationSchemes = Policies.BitpayAuthentication)]
public class InvoiceControllerAPI : Controller
{
[HttpPost]
[Route("invoices")]
[MediaTypeConstraint("application/json")]
[BitpayAPIConstraint]
public async Task<DataWrapper<InvoiceResponse>> CreateInvoice([FromBody] Invoice invoice)
{
var bitToken = await CheckTokenPermissionAsync(Facade.Merchant, invoice.Token);
var store = await FindStore(bitToken);
return await CreateInvoiceCore(invoice, store);
}
private InvoiceController _InvoiceController;
private InvoiceRepository _InvoiceRepository;
private BTCPayNetworkProvider _NetworkProvider;
[HttpGet]
[Route("invoices/{id}")]
[BitpayAPIConstraint]
public async Task<DataWrapper<InvoiceResponse>> GetInvoice(string id, string token)
{
var bitToken = await CheckTokenPermissionAsync(Facade.Merchant, token);
var store = await FindStore(bitToken);
var invoice = await _InvoiceRepository.GetInvoice(store.Id, id);
if(invoice == null)
throw new BitpayHttpException(404, "Object not found");
public InvoiceControllerAPI(InvoiceController invoiceController,
InvoiceRepository invoceRepository,
BTCPayNetworkProvider networkProvider)
{
this._InvoiceController = invoiceController;
this._InvoiceRepository = invoceRepository;
this._NetworkProvider = networkProvider;
}
var resp = invoice.EntityToDTO();
return new DataWrapper<InvoiceResponse>(resp);
}
[HttpPost]
[Route("invoices")]
[MediaTypeConstraint("application/json")]
public async Task<DataWrapper<InvoiceResponse>> CreateInvoice([FromBody] Invoice invoice)
{
return await _InvoiceController.CreateInvoiceCore(invoice, HttpContext.GetStoreData(), HttpContext.Request.GetAbsoluteRoot());
}
[HttpGet]
[Route("invoices")]
[BitpayAPIConstraint]
public async Task<DataWrapper<InvoiceResponse[]>> GetInvoices(
string token,
DateTimeOffset? dateStart = null,
DateTimeOffset? dateEnd = null,
string orderId = null,
string itemCode = null,
string status = null,
int? limit = null,
int? offset = null)
{
if(dateEnd != null)
dateEnd = dateEnd.Value + TimeSpan.FromDays(1); //Should include the end day
var bitToken = await CheckTokenPermissionAsync(Facade.Merchant, token);
var store = await FindStore(bitToken);
var query = new InvoiceQuery()
{
Count = limit,
Skip = offset,
EndDate = dateEnd,
StartDate = dateStart,
OrderId = orderId,
ItemCode = itemCode,
Status = status,
StoreId = store.Id
};
[HttpGet]
[Route("invoices/{id}")]
[AllowAnonymous]
public async Task<DataWrapper<InvoiceResponse>> GetInvoice(string id, string token)
{
var invoice = await _InvoiceRepository.GetInvoice(null, id);
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(
string token,
DateTimeOffset? dateStart = null,
DateTimeOffset? dateEnd = null,
string orderId = null,
string itemCode = null,
string status = null,
int? limit = null,
int? offset = null)
{
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,
Status = status == null ? null : new[] { status },
StoreId = new[] { this.HttpContext.GetStoreData().Id }
};
var entities = (await _InvoiceRepository.GetInvoices(query))
.Select((o) => o.EntityToDTO()).ToArray();
var entities = (await _InvoiceRepository.GetInvoices(query))
.Select((o) => o.EntityToDTO(_NetworkProvider)).ToArray();
return DataWrapper.Create(entities);
}
private async Task<BitTokenEntity> CheckTokenPermissionAsync(Facade facade, string expectedToken)
{
if(facade == null)
throw new ArgumentNullException(nameof(facade));
var actualTokens = (await _TokenRepository.GetTokens(this.GetBitIdentity().SIN)).ToArray();
actualTokens = actualTokens.SelectMany(t => GetCompatibleTokens(t)).ToArray();
var actualToken = actualTokens.FirstOrDefault(a => a.Value.Equals(expectedToken, StringComparison.Ordinal));
if(expectedToken == null || actualToken == null)
{
Logs.PayServer.LogDebug($"No token found for facade {facade} for SIN {this.GetBitIdentity().SIN}");
throw new BitpayHttpException(401, $"This endpoint does not support the `{actualTokens.Select(a => a.Facade).Concat(new[] { "user" }).FirstOrDefault()}` facade");
}
return actualToken;
}
private IEnumerable<BitTokenEntity> GetCompatibleTokens(BitTokenEntity token)
{
if(token.Facade == Facade.Merchant.ToString())
{
yield return token.Clone(Facade.User);
yield return token.Clone(Facade.PointOfSale);
}
if(token.Facade == Facade.PointOfSale.ToString())
{
yield return token.Clone(Facade.User);
}
yield return token;
}
private async Task<StoreData> FindStore(BitTokenEntity bitToken)
{
var store = await _StoreRepository.FindStore(bitToken.StoreId);
if(store == null)
throw new BitpayHttpException(401, "Unknown store");
return store;
}
}
return DataWrapper.Create(entities);
}
}
}

View File

@ -9,64 +9,79 @@ 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}")]
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest")]
public async Task<IActionResult> GetInvoiceRequest(string invoiceId)
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
if(invoice == null || invoice.IsExpired())
return NotFound();
[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();
PaymentRequest request = new PaymentRequest
{
DetailsVersion = 1
};
request.Details.Expires = invoice.ExpirationTime;
request.Details.Memo = invoice.ProductInformation.ItemDesc;
request.Details.Network = _Network;
request.Details.Outputs.Add(new PaymentOutput() { Amount = dto.BTCDue, Script = BitcoinAddress.Create(dto.BitcoinAddress, _Network).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 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");
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");
}
}
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);
}
return new PaymentRequestActionResult(request);
}
[HttpPost]
[Route("i/{invoiceId}", Order = 99)]
[MediaTypeConstraint("application/bitcoin-payment")]
public async Task<IActionResult> PostPayment(string invoiceId)
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
if(invoice == null || invoice.IsExpired())
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());
return new PaymentAckActionResult(payment.CreateACK(invoiceId + " is currently processing, thanks for your purchase..."));
}
}
[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,8 +1,9 @@
using BTCPayServer.Data;
using BTCPayServer.Filters;
using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Servcices.Invoices;
using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using NBitcoin;
@ -14,193 +15,541 @@ using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
using System.Net.WebSockets;
using System.Threading;
using BTCPayServer.Events;
using NBXplorer;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Security;
namespace BTCPayServer.Controllers
{
public partial class InvoiceController
{
[HttpGet]
[Route("invoices/{invoiceId}")]
public async Task<IActionResult> Invoice(string invoiceId)
{
var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery()
{
UserId = GetUserId(),
InvoiceId = invoiceId,
IncludeAddresses = true,
IncludeEvents = true
})).FirstOrDefault();
if (invoice == null)
return NotFound();
[HttpGet]
[Route("i/{invoiceId}")]
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest", false)]
public async Task<IActionResult> Checkout(string invoiceId)
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
if(invoice == null)
return NotFound();
var store = await _StoreRepository.FindStore(invoice.StoreId);
var dto = invoice.EntityToDTO();
var dto = invoice.EntityToDTO(_NetworkProvider);
var store = await _StoreRepository.FindStore(invoice.StoreId);
var model = new PaymentModel()
{
OrderId = invoice.OrderId,
InvoiceId = invoice.Id,
BTCAddress = invoice.DepositAddress.ToString(),
BTCAmount = (invoice.GetTotalCryptoDue() - invoice.TxFee).ToString(),
BTCTotalDue = invoice.GetTotalCryptoDue().ToString(),
BTCDue = invoice.GetCryptoDue().ToString(),
CustomerEmail = invoice.RefundMail,
ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds),
MaxTimeSeconds = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalSeconds,
ItemDesc = invoice.ProductInformation.ItemDesc,
Rate = invoice.Rate.ToString(),
RedirectUrl = invoice.RedirectURL,
StoreName = store.StoreName,
TxFees = invoice.TxFee.ToString(),
InvoiceBitcoinUrl = dto.PaymentUrls.BIP72,
TxCount = invoice.GetTxCount(),
BTCPaid = invoice.GetTotalPaid().ToString(),
Status = invoice.Status
};
InvoiceDetailsModel 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" :
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, _CurrencyNameTable),
NotificationUrl = invoice.NotificationURL,
RedirectUrl = invoice.RedirectURL,
ProductInformation = invoice.ProductInformation,
StatusException = invoice.ExceptionStatus,
Events = invoice.Events
};
var expiration = TimeSpan.FromSeconds((double)model.ExpirationSeconds);
model.TimeLeft = PrettyPrint(expiration);
return View(model);
}
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}";
private string PrettyPrint(TimeSpan expiration)
{
StringBuilder builder = new StringBuilder();
if(expiration.Days >= 1)
builder.Append(expiration.Days.ToString());
if(expiration.Hours >= 1)
builder.Append(expiration.Hours.ToString("00"));
builder.Append($"{expiration.Minutes.ToString("00")}:{expiration.Seconds.ToString("00")}");
return builder.ToString();
}
var onchainMethod = data.GetPaymentMethodDetails() as Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod;
if (onchainMethod != null)
{
cryptoPayment.Address = onchainMethod.DepositAddress;
}
cryptoPayment.Rate = ExchangeRate(data);
cryptoPayment.PaymentUrl = cryptoInfo.PaymentUrls.BIP21;
model.CryptoPayments.Add(cryptoPayment);
}
[HttpGet]
[Route("i/{invoiceId}/status")]
public async Task<IActionResult> GetStatus(string invoiceId)
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
if(invoice == null)
return NotFound();
return Content(invoice.Status);
}
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);
[HttpPost]
[Route("i/{invoiceId}/UpdateCustomer")]
public async Task<IActionResult> UpdateCustomer(string invoiceId, [FromBody]UpdateCustomerModel data)
{
if(!ModelState.IsValid)
{
return BadRequest(ModelState);
}
await _InvoiceRepository.UpdateInvoice(invoiceId, data).ConfigureAwait(false);
return Ok();
}
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);
}
[HttpGet]
[Route("invoices")]
[Authorize(AuthenticationSchemes = "Identity.Application")]
[BitpayAPIConstraint(false)]
public async Task<IActionResult> ListInvoices(string searchTerm = null, int skip = 0, int count = 20)
{
var model = new InvoicesModel();
foreach(var invoice in await _InvoiceRepository.GetInvoices(new InvoiceQuery()
{
TextSearch = searchTerm,
Count = count,
Skip = skip,
UserId = GetUserId()
}))
{
model.SearchTerm = searchTerm;
model.Invoices.Add(new InvoiceModel()
{
Status = invoice.Status,
Date = invoice.InvoiceTime,
InvoiceId = invoice.Id,
AmountCurrency = $"{invoice.ProductInformation.Price.ToString(CultureInfo.InvariantCulture)} {invoice.ProductInformation.Currency}"
});
}
model.Skip = skip;
model.Count = count;
model.StatusMessage = StatusMessage;
return View(model);
}
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;
return View(model);
}
[HttpGet]
[Route("invoices/create")]
[Authorize(AuthenticationSchemes = "Identity.Application")]
[BitpayAPIConstraint(false)]
public async Task<IActionResult> CreateInvoice()
{
var stores = await GetStores(GetUserId());
if(stores.Count() == 0)
{
StatusMessage = "Error: You need to create at least one store before creating a transaction";
return RedirectToAction(nameof(StoresController.ListStores), "Stores");
}
return View(new CreateInvoiceModel() { Stores = stores });
}
private string ToString(PaymentMethodId paymentMethodId)
{
var type = paymentMethodId.PaymentType.ToString();
switch (paymentMethodId.PaymentType)
{
case PaymentTypes.BTCLike:
type = "On-Chain";
break;
case PaymentTypes.LightningLike:
type = "Off-Chain";
break;
}
return $"{paymentMethodId.CryptoCode} ({type})";
}
[HttpPost]
[Route("invoices/create")]
[Authorize(AuthenticationSchemes = "Identity.Application")]
[BitpayAPIConstraint(false)]
public async Task<IActionResult> CreateInvoice(CreateInvoiceModel model)
{
if(!ModelState.IsValid)
{
model.Stores = await GetStores(GetUserId(), model.StoreId);
return View(model);
}
var store = await _StoreRepository.FindStore(model.StoreId, GetUserId());
if(string.IsNullOrEmpty(store.DerivationStrategy))
{
StatusMessage = "Error: You need to configure the derivation scheme in order to create an invoice";
return RedirectToAction(nameof(StoresController.UpdateStore), "Stores", new
{
storeId = store.Id
});
}
var result = await CreateInvoiceCore(new Invoice()
{
Price = model.Amount.Value,
Currency = "USD",
PosData = model.PosData,
OrderId = model.OrderId,
//RedirectURL = redirect + "redirect",
NotificationURL = model.NotificationUrl,
ItemDesc = model.ItemDesc,
FullNotifications = true,
BuyerEmail = model.BuyerEmail,
}, store);
StatusMessage = $"Invoice {result.Data.Id} just created!";
return RedirectToAction(nameof(ListInvoices));
}
[HttpGet]
[Route("i/{invoiceId}")]
[Route("i/{invoiceId}/{paymentMethodId}")]
[Route("invoice")]
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest", false)]
[XFrameOptionsAttribute(null)]
public async Task<IActionResult> Checkout(string invoiceId, string id = null, string paymentMethodId = null)
{
//Keep compatibility with Bitpay
invoiceId = invoiceId ?? id;
id = invoiceId;
////
private async Task<SelectList> GetStores(string userId, string storeId = null)
{
return new SelectList(await _StoreRepository.GetStoresByUserId(userId), nameof(StoreData.Id), nameof(StoreData.StoreName), storeId);
}
var model = await GetInvoiceModel(invoiceId, paymentMethodId);
if (model == null)
return NotFound();
[HttpPost]
[Authorize(AuthenticationSchemes = "Identity.Application")]
[BitpayAPIConstraint(false)]
public IActionResult SearchInvoice(InvoicesModel invoices)
{
return RedirectToAction(nameof(ListInvoices), new
{
searchTerm = invoices.SearchTerm,
skip = invoices.Skip,
count = invoices.Count,
});
}
return View(nameof(Checkout), model);
}
[TempData]
public string StatusMessage
{
get;
set;
}
private async Task<PaymentModel> GetInvoiceModel(string invoiceId, string paymentMethodIdStr)
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
if (invoice == null)
return null;
var store = await _StoreRepository.FindStore(invoice.StoreId);
bool isDefaultCrypto = false;
if (paymentMethodIdStr == null)
{
paymentMethodIdStr = store.GetDefaultCrypto();
isDefaultCrypto = true;
}
private string GetUserId()
{
return _UserManager.GetUserId(User);
}
}
var paymentMethodId = PaymentMethodId.Parse(paymentMethodIdStr);
var network = _NetworkProvider.GetNetwork(paymentMethodId.CryptoCode);
if (network == null && isDefaultCrypto)
{
network = _NetworkProvider.GetAll().FirstOrDefault();
paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
paymentMethodIdStr = paymentMethodId.ToString();
}
if (invoice == null || network == null)
return null;
if (!invoice.Support(paymentMethodId))
{
if (!isDefaultCrypto)
return null;
var paymentMethodTemp = invoice.GetPaymentMethods(_NetworkProvider).First();
network = paymentMethodTemp.Network;
paymentMethodId = paymentMethodTemp.GetId();
paymentMethodIdStr = paymentMethodId.ToString();
}
var paymentMethod = invoice.GetPaymentMethod(paymentMethodId, _NetworkProvider);
var paymentMethodDetails = paymentMethod.GetPaymentMethodDetails();
var dto = invoice.EntityToDTO(_NetworkProvider);
var cryptoInfo = dto.CryptoInfo.First(o => o.GetpaymentMethodId() == paymentMethodId);
var storeBlob = store.GetStoreBlob();
var currency = invoice.ProductInformation.Currency;
var accounting = paymentMethod.Calculate();
var model = new PaymentModel()
{
CryptoCode = network.CryptoCode,
PaymentMethodId = paymentMethodId.ToString(),
IsLightning = paymentMethodId.PaymentType == PaymentTypes.LightningLike,
ServerUrl = HttpContext.Request.GetAbsoluteRoot(),
OrderId = invoice.OrderId,
InvoiceId = invoice.Id,
DefaultLang = storeBlob.DefaultLang ?? "en-US",
HtmlTitle = storeBlob.HtmlTitle ?? "BTCPay Invoice",
CustomCSSLink = storeBlob.CustomCSS?.AbsoluteUri,
CustomLogoLink = storeBlob.CustomLogo?.AbsoluteUri,
BtcAddress = paymentMethodDetails.GetPaymentDestination(),
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 = ExchangeRate(paymentMethod),
MerchantRefLink = invoice.RedirectURL ?? "/",
StoreName = store.StoreName,
InvoiceBitcoinUrl = paymentMethodId.PaymentType == PaymentTypes.BTCLike ? cryptoInfo.PaymentUrls.BIP21 :
paymentMethodId.PaymentType == PaymentTypes.LightningLike ? cryptoInfo.PaymentUrls.BOLT11 :
throw new NotSupportedException(),
PeerInfo = (paymentMethodDetails as LightningLikePaymentMethodDetails)?.NodeInfo,
InvoiceBitcoinUrlQR = paymentMethodId.PaymentType == PaymentTypes.BTCLike ? cryptoInfo.PaymentUrls.BIP21 :
paymentMethodId.PaymentType == PaymentTypes.LightningLike ? cryptoInfo.PaymentUrls.BOLT11.ToUpperInvariant() :
throw new NotSupportedException(),
TxCount = accounting.TxRequired,
BtcPaid = accounting.Paid.ToString(),
Status = invoice.Status,
CryptoImage = "/" + GetImage(paymentMethodId, network),
NetworkFee = paymentMethodDetails.GetTxFee(),
IsMultiCurrency = invoice.GetPayments().Select(p => p.GetPaymentMethodId()).Concat(new[] { paymentMethod.GetId() }).Distinct().Count() > 1,
AllowCoinConversion = storeBlob.AllowCoinConversion,
AvailableCryptos = invoice.GetPaymentMethods(_NetworkProvider)
.Where(i => i.Network != null)
.Select(kv => new PaymentModel.AvailableCrypto()
{
PaymentMethodId = kv.GetId().ToString(),
CryptoImage = "/" + GetImage(kv.GetId(), kv.Network),
Link = Url.Action(nameof(Checkout), new { invoiceId = invoiceId, paymentMethodId = kv.GetId().ToString() })
}).Where(c => c.CryptoImage != "/")
.ToList()
};
var expiration = TimeSpan.FromSeconds(model.ExpirationSeconds);
model.TimeLeft = expiration.PrettyPrint();
return model;
}
private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network)
{
return (paymentMethodId.PaymentType == PaymentTypes.BTCLike ? Url.Content(network.CryptoImagePath) : Url.Content(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 FormatCurrency(productInformation.Price, productInformation.Currency, _CurrencyNameTable);
}
private string ExchangeRate(PaymentMethod paymentMethod)
{
string currency = paymentMethod.ParentEntity.ProductInformation.Currency;
return FormatCurrency(paymentMethod.Rate, currency, _CurrencyNameTable);
}
public static string FormatCurrency(decimal price, string currency, CurrencyNameTable currencies)
{
var provider = currencies.GetNumberFormatInfo(currency, true);
var currencyData = currencies.GetCurrencyData(currency, true);
var divisibility = currencyData.Divisibility;
while (true)
{
var rounded = decimal.Round(price, divisibility, MidpointRounding.AwayFromZero);
if ((Math.Abs(rounded - price) / price) < 0.001m)
{
price = rounded;
break;
}
divisibility++;
}
if (divisibility != provider.CurrencyDecimalDigits)
{
provider = (NumberFormatInfo)provider.Clone();
provider.CurrencyDecimalDigits = divisibility;
}
if (currencyData.Crypto)
return price.ToString("C", provider);
else
return price.ToString("C", provider) + $" ({currency})";
}
[HttpGet]
[Route("i/{invoiceId}/status")]
[Route("i/{invoiceId}/{paymentMethodId}/status")]
public async Task<IActionResult> GetStatus(string invoiceId, string paymentMethodId = null)
{
var model = await GetInvoiceModel(invoiceId, paymentMethodId);
if (model == null)
return NotFound();
return Json(model);
}
[HttpGet]
[Route("i/{invoiceId}/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")
return NotFound();
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
CompositeDisposable leases = new CompositeDisposable();
try
{
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)));
while (true)
{
var message = await webSocket.ReceiveAsync(DummyBuffer, default(CancellationToken));
if (message.MessageType == WebSocketMessageType.Close)
break;
}
}
finally
{
leases.Dispose();
await webSocket.CloseSocket();
}
return new EmptyResult();
}
ArraySegment<Byte> DummyBuffer = new ArraySegment<Byte>(new Byte[1]);
private async Task NotifySocket(WebSocket webSocket, string invoiceId, string expectedId)
{
if (invoiceId != expectedId || webSocket.State != WebSocketState.Open)
return;
CancellationTokenSource cts = new CancellationTokenSource();
cts.CancelAfter(5000);
try
{
await webSocket.SendAsync(DummyBuffer, WebSocketMessageType.Binary, true, cts.Token);
}
catch { try { webSocket.Dispose(); } catch { } }
}
[HttpPost]
[Route("i/{invoiceId}/UpdateCustomer")]
public async Task<IActionResult> UpdateCustomer(string invoiceId, [FromBody]UpdateCustomerModel data)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
await _InvoiceRepository.UpdateInvoice(invoiceId, data).ConfigureAwait(false);
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)
{
var model = new InvoicesModel();
var filterString = new SearchString(searchTerm);
foreach (var invoice in await _InvoiceRepository.GetInvoices(new InvoiceQuery()
{
TextSearch = filterString.TextSearch,
Count = count,
Skip = skip,
UserId = GetUserId(),
Unusual = !filterString.Filters.ContainsKey("unusual") ? null
: !bool.TryParse(filterString.Filters["unusual"].First(), out var r) ? (bool?)null
: r,
Status = filterString.Filters.ContainsKey("status") ? filterString.Filters["status"].ToArray() : null,
ExceptionStatus = filterString.Filters.ContainsKey("exceptionstatus") ? filterString.Filters["exceptionstatus"].ToArray() : null,
StoreId = filterString.Filters.ContainsKey("storeid") ? filterString.Filters["storeid"].ToArray() : null
}))
{
model.SearchTerm = searchTerm;
model.Invoices.Add(new InvoiceModel()
{
Status = invoice.Status + (invoice.ExceptionStatus == null ? string.Empty : $" ({invoice.ExceptionStatus})"),
ShowCheckout = invoice.Status == "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}"
});
}
model.Skip = skip;
model.Count = count;
model.StatusMessage = StatusMessage;
return View(model);
}
[HttpGet]
[Route("invoices/create")]
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
[BitpayAPIConstraint(false)]
public async Task<IActionResult> CreateInvoice()
{
var stores = new SelectList(await _StoreRepository.GetStoresByUserId(GetUserId()), nameof(StoreData.Id), nameof(StoreData.StoreName), null);
if (stores.Count() == 0)
{
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 });
}
[HttpPost]
[Route("invoices/create")]
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
[BitpayAPIConstraint(false)]
public async Task<IActionResult> CreateInvoice(CreateInvoiceModel model)
{
var stores = await _StoreRepository.GetStoresByUserId(GetUserId());
model.Stores = new SelectList(stores, nameof(StoreData.Id), nameof(StoreData.StoreName), model.StoreId);
var store = stores.FirstOrDefault(s => s.Id == model.StoreId);
if (store == null)
{
ModelState.AddModelError(nameof(model.StoreId), "Store not found");
}
if (!ModelState.IsValid)
{
return View(model);
}
StatusMessage = null;
if (!store.HasClaim(Policies.CanModifyStoreSettings.Key))
{
ModelState.AddModelError(nameof(model.StoreId), "You need to be owner of this store to create an invoice");
return View(model);
}
if (store.GetSupportedPaymentMethods(_NetworkProvider).Count() == 0)
{
ModelState.AddModelError(nameof(model.StoreId), "You need to configure the derivation scheme in order to create an invoice");
return View(model);
}
if (StatusMessage != null)
{
return RedirectToAction(nameof(StoresController.UpdateStore), "Stores", new
{
storeId = store.Id
});
}
try
{
var result = await CreateInvoiceCore(new Invoice()
{
Price = model.Amount.Value,
Currency = model.Currency,
PosData = model.PosData,
OrderId = model.OrderId,
//RedirectURL = redirect + "redirect",
NotificationURL = model.NotificationUrl,
ItemDesc = model.ItemDesc,
FullNotifications = true,
BuyerEmail = model.BuyerEmail,
}, store, HttpContext.Request.GetAbsoluteRoot());
StatusMessage = $"Invoice {result.Data.Id} just created!";
return RedirectToAction(nameof(ListInvoices));
}
catch (BitpayHttpException ex)
{
ModelState.TryAddModelError(nameof(model.Currency), $"Error: {ex.Message}");
return View(model);
}
}
[HttpPost]
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
[BitpayAPIConstraint(false)]
public IActionResult SearchInvoice(InvoicesModel invoices)
{
return RedirectToAction(nameof(ListInvoices), new
{
searchTerm = invoices.SearchTerm,
skip = invoices.Skip,
count = invoices.Count,
});
}
[HttpPost]
[Route("invoices/invalidatepaid")]
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
[BitpayAPIConstraint(false)]
public async Task<IActionResult> InvalidatePaidInvoice(string invoiceId)
{
await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoiceId);
_EventAggregator.Publish(new InvoiceEvent(invoiceId, 1008, "invoice_markedInvalid"));
return RedirectToAction(nameof(ListInvoices));
}
[TempData]
public string StatusMessage
{
get;
set;
}
private string GetUserId()
{
return _UserManager.GetUserId(User);
}
}
}

View File

@ -29,87 +29,283 @@ using BTCPayServer.Services;
using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;
using BTCPayServer.Services.Stores;
using BTCPayServer.Servcices.Invoices;
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 BTCPayServer.Payments;
using BTCPayServer.Rating;
namespace BTCPayServer.Controllers
{
public partial class InvoiceController : Controller
{
TokenRepository _TokenRepository;
InvoiceRepository _InvoiceRepository;
BTCPayWallet _Wallet;
IRateProvider _RateProvider;
private InvoiceWatcher _Watcher;
StoreRepository _StoreRepository;
Network _Network;
UserManager<ApplicationUser> _UserManager;
IFeeProvider _FeeProvider;
public partial class InvoiceController : Controller
{
InvoiceRepository _InvoiceRepository;
BTCPayRateProviderFactory _RateProvider;
StoreRepository _StoreRepository;
UserManager<ApplicationUser> _UserManager;
private CurrencyNameTable _CurrencyNameTable;
EventAggregator _EventAggregator;
BTCPayNetworkProvider _NetworkProvider;
private readonly BTCPayWalletProvider _WalletProvider;
IServiceProvider _ServiceProvider;
public InvoiceController(
IServiceProvider serviceProvider,
InvoiceRepository invoiceRepository,
CurrencyNameTable currencyNameTable,
UserManager<ApplicationUser> userManager,
BTCPayRateProviderFactory rateProvider,
StoreRepository storeRepository,
EventAggregator eventAggregator,
BTCPayWalletProvider walletProvider,
BTCPayNetworkProvider networkProvider)
{
_ServiceProvider = serviceProvider;
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
_StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository));
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
_RateProvider = rateProvider ?? throw new ArgumentNullException(nameof(rateProvider));
_UserManager = userManager;
_EventAggregator = eventAggregator;
_NetworkProvider = networkProvider;
_WalletProvider = walletProvider;
}
public InvoiceController(
Network network,
InvoiceRepository invoiceRepository,
UserManager<ApplicationUser> userManager,
TokenRepository tokenRepository,
BTCPayWallet wallet,
IRateProvider rateProvider,
StoreRepository storeRepository,
InvoiceWatcher watcher,
IFeeProvider feeProvider)
{
_StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository));
_Network = network ?? throw new ArgumentNullException(nameof(network));
_TokenRepository = tokenRepository ?? throw new ArgumentNullException(nameof(tokenRepository));
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
_Wallet = wallet ?? throw new ArgumentNullException(nameof(wallet));
_RateProvider = rateProvider ?? throw new ArgumentNullException(nameof(rateProvider));
_Watcher = watcher ?? throw new ArgumentNullException(nameof(watcher));
_UserManager = userManager;
_FeeProvider = feeProvider ?? throw new ArgumentNullException(nameof(feeProvider));
}
private async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store)
{
var derivationStrategy = store.DerivationStrategy;
var entity = new InvoiceEntity
{
InvoiceTime = DateTimeOffset.UtcNow,
DerivationStrategy = derivationStrategy ?? throw new BitpayHttpException(400, "This store has not configured the derivation strategy")
};
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 + TimeSpan.FromMinutes(15.0);
entity.ServerUrl = HttpContext.Request.GetAbsoluteRoot();
entity.FullNotifications = invoice.FullNotifications;
entity.NotificationURL = notificationUri?.AbsoluteUri;
entity.BuyerInformation = Map<Invoice, BuyerInformation>(invoice);
entity.RefundMail = EmailValidator.IsEmail(entity?.BuyerInformation?.BuyerEmail) ? entity.BuyerInformation.BuyerEmail : null;
entity.ProductInformation = Map<Invoice, ProductInformation>(invoice);
entity.RedirectURL = invoice.RedirectURL ?? store.StoreWebsite;
entity.Status = "new";
entity.SpeedPolicy = store.SpeedPolicy;
entity.TxFee = (await _FeeProvider.GetFeeRateAsync()).GetFee(100); // assume price for 100 bytes
entity.Rate = (double)await _RateProvider.GetRateAsync(invoice.Currency);
entity.PosData = invoice.PosData;
entity.DepositAddress = await _Wallet.ReserveAddressAsync(derivationStrategy);
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl)
{
var entity = new InvoiceEntity
{
InvoiceTime = DateTimeOffset.UtcNow
};
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity);
await _Wallet.MapAsync(entity.DepositAddress.ScriptPubKey, entity.Id);
await _Watcher.WatchAsync(entity.Id);
var resp = entity.EntityToDTO();
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
}
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);
entity.OrderId = invoice.OrderId;
entity.ServerUrl = serverUrl;
entity.FullNotifications = invoice.FullNotifications || invoice.ExtendedNotifications;
entity.ExtendedNotifications = invoice.ExtendedNotifications;
entity.NotificationURL = notificationUri?.AbsoluteUri;
entity.BuyerInformation = Map<Invoice, BuyerInformation>(invoice);
entity.PaymentTolerance = storeBlob.PaymentTolerance;
//Another way of passing buyer info to support
FillBuyerInfo(invoice.Buyer, entity.BuyerInformation);
if (entity?.BuyerInformation?.BuyerEmail != null)
{
if (!EmailValidator.IsEmail(entity.BuyerInformation.BuyerEmail))
throw new BitpayHttpException(400, "Invalid email");
entity.RefundMail = entity.BuyerInformation.BuyerEmail;
}
entity.ProductInformation = Map<Invoice, ProductInformation>(invoice);
entity.RedirectURL = invoice.RedirectURL ?? store.StoreWebsite;
if (!Uri.IsWellFormedUriString(entity.RedirectURL, UriKind.Absolute))
entity.RedirectURL = null;
private TDest Map<TFrom, TDest>(TFrom data)
{
return JsonConvert.DeserializeObject<TDest>(JsonConvert.SerializeObject(data));
}
}
entity.Status = "new";
entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy);
HashSet<CurrencyPair> currencyPairsToFetch = new HashSet<CurrencyPair>();
var rules = storeBlob.GetRateRules(_NetworkProvider);
foreach (var network in store.GetSupportedPaymentMethods(_NetworkProvider)
.Select(c => _NetworkProvider.GetNetwork(c.PaymentId.CryptoCode))
.Where(c => c != null))
{
currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, invoice.Currency));
if (storeBlob.LightningMaxValue != null)
currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, storeBlob.LightningMaxValue.Currency));
if (storeBlob.OnChainMinValue != null)
currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, storeBlob.OnChainMinValue.Currency));
}
var rateRules = storeBlob.GetRateRules(_NetworkProvider);
var fetchingByCurrencyPair = _RateProvider.FetchRates(currencyPairsToFetch, rateRules);
var supportedPaymentMethods = store.GetSupportedPaymentMethods(_NetworkProvider)
.Select(c =>
(Handler: (IPaymentMethodHandler)_ServiceProvider.GetService(typeof(IPaymentMethodHandler<>).MakeGenericType(c.GetType())),
SupportedPaymentMethod: c,
Network: _NetworkProvider.GetNetwork(c.PaymentId.CryptoCode)))
.Where(c => c.Network != null)
.Select(o =>
(SupportedPaymentMethod: o.SupportedPaymentMethod,
PaymentMethod: CreatePaymentMethodAsync(fetchingByCurrencyPair, o.Handler, o.SupportedPaymentMethod, o.Network, entity, store)))
.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()})");
}
}
if (supported.Count == 0)
{
StringBuilder errors = new StringBuilder();
errors.AppendLine("No payment method available for this store");
foreach (var error in paymentMethodErrors)
{
errors.AppendLine(error);
}
throw new BitpayHttpException(400, errors.ToString());
}
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"));
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)
{
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)
{
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 limitValueCrypto = Money.Coins(limitValue.Value / limitValueRate.Value.Value);
if (compare(paymentMethod.Calculate().Due, limitValueCrypto))
{
throw new PaymentMethodUnavailableException(errorMessage);
}
}
}
///////////////
#pragma warning disable CS0618
if (paymentMethod.GetId().IsBTCOnChain)
{
entity.TxFee = paymentMethod.TxFee;
entity.Rate = paymentMethod.Rate;
entity.DepositAddress = paymentMethod.DepositAddress;
}
#pragma warning restore CS0618
return paymentMethod;
}
private SpeedPolicy ParseSpeedPolicy(string transactionSpeed, SpeedPolicy defaultPolicy)
{
if (transactionSpeed == null)
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))
policy = defaultPolicy;
return policy;
}
private void FillBuyerInfo(Buyer buyer, BuyerInformation buyerInformation)
{
if (buyer == null)
return;
buyerInformation.BuyerAddress1 = buyerInformation.BuyerAddress1 ?? buyer.Address1;
buyerInformation.BuyerAddress2 = buyerInformation.BuyerAddress2 ?? buyer.Address2;
buyerInformation.BuyerCity = buyerInformation.BuyerCity ?? buyer.City;
buyerInformation.BuyerCountry = buyerInformation.BuyerCountry ?? buyer.country;
buyerInformation.BuyerEmail = buyerInformation.BuyerEmail ?? buyer.email;
buyerInformation.BuyerName = buyerInformation.BuyerName ?? buyer.Name;
buyerInformation.BuyerPhone = buyerInformation.BuyerPhone ?? buyer.phone;
buyerInformation.BuyerState = buyerInformation.BuyerState ?? buyer.State;
buyerInformation.BuyerZip = buyerInformation.BuyerZip ?? buyer.zip;
}
private TDest Map<TFrom, TDest>(TFrom data)
{
return JsonConvert.DeserializeObject<TDest>(JsonConvert.SerializeObject(data));
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -7,34 +7,34 @@ 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;
}
}
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

@ -7,34 +7,128 @@ using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Filters;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Rating;
using Newtonsoft.Json;
namespace BTCPayServer.Controllers
{
public class RateController : Controller
{
IRateProvider _RateProvider;
CurrencyNameTable _CurrencyNameTable;
public RateController(IRateProvider rateProvider, CurrencyNameTable currencyNameTable)
{
_RateProvider = rateProvider ?? throw new ArgumentNullException(nameof(rateProvider));
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
}
public class RateController : Controller
{
BTCPayRateProviderFactory _RateProviderFactory;
BTCPayNetworkProvider _NetworkProvider;
CurrencyNameTable _CurrencyNameTable;
StoreRepository _StoreRepo;
public RateController(
BTCPayRateProviderFactory rateProviderFactory,
BTCPayNetworkProvider networkProvider,
StoreRepository storeRepo,
CurrencyNameTable currencyNameTable)
{
_RateProviderFactory = rateProviderFactory ?? throw new ArgumentNullException(nameof(rateProviderFactory));
_NetworkProvider = networkProvider;
_StoreRepo = storeRepo;
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
}
[Route("rates")]
[HttpGet]
[BitpayAPIConstraint]
public async Task<DataWrapper<NBitpayClient.Rate[]>> GetRates()
{
var allRates = (await _RateProvider.GetRatesAsync());
return new DataWrapper<NBitpayClient.Rate[]>
(allRates.Select(r =>
new NBitpayClient.Rate()
{
Code = r.Currency,
Name = _CurrencyNameTable.GetCurrencyData(r.Currency)?.Name,
Value = r.Value
}).Where(n => n.Name != null).ToArray());
[Route("rates")]
[HttpGet]
[BitpayAPIConstraint]
public async Task<IActionResult> GetRates(string currencyPairs, string storeId)
{
storeId = storeId ?? this.HttpContext.GetStoreData()?.Id;
var result = await GetRates2(currencyPairs, storeId);
var rates = (result as JsonResult)?.Value as Rate[];
if (rates == null)
return result;
return Json(new DataWrapper<Rate[]>(rates));
}
}
}
[Route("api/rates")]
[HttpGet]
public async Task<IActionResult> GetRates2(string currencyPairs, string storeId)
{
if(storeId == null || currencyPairs == null)
{
var result = Json(new BitpayErrorsModel() { Error = "You need to specify storeId (in your store settings) and currencyPairs (eg. BTC_USD,LTC_CAD)" });
result.StatusCode = 400;
return result;
}
var store = this.HttpContext.GetStoreData();
if(store == null || store.Id != storeId)
store = await _StoreRepo.FindStore(storeId);
if (store == null)
{
var result = Json(new BitpayErrorsModel() { Error = "Store not found" });
result.StatusCode = 404;
return result;
}
var rules = store.GetStoreBlob().GetRateRules(_NetworkProvider);
HashSet<CurrencyPair> pairs = new HashSet<CurrencyPair>();
foreach(var currency in currencyPairs.Split(','))
{
if(!CurrencyPair.TryParse(currency, out var pair))
{
var result = Json(new BitpayErrorsModel() { Error = $"Currency pair {currency} uncorrectly formatted" });
result.StatusCode = 400;
return result;
}
pairs.Add(pair);
}
var fetching = _RateProviderFactory.FetchRates(pairs, rules);
await Task.WhenAll(fetching.Select(f => f.Value).ToArray());
return Json(pairs
.Select(r => (Pair: r, Value: fetching[r].GetAwaiter().GetResult().Value))
.Where(r => r.Value.HasValue)
.Select(r =>
new Rate()
{
CryptoCode = r.Pair.Left,
Code = r.Pair.Right,
CurrencyPair = r.Pair.ToString(),
Name = _CurrencyNameTable.GetCurrencyData(r.Pair.Right, true).Name,
Value = r.Value.Value
}).Where(n => n.Name != null).ToArray());
}
public class Rate
{
[JsonProperty(PropertyName = "name")]
public string Name
{
get;
set;
}
[JsonProperty(PropertyName = "cryptoCode")]
public string CryptoCode
{
get;
set;
}
[JsonProperty(PropertyName = "currencyPair")]
public string CurrencyPair
{
get;
set;
}
[JsonProperty(PropertyName = "code")]
public string Code
{
get;
set;
}
[JsonProperty(PropertyName = "rate")]
public decimal Value
{
get;
set;
}
}
}
}

View File

@ -1,7 +1,9 @@
using BTCPayServer.Models;
using BTCPayServer.HostedServices;
using BTCPayServer.Models;
using BTCPayServer.Models.ServerViewModels;
using BTCPayServer.Services;
using BTCPayServer.Services.Mails;
using BTCPayServer.Services.Rates;
using BTCPayServer.Validations;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
@ -11,87 +13,257 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Mail;
using System.Threading.Tasks;
namespace BTCPayServer.Controllers
{
[Authorize(Roles = Roles.ServerAdmin)]
public class ServerController : Controller
{
private UserManager<ApplicationUser> _UserManager;
SettingsRepository _SettingsRepository;
[Authorize(Policy = BTCPayServer.Security.Policies.CanModifyServerSettings.Key)]
public class ServerController : Controller
{
private UserManager<ApplicationUser> _UserManager;
SettingsRepository _SettingsRepository;
private BTCPayRateProviderFactory _RateProviderFactory;
public ServerController(UserManager<ApplicationUser> userManager, SettingsRepository settingsRepository)
{
_UserManager = userManager;
_SettingsRepository = settingsRepository;
}
public ServerController(UserManager<ApplicationUser> userManager,
BTCPayRateProviderFactory rateProviderFactory,
SettingsRepository settingsRepository)
{
_UserManager = userManager;
_SettingsRepository = settingsRepository;
_RateProviderFactory = rateProviderFactory;
}
[Route("server/users")]
public IActionResult ListUsers()
{
var users = new UsersViewModel();
users.Users
= _UserManager.Users.Select(u => new UsersViewModel.UserViewModel()
{
Name = u.UserName,
Email = u.Email
}).ToList();
return View(users);
}
[Route("server/rates")]
public async Task<IActionResult> Rates()
{
var rates = (await _SettingsRepository.GetSettingAsync<RatesSetting>()) ?? new RatesSetting();
[Route("server/emails")]
public async Task<IActionResult> Emails()
{
var data = (await _SettingsRepository.GetSettingAsync<EmailSettings>()) ?? new EmailSettings();
return View(new EmailsViewModel() { Settings = data });
}
var vm = new RatesViewModel()
{
CacheMinutes = rates.CacheInMinutes,
PrivateKey = rates.PrivateKey,
PublicKey = rates.PublicKey
};
await FetchRateLimits(vm);
return View(vm);
}
[Route("server/policies")]
public async Task<IActionResult> Policies()
{
var data = (await _SettingsRepository.GetSettingAsync<PoliciesSettings>()) ?? new PoliciesSettings();
return View(data);
}
[Route("server/policies")]
[HttpPost]
public async Task<IActionResult> Policies(PoliciesSettings settings)
{
await _SettingsRepository.UpdateSetting(settings);
TempData["StatusMessage"] = "Policies upadated successfully";
return View(settings);
}
private static async Task FetchRateLimits(RatesViewModel vm)
{
var coinAverage = GetCoinaverageService(vm, false);
if (coinAverage != null)
{
try
{
vm.RateLimits = await coinAverage.GetRateLimitsAsync();
}
catch { }
}
}
[Route("server/emails")]
[HttpPost]
public async Task<IActionResult> Emails(EmailsViewModel model, string command)
{
if(command == "Test")
{
if(!ModelState.IsValid)
return View(model);
try
{
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
{
ModelState.Remove(nameof(model.TestEmail));
if(!ModelState.IsValid)
return View(model);
await _SettingsRepository.UpdateSetting(model.Settings);
model.StatusMessage = "Email settings saved";
return View(model);
}
}
}
[Route("server/rates")]
[HttpPost]
public async Task<IActionResult> Rates(RatesViewModel vm)
{
var rates = (await _SettingsRepository.GetSettingAsync<RatesSetting>()) ?? new RatesSetting();
rates.PrivateKey = vm.PrivateKey;
rates.PublicKey = vm.PublicKey;
rates.CacheInMinutes = vm.CacheMinutes;
try
{
var service = GetCoinaverageService(vm, true);
if(service != null)
await service.TestAuthAsync();
}
catch
{
ModelState.AddModelError(nameof(vm.PrivateKey), "Invalid API key pair");
}
if (!ModelState.IsValid)
{
await FetchRateLimits(vm);
return View(vm);
}
await _SettingsRepository.UpdateSetting(rates);
StatusMessage = "Rate settings successfully updated";
return RedirectToAction(nameof(Rates));
}
private static CoinAverageRateProvider GetCoinaverageService(RatesViewModel vm, bool withAuth)
{
var settings = new CoinAverageSettings()
{
KeyPair = (vm.PublicKey, vm.PrivateKey)
};
if (!withAuth || settings.GetCoinAverageSignature() != null)
{
return new CoinAverageRateProvider()
{ Authenticator = settings };
}
return null;
}
[Route("server/users")]
public IActionResult ListUsers()
{
var users = new UsersViewModel();
users.StatusMessage = StatusMessage;
users.Users
= _UserManager.Users.Select(u => new UsersViewModel.UserViewModel()
{
Name = u.UserName,
Email = u.Email,
Id = u.Id
}).ToList();
return View(users);
}
[Route("server/users/{userId}")]
public new async Task<IActionResult> User(string userId)
{
var user = await _UserManager.FindByIdAsync(userId);
if (user == null)
return NotFound();
var roles = await _UserManager.GetRolesAsync(user);
var userVM = new UserViewModel();
userVM.Id = user.Id;
userVM.Email = user.Email;
userVM.IsAdmin = IsAdmin(roles);
return View(userVM);
}
private static bool IsAdmin(IList<string> roles)
{
return roles.Contains(Roles.ServerAdmin, StringComparer.Ordinal);
}
[Route("server/users/{userId}")]
[HttpPost]
public new async Task<IActionResult> User(string userId, UserViewModel viewModel)
{
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)
{
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);
}
[Route("server/users/{userId}/delete")]
public async Task<IActionResult> DeleteUser(string userId)
{
var user = userId == null ? null : await _UserManager.FindByIdAsync(userId);
if (user == null)
return NotFound();
return View("Confirm", new ConfirmModel()
{
Title = "Delete user " + user.Email,
Description = "This user will be permanently deleted",
Action = "Delete"
});
}
[Route("server/users/{userId}/delete")]
[HttpPost]
public async Task<IActionResult> DeleteUserPost(string userId)
{
var user = userId == null ? null : await _UserManager.FindByIdAsync(userId);
if (user == null)
return NotFound();
await _UserManager.DeleteAsync(user);
StatusMessage = "User deleted";
return RedirectToAction(nameof(ListUsers));
}
[TempData]
public string StatusMessage
{
get; set;
}
[Route("server/emails")]
public async Task<IActionResult> Emails()
{
var data = (await _SettingsRepository.GetSettingAsync<EmailSettings>()) ?? new EmailSettings();
return View(new EmailsViewModel() { Settings = data });
}
[Route("server/policies")]
public async Task<IActionResult> Policies()
{
var data = (await _SettingsRepository.GetSettingAsync<PoliciesSettings>()) ?? new PoliciesSettings();
return View(data);
}
[Route("server/policies")]
[HttpPost]
public async Task<IActionResult> Policies(PoliciesSettings settings)
{
await _SettingsRepository.UpdateSetting(settings);
TempData["StatusMessage"] = "Policies updated successfully";
return View(settings);
}
[Route("server/theme")]
public async Task<IActionResult> Theme()
{
var data = (await _SettingsRepository.GetSettingAsync<ThemeSettings>()) ?? new ThemeSettings();
return View(data);
}
[Route("server/theme")]
[HttpPost]
public async Task<IActionResult> Theme(ThemeSettings settings)
{
await _SettingsRepository.UpdateSetting(settings);
TempData["StatusMessage"] = "Theme settings updated successfully";
return View(settings);
}
[Route("server/emails")]
[HttpPost]
public async Task<IActionResult> Emails(EmailsViewModel model, string command)
{
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")
{
await _SettingsRepository.UpdateSetting(model.Settings);
model.StatusMessage = "Email settings saved";
return View(model);
}
}
}
}

View File

@ -0,0 +1,422 @@
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.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Services;
using LedgerWallet;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBXplorer.DerivationStrategy;
using Newtonsoft.Json;
namespace BTCPayServer.Controllers
{
public partial class StoresController
{
[HttpGet]
[Route("{storeId}/derivations/{cryptoCode}")]
public IActionResult AddDerivationScheme(string storeId, string cryptoCode)
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
var network = cryptoCode == null ? null : _ExplorerProvider.GetNetwork(cryptoCode);
if (network == null)
{
return NotFound();
}
DerivationSchemeViewModel vm = new DerivationSchemeViewModel();
vm.ServerUrl = GetStoreUrl(storeId);
vm.CryptoCode = cryptoCode;
vm.RootKeyPath = network.GetRootKeyPath();
SetExistingValues(store, vm);
return View(vm);
}
private void SetExistingValues(StoreData store, DerivationSchemeViewModel vm)
{
vm.DerivationScheme = GetExistingDerivationStrategy(vm.CryptoCode, store)?.DerivationStrategyBase.ToString();
}
private DerivationStrategy GetExistingDerivationStrategy(string cryptoCode, StoreData store)
{
var id = new PaymentMethodId(cryptoCode, PaymentTypes.BTCLike);
var existing = store.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<DerivationStrategy>()
.FirstOrDefault(d => d.PaymentId == id);
return existing;
}
[HttpPost]
[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)
return NotFound();
var network = cryptoCode == null ? null : _ExplorerProvider.GetNetwork(cryptoCode);
if (network == null)
{
return NotFound();
}
vm.RootKeyPath = network.GetRootKeyPath();
var wallet = _WalletProvider.GetWallet(network);
if (wallet == null)
{
return NotFound();
}
PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
DerivationStrategy strategy = null;
try
{
if (!string.IsNullOrEmpty(vm.DerivationScheme))
{
strategy = ParseDerivationStrategy(vm.DerivationScheme, null, network);
vm.DerivationScheme = strategy.ToString();
}
}
catch
{
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme");
vm.Confirmation = false;
return View(vm);
}
if (!vm.Confirmation && strategy != null)
return ShowAddresses(vm, strategy);
if (vm.Confirmation && !string.IsNullOrWhiteSpace(vm.HintAddress))
{
BitcoinAddress address = null;
try
{
address = BitcoinAddress.Create(vm.HintAddress, network.NBitcoinNetwork);
}
catch
{
ModelState.AddModelError(nameof(vm.HintAddress), "Invalid hint address");
return ShowAddresses(vm, strategy);
}
try
{
strategy = ParseDerivationStrategy(vm.DerivationScheme, address.ScriptPubKey, network);
}
catch
{
ModelState.AddModelError(nameof(vm.HintAddress), "Impossible to find a match with this address");
return ShowAddresses(vm, strategy);
}
vm.HintAddress = "";
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 });
}
}
private IActionResult ShowAddresses(DerivationSchemeViewModel vm, DerivationStrategy strategy)
{
vm.DerivationScheme = strategy.DerivationStrategyBase.ToString();
if (!string.IsNullOrEmpty(vm.DerivationScheme))
{
var line = strategy.DerivationStrategyBase.GetLineFor(DerivationFeature.Deposit);
for (int i = 0; i < 10; i++)
{
var address = line.Derive((uint)i);
vm.AddressSamples.Add((DerivationStrategyBase.GetKeyPath(DerivationFeature.Deposit).Derive((uint)i).ToString(), address.ScriptPubKey.GetDestinationAddress(strategy.Network.NBitcoinNetwork).ToString()));
}
}
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.GetKeyPath(network, strategy) == null)
{
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 send = new[] { (
destination: destinationAddress as IDestination,
amount: amountBTC,
substractFees: subsctractFeesValue) };
foreach (var element in send)
{
if (element.destination == null)
throw new ArgumentNullException(nameof(element.destination));
if (element.amount == null)
throw new ArgumentNullException(nameof(element.amount));
if (element.amount <= Money.Zero)
throw new ArgumentOutOfRangeException(nameof(element.amount), "The amount should be above zero");
}
var foundKeyPath = await hw.GetKeyPath(network, strategy);
if (foundKeyPath == null)
{
throw new HardwareWalletException($"This store is not configured to use this ledger");
}
TransactionBuilder builder = new TransactionBuilder();
builder.StandardTransactionPolicy.MinRelayTxFee = summary.Status.BitcoinStatus.MinRelayTxFee;
builder.SetConsensusFactory(network.NBitcoinNetwork);
builder.AddCoins(unspentCoins.Select(c => c.Coin).ToArray());
foreach (var element in send)
{
builder.Send(element.destination, element.amount);
if (element.substractFees)
builder.SubtractFees();
}
builder.SetChange(changeAddress.Item1);
builder.SendEstimatedFees(feeRateValue);
builder.Shuffle();
var unsigned = builder.BuildTransaction(false);
var keypaths = new Dictionary<Script, KeyPath>();
foreach (var c in unspentCoins)
{
keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath);
}
var hasChange = unsigned.Outputs.Count == 2;
var usedCoins = builder.FindSpentCoins(unsigned);
Dictionary<uint256, Transaction> parentTransactions = new Dictionary<uint256, Transaction>();
if(!strategy.Segwit)
{
var parentHashes = usedCoins.Select(c => c.Outpoint.Hash).ToHashSet();
var explorer = _ExplorerProvider.GetExplorerClient(network);
var getTransactionAsyncs = parentHashes.Select(h => (Op: explorer.GetTransactionAsync(h), Hash: h)).ToList();
foreach(var getTransactionAsync in getTransactionAsyncs)
{
var tx = (await getTransactionAsync.Op);
if(tx == null)
throw new Exception($"Parent transaction {getTransactionAsync.Hash} not found");
parentTransactions.Add(tx.Transaction.GetHash(), tx.Transaction);
}
}
var transaction = await hw.SignTransactionAsync(usedCoins.Select(c => new SignatureRequest
{
InputTransaction = parentTransactions.TryGet(c.Outpoint.Hash),
InputCoin = c,
KeyPath = foundKeyPath.Derive(keypaths[c.TxOut.ScriptPubKey]),
PubKey = strategy.Root.Derive(keypaths[c.TxOut.ScriptPubKey]).PubKey
}).ToArray(), unsigned, hasChange ? foundKeyPath.Derive(changeAddress.Item2) : null);
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;
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,154 @@
using System;
using Microsoft.Extensions.DependencyInjection;
using System.Collections.Generic;
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;
namespace BTCPayServer.Controllers
{
public partial class StoresController
{
[HttpGet]
[Route("{storeId}/lightning/{cryptoCode}")]
public IActionResult AddLightningNode(string storeId, string cryptoCode)
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
LightningNodeViewModel vm = new LightningNodeViewModel();
vm.CryptoCode = cryptoCode;
vm.InternalLightningNode = GetInternalLighningNode(cryptoCode)?.ToUri(true)?.AbsoluteUri;
SetExistingValues(store, vm);
return View(vm);
}
private void SetExistingValues(StoreData store, LightningNodeViewModel vm)
{
vm.Url = GetExistingLightningSupportedPaymentMethod(vm.CryptoCode, store)?.GetLightningUrl()?.ToString();
}
private LightningSupportedPaymentMethod GetExistingLightningSupportedPaymentMethod(string cryptoCode, StoreData store)
{
var id = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
var existing = store.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<LightningSupportedPaymentMethod>()
.FirstOrDefault(d => d.PaymentId == id);
return existing;
}
private LightningConnectionString GetInternalLighningNode(string cryptoCode)
{
if (_BtcpayServerOptions.InternalLightningByCryptoCode.TryGetValue(cryptoCode, out var connectionString))
{
return CanUseInternalLightning() ? connectionString : null;
}
return null;
}
[HttpPost]
[Route("{storeId}/lightning/{cryptoCode}")]
public async Task<IActionResult> AddLightningNode(string storeId, LightningNodeViewModel vm, string command, string cryptoCode)
{
vm.CryptoCode = cryptoCode;
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
var network = vm.CryptoCode == null ? null : _ExplorerProvider.GetNetwork(vm.CryptoCode);
var internalLightning = GetInternalLighningNode(network.CryptoCode);
vm.InternalLightningNode = internalLightning?.ToUri(true)?.AbsoluteUri;
if (network == null)
{
ModelState.AddModelError(nameof(vm.CryptoCode), "Invalid network");
return View(vm);
}
PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.LightningLike);
Payments.Lightning.LightningSupportedPaymentMethod paymentMethod = null;
if (!string.IsNullOrEmpty(vm.Url))
{
if (!LightningConnectionString.TryParse(vm.Url, out var connectionString, out var error))
{
ModelState.AddModelError(nameof(vm.Url), $"Invalid URL ({error})");
return View(vm);
}
var internalDomain = internalLightning?.ToUri(false)?.DnsSafeHost;
bool isLocal = (internalDomain == "127.0.0.1" || internalDomain == "localhost");
bool isInternalNode = connectionString.ConnectionType == LightningConnectionType.CLightning ||
connectionString.BaseUri.DnsSafeHost == internalDomain ||
isLocal;
if (connectionString.BaseUri.Scheme == "http" && !isLocal)
{
if (!isInternalNode || (isInternalNode && !CanUseInternalLightning()))
{
ModelState.AddModelError(nameof(vm.Url), "The url must be HTTPS");
return View(vm);
}
}
if (isInternalNode && !CanUseInternalLightning())
{
ModelState.AddModelError(nameof(vm.Url), "Unauthorized url");
return View(vm);
}
paymentMethod = new Payments.Lightning.LightningSupportedPaymentMethod()
{
CryptoCode = paymentMethodId.CryptoCode
};
paymentMethod.SetLightningUrl(connectionString);
}
if (command == "save")
{
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");
return View(vm);
}
var handler = (LightningLikePaymentHandler)_ServiceProvider.GetRequiredService<IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>>();
try
{
var info = await handler.Test(paymentMethod, network);
if (!vm.SkipPortTest)
{
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)))
{
await handler.TestConnection(info, cts.Token);
}
}
vm.StatusMessage = $"Connection to the lightning node succeed ({info})";
}
catch (Exception ex)
{
vm.StatusMessage = $"Error: {ex.Message}";
return View(vm);
}
return View(vm);
}
}
private bool CanUseInternalLightning()
{
return (_BTCPayEnv.IsDevelopping || User.IsInRole(Roles.ServerAdmin));
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,148 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Security;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using NBXplorer.DerivationStrategy;
namespace BTCPayServer.Controllers
{
[Route("stores")]
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
[AutoValidateAntiforgeryToken]
public partial class UserStoresController : Controller
{
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")]
public IActionResult CreateStore()
{
return View();
}
public string CreatedStoreId
{
get; set;
}
[HttpPost]
[Route("{storeId}/delete")]
public async Task<IActionResult> DeleteStorePost(string storeId)
{
var userId = GetUserId();
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
await _Repo.RemoveStore(storeId, userId);
StatusMessage = "Store removed successfully";
return RedirectToAction(nameof(ListStores));
}
[TempData]
public string StatusMessage { get; set; }
[HttpGet]
public async Task<IActionResult> ListStores()
{
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];
result.Stores.Add(new StoresViewModel.StoreViewModel()
{
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>()
});
}
return View(result);
}
[HttpPost]
[Route("create")]
public async Task<IActionResult> CreateStore(CreateStoreViewModel vm)
{
if (!ModelState.IsValid)
{
return View(vm);
}
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)))
{
try
{
return (await _.Wallet.GetBalance(_.DerivationStrategy, cts.Token)).ToString();
}
catch
{
return "--";
}
}
}
private string GetUserId()
{
return _UserManager.GetUserId(User);
}
}
}

View File

@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
namespace BTCPayServer
{
public class CurrencyValue
{
static Regex _Regex = new Regex("^([0-9]+(\\.[0-9]+)?)\\s*([a-zA-Z]+)$");
static CurrencyNameTable _CurrencyTable = new CurrencyNameTable();
public static bool TryParse(string str, out CurrencyValue value)
{
value = null;
var match = _Regex.Match(str);
if (!match.Success ||
!decimal.TryParse(match.Groups[1].Value, out var v))
return false;
var currency = match.Groups.Last().Value.ToUpperInvariant();
var currencyData = _CurrencyTable.GetCurrencyData(currency, false);
if (currencyData == null)
return false;
v = Math.Round(v, currencyData.Divisibility);
value = new CurrencyValue()
{
Value = v,
Currency = currency
};
return true;
}
public decimal Value { get; set; }
public string Currency { get; set; }
public override string ToString()
{
return Value.ToString(CultureInfo.InvariantCulture) + " " + Currency;
}
}
}

View File

@ -0,0 +1,98 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace BTCPayServer
{
class CustomThreadPool : IDisposable
{
CancellationTokenSource _Cancel = new CancellationTokenSource();
TaskCompletionSource<bool> _Exited;
int _ExitedCount = 0;
Thread[] _Threads;
Exception _UnhandledException;
BlockingCollection<(Action, TaskCompletionSource<object>)> _Actions = new BlockingCollection<(Action, TaskCompletionSource<object>)>(new ConcurrentQueue<(Action, TaskCompletionSource<object>)>());
public CustomThreadPool(int threadCount, string threadName)
{
if (threadCount <= 0)
throw new ArgumentOutOfRangeException(nameof(threadCount));
_Exited = new TaskCompletionSource<bool>();
_Threads = Enumerable.Range(0, threadCount).Select(_ => new Thread(RunLoop) { Name = threadName }).ToArray();
foreach (var t in _Threads)
t.Start();
}
public void Do(Action act)
{
DoAsync(act).GetAwaiter().GetResult();
}
public T Do<T>(Func<T> act)
{
return DoAsync(act).GetAwaiter().GetResult();
}
public async Task<T> DoAsync<T>(Func<T> act)
{
TaskCompletionSource<object> done = new TaskCompletionSource<object>();
_Actions.Add((() =>
{
try
{
done.TrySetResult(act());
}
catch (Exception ex) { done.TrySetException(ex); }
}
, done));
return (T)(await done.Task.ConfigureAwait(false));
}
public Task DoAsync(Action act)
{
return DoAsync<object>(() =>
{
act();
return null;
});
}
void RunLoop()
{
try
{
foreach (var act in _Actions.GetConsumingEnumerable(_Cancel.Token))
{
act.Item1();
}
}
catch (OperationCanceledException) when (_Cancel.IsCancellationRequested) { }
catch (Exception ex)
{
_Cancel.Cancel();
_UnhandledException = ex;
}
if (Interlocked.Increment(ref _ExitedCount) == _Threads.Length)
{
foreach (var action in _Actions)
{
try
{
action.Item2.TrySetCanceled();
}
catch { }
}
_Exited.TrySetResult(true);
}
}
public void Dispose()
{
_Cancel.Cancel();
_Exited.Task.GetAwaiter().GetResult();
}
}
}

View File

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Data
{
public class APIKeyData
{
[MaxLength(50)]
public string Id
{
get; set;
}
[MaxLength(50)]
public string StoreId
{
get; set;
}
}
}

View File

@ -2,24 +2,67 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Payments;
using BTCPayServer.Services.Invoices;
using NBitcoin;
namespace BTCPayServer.Data
{
public class AddressInvoiceData
{
public string Address
{
get; set;
}
public class AddressInvoiceData
{
/// <summary>
/// Some crypto currencies share same address prefix
/// For not having exceptions thrown by two address on different network, we suffix by "#CRYPTOCODE"
/// </summary>
[Obsolete("Use GetHash instead")]
public string Address
{
get; set;
}
public InvoiceData InvoiceData
{
get; set;
}
public string InvoiceDataId
{
get; set;
}
}
#pragma warning disable CS0618
public string GetAddress()
{
if (Address == null)
return null;
var index = Address.LastIndexOf("#", StringComparison.InvariantCulture);
if (index == -1)
return Address;
return Address.Substring(0, index);
}
public AddressInvoiceData Set(string address, PaymentMethodId paymentMethodId)
{
Address = address + "#" + paymentMethodId.ToString();
return this;
}
public PaymentMethodId GetpaymentMethodId()
{
if (Address == null)
return null;
var index = Address.LastIndexOf("#", StringComparison.InvariantCulture);
// Legacy AddressInvoiceData does not have the paymentMethodId attached to the Address
if (index == -1)
return PaymentMethodId.Parse("BTC");
/////////////////////////
return PaymentMethodId.Parse(Address.Substring(index + 1));
}
#pragma warning restore CS0618
public InvoiceData InvoiceData
{
get; set;
}
public string InvoiceDataId
{
get; set;
}
public DateTimeOffset? CreatedTime
{
get; set;
}
}
}

View File

@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace BTCPayServer.Data
{
public class AppData
{
public string Id { get; set; }
public string Name { get; set; }
public string StoreDataId
{
get; set;
}
public string AppType { get; set; }
public StoreData StoreData
{
get; set;
}
public DateTimeOffset Created
{
get; set;
}
public string Settings { get; set; }
public T GetSettings<T>() where T : class, new()
{
if (String.IsNullOrEmpty(Settings))
return new T();
return JsonConvert.DeserializeObject<T>(Settings);
}
public void SetSettings(object value)
{
Settings = value == null ? null : JsonConvert.SerializeObject(value);
}
}
}

View File

@ -10,110 +10,160 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
namespace BTCPayServer.Data
{
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public ApplicationDbContext()
{
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public ApplicationDbContext()
{
}
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
}
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
public DbSet<InvoiceData> Invoices
{
get; set;
}
public DbSet<InvoiceData> Invoices
{
get; set;
}
public DbSet<RefundAddressesData> RefundAddresses
{
get; set;
}
public DbSet<AppData> Apps
{
get; set;
}
public DbSet<PaymentData> Payments
{
get; set;
}
public DbSet<InvoiceEventData> InvoiceEvents
{
get; set;
}
public DbSet<StoreData> Stores
{
get; set;
}
public DbSet<HistoricalAddressInvoiceData> HistoricalAddressInvoices
{
get; set;
}
public DbSet<UserStore> UserStore
{
get; set;
}
public DbSet<PendingInvoiceData> PendingInvoices
{
get; set;
}
public DbSet<RefundAddressesData> RefundAddresses
{
get; set;
}
public DbSet<AddressInvoiceData> AddressInvoices
{
get; set;
}
public DbSet<PaymentData> Payments
{
get; set;
}
public DbSet<SettingData> Settings
{
get; set;
}
public DbSet<StoreData> Stores
{
get; set;
}
public DbSet<UserStore> UserStore
{
get; set;
}
public DbSet<AddressInvoiceData> AddressInvoices
{
get; set;
}
public DbSet<SettingData> Settings
{
get; set;
}
public DbSet<PairingCodeData> PairingCodes
{
get; set;
}
public DbSet<PairingCodeData> PairingCodes
{
get; set;
}
public DbSet<PairedSINData> PairedSINData
{
get; set;
}
public DbSet<PairedSINData> PairedSINData
{
get; set;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var isConfigured = optionsBuilder.Options.Extensions.OfType<RelationalOptionsExtension>().Any();
if(!isConfigured)
optionsBuilder.UseSqlite("Data Source=temp.db");
}
public DbSet<APIKeyData> ApiKeys
{
get; set;
}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<InvoiceData>()
.HasIndex(o => o.StoreDataId);
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var isConfigured = optionsBuilder.Options.Extensions.OfType<RelationalOptionsExtension>().Any();
if (!isConfigured)
optionsBuilder.UseSqlite("Data Source=temp.db");
}
builder.Entity<PaymentData>()
.HasIndex(o => o.InvoiceDataId);
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<InvoiceData>()
.HasIndex(o => o.StoreDataId);
builder.Entity<RefundAddressesData>()
.HasIndex(o => o.InvoiceDataId);
builder.Entity<PaymentData>()
.HasIndex(o => o.InvoiceDataId);
builder.Entity<UserStore>()
.HasKey(t => new
{
t.ApplicationUserId,
t.StoreDataId
});
builder.Entity<RefundAddressesData>()
.HasIndex(o => o.InvoiceDataId);
builder.Entity<UserStore>()
.HasOne(pt => pt.ApplicationUser)
.WithMany(p => p.UserStores)
.HasForeignKey(pt => pt.ApplicationUserId);
builder.Entity<UserStore>()
.HasKey(t => new
{
t.ApplicationUserId,
t.StoreDataId
});
builder.Entity<UserStore>()
.HasOne(pt => pt.StoreData)
.WithMany(t => t.UserStores)
.HasForeignKey(pt => pt.StoreDataId);
builder.Entity<APIKeyData>()
.HasIndex(o => o.StoreId);
builder.Entity<AddressInvoiceData>()
.HasKey(o => o.Address);
builder.Entity<AppData>()
.HasOne(a => a.StoreData);
builder.Entity<PairingCodeData>()
.HasKey(o => o.Id);
builder.Entity<UserStore>()
.HasOne(pt => pt.ApplicationUser)
.WithMany(p => p.UserStores)
.HasForeignKey(pt => pt.ApplicationUserId);
builder.Entity<PairedSINData>(b =>
{
b.HasIndex(o => o.SIN);
b.HasIndex(o => o.StoreDataId);
});
}
}
builder.Entity<UserStore>()
.HasOne(pt => pt.StoreData)
.WithMany(t => t.UserStores)
.HasForeignKey(pt => pt.StoreDataId);
builder.Entity<AddressInvoiceData>()
#pragma warning disable CS0618
.HasKey(o => o.Address);
#pragma warning restore CS0618
builder.Entity<PairingCodeData>()
.HasKey(o => o.Id);
builder.Entity<PairedSINData>(b =>
{
b.HasIndex(o => o.SIN);
b.HasIndex(o => o.StoreDataId);
});
builder.Entity<HistoricalAddressInvoiceData>()
.HasKey(o => new
{
o.InvoiceDataId,
#pragma warning disable CS0618
o.Address
#pragma warning restore CS0618
});
builder.Entity<InvoiceEventData>()
.HasKey(o => new
{
o.InvoiceDataId,
#pragma warning disable CS0618
o.UniqueId
#pragma warning restore CS0618
});
}
}
}

View File

@ -6,45 +6,96 @@ 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
}
public class ApplicationDbContextFactory
{
string _ConnectionString;
DatabaseType _Type;
public ApplicationDbContextFactory(DatabaseType type, string connectionString)
{
_ConnectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString));
_Type = type;
}
public enum DatabaseType
{
Sqlite,
Postgres
}
public class ApplicationDbContextFactory
{
string _ConnectionString;
DatabaseType _Type;
public ApplicationDbContextFactory(DatabaseType type, string connectionString)
{
_ConnectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString));
_Type = type;
}
public ApplicationDbContext CreateContext()
{
var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
ConfigureBuilder(builder);
return new ApplicationDbContext(builder.Options);
}
public ApplicationDbContext CreateContext()
{
var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
ConfigureBuilder(builder);
return new ApplicationDbContext(builder.Options);
}
public void ConfigureBuilder(DbContextOptionsBuilder builder)
{
if(_Type == DatabaseType.Sqlite)
builder.UseSqlite(_ConnectionString);
else if(_Type == DatabaseType.Postgres)
builder.UseNpgsql(_ConnectionString);
}
class CustomNpgsqlMigrationsSqlGenerator : NpgsqlMigrationsSqlGenerator
{
public CustomNpgsqlMigrationsSqlGenerator(MigrationsSqlGeneratorDependencies dependencies) : base(dependencies)
{
}
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);
}
}
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)
.ReplaceService<IMigrationsSqlGenerator, CustomNpgsqlMigrationsSqlGenerator>();
}
public void ConfigureHangfireBuilder(IGlobalConfiguration builder)
{
builder.UseMemoryStorage();
//We always use memory storage because of incompatibilities with the latest postgres in 2.1
//if (_Type == DatabaseType.Sqlite)
// builder.UseMemoryStorage(); //Sqlite provider does not support multiple workers
//else if (_Type == DatabaseType.Postgres)
// builder.UsePostgreSqlStorage(_ConnectionString);
}
}
}

View File

@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Data
{
public class HistoricalAddressInvoiceData
{
public string InvoiceDataId
{
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"
/// </summary>
[Obsolete("Use GetCryptoCode instead")]
public string Address
{
get; set;
}
[Obsolete("Use GetCryptoCode instead")]
public string CryptoCode { get; set; }
#pragma warning disable CS0618
public Payments.PaymentMethodId GetPaymentMethodId()
{
return string.IsNullOrEmpty(CryptoCode) ? new Payments.PaymentMethodId("BTC", Payments.PaymentTypes.BTCLike)
: Payments.PaymentMethodId.Parse(CryptoCode);
}
public string GetAddress()
{
if (Address == null)
return null;
var index = Address.IndexOf("#", StringComparison.InvariantCulture);
if (index == -1)
return Address;
return Address.Substring(0, index);
}
public HistoricalAddressInvoiceData SetAddress(string depositAddress, string cryptoCode)
{
Address = depositAddress + "#" + cryptoCode;
CryptoCode = cryptoCode;
return this;
}
#pragma warning restore CS0618
public DateTimeOffset Assigned
{
get; set;
}
public DateTimeOffset? UnAssigned
{
get; set;
}
}
}

View File

@ -6,65 +6,79 @@ using System.Threading.Tasks;
namespace BTCPayServer.Data
{
public class InvoiceData
{
public string StoreDataId
{
get; set;
}
public StoreData StoreData
{
get; set;
}
public class InvoiceData
{
public string StoreDataId
{
get; set;
}
public StoreData StoreData
{
get; set;
}
public string Id
{
get; set;
}
public string Id
{
get; set;
}
public DateTimeOffset Created
{
get; set;
}
public DateTimeOffset Created
{
get; set;
}
public List<PaymentData> Payments
{
get; set;
}
public List<PaymentData> Payments
{
get; set;
}
public List<RefundAddressesData> RefundAddresses
{
get; set;
}
public List<InvoiceEventData> Events
{
get; set;
}
public byte[] Blob
{
get; set;
}
public string ItemCode
{
get;
set;
}
public string OrderId
{
get;
set;
}
public string Status
{
get;
set;
}
public string ExceptionStatus
{
get;
set;
}
public string CustomerEmail
{
get;
set;
}
}
public List<RefundAddressesData> RefundAddresses
{
get; set;
}
public List<HistoricalAddressInvoiceData> HistoricalAddressInvoices
{
get; set;
}
public byte[] Blob
{
get; set;
}
public string ItemCode
{
get;
set;
}
public string OrderId
{
get;
set;
}
public string Status
{
get;
set;
}
public string ExceptionStatus
{
get;
set;
}
public string CustomerEmail
{
get;
set;
}
public List<AddressInvoiceData> AddressInvoices
{
get; set;
}
}
}

View File

@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Data
{
public class InvoiceEventData
{
public string InvoiceDataId
{
get; set;
}
public string UniqueId { get; internal set; }
public DateTimeOffset Timestamp
{
get; set;
}
public string Message { get; set; }
}
}

View File

@ -7,33 +7,33 @@ namespace BTCPayServer.Data
{
public class PairedSINData
{
public string Id
{
get; set;
}
public string Id
{
get; set;
}
public string Facade
{
get; set;
}
public string StoreDataId
{
get; set;
}
public string Label
{
get;
set;
}
public DateTimeOffset PairingTime
{
get;
set;
}
public string SIN
{
get; set;
}
}
public string Facade
{
get; set;
}
public string StoreDataId
{
get; set;
}
public string Label
{
get;
set;
}
public DateTimeOffset PairingTime
{
get;
set;
}
public string SIN
{
get; set;
}
}
}

View File

@ -7,44 +7,44 @@ namespace BTCPayServer.Data
{
public class PairingCodeData
{
public string Id
{
get; set;
}
public string Facade
{
get; set;
}
public string StoreDataId
{
get; set;
}
public DateTimeOffset Expiration
{
get;
set;
}
public string Id
{
get; set;
}
public string Label
{
get;
set;
}
public string SIN
{
get;
set;
}
public DateTime DateCreated
{
get;
set;
}
public string TokenValue
{
get;
set;
}
}
public string Facade
{
get; set;
}
public string StoreDataId
{
get; set;
}
public DateTimeOffset Expiration
{
get;
set;
}
public string Label
{
get;
set;
}
public string SIN
{
get;
set;
}
public DateTime DateCreated
{
get;
set;
}
public string TokenValue
{
get;
set;
}
}
}

View File

@ -5,25 +5,29 @@ using System.Threading.Tasks;
namespace BTCPayServer.Data
{
public class PaymentData
{
public string Id
{
get; set;
}
public class PaymentData
{
public string Id
{
get; set;
}
public string InvoiceDataId
{
get; set;
}
public InvoiceData InvoiceData
{
get; set;
}
public string InvoiceDataId
{
get; set;
}
public InvoiceData InvoiceData
{
get; set;
}
public byte[] Blob
{
get; set;
}
}
public byte[] Blob
{
get; set;
}
public bool Accounted
{
get; set;
}
}
}

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Data
{
public class PendingInvoiceData
{
public string Id
{
get; set;
}
}
}

View File

@ -7,21 +7,21 @@ namespace BTCPayServer.Data
{
public class RefundAddressesData
{
public string Id
{
get; set;
}
public string InvoiceDataId
{
get; set;
}
public InvoiceData InvoiceData
{
get; set;
}
public byte[] Blob
{
get; set;
}
}
public string Id
{
get; set;
}
public string InvoiceDataId
{
get; set;
}
public InvoiceData InvoiceData
{
get; set;
}
public byte[] Blob
{
get; set;
}
}
}

View File

@ -7,14 +7,14 @@ namespace BTCPayServer.Data
{
public class SettingData
{
public string Id
{
get; set;
}
public string Id
{
get; set;
}
public string Value
{
get; set;
}
}
public string Value
{
get; set;
}
}
}

View File

@ -1,55 +1,374 @@
using BTCPayServer.Models;
using BTCPayServer.Servcices.Invoices;
using BTCPayServer.Services.Invoices;
using NBitcoin;
using NBXplorer;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.ComponentModel;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using BTCPayServer.Services.Rates;
using BTCPayServer.Payments;
using BTCPayServer.JsonConverters;
using System.ComponentModel.DataAnnotations;
using BTCPayServer.Services;
using System.Security.Claims;
using BTCPayServer.Security;
using BTCPayServer.Rating;
namespace BTCPayServer.Data
{
public class StoreData
{
public string Id
{
get;
set;
}
public string Id
{
get;
set;
}
public List<UserStore> UserStores
{
get; set;
}
public List<UserStore> UserStores
{
get; set;
}
public string DerivationStrategy
{
get; set;
}
public List<AppData> Apps
{
get; set;
}
public string StoreName
{
get; set;
}
[Obsolete("Use GetDerivationStrategies instead")]
public string DerivationStrategy
{
get; set;
}
public SpeedPolicy SpeedPolicy
{
get; set;
}
[Obsolete("Use GetDerivationStrategies instead")]
public string DerivationStrategies
{
get;
set;
}
public string StoreWebsite
{
get; set;
}
public IEnumerable<ISupportedPaymentMethod> GetSupportedPaymentMethods(BTCPayNetworkProvider networks)
{
#pragma warning disable CS0618
bool btcReturned = false;
public byte[] StoreCertificate
{
get; set;
}
// Legacy stuff which should go away
if (!string.IsNullOrEmpty(DerivationStrategy))
{
if (networks.BTC != null)
{
btcReturned = true;
yield return BTCPayServer.DerivationStrategy.Parse(DerivationStrategy, networks.BTC);
}
}
[NotMapped]
public string Role
{
get; set;
}
}
if (!string.IsNullOrEmpty(DerivationStrategies))
{
JObject strategies = JObject.Parse(DerivationStrategies);
foreach (var strat in strategies.Properties())
{
var paymentMethodId = PaymentMethodId.Parse(strat.Name);
var network = networks.GetNetwork(paymentMethodId.CryptoCode);
if (network != null)
{
if (network == networks.BTC && paymentMethodId.PaymentType == PaymentTypes.BTCLike && btcReturned)
continue;
if (strat.Value.Type == JTokenType.Null)
continue;
yield return PaymentMethodExtensions.Deserialize(paymentMethodId, strat.Value, network);
}
}
}
#pragma warning restore CS0618
}
/// <summary>
/// Set or remove a new supported payment method for the store
/// </summary>
/// <param name="paymentMethodId">The paymentMethodId</param>
/// <param name="supportedPaymentMethod">The payment method, or null to remove</param>
public void SetSupportedPaymentMethod(PaymentMethodId paymentMethodId, ISupportedPaymentMethod supportedPaymentMethod)
{
if (supportedPaymentMethod != null && paymentMethodId != supportedPaymentMethod.PaymentId)
throw new InvalidOperationException("Argument mismatch");
#pragma warning disable CS0618
JObject strategies = string.IsNullOrEmpty(DerivationStrategies) ? new JObject() : JObject.Parse(DerivationStrategies);
bool existing = false;
foreach (var strat in strategies.Properties().ToList())
{
var stratId = PaymentMethodId.Parse(strat.Name);
if (stratId.IsBTCOnChain)
{
// Legacy stuff which should go away
DerivationStrategy = null;
}
if (stratId == paymentMethodId)
{
if (supportedPaymentMethod == null)
{
strat.Remove();
}
else
{
strat.Value = PaymentMethodExtensions.Serialize(supportedPaymentMethod);
}
existing = true;
break;
}
}
if (!existing && supportedPaymentMethod == null && paymentMethodId.IsBTCOnChain)
{
DerivationStrategy = null;
}
else if (!existing && supportedPaymentMethod != null)
strategies.Add(new JProperty(supportedPaymentMethod.PaymentId.ToString(), PaymentMethodExtensions.Serialize(supportedPaymentMethod)));
DerivationStrategies = strategies.ToString();
#pragma warning restore CS0618
}
public string StoreName
{
get; set;
}
public SpeedPolicy SpeedPolicy
{
get; set;
}
public string StoreWebsite
{
get; set;
}
public byte[] StoreCertificate
{
get; set;
}
[NotMapped]
[Obsolete]
public string Role
{
get; set;
}
public Claim[] GetClaims()
{
List<Claim> claims = new List<Claim>();
#pragma warning disable CS0612 // Type or member is obsolete
var role = Role;
#pragma warning restore CS0612 // Type or member is obsolete
if (role == StoreRoles.Owner)
{
claims.Add(new Claim(Policies.CanModifyStoreSettings.Key, Id));
claims.Add(new Claim(Policies.CanUseStore.Key, Id));
}
if (role == StoreRoles.Guest)
{
claims.Add(new Claim(Policies.CanUseStore.Key, Id));
}
return claims.ToArray();
}
public bool HasClaim(string claim)
{
return GetClaims().Any(c => c.Type == claim);
}
public byte[] StoreBlob
{
get;
set;
}
[Obsolete("Use GetDefaultCrypto instead")]
public string DefaultCrypto { get; set; }
#pragma warning disable CS0618
public string GetDefaultCrypto()
{
return DefaultCrypto ?? "BTC";
}
public void SetDefaultCrypto(string defaultCryptoCurrency)
{
DefaultCrypto = defaultCryptoCurrency;
}
#pragma warning restore CS0618
static Network Dummy = Network.Main;
public StoreBlob GetStoreBlob()
{
var result = StoreBlob == null ? new StoreBlob() : new Serializer(Dummy).ToObject<StoreBlob>(Encoding.UTF8.GetString(StoreBlob));
if (result.PreferredExchange == null)
result.PreferredExchange = CoinAverageRateProvider.CoinAverageName;
return result;
}
public bool SetStoreBlob(StoreBlob storeBlob)
{
var original = new Serializer(Dummy).ToString(GetStoreBlob());
var newBlob = new Serializer(Dummy).ToString(storeBlob);
if (original == newBlob)
return false;
StoreBlob = Encoding.UTF8.GetBytes(newBlob);
return true;
}
}
public class RateRule_Obsolete
{
public RateRule_Obsolete()
{
RuleName = "Multiplier";
}
public string RuleName { get; set; }
public double Multiplier { get; set; }
public decimal Apply(BTCPayNetwork network, decimal rate)
{
return rate * (decimal)Multiplier;
}
}
public class StoreBlob
{
public StoreBlob()
{
InvoiceExpiration = 15;
MonitoringExpiration = 60;
PaymentTolerance = 0;
RequiresRefundEmail = true;
}
public bool NetworkFeeDisabled
{
get; set;
}
public bool AllowCoinConversion
{
get; set;
}
public bool RequiresRefundEmail { get; set; }
public string DefaultLang { get; set; }
[DefaultValue(60)]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
public int MonitoringExpiration
{
get;
set;
}
[DefaultValue(15)]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
public int InvoiceExpiration
{
get;
set;
}
public void SetRateMultiplier(double rate)
{
RateRules = new List<RateRule_Obsolete>();
RateRules.Add(new RateRule_Obsolete() { Multiplier = rate });
}
public decimal GetRateMultiplier()
{
decimal rate = 1.0m;
if (RateRules == null || RateRules.Count == 0)
return rate;
foreach (var rule in RateRules)
{
rate = rule.Apply(null, rate);
}
return rate;
}
public List<RateRule_Obsolete> RateRules { get; set; } = new List<RateRule_Obsolete>();
public string PreferredExchange { get; set; }
[JsonConverter(typeof(CurrencyValueJsonConverter))]
public CurrencyValue LightningMaxValue { get; set; }
[JsonConverter(typeof(CurrencyValueJsonConverter))]
public CurrencyValue OnChainMinValue { get; set; }
[JsonConverter(typeof(UriJsonConverter))]
public Uri CustomLogo { get; set; }
[JsonConverter(typeof(UriJsonConverter))]
public Uri CustomCSS { get; set; }
public string HtmlTitle { get; set; }
public bool RateScripting { get; set; }
public string RateScript { get; set; }
string _LightningDescriptionTemplate;
public string LightningDescriptionTemplate
{
get
{
return _LightningDescriptionTemplate ?? "Paid to {StoreName} (Order ID: {OrderId})";
}
set
{
_LightningDescriptionTemplate = value;
}
}
[DefaultValue(0)]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
public double PaymentTolerance { get; set; }
public BTCPayServer.Rating.RateRules GetRateRules(BTCPayNetworkProvider networkProvider)
{
if (!RateScripting ||
string.IsNullOrEmpty(RateScript) ||
!BTCPayServer.Rating.RateRules.TryParse(RateScript, out var rules))
{
return GetDefaultRateRules(networkProvider);
}
else
{
rules.GlobalMultiplier = GetRateMultiplier();
return rules;
}
}
public RateRules GetDefaultRateRules(BTCPayNetworkProvider networkProvider)
{
StringBuilder builder = new StringBuilder();
foreach (var network in networkProvider.GetAll())
{
if (network.DefaultRateRules.Length != 0)
{
builder.AppendLine($"// Default rate rules for {network.CryptoCode}");
foreach (var line in network.DefaultRateRules)
{
builder.AppendLine(line);
}
builder.AppendLine($"////////");
builder.AppendLine();
}
}
var preferredExchange = string.IsNullOrEmpty(PreferredExchange) ? "coinaverage" : PreferredExchange;
builder.AppendLine($"X_X = {preferredExchange}(X_X);");
BTCPayServer.Rating.RateRules.TryParse(builder.ToString(), out var rules);
rules.GlobalMultiplier = GetRateMultiplier();
return rules;
}
}
}

View File

@ -6,29 +6,29 @@ using System.Threading.Tasks;
namespace BTCPayServer.Data
{
public class UserStore
{
public string ApplicationUserId
{
get; set;
}
public ApplicationUser ApplicationUser
{
get; set;
}
public class UserStore
{
public string ApplicationUserId
{
get; set;
}
public ApplicationUser ApplicationUser
{
get; set;
}
public string StoreDataId
{
get; set;
}
public StoreData StoreData
{
get; set;
}
public string Role
{
get;
set;
}
}
public string StoreDataId
{
get; set;
}
public StoreData StoreData
{
get; set;
}
public string Role
{
get;
set;
}
}
}

View File

@ -0,0 +1,181 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBXplorer;
using NBXplorer.DerivationStrategy;
namespace BTCPayServer
{
public class DerivationSchemeParser
{
public Network Network { get; set; }
public Script HintScriptPubKey { get; set; }
public DerivationSchemeParser(Network expectedNetwork)
{
Network = expectedNetwork;
}
public DerivationStrategyBase Parse(string str)
{
if (str == null)
throw new ArgumentNullException(nameof(str));
str = str.Trim();
HashSet<string> hintedLabels = new HashSet<string>();
var hintDestination = HintScriptPubKey?.GetDestination();
if (hintDestination != null)
{
if (hintDestination is KeyId)
{
hintedLabels.Add("legacy");
}
if (hintDestination is ScriptId)
{
hintedLabels.Add("p2sh");
}
}
if(!Network.Consensus.SupportSegwit)
hintedLabels.Add("legacy");
try
{
var result = new DerivationStrategyFactory(Network).Parse(str);
return FindMatch(hintedLabels, result);
}
catch
{
}
Dictionary<uint, string[]> electrumMapping = new Dictionary<uint, string[]>();
//Source https://github.com/spesmilo/electrum/blob/9edffd17542de5773e7284a8c8a2673c766bb3c3/lib/bitcoin.py
var standard = 0x0488b21eU;
electrumMapping.Add(standard, new[] { "legacy" });
var p2wpkh_p2sh = 0x049d7cb2U;
electrumMapping.Add(p2wpkh_p2sh, new string[] { "p2sh" });
var p2wpkh = 0x4b24746U;
electrumMapping.Add(p2wpkh, Array.Empty<string>());
var parts = str.Split('-');
for (int i = 0; i < parts.Length; i++)
{
if (IsLabel(parts[i]))
{
hintedLabels.Add(parts[i].Substring(1, parts[i].Length - 2).ToLowerInvariant());
continue;
}
try
{
var data = Encoders.Base58Check.DecodeData(parts[i]);
if (data.Length < 4)
continue;
var prefix = Utils.ToUInt32(data, false);
var standardPrefix = Utils.ToBytes(Network.NetworkType == NetworkType.Mainnet ? 0x0488b21eU : 0x043587cf, false);
for (int ii = 0; ii < 4; ii++)
data[ii] = standardPrefix[ii];
var derivationScheme = new BitcoinExtPubKey(Encoders.Base58Check.EncodeData(data), Network).ToString();
electrumMapping.TryGetValue(prefix, out string[] labels);
if (labels != null)
{
foreach (var label in labels)
{
hintedLabels.Add(label.ToLowerInvariant());
}
}
parts[i] = derivationScheme;
}
catch { continue; }
}
if (hintDestination != null)
{
if (hintDestination is WitKeyId)
{
hintedLabels.Remove("legacy");
hintedLabels.Remove("p2sh");
}
}
str = string.Join('-', parts.Where(p => !IsLabel(p)));
foreach (var label in hintedLabels)
{
str = $"{str}-[{label}]";
}
return FindMatch(hintedLabels, new DerivationStrategyFactory(Network).Parse(str));
}
private DerivationStrategyBase FindMatch(HashSet<string> hintLabels, DerivationStrategyBase result)
{
var facto = new DerivationStrategyFactory(Network);
var firstKeyPath = new KeyPath("0/0");
if (HintScriptPubKey == null)
return result;
if (HintScriptPubKey == result.Derive(firstKeyPath).ScriptPubKey)
return result;
if (result is MultisigDerivationStrategy)
hintLabels.Add("keeporder");
var resultNoLabels = result.ToString();
resultNoLabels = string.Join('-', resultNoLabels.Split('-').Where(p => !IsLabel(p)));
foreach (var labels in ItemCombinations(hintLabels.ToList()))
{
var hinted = facto.Parse(resultNoLabels + '-' + string.Join('-', labels.Select(l=>$"[{l}]").ToArray()));
if (HintScriptPubKey == hinted.Derive(firstKeyPath).ScriptPubKey)
return hinted;
}
throw new FormatException("Could not find any match");
}
private static bool IsLabel(string v)
{
return v.StartsWith('[') && v.EndsWith(']');
}
/// <summary>
/// Method to create lists containing possible combinations of an input list of items. This is
/// basically copied from code by user "jaolho" on this thread:
/// http://stackoverflow.com/questions/7802822/all-possible-combinations-of-a-list-of-values
/// </summary>
/// <typeparam name="T">type of the items on the input list</typeparam>
/// <param name="inputList">list of items</param>
/// <param name="minimumItems">minimum number of items wanted in the generated combinations,
/// if zero the empty combination is included,
/// default is one</param>
/// <param name="maximumItems">maximum number of items wanted in the generated combinations,
/// default is no maximum limit</param>
/// <returns>list of lists for possible combinations of the input items</returns>
public static List<List<T>> ItemCombinations<T>(List<T> inputList, int minimumItems = 1,
int maximumItems = int.MaxValue)
{
int nonEmptyCombinations = (int)Math.Pow(2, inputList.Count) - 1;
List<List<T>> listOfLists = new List<List<T>>(nonEmptyCombinations + 1);
if (minimumItems == 0) // Optimize default case
listOfLists.Add(new List<T>());
for (int i = 1; i <= nonEmptyCombinations; i++)
{
List<T> thisCombination = new List<T>(inputList.Count);
for (int j = 0; j < inputList.Count; j++)
{
if ((i >> j & 1) == 1)
thisCombination.Add(inputList[j]);
}
if (thisCombination.Count >= minimumItems && thisCombination.Count <= maximumItems)
listOfLists.Add(thisCombination);
}
return listOfLists;
}
}
}

View File

@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Payments;
using BTCPayServer.Services.Invoices;
using NBitcoin;
using NBXplorer.DerivationStrategy;
namespace BTCPayServer
{
public class DerivationStrategy : ISupportedPaymentMethod
{
private DerivationStrategyBase _DerivationStrategy;
private BTCPayNetwork _Network;
public DerivationStrategy(DerivationStrategyBase result, BTCPayNetwork network)
{
this._DerivationStrategy = result;
this._Network = network;
}
public static DerivationStrategy Parse(string derivationStrategy, BTCPayNetwork network)
{
if (network == null)
throw new ArgumentNullException(nameof(network));
if (derivationStrategy == null)
throw new ArgumentNullException(nameof(derivationStrategy));
var result = new NBXplorer.DerivationStrategy.DerivationStrategyFactory(network.NBitcoinNetwork).Parse(derivationStrategy);
return new DerivationStrategy(result, network);
}
public BTCPayNetwork Network { get { return this._Network; } }
public DerivationStrategyBase DerivationStrategyBase { get { return this._DerivationStrategy; } }
public PaymentMethodId PaymentId => new PaymentMethodId(Network.CryptoCode, PaymentTypes.BTCLike);
public override string ToString()
{
return _DerivationStrategy.ToString();
}
}
}

View File

@ -0,0 +1,154 @@
using System;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Logging;
using System.Threading;
namespace BTCPayServer
{
public interface IEventAggregatorSubscription : IDisposable
{
void Unsubscribe();
void Resubscribe();
}
public class EventAggregator : IDisposable
{
class Subscription : IEventAggregatorSubscription
{
private EventAggregator aggregator;
Type t;
public Subscription(EventAggregator aggregator, Type t)
{
this.aggregator = aggregator;
this.t = t;
}
public Action<Object> Act { get; set; }
bool _Disposed;
public void Dispose()
{
if (_Disposed)
return;
_Disposed = true;
lock (this.aggregator._Subscriptions)
{
if (this.aggregator._Subscriptions.TryGetValue(t, out Dictionary<Subscription, Action<object>> actions))
{
if (actions.Remove(this))
{
if (actions.Count == 0)
this.aggregator._Subscriptions.Remove(t);
}
}
}
}
public void Resubscribe()
{
aggregator.Subscribe(t, this);
}
public void Unsubscribe()
{
Dispose();
}
}
public Task<T> WaitNext<T>(CancellationToken cancellation = default(CancellationToken))
{
return WaitNext<T>(o => true, cancellation);
}
public async Task<T> WaitNext<T>(Func<T, bool> predicate, CancellationToken cancellation = default(CancellationToken))
{
TaskCompletionSource<T> tcs = new TaskCompletionSource<T>();
var subscription = Subscribe<T>((a, b) => { if (predicate(b)) { tcs.TrySetResult(b); a.Unsubscribe(); } });
using (cancellation.Register(() => { tcs.TrySetCanceled(); subscription.Unsubscribe(); }))
{
return await tcs.Task.ConfigureAwait(false);
}
}
public void Publish<T>(T evt) where T : class
{
Publish(evt, typeof(T));
}
public void Publish(object evt, Type evtType)
{
if (evt == null)
throw new ArgumentNullException(nameof(evt));
List<Action<object>> actionList = new List<Action<object>>();
lock (_Subscriptions)
{
if (_Subscriptions.TryGetValue(evtType, out Dictionary<Subscription, Action<object>> actions))
{
actionList = actions.Values.ToList();
}
}
var log = evt.ToString();
if(!String.IsNullOrEmpty(log))
Logs.Events.LogInformation(log);
foreach (var sub in actionList)
{
try
{
sub(evt);
}
catch (Exception ex)
{
Logs.Events.LogError(ex, $"Error while calling event handler");
}
}
}
public IEventAggregatorSubscription Subscribe<T>(Action<IEventAggregatorSubscription, T> subscription)
{
var eventType = typeof(T);
var s = new Subscription(this, eventType);
s.Act = (o) => subscription(s, (T)o);
return Subscribe(eventType, s);
}
private IEventAggregatorSubscription Subscribe(Type eventType, Subscription subscription)
{
lock (_Subscriptions)
{
if (!_Subscriptions.TryGetValue(eventType, out Dictionary<Subscription, Action<object>> actions))
{
actions = new Dictionary<Subscription, Action<object>>();
_Subscriptions.Add(eventType, actions);
}
actions.Add(subscription, subscription.Act);
}
return subscription;
}
Dictionary<Type, Dictionary<Subscription, Action<object>>> _Subscriptions = new Dictionary<Type, Dictionary<Subscription, Action<object>>>();
public IEventAggregatorSubscription Subscribe<T, TReturn>(Func<T, TReturn> subscription)
{
return Subscribe(new Action<T>((t) => subscription(t)));
}
public IEventAggregatorSubscription Subscribe<T, TReturn>(Func<IEventAggregatorSubscription, T, TReturn> subscription)
{
return Subscribe(new Action<IEventAggregatorSubscription, T>((sub, t) => subscription(sub, t)));
}
public IEventAggregatorSubscription Subscribe<T>(Action<T> subscription)
{
return Subscribe(new Action<IEventAggregatorSubscription, T>((sub, t) => subscription(t)));
}
public void Dispose()
{
lock (_Subscriptions)
{
_Subscriptions.Clear();
}
}
}
}

View File

@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Invoices;
namespace BTCPayServer.Events
{
public class InvoiceDataChangedEvent
{
public InvoiceDataChangedEvent(InvoiceEntity invoice)
{
InvoiceId = invoice.Id;
Status = invoice.Status;
ExceptionStatus = invoice.ExceptionStatus;
}
public string InvoiceId { get; set; }
public string Status { get; internal set; }
public string ExceptionStatus { get; internal set; }
public override string ToString()
{
if (string.IsNullOrEmpty(ExceptionStatus) || ExceptionStatus == "false")
{
return $"Invoice status is {Status}";
}
else
{
return $"Invoice status is {Status} (Exception status: {ExceptionStatus})";
}
}
}
}

View File

@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Invoices;
namespace BTCPayServer.Events
{
public class InvoiceEvent
{
public InvoiceEvent(InvoiceEntity invoice, int code, string name) : this(invoice.Id, code, name)
{
}
public InvoiceEvent(string invoiceId, int code, string name)
{
InvoiceId = invoiceId;
EventCode = code;
Name = name;
}
public string InvoiceId { get; set; }
public int EventCode { get; set; }
public string Name { get; set; }
public override string ToString()
{
return $"Invoice {InvoiceId} new event: {Name} ({EventCode})";
}
}
}

View File

@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Events
{
public class InvoiceIPNEvent
{
public InvoiceIPNEvent(string invoiceId, int? eventCode, string name)
{
InvoiceId = invoiceId;
EventCode = eventCode;
Name = name;
}
public int? EventCode { get; set; }
public string Name { get; set; }
public string InvoiceId { get; set; }
public string Error { get; set; }
public override string ToString()
{
string ipnType = "IPN";
if(EventCode.HasValue)
{
ipnType = $"IPN ({EventCode.Value} {Name})";
}
if (Error == null)
return $"{ipnType} sent for invoice {InvoiceId}";
return $"Error while sending {ipnType}: {Error}";
}
}
}

View File

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Events
{
public class InvoiceNeedUpdateEvent
{
public InvoiceNeedUpdateEvent(string invoiceId)
{
if (invoiceId == null)
throw new ArgumentNullException(nameof(invoiceId));
InvoiceId = invoiceId;
}
public string InvoiceId { get; set; }
public override string ToString()
{
return string.Empty;
}
}
}

View File

@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Events
{
public class InvoiceNewAddressEvent
{
public InvoiceNewAddressEvent(string invoiceId, string address, BTCPayNetwork network)
{
Address = address;
InvoiceId = invoiceId;
Network = network;
}
public string Address { get; set; }
public string InvoiceId { get; set; }
public BTCPayNetwork Network { get; set; }
public override string ToString()
{
return $"{Network.CryptoCode}: New address {Address} for invoice {InvoiceId}";
}
}
}

View File

@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Events
{
public class InvoiceStopWatchedEvent
{
public InvoiceStopWatchedEvent(string invoiceId)
{
this.InvoiceId = invoiceId;
}
public string InvoiceId { get; set; }
public override string ToString()
{
return $"Invoice {InvoiceId} is not monitored anymore.";
}
}
}

View File

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.HostedServices;
namespace BTCPayServer.Events
{
public class NBXplorerStateChangedEvent
{
public NBXplorerStateChangedEvent(BTCPayNetwork network, NBXplorerState old, NBXplorerState newState)
{
Network = network;
NewState = newState;
OldState = old;
}
public BTCPayNetwork Network { get; set; }
public NBXplorerState NewState { get; set; }
public NBXplorerState OldState { get; set; }
public override string ToString()
{
return $"NBXplorer {Network.CryptoCode}: {OldState} => {NewState}";
}
}
}

View File

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Events
{
public class NewBlockEvent
{
public string CryptoCode { get; set; }
public override string ToString()
{
return $"{CryptoCode}: New block";
}
}
}

View File

@ -0,0 +1,104 @@
using System;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Configuration;
using BTCPayServer.Logging;
using NBXplorer;
using BTCPayServer.HostedServices;
namespace BTCPayServer
{
public class ExplorerClientProvider
{
BTCPayNetworkProvider _NetworkProviders;
BTCPayServerOptions _Options;
public BTCPayNetworkProvider NetworkProviders => _NetworkProviders;
NBXplorerDashboard _Dashboard;
public ExplorerClientProvider(BTCPayNetworkProvider networkProviders, BTCPayServerOptions options, NBXplorerDashboard dashboard)
{
_Dashboard = dashboard;
_NetworkProviders = networkProviders;
_Options = options;
foreach (var setting in options.NBXplorerConnectionSettings)
{
var cookieFile = setting.CookieFile;
if (cookieFile.Trim() == "0" || string.IsNullOrEmpty(cookieFile.Trim()))
cookieFile = null;
Logs.Configuration.LogInformation($"{setting.CryptoCode}: Explorer url is {(setting.ExplorerUri.AbsoluteUri ?? "not set")}");
Logs.Configuration.LogInformation($"{setting.CryptoCode}: Cookie file is {(setting.CookieFile ?? "not set")}");
if (setting.ExplorerUri != null)
{
_Clients.TryAdd(setting.CryptoCode, CreateExplorerClient(_NetworkProviders.GetNetwork(setting.CryptoCode), setting.ExplorerUri, setting.CookieFile));
}
}
}
private static ExplorerClient CreateExplorerClient(BTCPayNetwork n, Uri uri, string cookieFile)
{
var explorer = new ExplorerClient(n.NBXplorerNetwork, uri);
if (cookieFile == null)
{
Logs.Configuration.LogWarning($"{n.CryptoCode}: Not using cookie authentication");
explorer.SetNoAuth();
}
if(!explorer.SetCookieAuth(cookieFile))
{
Logs.Configuration.LogWarning($"{n.CryptoCode}: Using cookie auth against NBXplorer, but {cookieFile} is not found");
}
return explorer;
}
Dictionary<string, ExplorerClient> _Clients = new Dictionary<string, ExplorerClient>();
public ExplorerClient GetExplorerClient(string cryptoCode)
{
var network = _NetworkProviders.GetNetwork(cryptoCode);
if (network == null)
return null;
_Clients.TryGetValue(network.CryptoCode, out ExplorerClient client);
return client;
}
public ExplorerClient GetExplorerClient(BTCPayNetwork network)
{
if (network == null)
throw new ArgumentNullException(nameof(network));
return GetExplorerClient(network.CryptoCode);
}
public bool IsAvailable(BTCPayNetwork network)
{
return IsAvailable(network.CryptoCode);
}
public bool IsAvailable(string cryptoCode)
{
return _Clients.ContainsKey(cryptoCode) && _Dashboard.IsFullySynched(cryptoCode, out var unused);
}
public BTCPayNetwork GetNetwork(string cryptoCode)
{
var network = _NetworkProviders.GetNetwork(cryptoCode);
if (network == null)
return null;
if (_Clients.ContainsKey(network.CryptoCode))
return network;
return null;
}
public IEnumerable<(BTCPayNetwork, ExplorerClient)> GetAll()
{
foreach (var net in _NetworkProviders.GetAll())
{
if (_Clients.TryGetValue(net.CryptoCode, out ExplorerClient explorer))
{
yield return (net, explorer);
}
}
}
}
}

View File

@ -1,48 +1,182 @@
using BTCPayServer.Authentication;
using BTCPayServer.Configuration;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.Encodings.Web;
using NBitcoin;
using System.Threading.Tasks;
using NBXplorer;
using NBXplorer.Models;
using System.Linq;
using System.Threading;
using BTCPayServer.Services.Wallets;
using System.IO;
using BTCPayServer.Logging;
using Microsoft.Extensions.Logging;
using System.Net.WebSockets;
using BTCPayServer.Services.Invoices;
using NBitpayClient;
using BTCPayServer.Payments;
using Microsoft.AspNetCore.Identity;
using BTCPayServer.Models;
using System.Security.Claims;
using System.Globalization;
using BTCPayServer.Services;
using BTCPayServer.Data;
namespace BTCPayServer
{
public static class Extensions
{
public static string WithTrailingSlash(this string str)
{
if(str.EndsWith("/"))
return str;
return str + "/";
}
public static string PrettyPrint(this TimeSpan expiration)
{
StringBuilder builder = new StringBuilder();
if (expiration.Days >= 1)
builder.Append(expiration.Days.ToString(CultureInfo.InvariantCulture));
if (expiration.Hours >= 1)
builder.Append(expiration.Hours.ToString("00", CultureInfo.InvariantCulture));
builder.Append($"{expiration.Minutes.ToString("00", CultureInfo.InvariantCulture)}:{expiration.Seconds.ToString("00", CultureInfo.InvariantCulture)}");
return builder.ToString();
}
public static decimal RoundUp(decimal value, int precision)
{
for (int i = 0; i < precision; i++)
{
value = value * 10m;
}
value = Math.Ceiling(value);
for (int i = 0; i < precision; i++)
{
value = value / 10m;
}
return value;
}
public static PaymentMethodId GetpaymentMethodId(this InvoiceCryptoInfo info)
{
return new PaymentMethodId(info.CryptoCode, Enum.Parse<PaymentTypes>(info.PaymentType));
}
public static async Task CloseSocket(this WebSocket webSocket)
{
try
{
if (webSocket.State == WebSocketState.Open)
{
CancellationTokenSource cts = new CancellationTokenSource();
cts.CancelAfter(5000);
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", cts.Token);
}
}
catch { }
finally { try { webSocket.Dispose(); } catch { } }
}
public static bool SupportDropColumn(this Microsoft.EntityFrameworkCore.Migrations.Migration migration, string activeProvider)
{
return activeProvider != "Microsoft.EntityFrameworkCore.Sqlite";
}
public static string GetAbsoluteRoot(this HttpRequest request)
{
return string.Concat(
request.Scheme,
"://",
request.Host.ToUriComponent(),
request.PathBase.ToUriComponent());
}
public static async Task<Dictionary<uint256, TransactionResult>> GetTransactions(this BTCPayWallet client, uint256[] hashes, CancellationToken cts = default(CancellationToken))
{
hashes = hashes.Distinct().ToArray();
var transactions = hashes
.Select(async o => await client.GetTransactionAsync(o, cts))
.ToArray();
await Task.WhenAll(transactions).ConfigureAwait(false);
return transactions.Select(t => t.Result).Where(t => t != null).ToDictionary(o => o.Transaction.GetHash());
}
public static string WithTrailingSlash(this string str)
{
if (str.EndsWith("/", StringComparison.InvariantCulture))
return str;
return str + "/";
}
public static IServiceCollection ConfigureBTCPayServer(this IServiceCollection services, IConfiguration conf)
{
services.Configure<BTCPayServerOptions>(o =>
{
o.LoadArgs(conf);
});
return services;
}
public static string GetAbsoluteRoot(this HttpRequest request)
{
return string.Concat(
request.Scheme,
"://",
request.Host.ToUriComponent(),
request.PathBase.ToUriComponent());
}
public static string GetAbsoluteUri(this HttpRequest request, string redirectUrl)
{
bool isRelative =
(redirectUrl.Length > 0 && redirectUrl[0] == '/')
|| !new Uri(redirectUrl, UriKind.RelativeOrAbsolute).IsAbsoluteUri;
return isRelative ? request.GetAbsoluteRoot() + redirectUrl : redirectUrl;
}
public static BitIdentity GetBitIdentity(this Controller controller, bool throws = true)
{
if(!(controller.User.Identity is BitIdentity))
return throws ? throw new UnauthorizedAccessException("no-bitid") : (BitIdentity)null;
return (BitIdentity)controller.User.Identity;
}
}
public static IServiceCollection ConfigureBTCPayServer(this IServiceCollection services, IConfiguration conf)
{
services.Configure<BTCPayServerOptions>(o =>
{
o.LoadArgs(conf);
});
return services;
}
public static string GetSIN(this ClaimsPrincipal principal)
{
return principal.Claims.Where(c => c.Type == Claims.SIN).Select(c => c.Value).FirstOrDefault();
}
public static string GetStoreId(this ClaimsPrincipal principal)
{
return principal.Claims.Where(c => c.Type == Claims.OwnStore).Select(c => c.Value).FirstOrDefault();
}
public static void SetIsBitpayAPI(this HttpContext ctx, bool value)
{
NBitcoin.Extensions.TryAdd(ctx.Items, "IsBitpayAPI", value);
}
public static void AddRange<T>(this HashSet<T> hashSet, IEnumerable<T> items)
{
foreach(var item in items)
{
hashSet.Add(item);
}
}
public static bool GetIsBitpayAPI(this HttpContext ctx)
{
return ctx.Items.TryGetValue("IsBitpayAPI", out object obj) &&
obj is bool b && b;
}
public static void SetBitpayAuth(this HttpContext ctx, (string Signature, String Id, String Authorization) value)
{
NBitcoin.Extensions.TryAdd(ctx.Items, "BitpayAuth", value);
}
public static (string Signature, String Id, String Authorization) GetBitpayAuth(this HttpContext ctx)
{
ctx.Items.TryGetValue("BitpayAuth", out object obj);
return ((string Signature, String Id, String Authorization))obj;
}
public static StoreData GetStoreData(this HttpContext ctx)
{
return ctx.Items.TryGet("BTCPAY.STOREDATA") as StoreData;
}
public static void SetStoreData(this HttpContext ctx, StoreData storeData)
{
ctx.Items["BTCPAY.STOREDATA"] = storeData;
}
private static JsonSerializerSettings jsonSettings = new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() };
public static string ToJson(this object o)
{
var res = JsonConvert.SerializeObject(o, Formatting.None, jsonSettings);
return res;
}
}
}

View File

@ -7,70 +7,70 @@ using System.Text;
namespace BTCPayServer.Filters
{
public class MediaTypeConstraintAttribute : Attribute, IActionConstraint
{
public MediaTypeConstraintAttribute(string mediaType)
{
MediaType = mediaType ?? throw new ArgumentNullException(nameof(mediaType));
}
public class MediaTypeConstraintAttribute : Attribute, IActionConstraint
{
public MediaTypeConstraintAttribute(string mediaType)
{
MediaType = mediaType ?? throw new ArgumentNullException(nameof(mediaType));
}
public string MediaType
{
get; set;
}
public string MediaType
{
get; set;
}
public int Order => 100;
public int Order => 100;
public bool Accept(ActionConstraintContext context)
{
var match = context.RouteContext.HttpContext.Request.ContentType?.StartsWith(MediaType, StringComparison.Ordinal);
return match.HasValue && match.Value;
}
}
public bool Accept(ActionConstraintContext context)
{
var match = context.RouteContext.HttpContext.Request.ContentType?.StartsWith(MediaType, StringComparison.Ordinal);
return match.HasValue && match.Value;
}
}
public class BitpayAPIConstraintAttribute : Attribute, IActionConstraint
{
public BitpayAPIConstraintAttribute(bool isBitpayAPI = true)
{
IsBitpayAPI = isBitpayAPI;
}
public class BitpayAPIConstraintAttribute : Attribute, IActionConstraint
{
public BitpayAPIConstraintAttribute(bool isBitpayAPI = true)
{
IsBitpayAPI = isBitpayAPI;
}
public bool IsBitpayAPI
{
get; set;
}
public int Order => 100;
public bool IsBitpayAPI
{
get; set;
}
public int Order => 100;
public bool Accept(ActionConstraintContext context)
{
return context.RouteContext.HttpContext.Request.Headers["x-accept-version"].Where(h => h == "2.0.0").Any() == IsBitpayAPI;
}
}
public bool Accept(ActionConstraintContext context)
{
return context.RouteContext.HttpContext.GetIsBitpayAPI() == IsBitpayAPI;
}
}
public class AcceptMediaTypeConstraintAttribute : Attribute, IActionConstraint
{
public AcceptMediaTypeConstraintAttribute(string mediaType, bool expectedValue = true)
{
MediaType = mediaType ?? throw new ArgumentNullException(nameof(mediaType));
ExpectedValue = expectedValue;
}
public class AcceptMediaTypeConstraintAttribute : Attribute, IActionConstraint
{
public AcceptMediaTypeConstraintAttribute(string mediaType, bool expectedValue = true)
{
MediaType = mediaType ?? throw new ArgumentNullException(nameof(mediaType));
ExpectedValue = expectedValue;
}
public bool ExpectedValue
{
get; set;
}
public bool ExpectedValue
{
get; set;
}
public string MediaType
{
get; set;
}
public string MediaType
{
get; set;
}
public int Order => 100;
public int Order => 100;
public bool Accept(ActionConstraintContext context)
{
var match = context.RouteContext.HttpContext.Request.Headers["Accept"].FirstOrDefault()?.StartsWith(MediaType, StringComparison.Ordinal);
return (match.HasValue && match.Value) == ExpectedValue;
}
}
public bool Accept(ActionConstraintContext context)
{
var hasHeader = context.RouteContext.HttpContext.Request.Headers["Accept"].Any(m => m.StartsWith(MediaType, StringComparison.Ordinal));
return hasHeader == ExpectedValue;
}
}
}

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