Compare commits

...

437 Commits

Author SHA1 Message Date
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 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 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 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 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 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 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
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
84cd9e570f Merge pull request 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 from bitmario/master
Add Portuguese (Portugal) translation
2018-04-16 11:28:54 +09:00
ed1464c405 Merge pull request from LinoxBE/dutch-translation
Dutch update txCount
2018-04-16 11:28:10 +09:00
f85631429b Merge pull request 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 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 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 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 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 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 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
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 2018-04-07 02:49:26 +09:00
1048dd516b Pass itemDesc to lightning invoice (Fix ) 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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
308 changed files with 37649 additions and 32121 deletions
.gitattributes.gitignore
BTCPayServer.Tests
BTCPayServer
Authentication
BTCPayNetwork.csBTCPayNetworkProvider.Bitcoin.csBTCPayNetworkProvider.Dogecoin.csBTCPayNetworkProvider.Litecoin.csBTCPayNetworkProvider.csBTCPayServer.csprojBTCPayServer.ruleset
Configuration
Controllers
CurrencyValue.cs
Data
DerivationSchemeParser.csDerivationStrategy.cs
Eclair
EventAggregator.cs
Events
ExplorerClientProvider.csExtensions.cs
Filters
HostedServices
Hosting
JsonConverters
Logging
Migrations
Models
MultiValueDictionary.cs
Payments
Properties
SearchString.cs
Services
StorePolicies.csSynchronizationContextRemover.cs
Views
bundleconfig.json
wwwroot
DockerfileREADME.mdbtcpayserver.slnbuild.ps1build.shpublish-docker.ps1run.ps1run.sh

17
.gitattributes vendored Normal 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

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

@ -4,11 +4,11 @@
<TargetFramework>netcoreapp2.0</TargetFramework>
<IsPackable>false</IsPackable>
<NoWarn>NU1701</NoWarn>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.5.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.0" />
<PackageReference Include="xunit" Version="2.3.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
</ItemGroup>

@ -1,5 +1,7 @@
using BTCPayServer.Configuration;
using BTCPayServer.Hosting;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Tests.Logging;
@ -45,7 +47,7 @@ namespace BTCPayServer.Tests
}
public Uri LTCNBXplorerUri { get; set; }
public Uri ServerUri
{
get;
@ -63,11 +65,14 @@ namespace BTCPayServer.Tests
get; set;
}
public bool MockRates { get; set; } = true;
public void Start()
{
if (!Directory.Exists(_Directory))
Directory.CreateDirectory(_Directory);
string chain = ChainType.Regtest.ToNetwork().Name;
string chain = NBXplorerDefaultSettings.GetFolderName(NetworkType.Regtest);
string chainDirectory = Path.Combine(_Directory, chain);
if (!Directory.Exists(chainDirectory))
Directory.CreateDirectory(chainDirectory);
@ -84,6 +89,8 @@ namespace BTCPayServer.Tests
config.AppendLine($"ltc.explorer.url={LTCNBXplorerUri.AbsoluteUri}");
config.AppendLine($"ltc.explorer.cookiefile=0");
config.AppendLine($"btc.lightning={IntegratedLightning.AbsoluteUri}");
if (Postgres != null)
config.AppendLine($"postgres=" + Postgres);
var confPath = Path.Combine(chainDirectory, "settings.config");
@ -91,18 +98,21 @@ namespace BTCPayServer.Tests
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 =>
{
var mockRates = new MockRateProviderFactory();
var btc = new MockRateProvider("BTC", new Rate("USD", 5000m));
var ltc = new MockRateProvider("LTC", new Rate("USD", 500m));
mockRates.AddMock(btc);
mockRates.AddMock(ltc);
s.AddSingleton<IRateProviderFactory>(mockRates);
if (MockRates)
{
var mockRates = new MockRateProviderFactory();
var btc = new MockRateProvider("BTC", new Rate("USD", 5000m), new Rate("CAD", 4500m));
var ltc = new MockRateProvider("LTC", new Rate("USD", 500m));
mockRates.AddMock(btc);
mockRates.AddMock(ltc);
s.AddSingleton<IRateProviderFactory>(mockRates);
}
s.AddLogging(l =>
{
l.SetMinimumLevel(LogLevel.Information)
@ -117,13 +127,15 @@ namespace BTCPayServer.Tests
_Host.Start();
InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository));
}
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>()
{

@ -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; }
}
}

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

@ -1,37 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Eclair;
namespace BTCPayServer.Tests
{
public class EclairTester
{
ServerTester parent;
public EclairTester(ServerTester parent, string environmentName, string defaultRPC, string defaultHost)
{
this.parent = parent;
//RPC = new EclairRPCClient(new Uri(parent.GetEnvironment(environmentName, defaultRPC)), parent.Network);
P2PHost = parent.GetEnvironment(environmentName + "_HOST", defaultHost);
}
public EclairRPCClient RPC { get; }
public string P2PHost { get; }
NodeInfo _NodeInfo;
public async Task<NodeInfo> GetNodeInfoAsync()
{
if (_NodeInfo != null)
return _NodeInfo;
var info = await RPC.GetInfoAsync();
_NodeInfo = new NodeInfo(info.NodeId, P2PHost, info.Port);
return _NodeInfo;
}
public NodeInfo GetNodeInfo()
{
return GetNodeInfoAsync().GetAwaiter().GetResult();
}
}
}

@ -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; }
}
}

@ -71,7 +71,11 @@ namespace BTCPayServer.Tests.Logging
public void LogInformation(string msg)
{
if (msg != null)
_Helper.WriteLine(DateTimeOffset.UtcNow + " :" + Name + ": " + msg);
try
{
_Helper.WriteLine(DateTimeOffset.UtcNow + " :" + Name + ": " + msg);
}
catch { }
}
}
public class Logs

@ -26,7 +26,7 @@ docker-compose down
If you want to stop, and remove all existing data
```
docker-compose down -v
docker-compose down --v
```
You can run the tests inside a container by running
@ -35,14 +35,50 @@ You can run the tests inside a container by running
docker-compose run --rm tests
```
## Send commands to bitcoind
## 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 exec -ti btcpayserver_dev_bitcoind bitcoin-cli -regtest -conf="/data/bitcoin.conf" -datadir="/data" sendtoaddress "mohu16LH66ptoWGEL1GtP6KHTBJYXMWhEf" 0.23111090
./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:
```
{ "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.

@ -17,7 +17,9 @@ using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using BTCPayServer.Eclair;
using System.Globalization;
using BTCPayServer.Payments.Lightning.CLightning;
using BTCPayServer.Payments.Lightning.Charge;
namespace BTCPayServer.Tests
{
@ -32,6 +34,34 @@ namespace BTCPayServer.Tests
public ServerTester(string scope)
{
_Directory = scope;
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);
ExplorerClient = new ExplorerClient(NetworkProvider.GetNetwork("BTC").NBXplorerNetwork, new Uri(GetEnvironment("TESTS_BTCNBXPLORERURL", "http://127.0.0.1:32838/")));
LTCExplorerClient = new ExplorerClient(NetworkProvider.GetNetwork("LTC").NBXplorerNetwork, new Uri(GetEnvironment("TESTS_LTCNBXPLORERURL", "http://127.0.0.1:32838/")));
var btc = NetworkProvider.GetNetwork("BTC").NBitcoinNetwork;
CustomerLightningD = new CLightningRPCClient(new Uri(GetEnvironment("TEST_CUSTOMERLIGHTNINGD", "tcp://127.0.0.1:30992/")), btc);
MerchantLightningD = new CLightningRPCClient(new Uri(GetEnvironment("TEST_MERCHANTLIGHTNINGD", "tcp://127.0.0.1:30993/")), btc);
MerchantCharge = new ChargeTester(this, "TEST_MERCHANTCHARGE", "http://api-token:foiewnccewuify@127.0.0.1:54938/", "merchant_lightningd", btc);
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 bool Dockerized
@ -41,62 +71,89 @@ namespace BTCPayServer.Tests
public void Start()
{
if (Directory.Exists(_Directory))
Utils.DeleteDirectory(_Directory);
if (!Directory.Exists(_Directory))
Directory.CreateDirectory(_Directory);
NetworkProvider = new BTCPayNetworkProvider(ChainType.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);
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/")));
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")
};
PayTester.Port = int.Parse(GetEnvironment("TESTS_PORT", Utils.FreeTcpPort().ToString()));
PayTester.HostName = GetEnvironment("TESTS_HOSTNAME", "127.0.0.1");
PayTester.Start();
MerchantEclair = new EclairTester(this, "TEST_ECLAIR1", "http://127.0.0.1:30992/", "eclair1");
CustomerEclair = new EclairTester(this, "TEST_ECLAIR2", "http://127.0.0.1:30993/", "eclair2");
}
/// <summary>
/// This will setup a channel going from customer to merchant
/// 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()
{
// Activate segwit
var blockCount = ExplorerNode.GetBlockCountAsync();
// Fetch node info, but that in cache
var merchant = MerchantEclair.GetNodeInfoAsync();
var customer = CustomerEclair.GetNodeInfoAsync();
var channels = CustomerEclair.RPC.ChannelsAsync();
var connect = CustomerEclair.RPC.ConnectAsync(merchant.Result);
await Task.WhenAll(blockCount, merchant, customer, channels, connect);
// Mine until segwit is activated
if (blockCount.Result <= 432)
while (true)
{
ExplorerNode.Generate(433 - blockCount.Result);
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 ?? "");
}
}
}
public EclairTester MerchantEclair { get; set; }
public EclairTester CustomerEclair { get; set; }
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)
{

@ -1,4 +1,5 @@
using BTCPayServer.Controllers;
using System.Linq;
using BTCPayServer.Models.AccountViewModels;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Services.Invoices;
@ -11,6 +12,8 @@ using System.Text;
using System.Threading.Tasks;
using Xunit;
using NBXplorer.DerivationStrategy;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
namespace BTCPayServer.Tests
{
@ -46,33 +49,24 @@ namespace BTCPayServer.Tests
Assert.IsType<ViewResult>(await store.RequestPairing(pairingCode.ToString()));
await store.Pair(pairingCode.ToString(), StoreId);
}
public StoresController CreateStore(string cryptoCode = null)
public StoresController CreateStore()
{
return CreateStoreAsync(cryptoCode).GetAwaiter().GetResult();
return CreateStoreAsync().GetAwaiter().GetResult();
}
public string CryptoCode { get; set; } = "BTC";
public async Task<StoresController> CreateStoreAsync(string cryptoCode = null)
public T GetController<T>() where T : Controller
{
cryptoCode = cryptoCode ?? CryptoCode;
SupportedNetwork = parent.NetworkProvider.GetNetwork(cryptoCode);
ExtKey = new ExtKey().GetWif(SupportedNetwork.NBitcoinNetwork);
var store = parent.PayTester.GetController<StoresController>(UserId);
return parent.PayTester.GetController<T>(UserId);
}
public async Task<StoresController> CreateStoreAsync()
{
var store = parent.PayTester.GetController<UserStoresController>(UserId);
await store.CreateStore(new CreateStoreViewModel() { Name = "Test Store" });
StoreId = store.CreatedStoreId;
DerivationScheme = new DerivationStrategyFactory(SupportedNetwork.NBitcoinNetwork).Parse(ExtKey.Neuter().ToString() + "-[legacy]");
var vm = (StoreViewModel)((ViewResult)await store.UpdateStore(StoreId)).Model;
vm.SpeedPolicy = SpeedPolicy.MediumSpeed;
await store.UpdateStore(StoreId, vm);
await store.AddDerivationScheme(StoreId, new DerivationSchemeViewModel()
{
CryptoCurrency = cryptoCode,
DerivationSchemeFormat = "BTCPay",
DerivationScheme = DerivationScheme.ToString(),
Confirmation = true
}, "Save");
return store;
var store2 = parent.PayTester.GetController<StoresController>(UserId);
store2.CreatedStoreId = store.CreatedStoreId;
return store2;
}
public BTCPayNetwork SupportedNetwork { get; set; }
@ -81,18 +75,21 @@ namespace BTCPayServer.Tests
{
RegisterDerivationSchemeAsync(crytoCode).GetAwaiter().GetResult();
}
public async Task RegisterDerivationSchemeAsync(string crytoCode)
public async Task RegisterDerivationSchemeAsync(string cryptoCode)
{
SupportedNetwork = parent.NetworkProvider.GetNetwork(cryptoCode);
var store = parent.PayTester.GetController<StoresController>(UserId);
var networkProvider = parent.PayTester.GetService<BTCPayNetworkProvider>();
var derivation = new DerivationStrategyFactory(networkProvider.GetNetwork(crytoCode).NBitcoinNetwork).Parse(ExtKey.Neuter().ToString() + "-[legacy]");
ExtKey = new ExtKey().GetWif(SupportedNetwork.NBitcoinNetwork);
DerivationScheme = new DerivationStrategyFactory(SupportedNetwork.NBitcoinNetwork).Parse(ExtKey.Neuter().ToString() + "-[legacy]");
var vm = (StoreViewModel)((ViewResult)await store.UpdateStore(StoreId)).Model;
vm.SpeedPolicy = SpeedPolicy.MediumSpeed;
await store.UpdateStore(StoreId, vm);
await store.AddDerivationScheme(StoreId, new DerivationSchemeViewModel()
{
CryptoCurrency = crytoCode,
DerivationSchemeFormat = crytoCode,
DerivationScheme = derivation.ToString(),
DerivationScheme = DerivationScheme.ToString(),
Confirmation = true
}, "Save");
}, cryptoCode);
}
public DerivationStrategyBase DerivationScheme { get; set; }
@ -122,5 +119,24 @@ namespace BTCPayServer.Tests
{
get; set;
}
}
public void RegisterLightningNode(string cryptoCode, LightningConnectionType connectionType)
{
RegisterLightningNodeAsync(cryptoCode, connectionType).GetAwaiter().GetResult();
}
public async Task RegisterLightningNodeAsync(string cryptoCode, LightningConnectionType connectionType)
{
var storeController = parent.PayTester.GetController<StoresController>(UserId);
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);
}
}
}

@ -22,10 +22,19 @@ using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using BTCPayServer.Services.Rates;
using Microsoft.Extensions.Caching.Memory;
using BTCPayServer.Eclair;
using System.Collections.Generic;
using BTCPayServer.Models.StoreViewModels;
using System.Threading.Tasks;
using System.Globalization;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.HostedServices;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Stores;
using System.Net.Http;
using System.Text;
namespace BTCPayServer.Tests
{
@ -37,6 +46,63 @@ namespace BTCPayServer.Tests
Logs.LogProvider = new XUnitLogProvider(helper);
}
[Fact]
public void CanCalculateCryptoDue2()
{
var dummy = new Key().PubKey.GetAddress(Network.RegTest).ToString();
#pragma warning disable CS0618
InvoiceEntity invoiceEntity = new InvoiceEntity();
invoiceEntity.Payments = new System.Collections.Generic.List<PaymentEntity>();
invoiceEntity.ProductInformation = new ProductInformation() { Price = 100 };
PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary();
paymentMethods.Add(new PaymentMethod()
{
CryptoCode = "BTC",
Rate = 10513.44m,
}.SetPaymentMethodDetails(new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod()
{
TxFee = Money.Coins(0.00000100m),
DepositAddress = dummy
}));
paymentMethods.Add(new PaymentMethod()
{
CryptoCode = "LTC",
Rate = 216.79m
}.SetPaymentMethodDetails(new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod()
{
TxFee = Money.Coins(0.00010000m),
DepositAddress = dummy
}));
invoiceEntity.SetPaymentMethods(paymentMethods);
var btc = invoiceEntity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null);
var accounting = btc.Calculate();
invoiceEntity.Payments.Add(new PaymentEntity() { Accounted = true, CryptoCode = "BTC" }.SetCryptoPaymentData(new BitcoinLikePaymentData()
{
Output = new TxOut() { Value = Money.Coins(0.00151263m) }
}));
accounting = btc.Calculate();
invoiceEntity.Payments.Add(new PaymentEntity() { Accounted = true, CryptoCode = "BTC" }.SetCryptoPaymentData(new BitcoinLikePaymentData()
{
Output = new TxOut() { Value = accounting.Due }
}));
accounting = btc.Calculate();
Assert.Equal(Money.Zero, accounting.Due);
Assert.Equal(Money.Zero, accounting.DueUncapped);
var ltc = invoiceEntity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike), null);
accounting = ltc.Calculate();
Assert.Equal(Money.Zero, accounting.Due);
// LTC might have over paid due to BTC paying above what it should (round 1 satoshi up)
Assert.True(accounting.DueUncapped < Money.Zero);
var paymentMethod = InvoiceWatcher.GetNearestClearedPayment(paymentMethods, out var accounting2, null);
Assert.Equal(btc.CryptoCode, paymentMethod.CryptoCode);
#pragma warning restore CS0618
}
[Fact]
public void CanCalculateCryptoDue()
{
@ -49,79 +115,81 @@ namespace BTCPayServer.Tests
entity.ProductInformation = new ProductInformation() { Price = 5000 };
// Some check that handling legacy stuff does not break things
var cryptoData = entity.GetCryptoData("BTC", null, true);
cryptoData.Calculate();
Assert.NotNull(cryptoData);
Assert.Null(entity.GetCryptoData("BTC", null, false));
entity.SetCryptoData(new CryptoData() { ParentEntity = entity, Rate = entity.Rate, CryptoCode = "BTC", TxFee = entity.TxFee });
Assert.NotNull(entity.GetCryptoData("BTC", null, false));
Assert.NotNull(entity.GetCryptoData("BTC", null, true));
var paymentMethod = entity.GetPaymentMethods(null, true).TryGet("BTC", PaymentTypes.BTCLike);
paymentMethod.Calculate();
Assert.NotNull(paymentMethod);
Assert.Null(entity.GetPaymentMethods(null, false).TryGet("BTC", PaymentTypes.BTCLike));
entity.SetPaymentMethod(new PaymentMethod() { ParentEntity = entity, Rate = entity.Rate, CryptoCode = "BTC", TxFee = entity.TxFee });
Assert.NotNull(entity.GetPaymentMethods(null, false).TryGet("BTC", PaymentTypes.BTCLike));
Assert.NotNull(entity.GetPaymentMethods(null, true).TryGet("BTC", PaymentTypes.BTCLike));
////////////////////
var accounting = cryptoData.Calculate();
var accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(1.1m), accounting.Due);
Assert.Equal(Money.Coins(1.1m), accounting.TotalDue);
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.5m), new Key()), Accounted = true });
accounting = cryptoData.Calculate();
accounting = paymentMethod.Calculate();
//Since we need to spend one more txout, it should be 1.1 - 0,5 + 0.1
Assert.Equal(Money.Coins(0.7m), accounting.Due);
Assert.Equal(Money.Coins(1.2m), accounting.TotalDue);
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()), Accounted = true });
accounting = cryptoData.Calculate();
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(0.6m), accounting.Due);
Assert.Equal(Money.Coins(1.3m), accounting.TotalDue);
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.6m), new Key()), Accounted = true });
accounting = cryptoData.Calculate();
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Zero, accounting.Due);
Assert.Equal(Money.Coins(1.3m), accounting.TotalDue);
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()), Accounted = true });
accounting = cryptoData.Calculate();
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Zero, accounting.Due);
Assert.Equal(Money.Coins(1.3m), accounting.TotalDue);
entity = new InvoiceEntity();
entity.ProductInformation = new ProductInformation() { Price = 5000 };
entity.SetCryptoData(new System.Collections.Generic.Dictionary<string, CryptoData>(new KeyValuePair<string, CryptoData>[] {
new KeyValuePair<string,CryptoData>("BTC", new CryptoData()
{
Rate = 1000,
TxFee = Money.Coins(0.1m)
}),
new KeyValuePair<string,CryptoData>("LTC", new CryptoData()
{
Rate = 500,
TxFee = Money.Coins(0.01m)
})
}));
PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary();
paymentMethods.Add(new PaymentMethod()
{
CryptoCode = "BTC",
Rate = 1000,
TxFee = Money.Coins(0.1m)
});
paymentMethods.Add(new PaymentMethod()
{
CryptoCode = "LTC",
Rate = 500,
TxFee = Money.Coins(0.01m)
});
entity.SetPaymentMethods(paymentMethods);
entity.Payments = new List<PaymentEntity>();
cryptoData = entity.GetCryptoData("BTC", null);
accounting = cryptoData.Calculate();
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null);
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(5.1m), accounting.Due);
cryptoData = entity.GetCryptoData("LTC", null);
accounting = cryptoData.Calculate();
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike), null);
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(10.01m), accounting.TotalDue);
entity.Payments.Add(new PaymentEntity() { CryptoCode = "BTC", Output = new TxOut(Money.Coins(1.0m), new Key()), Accounted = true });
cryptoData = entity.GetCryptoData("BTC", null);
accounting = cryptoData.Calculate();
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null);
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(4.2m), accounting.Due);
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
Assert.Equal(Money.Coins(1.0m), accounting.Paid);
Assert.Equal(Money.Coins(5.2m), accounting.TotalDue);
Assert.Equal(2, accounting.TxCount);
Assert.Equal(2, accounting.TxRequired);
cryptoData = entity.GetCryptoData("LTC", null);
accounting = cryptoData.Calculate();
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike), null);
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(10.01m + 0.1m * 2 - 2.0m /* 8.21m */), accounting.Due);
Assert.Equal(Money.Coins(0.0m), accounting.CryptoPaid);
Assert.Equal(Money.Coins(2.0m), accounting.Paid);
@ -130,44 +198,43 @@ namespace BTCPayServer.Tests
entity.Payments.Add(new PaymentEntity() { CryptoCode = "LTC", Output = new TxOut(Money.Coins(1.0m), new Key()), Accounted = true });
cryptoData = entity.GetCryptoData("BTC", null);
accounting = cryptoData.Calculate();
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null);
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(4.2m - 0.5m + 0.01m / 2), accounting.Due);
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
Assert.Equal(Money.Coins(1.5m), accounting.Paid);
Assert.Equal(Money.Coins(5.2m + 0.01m / 2), accounting.TotalDue); // The fee for LTC added
Assert.Equal(2, accounting.TxCount);
Assert.Equal(2, accounting.TxRequired);
cryptoData = entity.GetCryptoData("LTC", null);
accounting = cryptoData.Calculate();
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike), null);
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(8.21m - 1.0m + 0.01m), accounting.Due);
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
Assert.Equal(Money.Coins(3.0m), accounting.Paid);
Assert.Equal(Money.Coins(10.01m + 0.1m * 2 + 0.01m), accounting.TotalDue);
Assert.Equal(2, accounting.TxCount);
Assert.Equal(2, accounting.TxRequired);
var remaining = Money.Coins(4.2m - 0.5m + 0.01m / 2);
entity.Payments.Add(new PaymentEntity() { CryptoCode = "BTC", Output = new TxOut(remaining, new Key()), Accounted = true });
cryptoData = entity.GetCryptoData("BTC", null);
accounting = cryptoData.Calculate();
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null);
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Zero, accounting.Due);
Assert.Equal(Money.Coins(1.0m) + remaining, accounting.CryptoPaid);
Assert.Equal(Money.Coins(1.5m) + remaining, accounting.Paid);
Assert.Equal(Money.Coins(5.2m + 0.01m / 2), accounting.TotalDue);
Assert.Equal(accounting.Paid, accounting.TotalDue);
Assert.Equal(2, accounting.TxCount);
Assert.Equal(2, accounting.TxRequired);
cryptoData = entity.GetCryptoData("LTC", null);
accounting = cryptoData.Calculate();
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike), null);
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Zero, accounting.Due);
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
Assert.Equal(Money.Coins(3.0m) + remaining * 2, accounting.Paid);
// Paying 2 BTC fee, LTC fee removed because fully paid
Assert.Equal(Money.Coins(10.01m + 0.1m * 2 + 0.1m * 2 /* + 0.01m no need to pay this fee anymore */), accounting.TotalDue);
Assert.Equal(1, accounting.TxCount);
Assert.Equal(1, accounting.TxRequired);
Assert.Equal(accounting.Paid, accounting.TotalDue);
#pragma warning restore CS0618
}
@ -179,6 +246,7 @@ namespace BTCPayServer.Tests
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Buyer = new Buyer() { email = "test@fwf.com" },
@ -224,18 +292,171 @@ namespace BTCPayServer.Tests
{
var light = LightMoney.MilliSatoshis(1);
Assert.Equal("0.00000000001", light.ToString());
light = LightMoney.MilliSatoshis(200000);
Assert.Equal(200m, light.ToDecimal(LightMoneyUnit.Satoshi));
Assert.Equal(0.00000001m * 200m, light.ToDecimal(LightMoneyUnit.BTC));
}
//[Fact]
//public void CanSendLightningPayment()
//{
[Fact]
public void CanSetLightningServer()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
var storeController = tester.PayTester.GetController<StoresController>(user.UserId);
Assert.IsType<ViewResult>(storeController.UpdateStore(user.StoreId).GetAwaiter().GetResult());
Assert.IsType<ViewResult>(storeController.AddLightningNode(user.StoreId, "BTC").GetAwaiter().GetResult());
// using (var tester = ServerTester.Create())
// {
// tester.Start();
// tester.PrepareLightning();
// }
//}
var testResult = storeController.AddLightningNode(user.StoreId, new LightningNodeViewModel()
{
Url = tester.MerchantCharge.Client.Uri.AbsoluteUri
}, "test", "BTC").GetAwaiter().GetResult();
Assert.DoesNotContain("Error", ((LightningNodeViewModel)Assert.IsType<ViewResult>(testResult).Model).StatusMessage, StringComparison.OrdinalIgnoreCase);
Assert.True(storeController.ModelState.IsValid);
Assert.IsType<RedirectToActionResult>(storeController.AddLightningNode(user.StoreId, new LightningNodeViewModel()
{
Url = tester.MerchantCharge.Client.Uri.AbsoluteUri
}, "save", "BTC").GetAwaiter().GetResult());
var storeVm = Assert.IsType<Models.StoreViewModels.StoreViewModel>(Assert.IsType<ViewResult>(storeController.UpdateStore(user.StoreId).GetAwaiter().GetResult()).Model);
Assert.Single(storeVm.LightningNodes.Where(l => !string.IsNullOrEmpty(l.Address)));
}
}
[Fact]
public void CanParseLightningURL()
{
LightningConnectionString conn = null;
Assert.True(LightningConnectionString.TryParse("/test/a", out conn));
Assert.Equal("unix://test/a", conn.ToString());
Assert.Equal("unix://test/a", conn.ToUri(true).AbsoluteUri);
Assert.Equal("unix://test/a", conn.ToUri(false).AbsoluteUri);
Assert.Equal(LightningConnectionType.CLightning, conn.ConnectionType);
Assert.True(LightningConnectionString.TryParse("unix://test/a", out conn));
Assert.Equal("unix://test/a", conn.ToString());
Assert.Equal("unix://test/a", conn.ToUri(true).AbsoluteUri);
Assert.Equal("unix://test/a", conn.ToUri(false).AbsoluteUri);
Assert.Equal(LightningConnectionType.CLightning, conn.ConnectionType);
Assert.True(LightningConnectionString.TryParse("unix://test/a", out conn));
Assert.Equal("unix://test/a", conn.ToString());
Assert.Equal("unix://test/a", conn.ToUri(true).AbsoluteUri);
Assert.Equal("unix://test/a", conn.ToUri(false).AbsoluteUri);
Assert.Equal(LightningConnectionType.CLightning, conn.ConnectionType);
Assert.True(LightningConnectionString.TryParse("tcp://test/a", out conn));
Assert.Equal("tcp://test/a", conn.ToString());
Assert.Equal("tcp://test/a", conn.ToUri(true).AbsoluteUri);
Assert.Equal("tcp://test/a", conn.ToUri(false).AbsoluteUri);
Assert.Equal(LightningConnectionType.CLightning, conn.ConnectionType);
Assert.True(LightningConnectionString.TryParse("http://aaa:bbb@test/a", out conn));
Assert.Equal("http://aaa:bbb@test/a", conn.ToString());
Assert.Equal("http://aaa:bbb@test/a", conn.ToUri(true).AbsoluteUri);
Assert.Equal("http://test/a", conn.ToUri(false).AbsoluteUri);
Assert.Equal(LightningConnectionType.Charge, conn.ConnectionType);
Assert.Equal("aaa", conn.Username);
Assert.Equal("bbb", conn.Password);
Assert.False(LightningConnectionString.TryParse("lol://aaa:bbb@test/a", out conn));
Assert.False(LightningConnectionString.TryParse("https://test/a", out conn));
Assert.False(LightningConnectionString.TryParse("unix://dwewoi:dwdwqd@test/a", out conn));
}
[Fact]
public void CanSendLightningPayment2()
{
using (var tester = ServerTester.Create())
{
tester.Start();
tester.PrepareLightning();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterLightningNode("BTC", LightningConnectionType.CLightning);
user.RegisterDerivationScheme("BTC");
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 0.01,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description"
});
tester.SendLightningPayment(invoice);
Eventually(() =>
{
var localInvoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal("complete", localInvoice.Status);
Assert.Equal("False", localInvoice.ExceptionStatus.ToString());
});
}
}
[Fact]
public void CanSendLightningPayment()
{
using (var tester = ServerTester.Create())
{
tester.Start();
tester.PrepareLightning();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterLightningNode("BTC", LightningConnectionType.Charge);
user.RegisterDerivationScheme("BTC");
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 0.01,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description"
});
tester.SendLightningPayment(invoice);
Eventually(() =>
{
var localInvoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal("complete", localInvoice.Status);
Assert.Equal("False", localInvoice.ExceptionStatus.ToString());
});
Task.WaitAll(Enumerable.Range(0, 5)
.Select(_ => CanSendLightningPaymentCore(tester, user))
.ToArray());
}
}
async Task CanSendLightningPaymentCore(ServerTester tester, TestAccount user)
{
await Task.Delay(TimeSpan.FromSeconds(RandomUtils.GetUInt32() % 5));
var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice()
{
Price = 0.01,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description"
});
await tester.SendLightningPaymentAsync(invoice);
await EventuallyAsync(async () =>
{
var localInvoice = await user.BitPay.GetInvoiceAsync(invoice.Id);
Assert.Equal("complete", localInvoice.Status);
Assert.Equal("False", localInvoice.ExceptionStatus.ToString());
});
}
[Fact]
public void CanUseServerInitiatedPairingCode()
@ -272,6 +493,7 @@ namespace BTCPayServer.Tests
tester.Start();
var acc = tester.NewAccount();
acc.GrantAccess();
acc.RegisterDerivationScheme("BTC");
var invoice = acc.BitPay.CreateInvoice(new Invoice()
{
Price = 5.0,
@ -312,7 +534,7 @@ namespace BTCPayServer.Tests
pairingCode = acc.BitPay.RequestClientAuthorization("test1", Facade.Merchant);
var store2 = acc.CreateStore();
store2.Pair(pairingCode.ToString(), store2.CreatedStoreId).GetAwaiter().GetResult();
Assert.Contains(nameof(PairingResult.ReusedKey), store2.StatusMessage);
Assert.Contains(nameof(PairingResult.ReusedKey), store2.StatusMessage, StringComparison.CurrentCultureIgnoreCase);
}
}
@ -324,17 +546,17 @@ namespace BTCPayServer.Tests
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5000.0,
Currency = "USD"
}, Facade.Merchant);
var payment1 = Money.Coins(0.04m);
var payment2 = Money.Coins(0.08m);
var payment1 = invoice.BtcDue + Money.Coins(0.0001m);
var payment2 = invoice.BtcDue;
var tx1 = new uint256(tester.ExplorerNode.SendCommand("sendtoaddress", new object[]
{
invoice.BitcoinAddress.ToString(),
invoice.BitcoinAddress,
payment1.ToString(),
null, //comment
null, //comment_to
@ -347,6 +569,8 @@ namespace BTCPayServer.Tests
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal(payment1, invoice.BtcPaid);
Assert.Equal("paid", invoice.Status);
Assert.Equal("paidOver", invoice.ExceptionStatus.ToString());
invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, user.SupportedNetwork.NBitcoinNetwork);
});
@ -355,11 +579,9 @@ namespace BTCPayServer.Tests
{
input.ScriptSig = Script.Empty; //Strip signatures
}
var change = tx.Outputs.First(o => o.Value != payment1);
var output = tx.Outputs.First(o => o.Value == payment1);
output.Value = payment2;
output.ScriptPubKey = invoiceAddress.ScriptPubKey;
change.Value -= (payment2 - payment1) * 2; //Add more fees
var replaced = tester.ExplorerNode.SignRawTransaction(tx);
tester.ExplorerNode.SendRawTransaction(replaced);
var test = tester.ExplorerClient.GetUTXOs(user.DerivationScheme, null);
@ -367,6 +589,7 @@ namespace BTCPayServer.Tests
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal(payment2, invoice.BtcPaid);
Assert.Equal("False", invoice.ExceptionStatus.ToString());
});
}
}
@ -378,8 +601,18 @@ namespace BTCPayServer.Tests
var search = new SearchString(filter);
Assert.Equal("storeid:abc status:abed blabhbalh", search.ToString());
Assert.Equal("blabhbalh", search.TextSearch);
Assert.Equal("abc", search.Filters["storeid"]);
Assert.Equal("abed", search.Filters["status"]);
Assert.Single(search.Filters["storeid"]);
Assert.Single(search.Filters["status"]);
Assert.Equal("abc", search.Filters["storeid"].First());
Assert.Equal("abed", search.Filters["status"].First());
filter = "status:abed status:abed2";
search = new SearchString(filter);
Assert.Equal("status:abed status:abed2", search.ToString());
Assert.Throws<KeyNotFoundException>(() => search.Filters["test"]);
Assert.Equal(2, search.Filters["status"].Count);
Assert.Equal("abed", search.Filters["status"].First());
Assert.Equal("abed2", search.Filters["status"].Skip(1).First());
}
[Fact]
@ -391,20 +624,93 @@ namespace BTCPayServer.Tests
var user = tester.NewAccount();
Assert.False(user.BitPay.TestAccess(Facade.Merchant));
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
Assert.True(user.BitPay.TestAccess(Facade.Merchant));
// Can generate API Key
var repo = tester.PayTester.GetService<TokenRepository>();
Assert.Empty(repo.GetLegacyAPIKeys(user.StoreId).GetAwaiter().GetResult());
Assert.IsType<RedirectToActionResult>(user.GetController<StoresController>().GenerateAPIKey(user.StoreId).GetAwaiter().GetResult());
var apiKey = Assert.Single(repo.GetLegacyAPIKeys(user.StoreId).GetAwaiter().GetResult());
///////
// Generating a new one remove the previous
Assert.IsType<RedirectToActionResult>(user.GetController<StoresController>().GenerateAPIKey(user.StoreId).GetAwaiter().GetResult());
var apiKey2 = Assert.Single(repo.GetLegacyAPIKeys(user.StoreId).GetAwaiter().GetResult());
Assert.NotEqual(apiKey, apiKey2);
////////
apiKey = apiKey2;
// Can create an invoice with this new API Key
HttpClient client = new HttpClient();
HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Post, tester.PayTester.ServerUri.AbsoluteUri + "invoices");
message.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Encoders.Base64.EncodeData(Encoders.ASCII.DecodeData(apiKey)));
var invoice = new Invoice()
{
Price = 5000.0,
Currency = "USD"
};
message.Content = new StringContent(JsonConvert.SerializeObject(invoice), Encoding.UTF8, "application/json");
var result = client.SendAsync(message).GetAwaiter().GetResult();
result.EnsureSuccessStatusCode();
/////////////////////
}
}
[Fact]
public void CanUseExchangeSpecificRate()
{
using (var tester = ServerTester.Create())
{
tester.PayTester.MockRates = false;
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
List<decimal> rates = new List<decimal>();
rates.Add(CreateInvoice(tester, user, "coinaverage"));
var bitflyer = CreateInvoice(tester, user, "bitflyer");
var bitflyer2 = CreateInvoice(tester, user, "bitflyer");
Assert.Equal(bitflyer, bitflyer2); // Should be equal because cache
rates.Add(bitflyer);
foreach (var rate in rates)
{
Assert.Single(rates.Where(r => r == rate));
}
}
}
private static decimal CreateInvoice(ServerTester tester, TestAccount user, string exchange)
{
var storeController = tester.PayTester.GetController<StoresController>(user.UserId);
var vm = (StoreViewModel)((ViewResult)storeController.UpdateStore(user.StoreId).Result).Model;
vm.PreferredExchange = exchange;
storeController.UpdateStore(user.StoreId, vm).Wait();
var invoice2 = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5000.0,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
return invoice2.CryptoInfo[0].Rate;
}
[Fact]
public void CanTweakRate()
{
using (var tester = ServerTester.Create())
{
tester.PayTester.MockRates = false;
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
// First we try payment with a merchant having only BTC
var invoice1 = user.BitPay.CreateInvoice(new Invoice()
@ -446,8 +752,8 @@ namespace BTCPayServer.Tests
{
tester.Start();
var user = tester.NewAccount();
user.CryptoCode = "LTC";
user.GrantAccess();
user.RegisterDerivationScheme("LTC");
// First we try payment with a merchant having only BTC
var invoice = user.BitPay.CreateInvoice(new Invoice()
@ -506,7 +812,7 @@ namespace BTCPayServer.Tests
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
// First we try payment with a merchant having only BTC
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
@ -565,7 +871,7 @@ namespace BTCPayServer.Tests
var ltcCryptoInfo = invoice.CryptoInfo.FirstOrDefault(c => c.CryptoCode == "LTC");
Assert.NotNull(ltcCryptoInfo);
invoiceAddress = BitcoinAddress.Create(ltcCryptoInfo.Address, cashCow.Network);
var secondPayment = Money.Coins(decimal.Parse(ltcCryptoInfo.Due));
var secondPayment = Money.Coins(decimal.Parse(ltcCryptoInfo.Due, CultureInfo.InvariantCulture));
cashCow.Generate(2); // LTC is not worth a lot, so just to make sure we have money...
cashCow.SendToAddress(invoiceAddress, secondPayment);
Logs.Tester.LogInformation("Second payment sent to " + invoiceAddress);
@ -587,6 +893,185 @@ namespace BTCPayServer.Tests
}
}
[Fact]
public void CanParseCurrencyValue()
{
Assert.True(CurrencyValue.TryParse("1.50USD", out var result));
Assert.Equal("1.50 USD", result.ToString());
Assert.True(CurrencyValue.TryParse("1.50 USD", out result));
Assert.Equal("1.50 USD", result.ToString());
Assert.True(CurrencyValue.TryParse("1.50 usd", out result));
Assert.Equal("1.50 USD", result.ToString());
Assert.True(CurrencyValue.TryParse("1 usd", out result));
Assert.Equal("1 USD", result.ToString());
Assert.True(CurrencyValue.TryParse("1usd", out result));
Assert.Equal("1 USD", result.ToString());
Assert.True(CurrencyValue.TryParse("1.501 usd", out result));
Assert.Equal("1.50 USD", result.ToString());
Assert.False(CurrencyValue.TryParse("1.501 WTFF", out result));
Assert.False(CurrencyValue.TryParse("1,501 usd", out result));
Assert.False(CurrencyValue.TryParse("1.501", out result));
}
[Fact]
public void CanParseDerivationScheme()
{
var parser = new DerivationSchemeParser(Network.TestNet);
NBXplorer.DerivationStrategy.DerivationStrategyBase result;
// Passing electrum stuff
// Native
result = parser.Parse("zpub6nL6PUGurpU3DfPDSZaRS6WshpbNc9ctCFFzrCn54cssnheM31SZJZUcFHKtjJJNhAueMbh6ptFMfy1aeiMQJr3RJ4DDt1hAPx7sMTKV48t");
Assert.Equal("tpubD93CJNkmGjLXnsBqE2zGDqfEh1Q8iJ8wueordy3SeWt1RngbbuxXCsqASuVWFywmfoCwUE1rSfNJbaH4cBNcbp8WcyZgPiiRSTazLGL8U9w", result.ToString());
// P2SH
result = parser.Parse("ypub6QqdH2c5z79681jUgdxjGJzGW9zpL4ryPCuhtZE4GpvrJoZqM823XQN6iSQeVbbbp2uCRQ9UgpeMcwiyV6qjvxTWVcxDn2XEAnioMUwsrQ5");
Assert.Equal("tpubD6NzVbkrYhZ4YWjDJUACG9E8fJx2NqNY1iynTiPKEjJrzzRKAgha3nNnwGXr2BtvCJKJHW4nmG7rRqc2AGGy2AECgt16seMyV2FZivUmaJg-[p2sh]", result.ToString());
result = parser.Parse("xpub661MyMwAqRbcGeVGU5e5KBcau1HHEUGf9Wr7k4FyLa8yRPNQrrVa7Ndrgg8Afbe2UYXMSL6tJBFd2JewwWASsePPLjkcJFL1tTVEs3UQ23X");
Assert.Equal("tpubD6NzVbkrYhZ4YSg7vGdAX6wxE8NwDrmih9SR6cK7gUtsAg37w5LfFpJgviCxC6bGGT4G3uckqH5fiV9ZLN1gm5qgQLVuymzFUR5ed7U7ksu-[legacy]", result.ToString());
////////////////
var tpub = "tpubD6NzVbkrYhZ4Wc65tjhmcKdWFauAo7bGLRTxvggygkNyp6SMGutJp7iociwsinU33jyNBp1J9j2hJH5yQsayfiS3LEU2ZqXodAcnaygra8o";
result = parser.Parse(tpub);
Assert.Equal(tpub, result.ToString());
parser.HintScriptPubKey = BitcoinAddress.Create("tb1q4s33amqm8l7a07zdxcunqnn3gcsjcfz3xc573l", parser.Network).ScriptPubKey;
result = parser.Parse(tpub);
Assert.Equal(tpub, result.ToString());
parser.HintScriptPubKey = BitcoinAddress.Create("2N2humNio3YTApSfY6VztQ9hQwDnhDvaqFQ", parser.Network).ScriptPubKey;
result = parser.Parse(tpub);
Assert.Equal($"{tpub}-[p2sh]", result.ToString());
parser.HintScriptPubKey = BitcoinAddress.Create("mwD8bHS65cdgUf6rZUUSoVhi3wNQFu1Nfi", parser.Network).ScriptPubKey;
result = parser.Parse(tpub);
Assert.Equal($"{tpub}-[legacy]", result.ToString());
parser.HintScriptPubKey = BitcoinAddress.Create("2N2humNio3YTApSfY6VztQ9hQwDnhDvaqFQ", parser.Network).ScriptPubKey;
result = parser.Parse($"{tpub}-[legacy]");
Assert.Equal($"{tpub}-[p2sh]", result.ToString());
result = parser.Parse(tpub);
Assert.Equal($"{tpub}-[p2sh]", result.ToString());
}
[Fact]
public void CanSetPaymentMethodLimits()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
user.RegisterLightningNode("BTC", LightningConnectionType.Charge);
var vm = Assert.IsType<CheckoutExperienceViewModel>(Assert.IsType<ViewResult>(user.GetController<StoresController>().CheckoutExperience(user.StoreId).Result).Model);
vm.LightningMaxValue = "2 USD";
vm.OnChainMinValue = "5 USD";
Assert.IsType<RedirectToActionResult>(user.GetController<StoresController>().CheckoutExperience(user.StoreId, vm).Result);
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 1.5,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
Assert.Single(invoice.CryptoInfo);
Assert.Equal(PaymentTypes.LightningLike.ToString(), invoice.CryptoInfo[0].PaymentType);
invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5.5,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
Assert.Single(invoice.CryptoInfo);
Assert.Equal(PaymentTypes.BTCLike.ToString(), invoice.CryptoInfo[0].PaymentType);
}
}
[Fact]
public void CanUsePoSApp()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
var apps = user.GetController<AppsController>();
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model);
vm.Name = "test";
vm.SelectedAppType = AppType.PointOfSale.ToString();
Assert.IsType<RedirectToActionResult>(apps.CreateApp(vm).Result);
var appId = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model).Apps[0].Id;
var vmpos = Assert.IsType<UpdatePointOfSaleViewModel>(Assert.IsType<ViewResult>(apps.UpdatePointOfSale(appId).Result).Model);
vmpos.Title = "hello";
vmpos.Currency = "CAD";
vmpos.Template =
"apple:\n" +
" price: 5.0\n" +
" title: good apple\n" +
"orange:\n" +
" price: 10.0\n";
Assert.IsType<RedirectToActionResult>(apps.UpdatePointOfSale(appId, vmpos).Result);
vmpos = Assert.IsType<UpdatePointOfSaleViewModel>(Assert.IsType<ViewResult>(apps.UpdatePointOfSale(appId).Result).Model);
Assert.Equal("hello", vmpos.Title);
var vmview = Assert.IsType<ViewPointOfSaleViewModel>(Assert.IsType<ViewResult>(apps.ViewPointOfSale(appId).Result).Model);
Assert.Equal("hello", vmview.Title);
Assert.Equal(2, vmview.Items.Length);
Assert.Equal("good apple", vmview.Items[0].Title);
Assert.Equal("orange", vmview.Items[1].Title);
Assert.Equal(10.0m, vmview.Items[1].Price.Value);
Assert.Equal("$5.00", vmview.Items[0].Price.Formatted);
Assert.IsType<RedirectResult>(apps.ViewPointOfSale(appId, 0, "orange").Result);
var invoice = user.BitPay.GetInvoices().First();
Assert.Equal(10.00, invoice.Price);
Assert.Equal("CAD", invoice.Currency);
Assert.Equal("orange", invoice.ItemDesc);
}
}
[Fact]
public void CanCreateAndDeleteApps()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
var user2 = tester.NewAccount();
user2.GrantAccess();
var apps = user.GetController<AppsController>();
var apps2 = user2.GetController<AppsController>();
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model);
Assert.NotNull(vm.SelectedAppType);
Assert.Null(vm.Name);
vm.Name = "test";
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.CreateApp(vm).Result);
Assert.Equal(nameof(apps.ListApps), redirectToAction.ActionName);
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model);
var appList2 = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps2.ListApps().Result).Model);
Assert.Single(appList.Apps);
Assert.Empty(appList2.Apps);
Assert.Equal("test", appList.Apps[0].AppName);
Assert.True(appList.Apps[0].IsOwner);
Assert.Equal(user.StoreId, appList.Apps[0].StoreId);
Assert.IsType<NotFoundResult>(apps2.DeleteApp(appList.Apps[0].Id).Result);
Assert.IsType<ViewResult>(apps.DeleteApp(appList.Apps[0].Id).Result);
redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(appList.Apps[0].Id).Result);
Assert.Equal(nameof(apps.ListApps), redirectToAction.ActionName);
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model);
Assert.Empty(appList.Apps);
}
}
[Fact]
public void InvoiceFlowThroughDifferentStatesCorrectly()
{
@ -595,6 +1080,7 @@ namespace BTCPayServer.Tests
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5000.0,
@ -606,18 +1092,18 @@ namespace BTCPayServer.Tests
}, Facade.Merchant);
var repo = tester.PayTester.GetService<InvoiceRepository>();
var ctx = tester.PayTester.GetService<ApplicationDbContextFactory>().CreateContext();
Assert.Equal(0, invoice.CryptoInfo[0].TxCount);
Eventually(() =>
{
var textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery()
{
StoreId = user.StoreId,
StoreId = new[] { user.StoreId },
TextSearch = invoice.OrderId
}).GetAwaiter().GetResult();
Assert.Single(textSearchResult);
textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery()
{
StoreId = user.StoreId,
StoreId = new[] { user.StoreId },
TextSearch = invoice.Id
}).GetAwaiter().GetResult();
@ -629,11 +1115,11 @@ namespace BTCPayServer.Tests
Assert.Equal("new", invoice.Status);
Assert.False((bool)((JValue)invoice.ExceptionStatus).Value);
Assert.Single(user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime));
Assert.Empty(user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime + TimeSpan.FromDays(1)));
Assert.Single(user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5)));
Assert.Single(user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5), invoice.InvoiceTime.DateTime));
Assert.Empty(user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5), invoice.InvoiceTime.DateTime - TimeSpan.FromDays(1)));
Assert.Single(user.BitPay.GetInvoices(invoice.InvoiceTime.UtcDateTime));
Assert.Empty(user.BitPay.GetInvoices(invoice.InvoiceTime.UtcDateTime + TimeSpan.FromDays(2)));
Assert.Single(user.BitPay.GetInvoices(invoice.InvoiceTime.UtcDateTime - TimeSpan.FromDays(5)));
Assert.Single(user.BitPay.GetInvoices(invoice.InvoiceTime.UtcDateTime - TimeSpan.FromDays(5), invoice.InvoiceTime.DateTime + TimeSpan.FromDays(1.0)));
Assert.Empty(user.BitPay.GetInvoices(invoice.InvoiceTime.UtcDateTime - TimeSpan.FromDays(5), invoice.InvoiceTime.DateTime - TimeSpan.FromDays(1)));
var firstPayment = Money.Coins(0.04m);
@ -662,14 +1148,15 @@ namespace BTCPayServer.Tests
Assert.Equal(firstPayment, localInvoice.BtcPaid);
txFee = localInvoice.BtcDue - invoice.BtcDue;
Assert.Equal("paidPartial", localInvoice.ExceptionStatus.ToString());
Assert.Equal(1, localInvoice.CryptoInfo[0].TxCount);
Assert.NotEqual(localInvoice.BitcoinAddress, invoice.BitcoinAddress); //New address
Assert.True(IsMapped(invoice, ctx));
Assert.True(IsMapped(localInvoice, ctx));
invoiceEntity = repo.GetInvoice(null, invoice.Id, true).GetAwaiter().GetResult();
var historical1 = invoiceEntity.HistoricalAddresses.FirstOrDefault(h => h.GetAddress() == invoice.BitcoinAddress.ToString());
var historical1 = invoiceEntity.HistoricalAddresses.FirstOrDefault(h => h.GetAddress() == invoice.BitcoinAddress);
Assert.NotNull(historical1.UnAssigned);
var historical2 = invoiceEntity.HistoricalAddresses.FirstOrDefault(h => h.GetAddress() == localInvoice.BitcoinAddress.ToString());
var historical2 = invoiceEntity.HistoricalAddresses.FirstOrDefault(h => h.GetAddress() == localInvoice.BitcoinAddress);
Assert.Null(historical2.UnAssigned);
invoiceAddress = BitcoinAddress.Create(localInvoice.BitcoinAddress, cashCow.Network);
secondPayment = localInvoice.BtcDue;
@ -681,6 +1168,7 @@ namespace BTCPayServer.Tests
{
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("paid", localInvoice.Status);
Assert.Equal(2, localInvoice.CryptoInfo[0].TxCount);
Assert.Equal(firstPayment + secondPayment, localInvoice.BtcPaid);
Assert.Equal(Money.Zero, localInvoice.BtcDue);
Assert.Equal(localInvoice.BitcoinAddress, invoiceAddress.ToString()); //no new address generated
@ -740,6 +1228,26 @@ namespace BTCPayServer.Tests
}
}
[Fact]
public void CheckQuadrigacxRateProvider()
{
var quadri = new QuadrigacxRateProvider("BTC");
var rates = quadri.GetRatesAsync().GetAwaiter().GetResult();
Assert.NotEmpty(rates);
Assert.NotEqual(0.0m, rates.First().Value);
Assert.NotEqual(0.0m, quadri.GetRateAsync("CAD").GetAwaiter().GetResult());
Assert.NotEqual(0.0m, quadri.GetRateAsync("USD").GetAwaiter().GetResult());
Assert.Throws<RateUnavailableException>(() => quadri.GetRateAsync("IOEW").GetAwaiter().GetResult());
quadri = new QuadrigacxRateProvider("LTC");
rates = quadri.GetRatesAsync().GetAwaiter().GetResult();
Assert.NotEmpty(rates);
Assert.NotEqual(0.0m, rates.First().Value);
Assert.NotEqual(0.0m, quadri.GetRateAsync("CAD").GetAwaiter().GetResult());
Assert.Throws<RateUnavailableException>(() => quadri.GetRateAsync("IOEW").GetAwaiter().GetResult());
Assert.Throws<RateUnavailableException>(() => quadri.GetRateAsync("USD").GetAwaiter().GetResult());
}
[Fact]
public void CheckRatesProvider()
{
@ -761,8 +1269,8 @@ namespace BTCPayServer.Tests
private static bool IsMapped(Invoice invoice, ApplicationDbContext ctx)
{
var h = BitcoinAddress.Create(invoice.BitcoinAddress).ScriptPubKey.Hash;
return ctx.AddressInvoices.FirstOrDefault(i => i.InvoiceDataId == invoice.Id && i.GetHash() == h) != null;
var h = BitcoinAddress.Create(invoice.BitcoinAddress, Network.RegTest).ScriptPubKey.Hash.ToString();
return ctx.AddressInvoices.FirstOrDefault(i => i.InvoiceDataId == invoice.Id && i.GetAddress() == h) != null;
}
private void Eventually(Action act)
@ -781,5 +1289,22 @@ namespace BTCPayServer.Tests
}
}
}
private async Task EventuallyAsync(Func<Task> act)
{
CancellationTokenSource cts = new CancellationTokenSource(20000);
while (true)
{
try
{
await act();
break;
}
catch (XunitException) when (!cts.Token.IsCancellationRequested)
{
await Task.Delay(500);
}
}
}
}
}

@ -1,52 +0,0 @@
using NBitcoin;
using NBitcoin.DataEncoders;
using NBitpayClient;
using System;
using System.Collections.Generic;
using System.Text;
using Xunit;
namespace BTCPayServer.Tests
{
// Helper class for testing functionality and generating data needed during coding/debuging
public class UnitTestPeusa
{
// Unit test that generates temorary checkout Bitpay page
// https://forkbitpay.slack.com/archives/C7M093Z55/p1508293682000217
// Testnet of Bitpay down
//[Fact]
//public void BitpayCheckout()
//{
// var key = new Key(Encoders.Hex.DecodeData("7b70a06f35562873e3dcb46005ed0fe78e1991ad906e56adaaafa40ba861e056"));
// var url = new Uri("https://test.bitpay.com/");
// var btcpay = new Bitpay(key, url);
// var invoice = btcpay.CreateInvoice(new Invoice()
// {
// Price = 5.0,
// Currency = "USD",
// PosData = "posData",
// OrderId = "cdfd8a5f-6928-4c3b-ba9b-ddf438029e73",
// ItemDesc = "Hello from the otherside"
// }, Facade.Merchant);
// // go to invoice.Url
// Console.WriteLine(invoice.Url);
//}
// Generating Extended public key to use on http://localhost:14142/stores/{storeId}
[Fact]
public void GeneratePubkey()
{
var network = Network.RegTest;
ExtKey masterKey = new ExtKey();
Console.WriteLine("Master key : " + masterKey.ToString(network));
ExtPubKey masterPubKey = masterKey.Neuter();
ExtPubKey pubkey = masterPubKey.Derive(0);
Console.WriteLine("PubKey " + 0 + " : " + pubkey.ToString(network));
}
}
}

@ -1 +1 @@
docker exec -ti btcpayserver_dev_bitcoind bitcoin-cli -regtest -conf="/data/bitcoin.conf" -datadir="/data" $args
docker exec -ti btcpayservertests_bitcoind_1 bitcoin-cli -datadir="/data" $args

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

@ -1,7 +1,7 @@
version: "3"
# Run `docker-compose up dev` for bootstrapping your development environment
# Doing so will expose eclair API, NBXplorer, Bitcoind RPC and postgres port to the host so that tests can Run,
# 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:
@ -17,17 +17,23 @@ services:
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:
- nbxplorer
- postgres
- 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.15.0.1
image: nicolasdorier/docker-bitcoin:0.16.0
environment:
BITCOIN_EXTRA_ARGS: |
regtest=1
@ -35,9 +41,12 @@ services:
links:
- nbxplorer
- postgres
- customer_lightningd
- merchant_lightningd
- lightning-charged
nbxplorer:
image: nicolasdorier/nbxplorer:1.0.1.13
image: nicolasdorier/nbxplorer:1.0.2.2
ports:
- "32838:32838"
expose:
@ -61,8 +70,7 @@ services:
- litecoind
bitcoind:
container_name: btcpayserver_dev_bitcoind
image: nicolasdorier/docker-bitcoin:0.15.0.1
image: nicolasdorier/docker-bitcoin:0.16.0
environment:
BITCOIN_EXTRA_ARGS: |
rpcuser=ceiwHEbqWI83
@ -77,10 +85,74 @@ services:
expose:
- "43782" # RPC
- "39388" # P2P
volumes:
- "bitcoin_datadir:/data"
customer_lightningd:
image: nicolasdorier/clightning:0.0.0.11-dev
environment:
EXPOSE_TCP: "true"
LIGHTNINGD_OPT: |
bitcoin-datadir=/etc/bitcoin
bitcoin-rpcconnect=bitcoind
network=regtest
ipaddr=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.9
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.11-dev
environment:
EXPOSE_TCP: "true"
LIGHTNINGD_OPT: |
bitcoin-datadir=/etc/bitcoin
bitcoin-rpcconnect=bitcoind
ipaddr=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:
container_name: btcpayserver_dev_litecoind
image: nicolasdorier/docker-litecoin:0.14.2
image: nicolasdorier/docker-litecoin:0.15.1
environment:
BITCOIN_EXTRA_ARGS: |
rpcuser=ceiwHEbqWI83
@ -102,3 +174,9 @@ services:
- "39372:5432"
expose:
- "5432"
volumes:
bitcoin_datadir:
customer_lightningd_datadir:
merchant_lightningd_datadir:
lightning_charge_datadir:

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

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

@ -1 +1 @@
docker exec -ti btcpayserver_dev_litecoind litecoin-cli -regtest -conf="/data/litecoin.conf" -datadir="/data" $args
docker exec -ti btcpayservertests_litecoind_1 litecoin-cli -datadir="/data" $args

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

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

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

@ -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;
}
}

@ -33,6 +33,8 @@ namespace BTCPayServer.Authentication
public async Task<BitTokenEntity[]> GetTokens(string sin)
{
if (sin == null)
return Array.Empty<BitTokenEntity>();
using (var ctx = _Factory.CreateContext())
{
return (await ctx.PairedSINData
@ -43,6 +45,46 @@ namespace BTCPayServer.Authentication
}
}
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 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)];
}
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<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()

@ -3,6 +3,7 @@ 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;
@ -13,34 +14,28 @@ namespace BTCPayServer
{
static BTCPayDefaultSettings()
{
_Settings = new Dictionary<ChainType, BTCPayDefaultSettings>();
foreach (var chainType in new[] { ChainType.Main, ChainType.Test, ChainType.Regtest })
_Settings = new Dictionary<NetworkType, BTCPayDefaultSettings>();
foreach (var chainType in new[] { NetworkType.Mainnet, NetworkType.Testnet, NetworkType.Regtest })
{
var btcNetwork = (chainType == ChainType.Main ? Network.Main :
chainType == ChainType.Regtest ? Network.RegTest :
chainType == ChainType.Test ? Network.TestNet : throw new NotSupportedException(chainType.ToString()));
var settings = new BTCPayDefaultSettings();
_Settings.Add(chainType, settings);
settings.ChainType = chainType;
settings.DefaultDataDirectory = StandardConfiguration.DefaultDataDirectory.GetDirectory("BTCPayServer", btcNetwork.Name);
settings.DefaultDataDirectory = StandardConfiguration.DefaultDataDirectory.GetDirectory("BTCPayServer", NBXplorerDefaultSettings.GetFolderName(chainType));
settings.DefaultConfigurationFile = Path.Combine(settings.DefaultDataDirectory, "settings.config");
settings.DefaultPort = (chainType == ChainType.Main ? 23000 :
chainType == ChainType.Regtest ? 23002 :
chainType == ChainType.Test ? 23001 : throw new NotSupportedException(chainType.ToString()));
settings.DefaultPort = (chainType == NetworkType.Mainnet ? 23000 :
chainType == NetworkType.Regtest ? 23002 :
chainType == NetworkType.Testnet ? 23001 : throw new NotSupportedException(chainType.ToString()));
}
}
static Dictionary<ChainType, BTCPayDefaultSettings> _Settings;
static Dictionary<NetworkType, BTCPayDefaultSettings> _Settings;
public static BTCPayDefaultSettings GetDefaultSettings(ChainType chainType)
public static BTCPayDefaultSettings GetDefaultSettings(NetworkType chainType)
{
return _Settings[chainType];
}
public string DefaultDataDirectory { get; set; }
public string DefaultConfigurationFile { get; set; }
public ChainType ChainType { get; internal set; }
public int DefaultPort { get; set; }
}
public class BTCPayNetwork
@ -49,7 +44,7 @@ namespace BTCPayServer
public string CryptoCode { get; internal set; }
public string BlockExplorerLink { get; internal set; }
public string UriScheme { get; internal set; }
public IRateProvider DefaultRateProvider { get; set; }
public RateProviderDescription DefaultRateProvider { get; set; }
[Obsolete("Should not be needed")]
public bool IsBTC
@ -61,15 +56,22 @@ namespace BTCPayServer
}
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 override string ToString()
{
return CryptoCode;
}
internal KeyPath GetRootKeyPath()
{
return new KeyPath(NBitcoinNetwork.Consensus.SupportSegwit ? "49'" : "44'")
.Derive(CoinType);
}
}
}

@ -14,20 +14,21 @@ namespace BTCPayServer
public void InitBitcoin()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("BTC");
var coinaverage = new CoinAverageRateProvider("BTC");
var bitpay = new BitpayRateProvider(new Bitpay(new Key(), new Uri("https://bitpay.com/")));
var btcRate = new FallbackRateProvider(new IRateProvider[] { coinaverage, bitpay });
var coinaverage = new CoinAverageRateProviderDescription("BTC");
var bitpay = new BitpayRateProviderDescription();
var btcRate = new FallbackRateProviderDescription(new RateProviderDescription[] { coinaverage, bitpay });
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
BlockExplorerLink = NBXplorerNetworkProvider.ChainType == ChainType.Main? "https://www.smartbit.com.au/tx/{0}" : "https://testnet.smartbit.com.au/tx/{0}",
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",
DefaultRateProvider = btcRate,
CryptoImagePath = "imlegacy/bitcoin-symbol.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NBXplorerNetworkProvider.ChainType),
CoinType = NBXplorerNetworkProvider.ChainType == ChainType.Main ? new KeyPath("0'") : new KeyPath("1'")
LightningImagePath = "imlegacy/btc-lightning.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("0'") : new KeyPath("1'")
});
}
}

@ -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 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",
DefaultRateProvider = new CoinAverageRateProviderDescription("DOGE"),
CryptoImagePath = "imlegacy/dogecoin.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("3'") : new KeyPath("1'")
});
}
}
}

@ -12,21 +12,19 @@ namespace BTCPayServer
{
public void InitLitecoin()
{
NBXplorer.Altcoins.Litecoin.Networks.EnsureRegistered();
var ltcRate = new CoinAverageRateProvider("LTC");
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("LTC");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
BlockExplorerLink = NBXplorerNetworkProvider.ChainType == ChainType.Main ? "https://live.blockcypher.com/ltc/tx/{0}/" : "http://explorer.litecointools.com/tx/{0}",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://live.blockcypher.com/ltc/tx/{0}/" : "http://explorer.litecointools.com/tx/{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "litecoin",
DefaultRateProvider = ltcRate,
DefaultRateProvider = new CoinAverageRateProviderDescription("LTC"),
CryptoImagePath = "imlegacy/litecoin-symbol.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NBXplorerNetworkProvider.ChainType),
CoinType = NBXplorerNetworkProvider.ChainType == ChainType.Main ? new KeyPath("2'") : new KeyPath("3'")
LightningImagePath = "imlegacy/ltc-lightning.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("2'") : new KeyPath("1'")
});
}
}

@ -25,11 +25,39 @@ namespace BTCPayServer
}
}
public BTCPayNetworkProvider(ChainType chainType)
BTCPayNetworkProvider(BTCPayNetworkProvider filtered, string[] cryptoCodes)
{
_NBXplorerNetworkProvider = new NBXplorerNetworkProvider(chainType);
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();
}
/// <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")]
@ -43,7 +71,7 @@ namespace BTCPayServer
public void Add(BTCPayNetwork network)
{
_Networks.Add(network.CryptoCode, network);
_Networks.Add(network.CryptoCode.ToUpperInvariant(), network);
}
public IEnumerable<BTCPayNetwork> GetAll()
@ -51,6 +79,11 @@ namespace BTCPayServer
return _Networks.Values.ToArray();
}
public bool Support(string cryptoCode)
{
return _Networks.ContainsKey(cryptoCode.ToUpperInvariant());
}
public BTCPayNetwork GetNetwork(string cryptoCode)
{
_Networks.TryGetValue(cryptoCode.ToUpperInvariant(), out BTCPayNetwork network);

@ -2,14 +2,26 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<Version>1.0.1.28</Version>
<NoWarn>NU1701</NoWarn>
<Version>1.0.1.96</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" />
@ -18,27 +30,33 @@
<EmbeddedResource Include="Currencies.txt" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Hangfire" Version="1.6.17" />
<PackageReference Include="BuildBundlerMinifier" Version="2.6.375" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.4.0" />
<PackageReference Include="Hangfire" Version="1.6.19" />
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.2" />
<PackageReference Include="Hangfire.PostgreSql" Version="1.4.8.1" />
<PackageReference Include="LedgerWallet" Version="1.0.1.32" />
<PackageReference Include="LedgerWallet" Version="1.0.1.36" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="2.6.1" />
<PackageReference Include="Meziantou.AspNetCore.BundleTagHelpers" Version="1.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
<PackageReference Include="NBitcoin" Version="4.0.0.54" />
<PackageReference Include="NBitpayClient" Version="1.0.0.16" />
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="2.6.0" />
<PackageReference Include="NBitcoin" Version="4.1.1.3" />
<PackageReference Include="NBitpayClient" Version="1.0.0.18" />
<PackageReference Include="DBreeze" Version="1.87.0" />
<PackageReference Include="NBXplorer.Client" Version="1.0.1.9" />
<PackageReference Include="NBXplorer.Client" Version="1.0.2.3" />
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.1" />
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.2" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.13" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.0.1" />
<PackageReference Include="System.ValueTuple" Version="4.4.0" />
<PackageReference Include="System.Xml.XmlSerializer" Version="4.0.11" />
<PackageReference Include="Text.Analyzers" Version="2.6.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.1" PrivateAssets="All" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.0.2" PrivateAssets="All" />
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.2" PrivateAssets="All" />
<PackageReference Include="YamlDotNet" Version="4.3.1" />
</ItemGroup>
<ItemGroup>
@ -48,13 +66,8 @@
</ItemGroup>
<ItemGroup>
<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" />
@ -100,6 +113,7 @@
<ItemGroup>
<Folder Include="Build\" />
<Folder Include="wwwroot\vendor\clipboard.js\" />
</ItemGroup>
<ItemGroup>

@ -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>

@ -10,6 +10,7 @@ using System.Text;
using StandardConfiguration;
using Microsoft.Extensions.Configuration;
using NBXplorer;
using BTCPayServer.Payments.Lightning;
namespace BTCPayServer.Configuration
{
@ -22,7 +23,7 @@ namespace BTCPayServer.Configuration
public class BTCPayServerOptions
{
public ChainType ChainType
public NetworkType NetworkType
{
get; set;
}
@ -50,36 +51,59 @@ namespace BTCPayServer.Configuration
public void LoadArgs(IConfiguration conf)
{
ChainType = DefaultConfiguration.GetChainType(conf);
var defaultSettings = BTCPayDefaultSettings.GetDefaultSettings(ChainType);
NetworkType = DefaultConfiguration.GetNetworkType(conf);
var defaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType);
DataDir = conf.GetOrDefault<string>("datadir", defaultSettings.DefaultDataDirectory);
Logs.Configuration.LogInformation("Network: " + ChainType.ToString());
Logs.Configuration.LogInformation("Network: " + NetworkType.ToString());
var supportedChains = conf.GetOrDefault<string>("chains", "btc")
.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(t => t.ToUpperInvariant());
var validChains = new List<string>();
foreach (var net in new BTCPayNetworkProvider(ChainType).GetAll())
NetworkProvider = new BTCPayNetworkProvider(NetworkType).Filter(supportedChains.ToArray());
foreach (var chain in supportedChains)
{
if (supportedChains.Contains(net.CryptoCode))
if (NetworkProvider.GetNetwork(chain) == null)
throw new ConfigException($"Invalid chains \"{chain}\"");
}
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)
{
validChains.Add(net.CryptoCode);
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);
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);
}
}
var invalidChains = String.Join(',', supportedChains.Where(s => !validChains.Contains(s)).ToArray());
if(!string.IsNullOrEmpty(invalidChains))
throw new ConfigException($"Invalid chains {invalidChains}");
Logs.Configuration.LogInformation("Supported chains: " + String.Join(',', supportedChains.ToArray()));
PostgresConnectionString = conf.GetOrDefault<string>("postgres", null);
ExternalUrl = conf.GetOrDefault<Uri>("externalurl", null);
}
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;
@ -90,5 +114,19 @@ namespace BTCPayServer.Configuration
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();
}
}
}

@ -13,7 +13,7 @@ namespace BTCPayServer.Configuration
{
public static T GetOrDefault<T>(this IConfiguration configuration, string key, T defaultValue)
{
var str = configuration[key] ?? configuration[key.Replace(".", string.Empty)];
var str = configuration[key] ?? configuration[key.Replace(".", string.Empty, StringComparison.InvariantCulture)];
if (str == null)
return defaultValue;
if (typeof(T) == typeof(bool))
@ -27,17 +27,24 @@ namespace BTCPayServer.Configuration
throw new FormatException();
}
else if (typeof(T) == typeof(Uri))
return (T)(object)new Uri(str, UriKind.Absolute);
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(":");
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));
return (T)(object)new IPEndPoint(IPAddress.Parse(ip), int.Parse(port, CultureInfo.InvariantCulture));
}
else if (typeof(T) == typeof(int))
{

@ -18,7 +18,7 @@ namespace BTCPayServer.Configuration
{
protected override CommandLineApplication CreateCommandLineApplicationCore()
{
var provider = new BTCPayNetworkProvider(ChainType.Main);
var provider = new BTCPayNetworkProvider(NetworkType.Mainnet);
var chains = string.Join(",", provider.GetAll().Select(n => n.CryptoCode.ToLowerInvariant()).ToArray());
CommandLineApplication app = new CommandLineApplication(true)
{
@ -31,13 +31,16 @@ namespace BTCPayServer.Configuration
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);
}
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);
return app;
}
@ -45,12 +48,12 @@ namespace BTCPayServer.Configuration
protected override string GetDefaultDataDir(IConfiguration conf)
{
return BTCPayDefaultSettings.GetDefaultSettings(GetChainType(conf)).DefaultDataDirectory;
return BTCPayDefaultSettings.GetDefaultSettings(GetNetworkType(conf)).DefaultDataDirectory;
}
protected override string GetDefaultConfigurationFile(IConfiguration conf)
{
var network = BTCPayDefaultSettings.GetDefaultSettings(GetChainType(conf));
var network = BTCPayDefaultSettings.GetDefaultSettings(GetNetworkType(conf));
var dataDir = conf["datadir"];
if (dataDir == null)
return network.DefaultConfigurationFile;
@ -66,7 +69,7 @@ namespace BTCPayServer.Configuration
return Path.Combine(chainDir, fileName);
}
public static ChainType GetChainType(IConfiguration conf)
public static NetworkType GetNetworkType(IConfiguration conf)
{
var network = conf.GetOrDefault<string>("network", null);
if (network != null)
@ -76,17 +79,18 @@ namespace BTCPayServer.Configuration
{
throw new ConfigException($"Invalid network parameter '{network}'");
}
return n.ToChainType();
return n.NetworkType;
}
var net = conf.GetOrDefault<bool>("regtest", false) ? ChainType.Regtest :
conf.GetOrDefault<bool>("testnet", false) ? ChainType.Test : ChainType.Main;
var net = conf.GetOrDefault<bool>("regtest", false) ? NetworkType.Regtest :
conf.GetOrDefault<bool>("testnet", false) ? NetworkType.Testnet : NetworkType.Mainnet;
return net;
}
protected override string GetDefaultConfigurationFileTemplate(IConfiguration conf)
{
var defaultSettings = BTCPayDefaultSettings.GetDefaultSettings(GetChainType(conf));
var networkType = GetNetworkType(conf);
var defaultSettings = BTCPayDefaultSettings.GetDefaultSettings(networkType);
StringBuilder builder = new StringBuilder();
builder.AppendLine("### Global settings ###");
builder.AppendLine("#network=mainnet");
@ -99,10 +103,12 @@ namespace BTCPayServer.Configuration
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(defaultSettings.ChainType).GetAll())
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();
}
@ -111,7 +117,7 @@ namespace BTCPayServer.Configuration
protected override IPEndPoint GetDefaultEndpoint(IConfiguration conf)
{
return new IPEndPoint(IPAddress.Parse("127.0.0.1"), BTCPayDefaultSettings.GetDefaultSettings(GetChainType(conf)).DefaultPort);
return new IPEndPoint(IPAddress.Parse("127.0.0.1"), BTCPayDefaultSettings.GetDefaultSettings(GetNetworkType(conf)).DefaultPort);
}
}
}

@ -12,6 +12,7 @@ using System.Threading.Tasks;
namespace BTCPayServer.Controllers
{
[BitpayAPIConstraint]
public class AccessTokenController : Controller
{
TokenRepository _TokenRepository;
@ -23,7 +24,7 @@ namespace BTCPayServer.Controllers
[Route("tokens")]
public async Task<GetTokensResponse> Tokens()
{
var tokens = await _TokenRepository.GetTokens(this.GetBitIdentity().SIN);
var tokens = await _TokenRepository.GetTokens(this.User.GetSIN());
return new GetTokensResponse(tokens);
}
@ -51,7 +52,7 @@ namespace BTCPayServer.Controllers
}
else
{
var sin = this.GetBitIdentity(false)?.SIN ?? request.Id;
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");

@ -150,7 +150,7 @@ namespace BTCPayServer.Controllers
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);
@ -204,7 +204,7 @@ namespace BTCPayServer.Controllers
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);

@ -0,0 +1,225 @@
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;
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>();
return View(new UpdatePointOfSaleViewModel() { Title = settings.Title, ShowCustomAmount = settings.ShowCustomAmount, Currency = settings.Currency, Template = settings.Template });
}
[HttpPost]
[Route("{appId}/settings/pos")]
public async Task<IActionResult> UpdatePointOfSale(string appId, UpdatePointOfSaleViewModel vm)
{
if (_Currencies.GetCurrencyData(vm.Currency) == 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);
double step = currency == null ? 1 : Math.Pow(10, -(currency.Divisibility));
return View(new ViewPointOfSaleViewModel()
{
Title = settings.Title,
Step = step.ToString(CultureInfo.InvariantCulture),
ShowCustomAmount = settings.ShowCustomAmount,
Items = Parse(settings.Template, settings.Currency)
});
}
private async Task<AppData> GetApp(string appId, AppType appType)
{
using (var ctx = _ContextFactory.CreateContext())
{
return await ctx.Apps
.Where(us => us.Id == appId && us.AppType == appType.ToString())
.FirstOrDefaultAsync();
}
}
private ViewPointOfSaleViewModel.Item[] Parse(string template, string currency)
{
var input = new StringReader(template);
YamlStream stream = new YamlStream();
stream.Load(input);
var root = (YamlMappingNode)stream.Documents[0].RootNode;
return root
.Children
.Select(kv => new { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlMappingNode })
.Where(kv => kv.Value != null)
.Select(c => new ViewPointOfSaleViewModel.Item()
{
Id = c.Key,
Title = c.Value.Children
.Select(kv => new { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlScalarNode })
.Where(kv => kv.Value != null)
.Where(cc => cc.Key == "title")
.FirstOrDefault()?.Value?.Value ?? c.Key,
Price = c.Value.Children
.Select(kv => new { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlScalarNode })
.Where(kv => kv.Value != null)
.Where(cc => cc.Key == "price")
.Select(cc => new ViewPointOfSaleViewModel.Item.ItemPrice()
{
Value = decimal.Parse(cc.Value.Value, CultureInfo.InvariantCulture),
Formatted = FormatCurrency(cc.Value.Value, currency)
})
.Single()
})
.ToArray();
}
string FormatCurrency(string price, string currency)
{
return decimal.Parse(price, CultureInfo.InvariantCulture).ToString("C", _Currencies.GetCurrencyProvider(currency));
}
[HttpPost]
[Route("{appId}/pos")]
public async Task<IActionResult> ViewPointOfSale(string appId, double amount, string choiceKey)
{
var app = await GetApp(appId, AppType.PointOfSale);
if (string.IsNullOrEmpty(choiceKey) && amount <= 0)
{
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
}
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
if(string.IsNullOrEmpty(choiceKey) && !settings.ShowCustomAmount)
{
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
}
string title = null;
double price = 0.0;
if (!string.IsNullOrEmpty(choiceKey))
{
var choices = Parse(settings.Template, settings.Currency);
var choice = choices.FirstOrDefault(c => c.Id == choiceKey);
if (choice == null)
return NotFound();
title = choice.Title;
price = (double)choice.Price.Value;
}
else
{
price = amount;
title = settings.Title;
}
var store = await GetStore(app);
var invoice = await _InvoiceController.CreateInvoiceCore(new NBitpayClient.Invoice()
{
ItemDesc = title,
Currency = settings.Currency,
Price = price,
}, store, HttpContext.Request.GetAbsoluteRoot());
return Redirect(invoice.Data.Url);
}
private async Task<StoreData> GetStore(AppData app)
{
using (var ctx = _ContextFactory.CreateContext())
{
return await ctx.Stores.FirstOrDefaultAsync(s => s.Id == app.StoreDataId);
}
}
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();
}
}
}
}

@ -0,0 +1,203 @@
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 (type != null && type.Value.ToString() != app.AppType)
return null;
return app;
}
}
private async Task<StoreData[]> GetOwnedStores()
{
var userId = GetUserId();
using (var ctx = _ContextFactory.CreateContext())
{
return await ctx.UserStore
.Where(us => us.ApplicationUserId == userId && us.Role == StoreRoles.Owner)
.Select(u => u.StoreData)
.ToArrayAsync();
}
}
private async Task<bool> DeleteApp(AppData appData)
{
using (var ctx = _ContextFactory.CreateContext())
{
ctx.Apps.Add(appData);
ctx.Entry<AppData>(appData).State = EntityState.Deleted;
return await ctx.SaveChangesAsync() == 1;
}
}
private async Task<ListAppsViewModel.ListAppViewModel[]> GetAllApps()
{
var userId = GetUserId();
using (var ctx = _ContextFactory.CreateContext())
{
return await ctx.UserStore
.Where(us => us.ApplicationUserId == userId)
.Select(us => new
{
IsOwner = us.Role == StoreRoles.Owner,
StoreId = us.StoreDataId,
StoreName = us.StoreData.StoreName,
Apps = us.StoreData.Apps
})
.SelectMany(us => us.Apps.Select(app => new ListAppsViewModel.ListAppViewModel()
{
IsOwner = us.IsOwner,
AppName = app.Name,
AppType = app.AppType,
Id = app.Id,
StoreId = us.StoreId,
StoreName = us.StoreName
}))
.ToArrayAsync();
}
}
private string GetUserId()
{
return _UserManager.GetUserId(User);
}
}
}

@ -22,20 +22,14 @@ namespace BTCPayServer.Controllers
{
private InvoiceController _InvoiceController;
private InvoiceRepository _InvoiceRepository;
private TokenRepository _TokenRepository;
private StoreRepository _StoreRepository;
private BTCPayNetworkProvider _NetworkProvider;
public InvoiceControllerAPI(InvoiceController invoiceController,
InvoiceRepository invoceRepository,
TokenRepository tokenRepository,
StoreRepository storeRepository,
BTCPayNetworkProvider networkProvider)
{
this._InvoiceController = invoiceController;
this._InvoiceRepository = invoceRepository;
this._TokenRepository = tokenRepository;
this._StoreRepository = storeRepository;
this._NetworkProvider = networkProvider;
}
@ -44,21 +38,16 @@ namespace BTCPayServer.Controllers
[MediaTypeConstraint("application/json")]
public async Task<DataWrapper<InvoiceResponse>> CreateInvoice([FromBody] Invoice invoice)
{
var bitToken = await CheckTokenPermissionAsync(Facade.Merchant, invoice.Token);
var store = await FindStore(bitToken);
return await _InvoiceController.CreateInvoiceCore(invoice, store, HttpContext.Request.GetAbsoluteRoot());
return await _InvoiceController.CreateInvoiceCore(invoice, HttpContext.GetStoreData(), HttpContext.Request.GetAbsoluteRoot());
}
[HttpGet]
[Route("invoices/{id}")]
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);
var invoice = await _InvoiceRepository.GetInvoice(HttpContext.GetStoreData().Id, id);
if (invoice == null)
throw new BitpayHttpException(404, "Object not found");
var resp = invoice.EntityToDTO(_NetworkProvider);
return new DataWrapper<InvoiceResponse>(resp);
}
@ -77,8 +66,7 @@ namespace BTCPayServer.Controllers
{
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,
@ -87,55 +75,14 @@ namespace BTCPayServer.Controllers
StartDate = dateStart,
OrderId = orderId,
ItemCode = itemCode,
Status = status,
StoreId = store.Id
Status = status == null ? null : new[] { status },
StoreId = new[] { this.HttpContext.GetStoreData().Id }
};
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;
}
}
}

@ -9,13 +9,15 @@ 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}")]
[Route("i/{invoiceId}/{cryptoCode?}")]
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest")]
public async Task<IActionResult> GetInvoiceRequest(string invoiceId, string cryptoCode = null)
{
@ -23,11 +25,12 @@ namespace BTCPayServer.Controllers
cryptoCode = "BTC";
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
var network = _NetworkProvider.GetNetwork(cryptoCode);
if (invoice == null || invoice.IsExpired() || network == null || !invoice.Support(network))
var paymentMethodId = new PaymentMethodId(cryptoCode, Payments.PaymentTypes.BTCLike);
if (invoice == null || invoice.IsExpired() || network == null || !invoice.Support(paymentMethodId))
return NotFound();
var dto = invoice.EntityToDTO(_NetworkProvider);
var cryptoData = dto.CryptoInfo.First(c => c.CryptoCode.Equals(cryptoCode, StringComparison.OrdinalIgnoreCase));
var paymentMethod = dto.CryptoInfo.First(c => c.GetpaymentMethodId() == paymentMethodId);
PaymentRequest request = new PaymentRequest
{
DetailsVersion = 1
@ -35,7 +38,7 @@ namespace BTCPayServer.Controllers
request.Details.Expires = invoice.ExpirationTime;
request.Details.Memo = invoice.ProductInformation.ItemDesc;
request.Details.Network = network.NBitcoinNetwork;
request.Details.Outputs.Add(new PaymentOutput() { Amount = cryptoData.Due, Script = BitcoinAddress.Create(cryptoData.Address, network.NBitcoinNetwork).ScriptPubKey });
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);
@ -69,7 +72,7 @@ namespace BTCPayServer.Controllers
if (cryptoCode == null)
cryptoCode = "BTC";
var network = _NetworkProvider.GetNetwork(cryptoCode);
if (network == null || invoice == null || invoice.IsExpired() || !invoice.Support(network))
if (network == null || invoice == null || invoice.IsExpired() || !invoice.Support(new PaymentMethodId(cryptoCode, Payments.PaymentTypes.BTCLike)))
return NotFound();
var wallet = _WalletProvider.GetWallet(network);

@ -20,6 +20,8 @@ using System.Net.WebSockets;
using System.Threading;
using BTCPayServer.Events;
using NBXplorer;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
namespace BTCPayServer.Controllers
{
@ -63,163 +65,232 @@ namespace BTCPayServer.Controllers
Events = invoice.Events
};
foreach (var data in invoice.GetCryptoData(null))
foreach (var data in invoice.GetPaymentMethods(null))
{
var cryptoInfo = dto.CryptoInfo.First(o => o.CryptoCode.Equals(data.Key, StringComparison.OrdinalIgnoreCase));
var accounting = data.Value.Calculate();
var paymentNetwork = _NetworkProvider.GetNetwork(data.Key);
var cryptoInfo = dto.CryptoInfo.First(o => o.GetpaymentMethodId() == data.GetId());
var accounting = data.Calculate();
var paymentMethodId = data.GetId();
var cryptoPayment = new InvoiceDetailsModel.CryptoPayment();
cryptoPayment.CryptoCode = paymentNetwork.CryptoCode;
cryptoPayment.Due = accounting.Due.ToString() + $" {paymentNetwork.CryptoCode}";
cryptoPayment.Paid = accounting.CryptoPaid.ToString() + $" {paymentNetwork.CryptoCode}";
cryptoPayment.Address = data.Value.DepositAddress.ToString();
cryptoPayment.Rate = FormatCurrency(data.Value);
cryptoPayment.PaymentMethod = ToString(paymentMethodId);
cryptoPayment.Due = accounting.Due.ToString() + $" {paymentMethodId.CryptoCode}";
cryptoPayment.Paid = accounting.CryptoPaid.ToString() + $" {paymentMethodId.CryptoCode}";
var onchainMethod = data.GetPaymentMethodDetails() as Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod;
if (onchainMethod != null)
{
cryptoPayment.Address = onchainMethod.DepositAddress;
}
cryptoPayment.Rate = FormatCurrency(data);
cryptoPayment.PaymentUrl = cryptoInfo.PaymentUrls.BIP21;
model.CryptoPayments.Add(cryptoPayment);
}
var payments = invoice
var onChainPayments = invoice
.GetPayments()
.Select(async payment =>
.Select<PaymentEntity, Task<object>>(async payment =>
{
var m = new InvoiceDetailsModel.Payment();
var paymentNetwork = _NetworkProvider.GetNetwork(payment.GetCryptoCode());
m.CryptoCode = payment.GetCryptoCode();
m.DepositAddress = payment.GetScriptPubKey().GetDestinationAddress(paymentNetwork.NBitcoinNetwork);
m.Confirmations = (await _ExplorerClients.GetExplorerClient(payment.GetCryptoCode())?.GetTransactionAsync(payment.Outpoint.Hash))?.Confirmations ?? 0;
m.TransactionId = payment.Outpoint.Hash.ToString();
m.ReceivedTime = payment.ReceivedTime;
m.TransactionLink = string.Format(paymentNetwork.BlockExplorerLink, m.TransactionId);
m.Replaced = !payment.Accounted;
return m;
var paymentData = payment.GetCryptoPaymentData();
if (paymentData is Payments.Bitcoin.BitcoinLikePaymentData onChainPaymentData)
{
var m = new InvoiceDetailsModel.Payment();
m.Crypto = payment.GetPaymentMethodId().CryptoCode;
m.DepositAddress = onChainPaymentData.Output.ScriptPubKey.GetDestinationAddress(paymentNetwork.NBitcoinNetwork);
int confirmationCount = 0;
if ((onChainPaymentData.ConfirmationCount < paymentNetwork.MaxTrackedConfirmation && payment.Accounted)
&& (onChainPaymentData.Legacy || invoice.MonitoringExpiration < DateTimeOffset.UtcNow)) // The confirmation count in the paymentData is not up to date
{
confirmationCount = (await ((ExplorerClientProvider)_ServiceProvider.GetService(typeof(ExplorerClientProvider))).GetExplorerClient(payment.GetCryptoCode())?.GetTransactionAsync(onChainPaymentData.Outpoint.Hash))?.Confirmations ?? 0;
onChainPaymentData.ConfirmationCount = confirmationCount;
payment.SetCryptoPaymentData(onChainPaymentData);
await _InvoiceRepository.UpdatePayments(new List<PaymentEntity> { payment });
}
else
{
confirmationCount = onChainPaymentData.ConfirmationCount;
}
if (confirmationCount >= paymentNetwork.MaxTrackedConfirmation)
{
m.Confirmations = "At least " + (paymentNetwork.MaxTrackedConfirmation);
}
else
{
m.Confirmations = confirmationCount.ToString(CultureInfo.InvariantCulture);
}
m.TransactionId = onChainPaymentData.Outpoint.Hash.ToString();
m.ReceivedTime = payment.ReceivedTime;
m.TransactionLink = string.Format(CultureInfo.InvariantCulture, paymentNetwork.BlockExplorerLink, m.TransactionId);
m.Replaced = !payment.Accounted;
return m;
}
else
{
var lightningPaymentData = (Payments.Lightning.LightningLikePaymentData)paymentData;
return new InvoiceDetailsModel.OffChainPayment()
{
Crypto = paymentNetwork.CryptoCode,
BOLT11 = lightningPaymentData.BOLT11
};
}
})
.ToArray();
await Task.WhenAll(payments);
model.Addresses = invoice.HistoricalAddresses;
model.Payments = payments.Select(p => p.GetAwaiter().GetResult()).ToList();
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);
}
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})";
}
[HttpGet]
[Route("i/{invoiceId}")]
[Route("i/{invoiceId}/{cryptoCode}")]
[Route("i/{invoiceId}/{paymentMethodId}")]
[Route("invoice")]
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest", false)]
[XFrameOptionsAttribute(null)]
public async Task<IActionResult> Checkout(string invoiceId, string id = null, string cryptoCode = null)
public async Task<IActionResult> Checkout(string invoiceId, string id = null, string paymentMethodId = null)
{
//Keep compatibility with Bitpay
invoiceId = invoiceId ?? id;
id = invoiceId;
////
var model = await GetInvoiceModel(invoiceId, cryptoCode);
var model = await GetInvoiceModel(invoiceId, paymentMethodId);
if (model == null)
return NotFound();
return View(nameof(Checkout), model);
}
private async Task<PaymentModel> GetInvoiceModel(string invoiceId, string cryptoCode)
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 (cryptoCode == null)
{
cryptoCode = store.GetDefaultCrypto();
if (paymentMethodIdStr == null)
{
paymentMethodIdStr = store.GetDefaultCrypto();
isDefaultCrypto = true;
}
var network = _NetworkProvider.GetNetwork(cryptoCode);
var paymentMethodId = PaymentMethodId.Parse(paymentMethodIdStr);
var network = _NetworkProvider.GetNetwork(paymentMethodId.CryptoCode);
if (invoice == null || network == null)
return null;
if(!invoice.Support(network))
if (!invoice.Support(paymentMethodId))
{
if(!isDefaultCrypto)
if (!isDefaultCrypto)
return null;
network = invoice.GetCryptoData(_NetworkProvider).First().Value.Network;
var paymentMethodTemp = invoice.GetPaymentMethods(_NetworkProvider).First();
network = paymentMethodTemp.Network;
paymentMethodId = paymentMethodTemp.GetId();
}
var cryptoData = invoice.GetCryptoData(network, _NetworkProvider);
var paymentMethod = invoice.GetPaymentMethod(paymentMethodId, _NetworkProvider);
var paymentMethodDetails = paymentMethod.GetPaymentMethodDetails();
var dto = invoice.EntityToDTO(_NetworkProvider);
var cryptoInfo = dto.CryptoInfo.First(o => o.CryptoCode == network.CryptoCode);
var cryptoInfo = dto.CryptoInfo.First(o => o.GetpaymentMethodId() == paymentMethodId);
var storeBlob = store.GetStoreBlob();
var currency = invoice.ProductInformation.Currency;
var accounting = cryptoData.Calculate();
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,
BtcAddress = cryptoData.DepositAddress,
DefaultLang = storeBlob.DefaultLang ?? "en-US",
CustomCSSLink = storeBlob.CustomCSS?.AbsoluteUri,
CustomLogoLink = storeBlob.CustomLogo?.AbsoluteUri,
BtcAddress = paymentMethodDetails.GetPaymentDestination(),
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ToString(),
BtcDue = accounting.Due.ToString(),
CustomerEmail = invoice.RefundMail,
RequiresRefundEmail = storeBlob.RequiresRefundEmail,
ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds),
MaxTimeSeconds = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalSeconds,
MaxTimeMinutes = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalMinutes,
ItemDesc = invoice.ProductInformation.ItemDesc,
Rate = FormatCurrency(cryptoData),
Rate = FormatCurrency(paymentMethod),
MerchantRefLink = invoice.RedirectURL ?? "/",
StoreName = store.StoreName,
InvoiceBitcoinUrl = cryptoInfo.PaymentUrls.BIP21,
TxCount = accounting.TxCount,
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 = "/" + Url.Content(network.CryptoImagePath),
NetworkFeeDescription = $"{accounting.TxCount} transaction{(accounting.TxCount > 1 ? "s" : "")} x {cryptoData.TxFee} {network.CryptoCode}",
AvailableCryptos = invoice.GetCryptoData(_NetworkProvider)
.Where(i => i.Value.Network != null)
.Select(kv=> new PaymentModel.AvailableCrypto()
{
CryptoCode = kv.Key,
CryptoImage = "/" + kv.Value.Network.CryptoImagePath,
Link = Url.Action(nameof(Checkout), new { invoiceId = invoiceId, cryptoCode = kv.Key })
}).Where(c => c.CryptoImage != "/")
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 isMultiCurrency = invoice.GetPayments().Select(p=>p.GetCryptoCode()).Concat(new[] { network.CryptoCode }).Distinct().Count() > 1;
if (isMultiCurrency)
model.NetworkFeeDescription = $"{accounting.NetworkFee} {network.CryptoCode}";
var expiration = TimeSpan.FromSeconds(model.ExpirationSeconds);
model.TimeLeft = PrettyPrint(expiration);
model.TimeLeft = expiration.PrettyPrint();
return model;
}
private string FormatCurrency(CryptoData cryptoData)
private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network)
{
string currency = cryptoData.ParentEntity.ProductInformation.Currency;
return FormatCurrency(cryptoData.Rate, currency);
return (paymentMethodId.PaymentType == PaymentTypes.BTCLike ? Url.Content(network.CryptoImagePath) : Url.Content(network.LightningImagePath));
}
private string FormatCurrency(PaymentMethod paymentMethod)
{
string currency = paymentMethod.ParentEntity.ProductInformation.Currency;
return FormatCurrency(paymentMethod.Rate, currency);
}
public string FormatCurrency(decimal price, string currency)
{
return price.ToString("C", _CurrencyNameTable.GetCurrencyProvider(currency)) + $" ({currency})";
}
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();
}
[HttpGet]
[Route("i/{invoiceId}/status")]
[Route("i/{invoiceId}/{cryptoCode}/status")]
public async Task<IActionResult> GetStatus(string invoiceId, string cryptoCode)
[Route("i/{invoiceId}/{paymentMethodId}/status")]
public async Task<IActionResult> GetStatus(string invoiceId, string paymentMethodId = null)
{
var model = await GetInvoiceModel(invoiceId, cryptoCode);
var model = await GetInvoiceModel(invoiceId, paymentMethodId);
if (model == null)
return NotFound();
return Json(model);
@ -239,6 +310,7 @@ namespace BTCPayServer.Controllers
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)
{
@ -285,7 +357,7 @@ namespace BTCPayServer.Controllers
[Route("invoices")]
[Authorize(AuthenticationSchemes = "Identity.Application")]
[BitpayAPIConstraint(false)]
public async Task<IActionResult> ListInvoices(string searchTerm = null, int skip = 0, int count = 20)
public async Task<IActionResult> ListInvoices(string searchTerm = null, int skip = 0, int count = 50)
{
var model = new InvoicesModel();
var filterString = new SearchString(searchTerm);
@ -295,16 +367,18 @@ namespace BTCPayServer.Controllers
Count = count,
Skip = skip,
UserId = GetUserId(),
Status = filterString.Filters.TryGet("status"),
StoreId = filterString.Filters.TryGet("storeid")
Status = filterString.Filters.ContainsKey("status") ? filterString.Filters["status"].ToArray() : null,
StoreId = filterString.Filters.ContainsKey("storeid") ? filterString.Filters["storeid"].ToArray() : null
}))
{
model.SearchTerm = searchTerm;
model.Invoices.Add(new InvoiceModel()
{
Status = invoice.Status,
Date = invoice.InvoiceTime,
Date = (DateTimeOffset.UtcNow - invoice.InvoiceTime).Prettify() + " ago",
InvoiceId = invoice.Id,
OrderId = invoice.OrderId ?? string.Empty,
RedirectUrl = invoice.RedirectURL ?? string.Empty,
AmountCurrency = $"{invoice.ProductInformation.Price.ToString(CultureInfo.InvariantCulture)} {invoice.ProductInformation.Currency}"
});
}
@ -324,7 +398,7 @@ namespace BTCPayServer.Controllers
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 RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
}
return View(new CreateInvoiceModel() { Stores = stores });
}
@ -341,9 +415,21 @@ namespace BTCPayServer.Controllers
return View(model);
}
var store = await _StoreRepository.FindStore(model.StoreId, GetUserId());
if (store.GetDerivationStrategies(_NetworkProvider).Count() == 0)
StatusMessage = null;
if (store.Role != StoreRoles.Owner)
{
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)
{
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

@ -39,55 +39,50 @@ using Microsoft.AspNetCore.Mvc.Routing;
using NBXplorer.DerivationStrategy;
using NBXplorer;
using BTCPayServer.HostedServices;
using BTCPayServer.Payments;
namespace BTCPayServer.Controllers
{
public partial class InvoiceController : Controller
{
InvoiceRepository _InvoiceRepository;
BTCPayWalletProvider _WalletProvider;
IRateProviderFactory _RateProviders;
StoreRepository _StoreRepository;
UserManager<ApplicationUser> _UserManager;
IFeeProviderFactory _FeeProviderFactory;
private CurrencyNameTable _CurrencyNameTable;
EventAggregator _EventAggregator;
BTCPayNetworkProvider _NetworkProvider;
ExplorerClientProvider _ExplorerClients;
public InvoiceController(InvoiceRepository invoiceRepository,
private readonly BTCPayWalletProvider _WalletProvider;
IServiceProvider _ServiceProvider;
public InvoiceController(
IServiceProvider serviceProvider,
InvoiceRepository invoiceRepository,
CurrencyNameTable currencyNameTable,
UserManager<ApplicationUser> userManager,
BTCPayWalletProvider walletProvider,
IRateProviderFactory rateProviders,
StoreRepository storeRepository,
EventAggregator eventAggregator,
BTCPayNetworkProvider networkProvider,
ExplorerClientProvider explorerClientProviders,
IFeeProviderFactory feeProviderFactory)
BTCPayWalletProvider walletProvider,
BTCPayNetworkProvider networkProvider)
{
_ExplorerClients = explorerClientProviders;
_ServiceProvider = serviceProvider;
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
_StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository));
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
_WalletProvider = walletProvider ?? throw new ArgumentNullException(nameof(walletProvider));
_RateProviders = rateProviders ?? throw new ArgumentNullException(nameof(rateProviders));
_UserManager = userManager;
_FeeProviderFactory = feeProviderFactory ?? throw new ArgumentNullException(nameof(feeProviderFactory));
_EventAggregator = eventAggregator;
_NetworkProvider = networkProvider;
_WalletProvider = walletProvider;
}
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl)
{
var derivationStrategies = store.GetDerivationStrategies(_NetworkProvider).Where(c => _ExplorerClients.IsAvailable(c.Network.CryptoCode)).ToList();
if (derivationStrategies.Count == 0)
throw new BitpayHttpException(400, "No derivation strategy are available now for this store");
var entity = new InvoiceEntity
{
InvoiceTime = DateTimeOffset.UtcNow
};
entity.SetDerivationStrategies(derivationStrategies);
var storeBlob = store.GetStoreBlob();
Uri notificationUri = Uri.IsWellFormedUriString(invoice.NotificationURL, UriKind.Absolute) ? new Uri(invoice.NotificationURL, UriKind.Absolute) : null;
@ -115,56 +110,62 @@ namespace BTCPayServer.Controllers
entity.Status = "new";
entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy);
var queries = derivationStrategies
.Select(derivationStrategy => (Wallet: _WalletProvider.GetWallet(derivationStrategy.Network),
DerivationStrategy: derivationStrategy.DerivationStrategyBase,
Network: derivationStrategy.Network,
RateProvider: _RateProviders.GetRateProvider(derivationStrategy.Network, false),
FeeRateProvider: _FeeProviderFactory.CreateFeeProvider(derivationStrategy.Network)))
.Where(_ => _.Wallet != null &&
_.FeeRateProvider != null &&
_.RateProvider != null)
.Select(_ =>
{
return new
{
network = _.Network,
getFeeRate = _.FeeRateProvider.GetFeeRateAsync(),
getRate = storeBlob.ApplyRateRules(_.Network, _.RateProvider).GetRateAsync(invoice.Currency),
getAddress = _.Wallet.ReserveAddressAsync(_.DerivationStrategy)
};
});
bool legacyBTCisSet = false;
var cryptoDatas = new Dictionary<string, CryptoData>();
foreach (var q in queries)
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(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 o in supportedPaymentMethods)
{
CryptoData cryptoData = new CryptoData();
cryptoData.CryptoCode = q.network.CryptoCode;
cryptoData.FeeRate = (await q.getFeeRate);
cryptoData.TxFee = GetTxFee(storeBlob, cryptoData.FeeRate); // assume price for 100 bytes
cryptoData.Rate = await q.getRate;
cryptoData.DepositAddress = (await q.getAddress).ToString();
#pragma warning disable CS0618
if (q.network.IsBTC)
try
{
legacyBTCisSet = true;
entity.TxFee = cryptoData.TxFee;
entity.Rate = cryptoData.Rate;
entity.DepositAddress = cryptoData.DepositAddress;
var paymentMethod = await o.PaymentMethod;
if (paymentMethod == null)
throw new PaymentMethodUnavailableException("Payment method unavailable (The handler returned null)");
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()})");
}
#pragma warning restore CS0618
cryptoDatas.Add(cryptoData.CryptoCode, cryptoData);
}
if (!legacyBTCisSet)
if (supported.Count == 0)
{
// Legacy Bitpay clients expect information for BTC information, even if the store do not support it
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);
#pragma warning disable CS0618
// Legacy Bitpay clients expect information for BTC information, even if the store do not support it
var legacyBTCisSet = paymentMethods.Any(p => p.GetId().IsBTCOnChain);
if (!legacyBTCisSet && _NetworkProvider.BTC != null)
{
var btc = _NetworkProvider.BTC;
var feeProvider = _FeeProviderFactory.CreateFeeProvider(btc);
var rateProvider = storeBlob.ApplyRateRules(btc, _RateProviders.GetRateProvider(btc, false));
var feeProvider = ((IFeeProviderFactory)_ServiceProvider.GetService(typeof(IFeeProviderFactory))).CreateFeeProvider(btc);
var rateProvider = _RateProviders.GetRateProvider(btc, storeBlob.GetRateRules());
if (feeProvider != null && rateProvider != null)
{
var gettingFee = feeProvider.GetFeeRateAsync();
@ -174,19 +175,80 @@ namespace BTCPayServer.Controllers
}
#pragma warning restore CS0618
}
entity.SetCryptoData(cryptoDatas);
entity.PosData = invoice.PosData;
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, _NetworkProvider);
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(IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network, InvoiceEntity entity, StoreData store)
{
var storeBlob = store.GetStoreBlob();
var rate = await _RateProviders.GetRateProvider(network, storeBlob.GetRateRules()).GetRateAsync(entity.ProductInformation.Currency);
PaymentMethod paymentMethod = new PaymentMethod();
paymentMethod.ParentEntity = entity;
paymentMethod.Network = network;
paymentMethod.SetId(supportedPaymentMethod.PaymentId);
paymentMethod.Rate = rate;
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 = 0.0m;
if (limitValue.Currency == entity.ProductInformation.Currency)
limitValueRate = paymentMethod.Rate;
else
limitValueRate = await _RateProviders.GetRateProvider(network, storeBlob.GetRateRules()).GetRateAsync(limitValue.Currency);
var limitValueCrypto = Money.Coins(limitValue.Value / limitValueRate);
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;
}
#pragma warning disable CS0618
private static Money GetTxFee(StoreBlob storeBlob, FeeRate feeRate)
{
return storeBlob.NetworkFeeDisabled ? Money.Zero : feeRate.GetFee(100);
}
#pragma warning restore CS0618
private SpeedPolicy ParseSpeedPolicy(string transactionSpeed, SpeedPolicy defaultPolicy)
{
@ -216,11 +278,6 @@ namespace BTCPayServer.Controllers
buyerInformation.BuyerZip = buyerInformation.BuyerZip ?? buyer.zip;
}
private DerivationStrategyBase ParseDerivationStrategy(string derivationStrategy, BTCPayNetwork network)
{
return new DerivationStrategyFactory(network.NBitcoinNetwork).Parse(derivationStrategy);
}
private TDest Map<TFrom, TDest>(TFrom data)
{
return JsonConvert.DeserializeObject<TDest>(JsonConvert.SerializeObject(data));

@ -20,6 +20,7 @@ using NBitcoin;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
using BTCPayServer.Services.Mails;
using System.Globalization;
namespace BTCPayServer.Controllers
{
@ -367,7 +368,7 @@ namespace BTCPayServer.Controllers
if (!user.TwoFactorEnabled)
{
throw new ApplicationException($"Unexpected error occured disabling 2FA for user with ID '{user.Id}'.");
throw new ApplicationException($"Unexpected error occurred disabling 2FA for user with ID '{user.Id}'.");
}
return View(nameof(Disable2fa));
@ -386,7 +387,7 @@ namespace BTCPayServer.Controllers
var disable2faResult = await _userManager.SetTwoFactorEnabledAsync(user, false);
if (!disable2faResult.Succeeded)
{
throw new ApplicationException($"Unexpected error occured disabling 2FA for user with ID '{user.Id}'.");
throw new ApplicationException($"Unexpected error occurred disabling 2FA for user with ID '{user.Id}'.");
}
_logger.LogInformation("User with ID {UserId} has disabled 2fa.", user.Id);
@ -434,7 +435,7 @@ namespace BTCPayServer.Controllers
}
// Strip spaces and hypens
var verificationCode = model.Code.Replace(" ", string.Empty).Replace("-", string.Empty);
var verificationCode = model.Code.Replace(" ", string.Empty, StringComparison.InvariantCulture).Replace("-", string.Empty, StringComparison.InvariantCulture);
var is2faTokenValid = await _userManager.VerifyTwoFactorTokenAsync(
user, _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode);
@ -524,7 +525,7 @@ namespace BTCPayServer.Controllers
private string GenerateQrCodeUri(string email, string unformattedKey)
{
return string.Format(
return string.Format(CultureInfo.InvariantCulture,
AuthenicatorUriFormat,
_urlEncoder.Encode("BTCPayServer"),
_urlEncoder.Encode(email),

@ -49,18 +49,20 @@ namespace BTCPayServer.Controllers
var network = _NetworkProvider.GetNetwork(cryptoCode);
if (network == null)
return NotFound();
var rateProvider = _RateProviderFactory.GetRateProvider(network, true);
if (rateProvider == null)
return NotFound();
RateRules rules = null;
if (storeId != null)
{
var store = await _StoreRepo.FindStore(storeId);
if (store == null)
return NotFound();
rateProvider = store.GetStoreBlob().ApplyRateRules(network, rateProvider);
rules = store.GetStoreBlob().GetRateRules();
}
var rateProvider = _RateProviderFactory.GetRateProvider(network, rules);
if (rateProvider == null)
return NotFound();
var allRates = (await rateProvider.GetRatesAsync());
return Json(allRates.Select(r =>
new NBitpayClient.Rate()

@ -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,6 +13,7 @@ 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;
@ -21,11 +24,85 @@ namespace BTCPayServer.Controllers
{
private UserManager<ApplicationUser> _UserManager;
SettingsRepository _SettingsRepository;
private IRateProviderFactory _RateProviderFactory;
public ServerController(UserManager<ApplicationUser> userManager, SettingsRepository settingsRepository)
public ServerController(UserManager<ApplicationUser> userManager,
IRateProviderFactory rateProviderFactory,
SettingsRepository settingsRepository)
{
_UserManager = userManager;
_SettingsRepository = settingsRepository;
_RateProviderFactory = rateProviderFactory;
}
[Route("server/rates")]
public async Task<IActionResult> Rates()
{
var rates = (await _SettingsRepository.GetSettingAsync<RatesSetting>()) ?? new RatesSetting();
var vm = new RatesViewModel()
{
CacheMinutes = rates.CacheInMinutes,
PrivateKey = rates.PrivateKey,
PublicKey = rates.PublicKey
};
await FetchRateLimits(vm);
return View(vm);
}
private static async Task FetchRateLimits(RatesViewModel vm)
{
var coinAverage = GetCoinaverageService(vm, false);
if (coinAverage != null)
{
try
{
vm.RateLimits = await coinAverage.GetRateLimitsAsync();
}
catch { }
}
}
[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("BTC")
{ Authenticator = settings };
}
return null;
}
[Route("server/users")]
@ -43,6 +120,51 @@ namespace BTCPayServer.Controllers
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)
@ -94,7 +216,22 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> Policies(PoliciesSettings settings)
{
await _SettingsRepository.UpdateSetting(settings);
TempData["StatusMessage"] = "Policies upadated successfully";
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);
}

@ -0,0 +1,358 @@
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 Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBXplorer.DerivationStrategy;
using Newtonsoft.Json;
namespace BTCPayServer.Controllers
{
public partial class StoresController
{
[HttpGet]
[Route("{storeId}/derivations/{cryptoCode}")]
public async Task<IActionResult> AddDerivationScheme(string storeId, string cryptoCode)
{
var store = await _Repo.FindStore(storeId, GetUserId());
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 = await _Repo.FindStore(storeId, GetUserId());
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 = await _Repo.FindStore(storeId, GetUserId());
if (store == null)
return NotFound();
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
var hw = new HardwareWalletService(webSocket);
object result = null;
try
{
BTCPayNetwork network = null;
if (cryptoCode != null)
{
network = _NetworkProvider.GetNetwork(cryptoCode);
if (network == null)
throw new FormatException("Invalid value for crypto code");
}
BitcoinAddress destinationAddress = null;
if (destination != null)
{
try
{
destinationAddress = BitcoinAddress.Create(destination, network.NBitcoinNetwork);
}
catch { }
if (destinationAddress == null)
throw new FormatException("Invalid value for destination");
}
FeeRate feeRateValue = null;
if (feeRate != null)
{
try
{
feeRateValue = new FeeRate(Money.Satoshis(int.Parse(feeRate, CultureInfo.InvariantCulture)), 1);
}
catch { }
if (feeRateValue == null || feeRateValue.FeePerK <= Money.Zero)
throw new FormatException("Invalid value for fee rate");
}
Money amountBTC = null;
if (amount != null)
{
try
{
amountBTC = Money.Parse(amount);
}
catch { }
if (amountBTC == null || amountBTC <= Money.Zero)
throw new FormatException("Invalid value for amount");
}
bool subsctractFeesValue = false;
if (substractFees != null)
{
try
{
subsctractFeesValue = bool.Parse(substractFees);
}
catch { throw new FormatException("Invalid value for subtract fees"); }
}
if (command == "test")
{
result = await hw.Test();
}
if (command == "getxpub")
{
var getxpubResult = await hw.GetExtPubKey(network, account);
result = getxpubResult;
}
if (command == "getinfo")
{
var strategy = GetDirectDerivationStrategy(store, network);
var strategyBase = GetDerivationStrategy(store, network);
if (strategy == null || !await hw.SupportDerivation(network, strategy))
{
throw new Exception($"This store is not configured to use this ledger");
}
var feeProvider = _FeeRateProvider.CreateFeeProvider(network);
var recommendedFees = feeProvider.GetFeeRateAsync();
var balance = _WalletProvider.GetWallet(network).GetBalance(strategyBase);
result = new GetInfoResult() { Balance = (double)(await balance).ToDecimal(MoneyUnit.BTC), RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi };
}
if (command == "sendtoaddress")
{
if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
throw new Exception($"{network.CryptoCode}: not started or fully synched");
var strategy = GetDirectDerivationStrategy(store, network);
var strategyBase = GetDerivationStrategy(store, network);
var wallet = _WalletProvider.GetWallet(network);
var change = wallet.GetChangeAddressAsync(strategyBase);
var unspentCoins = await wallet.GetUnspentCoins(strategyBase);
var changeAddress = await change;
var transaction = await hw.SendToAddress(strategy, unspentCoins, network,
new[] { (destinationAddress as IDestination, amountBTC, subsctractFeesValue) },
feeRateValue,
changeAddress.Item1,
changeAddress.Item2, summary.Status.BitcoinStatus.MinRelayTxFee);
try
{
var broadcastResult = await wallet.BroadcastTransactionsAsync(new List<Transaction>() { transaction });
if (!broadcastResult[0].Success)
{
throw new Exception($"RPC Error while broadcasting: {broadcastResult[0].RPCCode} {broadcastResult[0].RPCCodeMessage} {broadcastResult[0].RPCMessage}");
}
}
catch (Exception ex)
{
throw new Exception("Error while broadcasting: " + ex.Message);
}
wallet.InvalidateCache(strategyBase);
result = new SendToAddressResult() { TransactionId = transaction.GetHash().ToString() };
}
}
catch (OperationCanceledException)
{ result = new LedgerTestResult() { Success = false, Error = "Timeout" }; }
catch (Exception ex)
{ result = new LedgerTestResult() { Success = false, Error = ex.Message }; }
try
{
if (result != null)
{
UTF8Encoding UTF8NOBOM = new UTF8Encoding(false);
var bytes = UTF8NOBOM.GetBytes(JsonConvert.SerializeObject(result, _MvcJsonOptions.SerializerSettings));
await webSocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, new CancellationTokenSource(2000).Token);
}
}
catch { }
finally
{
await webSocket.CloseSocket();
}
return new EmptyResult();
}
private DirectDerivationStrategy GetDirectDerivationStrategy(StoreData store, BTCPayNetwork network)
{
var strategy = GetDerivationStrategy(store, network);
var directStrategy = strategy as DirectDerivationStrategy;
if (directStrategy == null)
directStrategy = (strategy as P2SHDerivationStrategy).Inner as DirectDerivationStrategy;
if (!directStrategy.Segwit)
return null;
return directStrategy;
}
private DerivationStrategyBase GetDerivationStrategy(StoreData store, BTCPayNetwork network)
{
var strategy = store
.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<DerivationStrategy>()
.FirstOrDefault(s => s.Network.NBitcoinNetwork == network.NBitcoinNetwork);
if (strategy == null)
{
throw new Exception($"Derivation strategy for {network.CryptoCode} is not set");
}
return strategy.DerivationStrategyBase;
}
}
}

@ -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 async Task<IActionResult> AddLightningNode(string storeId, string cryptoCode)
{
var store = await _Repo.FindStore(storeId, GetUserId());
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 = await _Repo.FindStore(storeId, GetUserId());
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));
}
}
}

@ -1,13 +1,13 @@
using BTCPayServer.Authentication;
using BTCPayServer.Configuration;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Services;
using BTCPayServer.Services.Fees;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
using LedgerWallet;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
@ -16,15 +16,11 @@ using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.Extensions.Options;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBitpayClient;
using NBXplorer.DerivationStrategy;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@ -32,11 +28,16 @@ namespace BTCPayServer.Controllers
{
[Route("stores")]
[Authorize(AuthenticationSchemes = "Identity.Application")]
[Authorize(Policy = "CanAccessStore")]
[Authorize(Policy = StorePolicies.OwnStore)]
[AutoValidateAntiforgeryToken]
public class StoresController : Controller
public partial class StoresController : Controller
{
public string CreatedStoreId { get; set; }
public StoresController(
NBXplorerDashboard dashboard,
IServiceProvider serviceProvider,
BTCPayServerOptions btcpayServerOptions,
BTCPayServerEnvironment btcpayEnv,
IOptions<MvcJsonOptions> mvcJsonOptions,
StoreRepository repo,
TokenRepository tokenRepo,
@ -46,11 +47,15 @@ namespace BTCPayServer.Controllers
BTCPayNetworkProvider networkProvider,
ExplorerClientProvider explorerProvider,
IFeeProviderFactory feeRateProvider,
IHostingEnvironment env)
LanguageService langService,
IHostingEnvironment env,
CoinAverageSettings coinAverage)
{
_Dashboard = dashboard;
_Repo = repo;
_TokenRepository = tokenRepo;
_UserManager = userManager;
_LangService = langService;
_TokenController = tokenController;
_WalletProvider = walletProvider;
_Env = env;
@ -58,7 +63,16 @@ namespace BTCPayServer.Controllers
_ExplorerProvider = explorerProvider;
_MvcJsonOptions = mvcJsonOptions.Value;
_FeeRateProvider = feeRateProvider;
_ServiceProvider = serviceProvider;
_BtcpayServerOptions = btcpayServerOptions;
_BTCPayEnv = btcpayEnv;
_CoinAverage = coinAverage;
}
CoinAverageSettings _CoinAverage;
NBXplorerDashboard _Dashboard;
BTCPayServerOptions _BtcpayServerOptions;
BTCPayServerEnvironment _BTCPayEnv;
IServiceProvider _ServiceProvider;
BTCPayNetworkProvider _NetworkProvider;
private ExplorerClientProvider _ExplorerProvider;
private MvcJsonOptions _MvcJsonOptions;
@ -68,6 +82,7 @@ namespace BTCPayServer.Controllers
StoreRepository _Repo;
TokenRepository _TokenRepository;
UserManager<ApplicationUser> _UserManager;
private LanguageService _LangService;
IHostingEnvironment _Env;
[TempData]
@ -77,41 +92,15 @@ namespace BTCPayServer.Controllers
}
[HttpGet]
[Route("create")]
public IActionResult CreateStore()
{
return View();
}
[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));
}
public string CreatedStoreId
{
get; set;
}
[HttpGet]
[Route("{storeId}/wallet")]
public async Task<IActionResult> Wallet(string storeId)
[Route("{storeId}/wallet/{cryptoCode}")]
public async Task<IActionResult> Wallet(string storeId, string cryptoCode)
{
var store = await _Repo.FindStore(storeId, GetUserId());
if (store == null)
return NotFound();
WalletModel model = new WalletModel();
model.ServerUrl = GetStoreUrl(storeId);
model.SetCryptoCurrencies(_ExplorerProvider, store.GetDefaultCrypto());
model.CryptoCurrency = cryptoCode;
return View(model);
}
@ -120,260 +109,164 @@ namespace BTCPayServer.Controllers
return HttpContext.Request.GetAbsoluteRoot() + "/stores/" + storeId + "/";
}
public class GetInfoResult
[HttpGet]
[Route("{storeId}/users")]
public async Task<IActionResult> StoreUsers(string storeId)
{
public int RecommendedSatoshiPerByte { get; set; }
public double Balance { get; set; }
StoreUsersViewModel vm = new StoreUsersViewModel();
await FillUsers(storeId, vm);
return View(vm);
}
public class SendToAddressResult
private async Task FillUsers(string storeId, StoreUsersViewModel vm)
{
public string TransactionId { get; set; }
var users = await _Repo.GetStoreUsers(storeId);
vm.StoreId = storeId;
vm.Users = users.Select(u => new StoreUsersViewModel.StoreUserViewModel()
{
Email = u.Email,
Id = u.Id,
Role = u.Role
}).ToList();
}
[HttpPost]
[Route("{storeId}/users")]
public async Task<IActionResult> StoreUsers(string storeId, StoreUsersViewModel vm)
{
await FillUsers(storeId, vm);
if (!ModelState.IsValid)
{
return View(vm);
}
var user = await _UserManager.FindByEmailAsync(vm.Email);
if (user == null)
{
ModelState.AddModelError(nameof(vm.Email), "User not found");
return View(vm);
}
if (!StoreRoles.AllRoles.Contains(vm.Role))
{
ModelState.AddModelError(nameof(vm.Role), "Invalid role");
return View(vm);
}
if (!await _Repo.AddStoreUser(storeId, user.Id, vm.Role))
{
ModelState.AddModelError(nameof(vm.Email), "The user already has access to this store");
return View(vm);
}
StatusMessage = "User added successfully";
return RedirectToAction(nameof(StoreUsers));
}
[HttpGet]
[Route("{storeId}/ws/ledger")]
public async Task<IActionResult> LedgerConnection(
string storeId,
string command,
// getinfo
string cryptoCode = null,
// sendtoaddress
string destination = null, string amount = null, string feeRate = null, string substractFees = null
)
[Route("{storeId}/users/{userId}/delete")]
public async Task<IActionResult> DeleteStoreUser(string storeId, string userId)
{
if (!HttpContext.WebSockets.IsWebSocketRequest)
return NotFound();
var store = await _Repo.FindStore(storeId, GetUserId());
StoreUsersViewModel vm = new StoreUsersViewModel();
var store = await _Repo.FindStore(storeId, userId);
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);
}
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)), 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 substract fees"); }
}
if (command == "test")
{
result = await hw.Test();
}
if (command == "getxpub")
{
result = await hw.GetExtPubKey(network);
}
if (command == "getinfo")
{
var strategy = GetDirectDerivationStrategy(store, network);
var strategyBase = GetDerivationStrategy(store, network);
if (!await hw.SupportDerivation(network, strategy))
{
throw new Exception($"This store is not configured to use this ledger");
}
var feeProvider = _FeeRateProvider.CreateFeeProvider(network);
var recommendedFees = feeProvider.GetFeeRateAsync();
var balance = _WalletProvider.GetWallet(network).GetBalance(strategyBase);
result = new GetInfoResult() { Balance = (double)(await balance).ToDecimal(MoneyUnit.BTC), RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi };
}
if (command == "sendtoaddress")
{
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;
unspentCoins.Item2.TryAdd(changeAddress.Item1.ScriptPubKey, changeAddress.Item2);
var transaction = await hw.SendToAddress(strategy, unspentCoins.Item1, network,
new[] { (destinationAddress as IDestination, amountBTC, subsctractFeesValue) },
feeRateValue,
changeAddress.Item1,
unspentCoins.Item2);
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);
}
result = new SendToAddressResult() { TransactionId = transaction.GetHash().ToString() };
}
}
catch (OperationCanceledException)
{ result = new LedgerTestResult() { Success = false, Error = "Timeout" }; }
catch (Exception ex)
{ result = new LedgerTestResult() { Success = false, Error = ex.Message }; }
try
{
if (result != null)
{
UTF8Encoding UTF8NOBOM = new UTF8Encoding(false);
var bytes = UTF8NOBOM.GetBytes(JsonConvert.SerializeObject(result, _MvcJsonOptions.SerializerSettings));
await webSocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, new CancellationTokenSource(2000).Token);
}
}
catch { }
finally
{
await webSocket.CloseSocket();
}
return new EmptyResult();
}
private DirectDerivationStrategy GetDirectDerivationStrategy(StoreData store, BTCPayNetwork network)
{
var strategy = GetDerivationStrategy(store, network);
var directStrategy = strategy as DirectDerivationStrategy;
if (directStrategy == null)
directStrategy = (strategy as P2SHDerivationStrategy).Inner as DirectDerivationStrategy;
if (!directStrategy.Segwit)
return null;
return directStrategy;
}
private DerivationStrategyBase GetDerivationStrategy(StoreData store, BTCPayNetwork network)
{
var strategy = store.GetDerivationStrategies(_NetworkProvider).FirstOrDefault(s => s.Network.NBitcoinNetwork == network.NBitcoinNetwork);
if (strategy == null)
{
throw new Exception($"Derivation strategy for {network.CryptoCode} is not set");
}
return strategy.DerivationStrategyBase;
}
[HttpGet]
public async Task<IActionResult> ListStores()
{
StoresViewModel result = new StoresViewModel();
result.StatusMessage = StatusMessage;
var stores = await _Repo.GetStoresByUserId(GetUserId());
var balances = stores
.Select(s => s.GetDerivationStrategies(_NetworkProvider)
.Select(d => ((Wallet: _WalletProvider.GetWallet(d.Network),
DerivationStrategy: d.DerivationStrategyBase)))
.Where(_ => _.Wallet != null)
.Select(async _ => (await GetBalanceString(_)).ToString() + " " + _.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,
Balances = balances[i].Select(t => t.Result).ToArray()
});
}
return View(result);
}
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 "--";
}
}
}
[HttpGet]
[Route("{storeId}/delete")]
public async Task<IActionResult> DeleteStore(string storeId)
{
var store = await _Repo.FindStore(storeId, GetUserId());
if (store == null)
var user = await _UserManager.FindByIdAsync(userId);
if (user == null)
return NotFound();
return View("Confirm", new ConfirmModel()
{
Title = "Delete store " + store.StoreName,
Description = "This store will still be accessible to users sharing it",
Title = $"Remove store user",
Description = $"Are you sure to remove access to remove {store.Role} access to {user.Email}?",
Action = "Delete"
});
}
[HttpPost]
[Route("{storeId}/delete")]
public async Task<IActionResult> DeleteStorePost(string storeId)
[Route("{storeId}/users/{userId}/delete")]
public async Task<IActionResult> DeleteStoreUserPost(string storeId, string userId)
{
await _Repo.RemoveStoreUser(storeId, userId);
StatusMessage = "User removed successfully";
return RedirectToAction(nameof(StoreUsers), new { storeId = storeId, userId = userId });
}
[HttpGet]
[Route("{storeId}/checkout")]
public async Task<IActionResult> CheckoutExperience(string storeId)
{
var userId = GetUserId();
var store = await _Repo.FindStore(storeId, GetUserId());
if (store == null)
return NotFound();
await _Repo.RemoveStore(storeId, userId);
StatusMessage = "Store removed successfully";
return RedirectToAction(nameof(ListStores));
var storeBlob = store.GetStoreBlob();
var vm = new CheckoutExperienceViewModel();
vm.SetCryptoCurrencies(_ExplorerProvider, store.GetDefaultCrypto());
vm.SetLanguages(_LangService, storeBlob.DefaultLang);
vm.LightningMaxValue = storeBlob.LightningMaxValue?.ToString() ?? "";
vm.OnChainMinValue = storeBlob.OnChainMinValue?.ToString() ?? "";
vm.AllowCoinConversion = storeBlob.AllowCoinConversion;
vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail;
vm.CustomCSS = storeBlob.CustomCSS?.AbsoluteUri;
vm.CustomLogo = storeBlob.CustomLogo?.AbsoluteUri;
return View(vm);
}
[HttpPost]
[Route("{storeId}/checkout")]
public async Task<IActionResult> CheckoutExperience(string storeId, CheckoutExperienceViewModel model)
{
CurrencyValue lightningMaxValue = null;
if (!string.IsNullOrWhiteSpace(model.LightningMaxValue))
{
if (!CurrencyValue.TryParse(model.LightningMaxValue, out lightningMaxValue))
{
ModelState.AddModelError(nameof(model.LightningMaxValue), "Invalid lightning max value");
}
}
CurrencyValue onchainMinValue = null;
if (!string.IsNullOrWhiteSpace(model.OnChainMinValue))
{
if (!CurrencyValue.TryParse(model.OnChainMinValue, out onchainMinValue))
{
ModelState.AddModelError(nameof(model.OnChainMinValue), "Invalid on chain min value");
}
}
var store = await _Repo.FindStore(storeId, GetUserId());
if (store == null)
return NotFound();
bool needUpdate = false;
var blob = store.GetStoreBlob();
if (store.GetDefaultCrypto() != model.DefaultCryptoCurrency)
{
needUpdate = true;
store.SetDefaultCrypto(model.DefaultCryptoCurrency);
}
model.SetCryptoCurrencies(_ExplorerProvider, model.DefaultCryptoCurrency);
model.SetLanguages(_LangService, model.DefaultLang);
if (!ModelState.IsValid)
{
return View(model);
}
blob.DefaultLang = model.DefaultLang;
blob.AllowCoinConversion = model.AllowCoinConversion;
blob.RequiresRefundEmail = model.RequiresRefundEmail;
blob.LightningMaxValue = lightningMaxValue;
blob.OnChainMinValue = onchainMinValue;
blob.CustomLogo = string.IsNullOrWhiteSpace(model.CustomLogo) ? null : new Uri(model.CustomLogo, UriKind.Absolute);
blob.CustomCSS = string.IsNullOrWhiteSpace(model.CustomCSS) ? null : new Uri(model.CustomCSS, UriKind.Absolute);
if (store.SetStoreBlob(blob))
{
needUpdate = true;
}
if (needUpdate)
{
await _Repo.UpdateStore(store);
StatusMessage = "Store successfully updated";
}
return RedirectToAction(nameof(CheckoutExperience), new
{
storeId = storeId
});
}
[HttpGet]
@ -386,128 +279,51 @@ namespace BTCPayServer.Controllers
var storeBlob = store.GetStoreBlob();
var vm = new StoreViewModel();
vm.SetExchangeRates(GetSupportedExchanges(), storeBlob.PreferredExchange.IsCoinAverage() ? "coinaverage" : storeBlob.PreferredExchange);
vm.Id = store.Id;
vm.StoreName = store.StoreName;
vm.SetCryptoCurrencies(_ExplorerProvider, store.GetDefaultCrypto());
vm.StoreWebsite = store.StoreWebsite;
vm.NetworkFee = !storeBlob.NetworkFeeDisabled;
vm.SpeedPolicy = store.SpeedPolicy;
AddDerivationSchemes(store, vm);
vm.StatusMessage = StatusMessage;
AddPaymentMethods(store, vm);
vm.MonitoringExpiration = storeBlob.MonitoringExpiration;
vm.InvoiceExpiration = storeBlob.InvoiceExpiration;
vm.RateMultiplier = (double)storeBlob.GetRateMultiplier();
vm.PreferredExchange = storeBlob.PreferredExchange.IsCoinAverage() ? "coinaverage" : storeBlob.PreferredExchange;
vm.LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate;
return View(vm);
}
private void AddDerivationSchemes(StoreData store, StoreViewModel vm)
private void AddPaymentMethods(StoreData store, StoreViewModel vm)
{
var strategies = store
.GetDerivationStrategies(_NetworkProvider)
.ToDictionary(s => s.Network.CryptoCode);
foreach (var explorerProvider in _ExplorerProvider.GetAll())
var derivationByCryptoCode =
store
.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<DerivationStrategy>()
.ToDictionary(c => c.Network.CryptoCode);
foreach (var network in _NetworkProvider.GetAll())
{
if (strategies.TryGetValue(explorerProvider.Item1.CryptoCode, out DerivationStrategy strat))
var strategy = derivationByCryptoCode.TryGet(network.CryptoCode);
vm.DerivationSchemes.Add(new StoreViewModel.DerivationScheme()
{
vm.DerivationSchemes.Add(new StoreViewModel.DerivationScheme()
{
Crypto = explorerProvider.Item1.CryptoCode,
Value = strat.DerivationStrategyBase.ToString()
});
}
}
}
[HttpGet]
[Route("{storeId}/derivations")]
public async Task<IActionResult> AddDerivationScheme(string storeId, string selectedScheme = null)
{
selectedScheme = selectedScheme ?? "BTC";
var store = await _Repo.FindStore(storeId, GetUserId());
if (store == null)
return NotFound();
DerivationSchemeViewModel vm = new DerivationSchemeViewModel();
vm.ServerUrl = GetStoreUrl(storeId);
vm.SetCryptoCurrencies(_ExplorerProvider, selectedScheme);
return View(vm);
}
[HttpPost]
[Route("{storeId}/derivations")]
public async Task<IActionResult> AddDerivationScheme(string storeId, DerivationSchemeViewModel vm, string selectedScheme = null)
{
selectedScheme = selectedScheme ?? "BTC";
vm.ServerUrl = GetStoreUrl(storeId);
var store = await _Repo.FindStore(storeId, GetUserId());
if (store == null)
return NotFound();
var network = vm.CryptoCurrency == null ? null : _ExplorerProvider.GetNetwork(vm.CryptoCurrency);
vm.SetCryptoCurrencies(_ExplorerProvider, selectedScheme);
if (network == null)
{
ModelState.AddModelError(nameof(vm.CryptoCurrency), "Invalid network");
return View(vm);
}
var wallet = _WalletProvider.GetWallet(network);
if (wallet == null)
{
ModelState.AddModelError(nameof(vm.CryptoCurrency), "Invalid network");
return View(vm);
Crypto = network.CryptoCode,
Value = strategy?.DerivationStrategyBase?.ToString() ?? string.Empty
});
}
var lightningByCryptoCode = store
.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<Payments.Lightning.LightningSupportedPaymentMethod>()
.ToDictionary(c => c.CryptoCode);
DerivationStrategyBase strategy = null;
try
foreach (var network in _NetworkProvider.GetAll())
{
if (!string.IsNullOrEmpty(vm.DerivationScheme))
var lightning = lightningByCryptoCode.TryGet(network.CryptoCode);
vm.LightningNodes.Add(new StoreViewModel.LightningNode()
{
strategy = ParseDerivationStrategy(vm.DerivationScheme, vm.DerivationSchemeFormat, network);
vm.DerivationScheme = strategy.ToString();
}
store.SetDerivationStrategy(network, vm.DerivationScheme);
}
catch
{
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme");
vm.Confirmation = false;
return View(vm);
}
if (strategy == null || vm.Confirmation)
{
try
{
if (strategy != null)
await wallet.TrackAsync(strategy);
store.SetDerivationStrategy(network, vm.DerivationScheme);
}
catch
{
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme");
return View(vm);
}
await _Repo.UpdateStore(store);
StatusMessage = $"Derivation scheme for {network.CryptoCode} has been modified.";
return RedirectToAction(nameof(UpdateStore), new { storeId = storeId });
}
else
{
if (!string.IsNullOrEmpty(vm.DerivationScheme))
{
var line = strategy.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(network.NBitcoinNetwork).ToString()));
}
}
vm.Confirmation = true;
return View(vm);
CryptoCode = network.CryptoCode,
Address = lightning?.GetLightningUrl()?.BaseUri.AbsoluteUri ?? string.Empty
});
}
}
@ -515,6 +331,7 @@ namespace BTCPayServer.Controllers
[Route("{storeId}")]
public async Task<IActionResult> UpdateStore(string storeId, StoreViewModel model)
{
model.SetExchangeRates(GetSupportedExchanges(), model.PreferredExchange);
if (!ModelState.IsValid)
{
return View(model);
@ -524,7 +341,7 @@ namespace BTCPayServer.Controllers
var store = await _Repo.FindStore(storeId, GetUserId());
if (store == null)
return NotFound();
AddDerivationSchemes(store, model);
AddPaymentMethods(store, model);
bool needUpdate = false;
if (store.SpeedPolicy != model.SpeedPolicy)
@ -543,17 +360,11 @@ namespace BTCPayServer.Controllers
store.StoreWebsite = model.StoreWebsite;
}
if (store.GetDefaultCrypto() != model.DefaultCryptoCurrency)
{
needUpdate = true;
store.SetDefaultCrypto(model.DefaultCryptoCurrency);
}
model.SetCryptoCurrencies(_ExplorerProvider, model.DefaultCryptoCurrency);
var blob = store.GetStoreBlob();
blob.NetworkFeeDisabled = !model.NetworkFee;
blob.MonitoringExpiration = model.MonitoringExpiration;
blob.InvoiceExpiration = model.InvoiceExpiration;
blob.LightningDescriptionTemplate = model.LightningDescriptionTemplate ?? string.Empty;
bool newExchange = blob.PreferredExchange != model.PreferredExchange;
blob.PreferredExchange = model.PreferredExchange;
@ -567,14 +378,11 @@ namespace BTCPayServer.Controllers
if (!blob.PreferredExchange.IsCoinAverage() && newExchange)
{
using (HttpClient client = new HttpClient())
if (!GetSupportedExchanges().Select(c => c.Name).Contains(blob.PreferredExchange, StringComparer.OrdinalIgnoreCase))
{
var rate = await client.GetAsync(model.RateSource);
if (rate.StatusCode == System.Net.HttpStatusCode.NotFound)
{
ModelState.AddModelError(nameof(model.PreferredExchange), $"Unsupported exchange ({model.RateSource})");
return View(model);
}
ModelState.AddModelError(nameof(model.PreferredExchange), $"Unsupported exchange ({model.RateSource})");
return View(model);
}
}
@ -590,41 +398,19 @@ namespace BTCPayServer.Controllers
});
}
private DerivationStrategyBase ParseDerivationStrategy(string derivationScheme, string format, BTCPayNetwork network)
private (String DisplayName, String Name)[] GetSupportedExchanges()
{
if (format == "Electrum")
{
//Unsupported Electrum
//var p2wsh_p2sh = 0x295b43fU;
//var p2wsh = 0x2aa7ed3U;
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, new string[] { });
return new[] { ("Coin Average", "coinaverage") }
.Concat(_CoinAverage.AvailableExchanges)
.OrderBy(s => s.Item1, StringComparer.OrdinalIgnoreCase)
.ToArray();
}
var data = Encoders.Base58Check.DecodeData(derivationScheme);
if (data.Length < 4)
throw new FormatException("data.Length < 4");
var prefix = Utils.ToUInt32(data, false);
if (!electrumMapping.TryGetValue(prefix, out string[] labels))
throw new FormatException("!electrumMapping.TryGetValue(prefix, out string[] labels)");
var standardPrefix = Utils.ToBytes(network.NBXplorerNetwork.DefaultSettings.ChainType == NBXplorer.ChainType.Main ? 0x0488b21eU : 0x043587cf, false);
for (int i = 0; i < 4; i++)
data[i] = standardPrefix[i];
derivationScheme = new BitcoinExtPubKey(Encoders.Base58Check.EncodeData(data), network.NBitcoinNetwork).ToString();
foreach (var label in labels)
{
derivationScheme = derivationScheme + $"-[{label}]";
}
}
return new DerivationStrategyFactory(network.NBitcoinNetwork).Parse(derivationScheme);
private DerivationStrategy ParseDerivationStrategy(string derivationScheme, Script hint, BTCPayNetwork network)
{
var parser = new DerivationSchemeParser(network.NBitcoinNetwork);
parser.HintScriptPubKey = hint;
return new DerivationStrategy(parser.Parse(derivationScheme), network);
}
[HttpGet]
@ -641,6 +427,12 @@ namespace BTCPayServer.Controllers
SIN = t.SIN,
Id = t.Value
}).ToArray();
model.ApiKey = (await _TokenRepository.GetLegacyAPIKeys(storeId)).FirstOrDefault();
if (model.ApiKey == null)
model.EncodedApiKey = "*API Key*";
else
model.EncodedApiKey = Encoders.Base64.EncodeData(Encoders.ASCII.DecodeData(model.ApiKey));
return View(model);
}
@ -654,15 +446,17 @@ namespace BTCPayServer.Controllers
return View(model);
}
model.Label = model.Label ?? String.Empty;
if (storeId == null) // Permissions are not checked by Policy if the storeId is not passed by url
storeId = model.StoreId ?? storeId;
var userId = GetUserId();
if (userId == null)
return Unauthorized();
var store = await _Repo.FindStore(storeId, userId);
if (store == null)
return Unauthorized();
if (store.Role != StoreRoles.Owner)
{
storeId = model.StoreId;
var userId = GetUserId();
if (userId == null)
return Unauthorized();
var store = await _Repo.FindStore(storeId, userId);
if (store == null)
return Unauthorized();
StatusMessage = "Error: You need to be owner of this store to request pairing codes";
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
}
var tokenRequest = new TokenRequest()
@ -737,16 +531,29 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(ListTokens));
}
[HttpPost]
[Route("{storeId}/tokens/apikey")]
public async Task<IActionResult> GenerateAPIKey(string storeId)
{
var store = await _Repo.FindStore(storeId, GetUserId());
if (store == null)
return NotFound();
await _TokenRepository.GenerateLegacyAPIKey(storeId);
StatusMessage = "API Key re-generated";
return RedirectToAction(nameof(ListTokens));
}
[HttpGet]
[Route("/api-access-request")]
public async Task<IActionResult> RequestPairing(string pairingCode, string selectedStore = null)
{
if (pairingCode == null)
return NotFound();
var pairing = await _TokenRepository.GetPairingAsync(pairingCode);
if (pairing == null)
{
StatusMessage = "Unknown pairing code";
return RedirectToAction(nameof(ListStores));
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
}
else
{
@ -768,7 +575,7 @@ namespace BTCPayServer.Controllers
}
[HttpPost]
[Route("api-access-request")]
[Route("/api-access-request")]
public async Task<IActionResult> Pair(string pairingCode, string selectedStore)
{
if (pairingCode == null)
@ -778,10 +585,16 @@ namespace BTCPayServer.Controllers
if (store == null || pairing == null)
return NotFound();
if (store.Role != StoreRoles.Owner)
{
StatusMessage = "Error: You can't approve a pairing without being owner of the store";
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
}
var pairingResult = await _TokenRepository.PairWithStoreAsync(pairingCode, store.Id);
if (pairingResult == PairingResult.Complete || pairingResult == PairingResult.Partial)
{
StatusMessage = "Pairing is successfull";
StatusMessage = "Pairing is successful";
if (pairingResult == PairingResult.Partial)
StatusMessage = "Server initiated pairing code: " + pairingCode;
return RedirectToAction(nameof(ListTokens), new

@ -0,0 +1,147 @@
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.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 = "Identity.Application")]
[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 async Task<IActionResult> DeleteStore(string storeId)
{
var store = await _Repo.FindStore(storeId, GetUserId());
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 = await _Repo.FindStore(storeId, GetUserId());
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.Role == StoreRoles.Owner,
Balances = store.Role == StoreRoles.Owner ? 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);
}
}
}

@ -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);
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;
}
}
}

@ -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;
}
}
}

@ -2,6 +2,8 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Payments;
using BTCPayServer.Services.Invoices;
using NBitcoin;
namespace BTCPayServer.Data
@ -20,28 +22,30 @@ namespace BTCPayServer.Data
#pragma warning disable CS0618
public ScriptId GetHash()
public string GetAddress()
{
if (Address == null)
return null;
var index = Address.IndexOf("#");
var index = Address.LastIndexOf("#", StringComparison.InvariantCulture);
if (index == -1)
return new ScriptId(Address);
return new ScriptId(Address.Substring(0, index));
return Address;
return Address.Substring(0, index);
}
public AddressInvoiceData SetHash(ScriptId scriptId, string cryptoCode)
public AddressInvoiceData Set(string address, PaymentMethodId paymentMethodId)
{
Address = scriptId + "#" + cryptoCode;
Address = address + "#" + paymentMethodId.ToString();
return this;
}
public string GetCryptoCode()
public PaymentMethodId GetpaymentMethodId()
{
if (Address == null)
return null;
var index = Address.IndexOf("#");
var index = Address.LastIndexOf("#", StringComparison.InvariantCulture);
// Legacy AddressInvoiceData does not have the paymentMethodId attached to the Address
if (index == -1)
return "BTC";
return Address.Substring(index + 1);
return PaymentMethodId.Parse("BTC");
/////////////////////////
return PaymentMethodId.Parse(Address.Substring(index + 1));
}
#pragma warning restore CS0618

@ -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);
}
}
}

@ -26,6 +26,11 @@ namespace BTCPayServer.Data
get; set;
}
public DbSet<AppData> Apps
{
get; set;
}
public DbSet<InvoiceEventData> InvoiceEvents
{
get; set;
@ -81,6 +86,11 @@ namespace BTCPayServer.Data
get; set;
}
public DbSet<APIKeyData> ApiKeys
{
get; set;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var isConfigured = optionsBuilder.Options.Extensions.OfType<RelationalOptionsExtension>().Any();
@ -107,6 +117,12 @@ namespace BTCPayServer.Data
t.StoreDataId
});
builder.Entity<APIKeyData>()
.HasIndex(o => o.StoreId);
builder.Entity<AppData>()
.HasOne(a => a.StoreData);
builder.Entity<UserStore>()
.HasOne(pt => pt.ApplicationUser)
.WithMany(p => p.UserStores)

@ -27,15 +27,16 @@ namespace BTCPayServer.Data
public string CryptoCode { get; set; }
#pragma warning disable CS0618
public string GetCryptoCode()
public Payments.PaymentMethodId GetPaymentMethodId()
{
return string.IsNullOrEmpty(CryptoCode) ? "BTC" : CryptoCode;
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("#");
var index = Address.IndexOf("#", StringComparison.InvariantCulture);
if (index == -1)
return Address;
return Address.Substring(0, index);

@ -12,6 +12,9 @@ using System.ComponentModel;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using BTCPayServer.Services.Rates;
using BTCPayServer.Payments;
using BTCPayServer.JsonConverters;
using System.ComponentModel.DataAnnotations;
namespace BTCPayServer.Data
{
@ -28,6 +31,11 @@ namespace BTCPayServer.Data
get; set;
}
public List<AppData> Apps
{
get; set;
}
[Obsolete("Use GetDerivationStrategies instead")]
public string DerivationStrategy
{
@ -41,10 +49,12 @@ namespace BTCPayServer.Data
set;
}
public IEnumerable<DerivationStrategy> GetDerivationStrategies(BTCPayNetworkProvider networks)
public IEnumerable<ISupportedPaymentMethod> GetSupportedPaymentMethods(BTCPayNetworkProvider networks)
{
#pragma warning disable CS0618
bool btcReturned = false;
// Legacy stuff which should go away
if (!string.IsNullOrEmpty(DerivationStrategy))
{
if (networks.BTC != null)
@ -60,54 +70,63 @@ namespace BTCPayServer.Data
JObject strategies = JObject.Parse(DerivationStrategies);
foreach (var strat in strategies.Properties())
{
var network = networks.GetNetwork(strat.Name);
var paymentMethodId = PaymentMethodId.Parse(strat.Name);
var network = networks.GetNetwork(paymentMethodId.CryptoCode);
if (network != null)
{
if (network == networks.BTC && btcReturned)
if (network == networks.BTC && paymentMethodId.PaymentType == PaymentTypes.BTCLike && btcReturned)
continue;
if (strat.Value.Type == JTokenType.Null)
continue;
yield return BTCPayServer.DerivationStrategy.Parse(strat.Value.Value<string>(), network);
yield return PaymentMethodExtensions.Deserialize(paymentMethodId, strat.Value, network);
}
}
}
#pragma warning restore CS0618
}
public void SetDerivationStrategy(BTCPayNetwork network, string derivationScheme)
/// <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())
{
if (strat.Name == network.CryptoCode)
var stratId = PaymentMethodId.Parse(strat.Name);
if (stratId.IsBTCOnChain)
{
if (network.IsBTC)
DerivationStrategy = null;
if (string.IsNullOrEmpty(derivationScheme))
// Legacy stuff which should go away
DerivationStrategy = null;
}
if (stratId == paymentMethodId)
{
if (supportedPaymentMethod == null)
{
strat.Remove();
}
else
{
strat.Value = new JValue(derivationScheme);
strat.Value = PaymentMethodExtensions.Serialize(supportedPaymentMethod);
}
existing = true;
break;
}
}
if (!existing && string.IsNullOrEmpty(derivationScheme))
if (!existing && supportedPaymentMethod == null && paymentMethodId.IsBTCOnChain)
{
if (network.IsBTC)
DerivationStrategy = null;
DerivationStrategy = null;
}
else if (!existing)
strategies.Add(new JProperty(network.CryptoCode, new JValue(derivationScheme)));
// This is deprecated so we don't have to set anymore
//if (network.IsBTC)
// DerivationStrategy = derivationScheme;
else if (!existing && supportedPaymentMethod != null)
strategies.Add(new JProperty(supportedPaymentMethod.PaymentId.ToString(), PaymentMethodExtensions.Serialize(supportedPaymentMethod)));
DerivationStrategies = strategies.ToString();
#pragma warning restore CS0618
}
@ -196,11 +215,20 @@ namespace BTCPayServer.Data
{
InvoiceExpiration = 15;
MonitoringExpiration = 60;
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
@ -237,30 +265,36 @@ namespace BTCPayServer.Data
public List<RateRule> RateRules { get; set; } = new List<RateRule>();
public string PreferredExchange { get; set; }
public IRateProvider ApplyRateRules(BTCPayNetwork network, IRateProvider rateProvider)
[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; }
string _LightningDescriptionTemplate;
public string LightningDescriptionTemplate
{
if (!PreferredExchange.IsCoinAverage())
get
{
// If the original rateProvider is a cache, use the same inner provider as fallback, and same memory cache to wrap it all
if (rateProvider is CachedRateProvider cachedRateProvider)
{
rateProvider = new FallbackRateProvider(new IRateProvider[] {
new CoinAverageRateProvider(network.CryptoCode) { Exchange = PreferredExchange },
cachedRateProvider.Inner
});
rateProvider = new CachedRateProvider(network.CryptoCode, rateProvider, cachedRateProvider.MemoryCache) { AdditionalScope = PreferredExchange };
}
else
{
rateProvider = new FallbackRateProvider(new IRateProvider[] {
new CoinAverageRateProvider(network.CryptoCode) { Exchange = PreferredExchange },
rateProvider
});
}
return _LightningDescriptionTemplate ?? "Paid to {StoreName} (Order ID: {OrderId})";
}
if (RateRules == null || RateRules.Count == 0)
return rateProvider;
return new TweakRateProvider(network, rateProvider, RateRules.ToList());
set
{
_LightningDescriptionTemplate = value;
}
}
public RateRules GetRateRules()
{
return new RateRules(RateRules)
{
PreferredExchange = PreferredExchange
};
}
}
}

@ -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;
}
}
}

@ -2,17 +2,19 @@
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
public class DerivationStrategy : ISupportedPaymentMethod
{
private DerivationStrategyBase _DerivationStrategy;
private BTCPayNetwork _Network;
DerivationStrategy(DerivationStrategyBase result, BTCPayNetwork network)
public DerivationStrategy(DerivationStrategyBase result, BTCPayNetwork network)
{
this._DerivationStrategy = result;
this._Network = network;
@ -32,6 +34,8 @@ namespace BTCPayServer
public DerivationStrategyBase DerivationStrategyBase { get { return this._DerivationStrategy; } }
public PaymentMethodId PaymentId => new PaymentMethodId(Network.CryptoCode, PaymentTypes.BTCLike);
public override string ToString()
{
return _DerivationStrategy.ToString();

@ -1,14 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Eclair
{
public class AllChannelResponse
{
public string ShortChannelId { get; set; }
public string NodeId1 { get; set; }
public string NodeId2 { get; set; }
}
}

@ -1,21 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Eclair
{
public class ChannelResponse
{
public string NodeId { get; set; }
public string ChannelId { get; set; }
public string State { get; set; }
}
public static class ChannelStates
{
public const string WAIT_FOR_FUNDING_CONFIRMED = "WAIT_FOR_FUNDING_CONFIRMED";
public const string NORMAL = "NORMAL";
}
}

@ -1,230 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using NBitcoin;
using NBitcoin.JsonConverters;
using NBitcoin.RPC;
namespace BTCPayServer.Eclair
{
public class EclairRPCClient
{
public EclairRPCClient(Uri address, Network network)
{
if (address == null)
throw new ArgumentNullException(nameof(address));
if (network == null)
throw new ArgumentNullException(nameof(network));
Address = address;
Network = network;
}
public Network Network { get; private set; }
public GetInfoResponse GetInfo()
{
return GetInfoAsync().GetAwaiter().GetResult();
}
public Task<GetInfoResponse> GetInfoAsync()
{
return SendCommandAsync<GetInfoResponse>(new RPCRequest("getinfo", new object[] { }));
}
public async Task<T> SendCommandAsync<T>(RPCRequest request, bool throwIfRPCError = true)
{
var response = await SendCommandAsync(request, throwIfRPCError);
return Serializer.ToObject<T>(response.ResultString, Network);
}
public async Task<RPCResponse> SendCommandAsync(RPCRequest request, bool throwIfRPCError = true)
{
RPCResponse response = null;
HttpWebRequest webRequest = response == null ? CreateWebRequest() : null;
if (response == null)
{
var writer = new StringWriter();
request.WriteJSON(writer);
writer.Flush();
var json = writer.ToString();
var bytes = Encoding.UTF8.GetBytes(json);
#if !(PORTABLE || NETCORE)
webRequest.ContentLength = bytes.Length;
#endif
var dataStream = await webRequest.GetRequestStreamAsync().ConfigureAwait(false);
await dataStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
await dataStream.FlushAsync().ConfigureAwait(false);
dataStream.Dispose();
}
WebResponse webResponse = null;
WebResponse errorResponse = null;
try
{
webResponse = response == null ? await webRequest.GetResponseAsync().ConfigureAwait(false) : null;
response = response ?? RPCResponse.Load(await ToMemoryStreamAsync(webResponse.GetResponseStream()).ConfigureAwait(false));
if (throwIfRPCError)
response.ThrowIfError();
}
catch (WebException ex)
{
if (ex.Response == null || ex.Response.ContentLength == 0 ||
!ex.Response.ContentType.Equals("application/json", StringComparison.Ordinal))
throw;
errorResponse = ex.Response;
response = RPCResponse.Load(await ToMemoryStreamAsync(errorResponse.GetResponseStream()).ConfigureAwait(false));
if (throwIfRPCError)
response.ThrowIfError();
}
finally
{
if (errorResponse != null)
{
errorResponse.Dispose();
errorResponse = null;
}
if (webResponse != null)
{
webResponse.Dispose();
webResponse = null;
}
}
return response;
}
public AllChannelResponse[] AllChannels()
{
return AllChannelsAsync().GetAwaiter().GetResult();
}
public async Task<AllChannelResponse[]> AllChannelsAsync()
{
return await SendCommandAsync<AllChannelResponse[]>(new RPCRequest("allchannels", new object[] { })).ConfigureAwait(false);
}
public string[] Channels()
{
return ChannelsAsync().GetAwaiter().GetResult();
}
public async Task<string[]> ChannelsAsync()
{
return await SendCommandAsync<string[]>(new RPCRequest("channels", new object[] { })).ConfigureAwait(false);
}
public void Close(string channelId)
{
CloseAsync(channelId).GetAwaiter().GetResult();
}
public async Task CloseAsync(string channelId)
{
if (channelId == null)
throw new ArgumentNullException(nameof(channelId));
try
{
await SendCommandAsync(new RPCRequest("close", new object[] { channelId })).ConfigureAwait(false);
}
catch (RPCException ex) when (ex.Message == "closing already in progress")
{
}
}
public ChannelResponse Channel(string channelId)
{
return ChannelAsync(channelId).GetAwaiter().GetResult();
}
public async Task<ChannelResponse> ChannelAsync(string channelId)
{
if (channelId == null)
throw new ArgumentNullException(nameof(channelId));
return await SendCommandAsync<ChannelResponse>(new RPCRequest("channel", new object[] { channelId })).ConfigureAwait(false);
}
public string[] AllNodes()
{
return AllNodesAsync().GetAwaiter().GetResult();
}
public async Task<string[]> AllNodesAsync()
{
return await SendCommandAsync<string[]>(new RPCRequest("allnodes", new object[] { })).ConfigureAwait(false);
}
public Uri Address { get; private set; }
private HttpWebRequest CreateWebRequest()
{
var webRequest = (HttpWebRequest)WebRequest.Create(Address.AbsoluteUri);
webRequest.ContentType = "application/json";
webRequest.Method = "POST";
return webRequest;
}
private async Task<Stream> ToMemoryStreamAsync(Stream stream)
{
MemoryStream ms = new MemoryStream();
await stream.CopyToAsync(ms).ConfigureAwait(false);
ms.Position = 0;
return ms;
}
public string Open(NodeInfo node, Money fundingSatoshi, LightMoney pushAmount = null)
{
return OpenAsync(node, fundingSatoshi, pushAmount).GetAwaiter().GetResult();
}
public string Connect(NodeInfo node)
{
return ConnectAsync(node).GetAwaiter().GetResult();
}
public async Task<string> ConnectAsync(NodeInfo node)
{
if (node == null)
throw new ArgumentNullException(nameof(node));
return (await SendCommandAsync(new RPCRequest("connect", new object[] { node.NodeId, node.Host, node.Port })).ConfigureAwait(false)).ResultString;
}
public string Receive(LightMoney amount, string description = null)
{
return ReceiveAsync(amount, description).GetAwaiter().GetResult();
}
public async Task<string> ReceiveAsync(LightMoney amount, string description = null)
{
if (amount == null)
throw new ArgumentNullException(nameof(amount));
List<object> args = new List<object>();
args.Add(amount.MilliSatoshi);
if(description != null)
{
args.Add(description);
}
return (await SendCommandAsync(new RPCRequest("receive", args.ToArray())).ConfigureAwait(false)).ResultString;
}
public async Task<string> OpenAsync(NodeInfo node, Money fundingSatoshi, LightMoney pushAmount = null)
{
if (fundingSatoshi == null)
throw new ArgumentNullException(nameof(fundingSatoshi));
if (node == null)
throw new ArgumentNullException(nameof(node));
pushAmount = pushAmount ?? LightMoney.Zero;
var result = await SendCommandAsync(new RPCRequest("open", new object[] { node.NodeId, node.Host, node.Port, fundingSatoshi.Satoshi, pushAmount.MilliSatoshi }));
return result.ResultString;
}
}
}

@ -1,18 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NBitcoin;
using Newtonsoft.Json;
namespace BTCPayServer.Eclair
{
public class GetInfoResponse
{
public string NodeId { get; set; }
public string Alias { get; set; }
public int Port { get; set; }
public uint256 ChainHash { get; set; }
public int BlockHeight { get; set; }
}
}

@ -88,7 +88,9 @@ namespace BTCPayServer
}
}
Logs.Events.LogInformation(evt.ToString());
var log = evt.ToString();
if(!String.IsNullOrEmpty(log))
Logs.Events.LogInformation(log);
foreach (var sub in actionList)
{
try

@ -2,11 +2,18 @@
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; }

@ -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;
}
}
}

@ -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}";
}
}
}

@ -7,6 +7,10 @@ namespace BTCPayServer.Events
{
public class InvoiceStopWatchedEvent
{
public InvoiceStopWatchedEvent(string invoiceId)
{
this.InvoiceId = invoiceId;
}
public string InvoiceId { get; set; }
public override string ToString()
{

@ -1,22 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NBitcoin;
using NBXplorer.DerivationStrategy;
namespace BTCPayServer.Events
{
public class TxOutReceivedEvent
{
public BTCPayNetwork Network { get; set; }
public Script ScriptPubKey { get; set; }
public DerivationStrategyBase DerivationStrategy { get; set; }
public override string ToString()
{
String address = ScriptPubKey.GetDestinationAddress(Network.NBitcoinNetwork)?.ToString() ?? ScriptPubKey.ToString();
return $"{address} received a transaction ({Network.CryptoCode})";
}
}
}

@ -77,7 +77,7 @@ namespace BTCPayServer
public bool IsAvailable(string cryptoCode)
{
return _Clients.ContainsKey(cryptoCode) && _Dashboard.IsFullySynched(cryptoCode);
return _Clients.ContainsKey(cryptoCode) && _Dashboard.IsFullySynched(cryptoCode, out var unused);
}
public BTCPayNetwork GetNetwork(string cryptoCode)

@ -22,11 +22,69 @@ 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 Prettify(this TimeSpan timeSpan)
{
if (timeSpan.TotalMinutes < 1)
{
return $"{(int)timeSpan.TotalSeconds} second{Plural((int)timeSpan.TotalSeconds)}";
}
if (timeSpan.TotalHours < 1)
{
return $"{(int)timeSpan.TotalMinutes} minute{Plural((int)timeSpan.TotalMinutes)}";
}
if (timeSpan.Days < 1)
{
return $"{(int)timeSpan.TotalHours} hour{Plural((int)timeSpan.TotalHours)}";
}
return $"{(int)timeSpan.TotalDays} day{Plural((int)timeSpan.TotalDays)}";
}
private static string Plural(int totalDays)
{
return totalDays > 1 ? "s" : string.Empty;
}
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
@ -63,7 +121,7 @@ namespace BTCPayServer
}
public static string WithTrailingSlash(this string str)
{
if (str.EndsWith("/"))
if (str.EndsWith("/", StringComparison.InvariantCulture))
return str;
return str + "/";
}
@ -77,6 +135,14 @@ namespace BTCPayServer
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 IServiceCollection ConfigureBTCPayServer(this IServiceCollection services, IConfiguration conf)
{
services.Configure<BTCPayServerOptions>(o =>
@ -86,12 +152,35 @@ namespace BTCPayServer
return services;
}
public static BitIdentity GetBitIdentity(this Controller controller, bool throws = true)
public static string GetSIN(this ClaimsPrincipal principal)
{
if (!(controller.User.Identity is BitIdentity))
return throws ? throw new UnauthorizedAccessException("no-bitid") : (BitIdentity)null;
return (BitIdentity)controller.User.Identity;
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 bool GetIsBitpayAPI(this HttpContext ctx)
{
return ctx.Items.TryGetValue("IsBitpayAPI", out object obj) &&
obj is bool b && b;
}
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() };

@ -43,9 +43,7 @@ namespace BTCPayServer.Filters
public bool Accept(ActionConstraintContext context)
{
var hasVersion = context.RouteContext.HttpContext.Request.Headers["x-accept-version"].Where(h => h == "2.0.0").Any();
var hasIdentity = context.RouteContext.HttpContext.Request.Headers["x-identity"].Any();
return (hasVersion || hasIdentity) == IsBitpayAPI;
return context.RouteContext.HttpContext.GetIsBitpayAPI() == IsBitpayAPI;
}
}

@ -0,0 +1,66 @@
using System;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Services;
using BTCPayServer.Services.Rates;
using Microsoft.Extensions.Hosting;
using BTCPayServer.Logging;
using System.Runtime.CompilerServices;
using System.IO;
using System.Text;
namespace BTCPayServer.HostedServices
{
public abstract class BaseAsyncService : IHostedService
{
private CancellationTokenSource _Cts;
protected Task[] _Tasks;
public Task StartAsync(CancellationToken cancellationToken)
{
_Cts = new CancellationTokenSource();
_Tasks = InitializeTasks();
return Task.CompletedTask;
}
internal abstract Task[] InitializeTasks();
protected CancellationToken Cancellation
{
get { return _Cts.Token; }
}
protected async Task CreateLoopTask(Func<Task> act, [CallerMemberName]string caller = null)
{
await new SynchronizationContextRemover();
while (!_Cts.IsCancellationRequested)
{
try
{
await act();
}
catch (OperationCanceledException) when (_Cts.IsCancellationRequested)
{
}
catch (Exception ex)
{
Logs.PayServer.LogWarning(ex, caller + " failed");
try
{
await Task.Delay(TimeSpan.FromMinutes(1), _Cts.Token);
}
catch (OperationCanceledException) when (_Cts.IsCancellationRequested) { }
}
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
_Cts.Cancel();
return Task.WhenAll(_Tasks);
}
}
}

@ -0,0 +1,74 @@
using System;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Logging;
using Microsoft.Extensions.Hosting;
using NBXplorer;
using NBXplorer.Models;
using System.Collections.Concurrent;
using BTCPayServer.Events;
using BTCPayServer.Services;
namespace BTCPayServer.HostedServices
{
public class CssThemeManager
{
public void Update(ThemeSettings data)
{
if (String.IsNullOrWhiteSpace(data.BootstrapCssUri))
_bootstrapUri = "/vendor/bootstrap4/css/bootstrap.css?v=" + DateTime.Now.Ticks;
else
_bootstrapUri = data.BootstrapCssUri;
if (String.IsNullOrWhiteSpace(data.CreativeStartCssUri))
_creativeStartUri = "/vendor/bootstrap4-creativestart/creative.css?v=" + DateTime.Now.Ticks;
else
_creativeStartUri = data.CreativeStartCssUri;
}
private string _bootstrapUri;
public string BootstrapUri
{
get { return _bootstrapUri; }
}
private string _creativeStartUri;
public string CreativeStartUri
{
get { return _creativeStartUri; }
}
}
public class CssThemeManagerHostedService : BaseAsyncService
{
private SettingsRepository _SettingsRepository;
private CssThemeManager _CssThemeManager;
public CssThemeManagerHostedService(SettingsRepository settingsRepository, CssThemeManager cssThemeManager)
{
_SettingsRepository = settingsRepository;
_CssThemeManager = cssThemeManager;
}
internal override Task[] InitializeTasks()
{
return new[]
{
CreateLoopTask(ListenForThemeChanges)
};
}
async Task ListenForThemeChanges()
{
await new SynchronizationContextRemover();
var data = (await _SettingsRepository.GetSettingAsync<ThemeSettings>()) ?? new ThemeSettings();
_CssThemeManager.Update(data);
await _SettingsRepository.WaitSettingsChanged<ThemeSettings>(Cancellation);
}
}
}

@ -19,6 +19,7 @@ using Microsoft.Extensions.Hosting;
using BTCPayServer.Events;
using NBXplorer;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Payments;
namespace BTCPayServer.HostedServices
{
@ -74,7 +75,8 @@ namespace BTCPayServer.HostedServices
if (string.IsNullOrEmpty(invoice.NotificationURL))
return;
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id, eventCode, name));
await SendNotification(invoice, eventCode, name, cts.Token);
var response = await SendNotification(invoice, eventCode, name, cts.Token);
response.EnsureSuccessStatusCode();
return;
}
catch (OperationCanceledException) when (cts.IsCancellationRequested)
@ -111,7 +113,7 @@ namespace BTCPayServer.HostedServices
try
{
HttpResponseMessage response = await SendNotification(job.Invoice, job.EventCode, job.Message, cts.Token);
reschedule = response.StatusCode != System.Net.HttpStatusCode.OK;
reschedule = !response.IsSuccessStatusCode;
Logger.LogInformation("Job " + jobId + " returned " + response.StatusCode);
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
@ -137,7 +139,7 @@ namespace BTCPayServer.HostedServices
reschedule = true;
List<string> messages = new List<string>();
while(ex != null)
while (ex != null)
{
messages.Add(ex.Message);
ex = ex.InnerException;
@ -201,7 +203,7 @@ namespace BTCPayServer.HostedServices
// We keep backward compatibility with bitpay by passing BTC info to the notification
// we don't pass other info, as it is a bad idea to use IPN data for logic processing (can be faked)
var btcCryptoInfo = dto.CryptoInfo.FirstOrDefault(c => c.CryptoCode == "BTC");
var btcCryptoInfo = dto.CryptoInfo.FirstOrDefault(c => c.GetpaymentMethodId() == new PaymentMethodId("BTC", Payments.PaymentTypes.BTCLike));
if (btcCryptoInfo != null)
{
#pragma warning disable CS0618
@ -228,10 +230,68 @@ namespace BTCPayServer.HostedServices
request.RequestUri = new Uri(invoice.NotificationURL, UriKind.Absolute);
request.Content = new StringContent(notificationString, UTF8, "application/json");
var response = await _Client.SendAsync(request, cancellation);
var response = await Enqueue(invoice.Id, async () => await _Client.SendAsync(request, cancellation));
return response;
}
Dictionary<string, Task> _SendingRequestsByInvoiceId = new Dictionary<string, Task>();
/// <summary>
/// Will make sure only one callback is called at once on the same invoiceId
/// </summary>
/// <param name="id"></param>
/// <param name="sendRequest"></param>
/// <returns></returns>
private async Task<T> Enqueue<T>(string id, Func<Task<T>> sendRequest)
{
Task<T> sending = null;
lock (_SendingRequestsByInvoiceId)
{
if (_SendingRequestsByInvoiceId.TryGetValue(id, out var executing))
{
var completion = new TaskCompletionSource<T>();
sending = completion.Task;
_SendingRequestsByInvoiceId.Remove(id);
_SendingRequestsByInvoiceId.Add(id, sending);
executing.ContinueWith(_ =>
{
sendRequest()
.ContinueWith(t =>
{
if(t.Status == TaskStatus.RanToCompletion)
{
completion.TrySetResult(t.Result);
}
if(t.Status == TaskStatus.Faulted)
{
completion.TrySetException(t.Exception);
}
if(t.Status == TaskStatus.Canceled)
{
completion.TrySetCanceled();
}
}, TaskScheduler.Default);
}, TaskScheduler.Default);
}
else
{
sending = sendRequest();
_SendingRequestsByInvoiceId.Add(id, sending);
}
sending.ContinueWith(o =>
{
lock (_SendingRequestsByInvoiceId)
{
_SendingRequestsByInvoiceId.TryGetValue(id, out var executing2);
if(executing2 == sending)
_SendingRequestsByInvoiceId.Remove(id);
}
}, TaskScheduler.Default);
}
return await sending;
}
int MaxTry = 6;
private static string GetHttpJobId(InvoiceEntity invoice)

@ -25,9 +25,9 @@ namespace BTCPayServer.HostedServices
{
class UpdateInvoiceContext
{
public UpdateInvoiceContext()
public UpdateInvoiceContext(InvoiceEntity invoice)
{
Invoice = invoice;
}
public InvoiceEntity Invoice { get; set; }
public List<object> Events { get; set; } = new List<object>();
@ -43,99 +43,20 @@ namespace BTCPayServer.HostedServices
InvoiceRepository _InvoiceRepository;
EventAggregator _EventAggregator;
BTCPayWalletProvider _WalletProvider;
BTCPayNetworkProvider _NetworkProvider;
public InvoiceWatcher(
IHostingEnvironment env,
BTCPayNetworkProvider networkProvider,
InvoiceRepository invoiceRepository,
EventAggregator eventAggregator,
BTCPayWalletProvider walletProvider)
EventAggregator eventAggregator)
{
PollInterval = TimeSpan.FromMinutes(1.0);
_WalletProvider = walletProvider ?? throw new ArgumentNullException(nameof(walletProvider));
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
_EventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
_NetworkProvider = networkProvider;
}
CompositeDisposable leases = new CompositeDisposable();
async Task NotifyReceived(Events.TxOutReceivedEvent evt)
{
var invoiceId = await _InvoiceRepository.GetInvoiceIdFromScriptPubKey(evt.ScriptPubKey, evt.Network.CryptoCode);
if (invoiceId != null)
{
String address = evt.ScriptPubKey.GetDestinationAddress(evt.Network.NBitcoinNetwork)?.ToString() ?? evt.ScriptPubKey.ToString();
_WalletProvider.GetWallet(evt.Network).InvalidateCache(evt.DerivationStrategy);
Logs.PayServer.LogInformation($"{address} is mapping to invoice {invoiceId}");
_WatchRequests.Add(invoiceId);
}
}
async Task NotifyBlock()
{
foreach (var invoice in await _InvoiceRepository.GetPendingInvoices())
{
_WatchRequests.Add(invoice);
}
}
private async Task UpdateInvoice(string invoiceId, CancellationToken cancellation)
{
Dictionary<BTCPayNetwork, KnownState> changes = new Dictionary<BTCPayNetwork, KnownState>();
int maxLoop = 5;
int loopCount = -1;
while (!cancellation.IsCancellationRequested && loopCount < maxLoop)
{
loopCount++;
try
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, true).ConfigureAwait(false);
if (invoice == null)
break;
var stateBefore = invoice.Status;
var updateContext = new UpdateInvoiceContext()
{
Invoice = invoice
};
await UpdateInvoice(updateContext).ConfigureAwait(false);
if (updateContext.Dirty)
{
await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.Status, invoice.ExceptionStatus).ConfigureAwait(false);
updateContext.Events.Add(new InvoiceDataChangedEvent() { Status = invoice.Status, ExceptionStatus = invoice.ExceptionStatus, InvoiceId = invoice.Id });
}
var changed = stateBefore != invoice.Status;
foreach (var evt in updateContext.Events)
{
_EventAggregator.Publish(evt, evt.GetType());
}
if (invoice.Status == "complete" ||
((invoice.Status == "invalid" || invoice.Status == "expired") && invoice.MonitoringExpiration < DateTimeOffset.UtcNow))
{
if (await _InvoiceRepository.RemovePendingInvoice(invoice.Id).ConfigureAwait(false))
_EventAggregator.Publish<InvoiceStopWatchedEvent>(new InvoiceStopWatchedEvent() { InvoiceId = invoice.Id });
break;
}
if (updateContext.Events.Count == 0 || cancellation.IsCancellationRequested)
break;
}
catch (OperationCanceledException) when (cancellation.IsCancellationRequested)
{
break;
}
catch (Exception ex)
{
Logs.PayServer.LogError(ex, "Unhandled error on watching invoice " + invoiceId);
await Task.Delay(10000, cancellation).ConfigureAwait(false);
}
}
}
private async Task UpdateInvoice(UpdateInvoiceContext context)
{
@ -149,241 +70,118 @@ namespace BTCPayServer.HostedServices
invoice.Status = "expired";
}
var derivationStrategies = invoice.GetDerivationStrategies(_NetworkProvider).ToArray();
var payments = await GetPaymentsWithTransaction(derivationStrategies, invoice);
foreach (Task<NetworkCoins> coinsAsync in GetCoinsPerNetwork(context, invoice, derivationStrategies))
var payments = invoice.GetPayments().Where(p => p.Accounted).ToArray();
var allPaymentMethods = invoice.GetPaymentMethods(_NetworkProvider);
var paymentMethod = GetNearestClearedPayment(allPaymentMethods, out var accounting, _NetworkProvider);
if (paymentMethod == null)
return;
var network = _NetworkProvider.GetNetwork(paymentMethod.GetId().CryptoCode);
if (invoice.Status == "new" || invoice.Status == "expired")
{
var coins = await coinsAsync;
if (coins.TimestampedCoins.Length == 0)
continue;
bool dirtyAddress = false;
var alreadyAccounted = new HashSet<OutPoint>(invoice.GetPayments(coins.Wallet.Network).Select(p => p.Outpoint));
foreach (var coin in coins.TimestampedCoins.Where(c => !alreadyAccounted.Contains(c.Coin.Outpoint)))
if (accounting.Paid >= accounting.TotalDue)
{
var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin.DateTime, coin.Coin, coins.Wallet.Network.CryptoCode).ConfigureAwait(false);
#pragma warning disable CS0618
invoice.Payments.Add(payment);
#pragma warning restore CS0618
alreadyAccounted.Add(coin.Coin.Outpoint);
context.Events.Add(new InvoiceEvent(invoice, 1002, "invoice_receivedPayment"));
dirtyAddress = true;
}
if (dirtyAddress)
{
payments = await GetPaymentsWithTransaction(derivationStrategies, invoice);
}
var network = coins.Wallet.Network;
var cryptoData = invoice.GetCryptoData(network, _NetworkProvider);
var cryptoDataAll = invoice.GetCryptoData(_NetworkProvider);
var accounting = cryptoData.Calculate();
if (invoice.Status == "new" || invoice.Status == "expired")
{
var totalPaid = payments.Select(p => p.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum();
if (totalPaid >= accounting.TotalDue)
{
if (invoice.Status == "new")
{
context.Events.Add(new InvoiceEvent(invoice, 1003, "invoice_paidInFull"));
invoice.Status = "paid";
invoice.ExceptionStatus = totalPaid > accounting.TotalDue ? "paidOver" : null;
await _InvoiceRepository.UnaffectAddress(invoice.Id);
context.MarkDirty();
}
else if (invoice.Status == "expired" && invoice.ExceptionStatus != "paidLate")
{
invoice.ExceptionStatus = "paidLate";
context.Events.Add(new InvoiceEvent(invoice, 1009, "invoice_paidAfterExpiration"));
context.MarkDirty();
}
}
if (totalPaid < accounting.TotalDue && invoice.GetPayments().Count != 0 && invoice.ExceptionStatus != "paidPartial")
{
invoice.ExceptionStatus = "paidPartial";
context.MarkDirty();
if (dirtyAddress)
{
var address = await coins.Wallet.ReserveAddressAsync(coins.Strategy);
Logs.PayServer.LogInformation("Generate new " + address);
await _InvoiceRepository.NewAddress(invoice.Id, address, network);
}
}
}
if (invoice.Status == "paid")
{
IEnumerable<AccountedPaymentEntity> transactions = payments;
if (invoice.SpeedPolicy == SpeedPolicy.HighSpeed)
{
transactions = transactions.Where(t => t.Confirmations >= 1 || !t.Transaction.RBF);
}
else if (invoice.SpeedPolicy == SpeedPolicy.MediumSpeed)
{
transactions = transactions.Where(t => t.Confirmations >= 1);
}
else if (invoice.SpeedPolicy == SpeedPolicy.LowSpeed)
{
transactions = transactions.Where(t => t.Confirmations >= 6);
}
var totalConfirmed = transactions.Select(t => t.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum();
if (// Is after the monitoring deadline
(invoice.MonitoringExpiration < DateTimeOffset.UtcNow)
&&
// And not enough amount confirmed
(totalConfirmed < accounting.TotalDue))
if (invoice.Status == "new")
{
context.Events.Add(new InvoiceEvent(invoice, 1003, "invoice_paidInFull"));
invoice.Status = "paid";
invoice.ExceptionStatus = accounting.Paid > accounting.TotalDue ? "paidOver" : null;
await _InvoiceRepository.UnaffectAddress(invoice.Id);
context.Events.Add(new InvoiceEvent(invoice, 1013, "invoice_failedToConfirm"));
invoice.Status = "invalid";
context.MarkDirty();
}
else if (totalConfirmed >= accounting.TotalDue)
else if (invoice.Status == "expired" && invoice.ExceptionStatus != "paidLate")
{
await _InvoiceRepository.UnaffectAddress(invoice.Id);
context.Events.Add(new InvoiceEvent(invoice, 1005, "invoice_confirmed"));
invoice.Status = "confirmed";
invoice.ExceptionStatus = "paidLate";
context.Events.Add(new InvoiceEvent(invoice, 1009, "invoice_paidAfterExpiration"));
context.MarkDirty();
}
}
if (invoice.Status == "confirmed")
if (accounting.Paid < accounting.TotalDue && invoice.GetPayments().Count != 0 && invoice.ExceptionStatus != "paidPartial")
{
IEnumerable<AccountedPaymentEntity> transactions = payments;
transactions = transactions.Where(t => t.Confirmations >= 6);
var totalConfirmed = transactions.Select(t => t.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum();
if (totalConfirmed >= accounting.TotalDue)
{
context.Events.Add(new InvoiceEvent(invoice, 1006, "invoice_completed"));
invoice.Status = "complete";
context.MarkDirty();
}
invoice.ExceptionStatus = "paidPartial";
context.MarkDirty();
}
}
}
private IEnumerable<Task<NetworkCoins>> GetCoinsPerNetwork(UpdateInvoiceContext context, InvoiceEntity invoice, DerivationStrategy[] strategies)
{
return strategies
.Select(d => (Wallet: _WalletProvider.IsAvailable(d.Network) ? _WalletProvider.GetWallet(d.Network) : null,
Network: d.Network,
Strategy: d.DerivationStrategyBase))
.Where(d => d.Wallet != null)
.Select(d => (Network: d.Network,
Coins: d.Wallet.GetCoins(d.Strategy)))
.Select(async d =>
{
var coins = await d.Coins;
// Keep only coins from the invoice
coins.TimestampedCoins = coins.TimestampedCoins.Where(c => invoice.AvailableAddressHashes.Contains(c.Coin.ScriptPubKey.Hash.ToString() + d.Network.CryptoCode)).ToArray();
return coins;
})
.ToArray();
}
private async Task<IEnumerable<AccountedPaymentEntity>> GetPaymentsWithTransaction(DerivationStrategy[] derivations, InvoiceEntity invoice)
{
List<PaymentEntity> updatedPaymentEntities = new List<PaymentEntity>();
List<AccountedPaymentEntity> accountedPayments = new List<AccountedPaymentEntity>();
foreach (var network in derivations.Select(d => d.Network))
// Just make sure RBF did not cancelled a payment
if (invoice.Status == "paid")
{
var wallet = _WalletProvider.GetWallet(network);
if (wallet == null)
if (accounting.Paid == accounting.TotalDue && invoice.ExceptionStatus == "paidOver")
{
invoice.ExceptionStatus = null;
context.MarkDirty();
}
if (accounting.Paid > accounting.TotalDue && invoice.ExceptionStatus != "paidOver")
{
invoice.ExceptionStatus = "paidOver";
context.MarkDirty();
}
if (accounting.Paid < accounting.TotalDue)
{
invoice.Status = "new";
invoice.ExceptionStatus = accounting.Paid == Money.Zero ? null : "paidPartial";
context.MarkDirty();
}
}
if (invoice.Status == "paid")
{
var confirmedAccounting = paymentMethod.Calculate(p => p.GetCryptoPaymentData().PaymentConfirmed(p, invoice.SpeedPolicy, network));
if (// Is after the monitoring deadline
(invoice.MonitoringExpiration < DateTimeOffset.UtcNow)
&&
// And not enough amount confirmed
(confirmedAccounting.Paid < accounting.TotalDue))
{
await _InvoiceRepository.UnaffectAddress(invoice.Id);
context.Events.Add(new InvoiceEvent(invoice, 1013, "invoice_failedToConfirm"));
invoice.Status = "invalid";
context.MarkDirty();
}
else if (confirmedAccounting.Paid >= accounting.TotalDue)
{
await _InvoiceRepository.UnaffectAddress(invoice.Id);
context.Events.Add(new InvoiceEvent(invoice, 1005, "invoice_confirmed"));
invoice.Status = "confirmed";
context.MarkDirty();
}
}
if (invoice.Status == "confirmed")
{
var completedAccounting = paymentMethod.Calculate(p => p.GetCryptoPaymentData().PaymentCompleted(p, network));
if (completedAccounting.Paid >= accounting.TotalDue)
{
context.Events.Add(new InvoiceEvent(invoice, 1006, "invoice_completed"));
invoice.Status = "complete";
context.MarkDirty();
}
}
}
public static PaymentMethod GetNearestClearedPayment(PaymentMethodDictionary allPaymentMethods, out PaymentMethodAccounting accounting, BTCPayNetworkProvider networkProvider)
{
PaymentMethod result = null;
accounting = null;
decimal nearestToZero = 0.0m;
foreach (var paymentMethod in allPaymentMethods)
{
if (networkProvider != null && networkProvider.GetNetwork(paymentMethod.GetId().CryptoCode) == null)
continue;
var transactions = await wallet.GetTransactions(invoice.GetPayments(wallet.Network)
.Select(t => t.Outpoint.Hash)
.ToArray());
var conflicts = GetConflicts(transactions.Select(t => t.Value));
foreach (var payment in invoice.GetPayments(network))
var currentAccounting = paymentMethod.Calculate();
var distanceFromZero = Math.Abs(currentAccounting.DueUncapped.ToDecimal(MoneyUnit.BTC));
if (result == null || distanceFromZero < nearestToZero)
{
if (!transactions.TryGetValue(payment.Outpoint.Hash, out TransactionResult tx))
continue;
AccountedPaymentEntity accountedPayment = new AccountedPaymentEntity()
{
Confirmations = tx.Confirmations,
Transaction = tx.Transaction,
Payment = payment
};
var txId = accountedPayment.Transaction.GetHash();
var txConflict = conflicts.GetConflict(txId);
var accounted = txConflict == null || txConflict.IsWinner(txId);
if (accounted != payment.Accounted)
{
updatedPaymentEntities.Add(payment);
payment.Accounted = accounted;
}
if (accounted)
accountedPayments.Add(accountedPayment);
result = paymentMethod;
nearestToZero = distanceFromZero;
accounting = currentAccounting;
}
}
await _InvoiceRepository.UpdatePayments(updatedPaymentEntities);
return accountedPayments;
}
class TransactionConflict
{
public Dictionary<uint256, TransactionResult> Transactions { get; set; } = new Dictionary<uint256, TransactionResult>();
uint256 _Winner;
public bool IsWinner(uint256 txId)
{
if (_Winner == null)
{
var confirmed = Transactions.FirstOrDefault(t => t.Value.Confirmations >= 1);
if (!confirmed.Equals(default(KeyValuePair<uint256, TransactionResult>)))
{
_Winner = confirmed.Key;
}
else
{
// Take the most recent (bitcoin node would not forward a conflict without a successfull RBF)
_Winner = Transactions
.OrderByDescending(t => t.Value.Timestamp)
.First()
.Key;
}
}
return _Winner == txId;
}
}
class TransactionConflicts : List<TransactionConflict>
{
public TransactionConflicts(IEnumerable<TransactionConflict> collection) : base(collection)
{
}
public TransactionConflict GetConflict(uint256 txId)
{
return this.FirstOrDefault(c => c.Transactions.ContainsKey(txId));
}
}
private TransactionConflicts GetConflicts(IEnumerable<TransactionResult> transactions)
{
Dictionary<OutPoint, TransactionConflict> conflictsByOutpoint = new Dictionary<OutPoint, TransactionConflict>();
foreach (var tx in transactions)
{
var hash = tx.Transaction.GetHash();
foreach (var input in tx.Transaction.Inputs)
{
TransactionConflict conflict = new TransactionConflict();
if (!conflictsByOutpoint.TryAdd(input.PrevOut, conflict))
{
conflict = conflictsByOutpoint[input.PrevOut];
}
if (!conflict.Transactions.ContainsKey(hash))
conflict.Transactions.Add(hash, tx);
}
}
return new TransactionConflicts(conflictsByOutpoint.Where(c => c.Value.Transactions.Count > 1).Select(c => c.Value));
return result;
}
TimeSpan _PollInterval;
@ -399,115 +197,136 @@ namespace BTCPayServer.HostedServices
}
}
private async Task Watch(string invoiceId)
private void Watch(string invoiceId)
{
if (invoiceId == null)
throw new ArgumentNullException(nameof(invoiceId));
_WatchRequests.Add(invoiceId);
}
private async Task Wait(string invoiceId)
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
try
{
var now = DateTimeOffset.UtcNow;
if (invoice.ExpirationTime > now)
var delay = invoice.ExpirationTime - DateTimeOffset.UtcNow;
if (delay > TimeSpan.Zero)
{
await Task.Delay(invoice.ExpirationTime - now, _Cts.Token);
_WatchRequests.Add(invoiceId);
await Task.Delay(delay, _Cts.Token);
}
Watch(invoiceId);
delay = invoice.MonitoringExpiration - DateTimeOffset.UtcNow;
if (delay > TimeSpan.Zero)
{
await Task.Delay(delay, _Cts.Token);
}
Watch(invoiceId);
}
catch when (_Cts.IsCancellationRequested)
{ }
}
BlockingCollection<string> _WatchRequests = new BlockingCollection<string>(new ConcurrentQueue<string>());
Task _Poller;
Task _Loop;
Task _WaitingInvoices;
CancellationTokenSource _Cts;
public Task StartAsync(CancellationToken cancellationToken)
{
_Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
_Poller = StartPoller(_Cts.Token);
_Loop = StartLoop(_Cts.Token);
_WaitingInvoices = WaitPendingInvoices();
leases.Add(_EventAggregator.Subscribe<Events.NewBlockEvent>(async b => { await NotifyBlock(); }));
leases.Add(_EventAggregator.Subscribe<Events.TxOutReceivedEvent>(async b => { await NotifyReceived(b); }));
leases.Add(_EventAggregator.Subscribe<Events.InvoiceNeedUpdateEvent>(b =>
{
Watch(b.InvoiceId);
}));
leases.Add(_EventAggregator.Subscribe<Events.InvoiceEvent>(async b =>
{
if (b.Name == "invoice_created")
{
await Watch(b.InvoiceId);
Watch(b.InvoiceId);
await Wait(b.InvoiceId);
}
if (b.Name == "invoice_receivedPayment")
{
Watch(b.InvoiceId);
}
}));
return Task.CompletedTask;
}
private async Task StartPoller(CancellationToken cancellation)
private async Task WaitPendingInvoices()
{
try
{
while (!cancellation.IsCancellationRequested)
{
try
{
foreach (var pending in await _InvoiceRepository.GetPendingInvoices())
{
_WatchRequests.Add(pending);
}
await Task.Delay(PollInterval, cancellation);
}
catch (Exception ex) when (!cancellation.IsCancellationRequested)
{
Logs.PayServer.LogError(ex, $"Unhandled exception in InvoiceWatcher poller");
await Task.Delay(PollInterval, cancellation);
}
}
}
catch when (cancellation.IsCancellationRequested) { }
await Task.WhenAll((await _InvoiceRepository.GetPendingInvoices())
.Select(id => Wait(id)).ToArray());
_WaitingInvoices = null;
}
async Task StartLoop(CancellationToken cancellation)
{
Logs.PayServer.LogInformation("Start watching invoices");
await Task.Delay(1).ConfigureAwait(false); // Small hack so that the caller does not block on GetConsumingEnumerable
ConcurrentDictionary<string, Lazy<Task>> executing = new ConcurrentDictionary<string, Lazy<Task>>();
try
{
// This loop just make sure an invoice will not be updated at the same time by two tasks.
// If an update is happening while a request come, then the update is deferred when the executing task is over
foreach (var item in _WatchRequests.GetConsumingEnumerable(cancellation))
foreach (var invoiceId in _WatchRequests.GetConsumingEnumerable(cancellation))
{
var localItem = item;
var toExecute = new Lazy<Task>(async () =>
int maxLoop = 5;
int loopCount = -1;
while (!cancellation.IsCancellationRequested && loopCount < maxLoop)
{
loopCount++;
try
{
await UpdateInvoice(localItem, cancellation);
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, true);
if (invoice == null)
break;
var updateContext = new UpdateInvoiceContext(invoice);
await UpdateInvoice(updateContext);
if (updateContext.Dirty)
{
await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.Status, invoice.ExceptionStatus);
updateContext.Events.Add(new InvoiceDataChangedEvent(invoice));
}
foreach (var evt in updateContext.Events)
{
_EventAggregator.Publish(evt, evt.GetType());
}
if (invoice.Status == "complete" ||
((invoice.Status == "invalid" || invoice.Status == "expired") && invoice.MonitoringExpiration < DateTimeOffset.UtcNow))
{
if (await _InvoiceRepository.RemovePendingInvoice(invoice.Id))
_EventAggregator.Publish(new InvoiceStopWatchedEvent(invoice.Id));
break;
}
if (updateContext.Events.Count == 0 || cancellation.IsCancellationRequested)
break;
}
finally
catch (OperationCanceledException) when (cancellation.IsCancellationRequested)
{
executing.TryRemove(localItem, out Lazy<Task> unused);
break;
}
catch (Exception ex)
{
Logs.PayServer.LogError(ex, "Unhandled error on watching invoice " + invoiceId);
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
Task.Delay(10000, cancellation)
.ContinueWith(t => _WatchRequests.Add(invoiceId), TaskScheduler.Default);
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
break;
}
}, false);
var executingTask = executing.GetOrAdd(item, toExecute);
executingTask.Value.GetAwaiter(); // Make sure it run
if (executingTask != toExecute)
{
// What was planned can't run for now, rebook it when the executingTask finish
var unused = executingTask.Value.ContinueWith(t => _WatchRequests.Add(localItem));
}
}
}
catch when (cancellation.IsCancellationRequested)
{
}
finally
{
await Task.WhenAll(executing.Values.Select(v => v.Value).ToArray());
}
Logs.PayServer.LogInformation("Stop watching invoices");
}
@ -515,7 +334,8 @@ namespace BTCPayServer.HostedServices
{
leases.Dispose();
_Cts.Cancel();
return Task.WhenAll(_Poller, _Loop);
var waitingPendingInvoices = _WaitingInvoices ?? Task.CompletedTask;
return Task.WhenAll(waitingPendingInvoices, _Loop);
}
}
}

@ -1,209 +0,0 @@
using System;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Logging;
using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using NBXplorer;
using System.Collections.Concurrent;
using NBXplorer.DerivationStrategy;
using BTCPayServer.Events;
using BTCPayServer.Services;
namespace BTCPayServer.HostedServices
{
public class NBXplorerListener : IHostedService
{
EventAggregator _Aggregator;
ExplorerClientProvider _ExplorerClients;
IApplicationLifetime _Lifetime;
InvoiceRepository _InvoiceRepository;
private TaskCompletionSource<bool> _RunningTask;
private CancellationTokenSource _Cts;
NBXplorerDashboard _Dashboards;
public NBXplorerListener(ExplorerClientProvider explorerClients,
NBXplorerDashboard dashboard,
InvoiceRepository invoiceRepository,
EventAggregator aggregator, IApplicationLifetime lifetime)
{
PollInterval = TimeSpan.FromMinutes(1.0);
_Dashboards = dashboard;
_InvoiceRepository = invoiceRepository;
_ExplorerClients = explorerClients;
_Aggregator = aggregator;
_Lifetime = lifetime;
}
CompositeDisposable leases = new CompositeDisposable();
ConcurrentDictionary<string, NotificationSession> _Sessions = new ConcurrentDictionary<string, NotificationSession>();
private Timer _ListenPoller;
TimeSpan _PollInterval;
public TimeSpan PollInterval
{
get
{
return _PollInterval;
}
set
{
_PollInterval = value;
if (_ListenPoller != null)
{
_ListenPoller.Change(0, (int)value.TotalMilliseconds);
}
}
}
public Task StartAsync(CancellationToken cancellationToken)
{
_RunningTask = new TaskCompletionSource<bool>();
_Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
leases.Add(_Aggregator.Subscribe<Events.NBXplorerStateChangedEvent>(async nbxplorerEvent =>
{
if (nbxplorerEvent.NewState == NBXplorerState.Ready)
{
await Listen(nbxplorerEvent.Network);
}
}));
_ListenPoller = new Timer(async s =>
{
foreach (var nbxplorerState in _Dashboards.GetAll())
{
if (nbxplorerState.Status != null && nbxplorerState.Status.IsFullySynched)
{
await Listen(nbxplorerState.Network);
}
}
}, null, 0, (int)PollInterval.TotalMilliseconds);
leases.Add(_ListenPoller);
leases.Add(_Aggregator.Subscribe<Events.InvoiceEvent>(async inv =>
{
if (inv.Name == "invoice_created")
{
var invoice = await _InvoiceRepository.GetInvoice(null, inv.InvoiceId);
List<Task> listeningDerivations = new List<Task>();
foreach (var notificationSessions in _Sessions)
{
var derivationStrategy = GetStrategy(notificationSessions.Key, invoice);
if (derivationStrategy != null)
{
listeningDerivations.Add(notificationSessions.Value.ListenDerivationSchemesAsync(new[] { derivationStrategy }, _Cts.Token));
}
}
await Task.WhenAll(listeningDerivations.ToArray()).ConfigureAwait(false);
}
}));
return Task.CompletedTask;
}
private async Task Listen(BTCPayNetwork network)
{
bool cleanup = false;
try
{
if (_Sessions.ContainsKey(network.CryptoCode))
return;
var client = _ExplorerClients.GetExplorerClient(network);
if (client == null)
return;
if (_Cts.IsCancellationRequested)
return;
var session = await client.CreateNotificationSessionAsync(_Cts.Token).ConfigureAwait(false);
if (!_Sessions.TryAdd(network.CryptoCode, session))
{
await session.DisposeAsync();
return;
}
cleanup = true;
using (session)
{
await session.ListenNewBlockAsync(_Cts.Token).ConfigureAwait(false);
await session.ListenDerivationSchemesAsync((await GetStrategies(network)).ToArray(), _Cts.Token).ConfigureAwait(false);
Logs.PayServer.LogInformation($"Connected to WebSocket of NBXplorer ({network.CryptoCode})");
while (!_Cts.IsCancellationRequested)
{
var newEvent = await session.NextEventAsync(_Cts.Token).ConfigureAwait(false);
switch (newEvent)
{
case NBXplorer.Models.NewBlockEvent evt:
_Aggregator.Publish(new Events.NewBlockEvent() { CryptoCode = evt.CryptoCode });
break;
case NBXplorer.Models.NewTransactionEvent evt:
foreach (var txout in evt.Outputs)
{
_Aggregator.Publish(new Events.TxOutReceivedEvent()
{
Network = network,
ScriptPubKey = txout.ScriptPubKey,
DerivationStrategy = txout.DerivationStrategy
});
}
break;
default:
Logs.PayServer.LogWarning("Received unknown message from NBXplorer");
break;
}
}
}
}
catch when (_Cts.IsCancellationRequested) { }
catch (Exception ex)
{
Logs.PayServer.LogError(ex, $"Error while connecting to WebSocket of NBXplorer ({network.CryptoCode})");
}
finally
{
if (cleanup)
{
Logs.PayServer.LogInformation($"Disconnected from WebSocket of NBXplorer ({network.CryptoCode})");
_Sessions.TryRemove(network.CryptoCode, out NotificationSession unused);
if (_Sessions.Count == 0 && _Cts.IsCancellationRequested)
{
_RunningTask.TrySetResult(true);
}
}
}
}
private async Task<List<DerivationStrategyBase>> GetStrategies(BTCPayNetwork network)
{
List<DerivationStrategyBase> strategies = new List<DerivationStrategyBase>();
foreach (var invoiceId in await _InvoiceRepository.GetPendingInvoices())
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
var strategy = GetStrategy(network.CryptoCode, invoice);
if (strategy != null)
strategies.Add(strategy);
}
return strategies;
}
private DerivationStrategyBase GetStrategy(string cryptoCode, InvoiceEntity invoice)
{
foreach (var derivationStrategy in invoice.GetDerivationStrategies(_ExplorerClients.NetworkProviders))
{
if (derivationStrategy.Network.CryptoCode == cryptoCode)
{
return derivationStrategy.DerivationStrategyBase;
}
}
return null;
}
public Task StopAsync(CancellationToken cancellationToken)
{
leases.Dispose();
_Cts.Cancel();
return Task.WhenAny(_RunningTask.Task, Task.Delay(-1, cancellationToken));
}
}
}

@ -41,9 +41,11 @@ namespace BTCPayServer.HostedServices
return _Summaries.All(s => s.Value.Status != null && s.Value.Status.IsFullySynched);
}
public bool IsFullySynched(string cryptoCode)
public bool IsFullySynched(string cryptoCode, out NBXplorerSummary summary)
{
return _Summaries.Any(s => s.Key.Equals(cryptoCode, StringComparison.OrdinalIgnoreCase) && s.Value.Status != null && s.Value.Status.IsFullySynched);
return _Summaries.TryGetValue(cryptoCode, out summary) &&
summary.Status != null &&
summary.Status.IsFullySynched;
}
public IEnumerable<NBXplorerSummary> GetAll()
@ -182,8 +184,8 @@ namespace BTCPayServer.HostedServices
if(status != null && error == null)
{
if(status.ChainType != _Network.NBXplorerNetwork.DefaultSettings.ChainType)
error = $"{_Network.CryptoCode}: NBXplorer is on a different ChainType (actual: {status.ChainType}, expected: {_Network.NBXplorerNetwork.DefaultSettings.ChainType})";
if(status.NetworkType != _Network.NBitcoinNetwork.NetworkType)
error = $"{_Network.CryptoCode}: NBXplorer is on a different ChainType (actual: {status.NetworkType}, expected: {_Network.NBitcoinNetwork.NetworkType})";
}
if (error != null)
@ -193,6 +195,7 @@ namespace BTCPayServer.HostedServices
_Aggregator.Publish(new NBXplorerErrorEvent(_Network, error));
}
_Dashboard.Publish(_Network, State, status, error);
if (oldState != State)
{
if (State == NBXplorerState.Synching)
@ -205,7 +208,6 @@ namespace BTCPayServer.HostedServices
}
_Aggregator.Publish(new NBXplorerStateChangedEvent(_Network, oldState, State));
}
_Dashboard.Publish(_Network, State, status, error);
return oldState != State;
}

@ -0,0 +1,67 @@
using System;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Services;
using BTCPayServer.Services.Rates;
using Microsoft.Extensions.Hosting;
using BTCPayServer.Logging;
using System.Runtime.CompilerServices;
using System.IO;
using System.Text;
namespace BTCPayServer.HostedServices
{
public class RatesHostedService : BaseAsyncService
{
private SettingsRepository _SettingsRepository;
private IRateProviderFactory _RateProviderFactory;
private CoinAverageSettings _coinAverageSettings;
public RatesHostedService(SettingsRepository repo,
CoinAverageSettings coinAverageSettings,
IRateProviderFactory rateProviderFactory)
{
this._SettingsRepository = repo;
_RateProviderFactory = rateProviderFactory;
_coinAverageSettings = coinAverageSettings;
}
internal override Task[] InitializeTasks()
{
return new[]
{
CreateLoopTask(RefreshCoinAverageSupportedExchanges),
CreateLoopTask(RefreshCoinAverageSettings)
};
}
async Task RefreshCoinAverageSupportedExchanges()
{
await new SynchronizationContextRemover();
var tickers = await new CoinAverageRateProvider("BTC") { Authenticator = _coinAverageSettings }.GetExchangeTickersAsync();
_coinAverageSettings.AvailableExchanges = tickers
.Exchanges
.Select(c => (c.DisplayName, c.Name))
.ToArray();
await Task.Delay(TimeSpan.FromHours(5), Cancellation);
}
async Task RefreshCoinAverageSettings()
{
await new SynchronizationContextRemover();
var rates = (await _SettingsRepository.GetSettingAsync<RatesSetting>()) ?? new RatesSetting();
_RateProviderFactory.CacheSpan = TimeSpan.FromMinutes(rates.CacheInMinutes);
if (!string.IsNullOrWhiteSpace(rates.PrivateKey) && !string.IsNullOrWhiteSpace(rates.PublicKey))
{
_coinAverageSettings.KeyPair = (rates.PublicKey, rates.PrivateKey);
}
else
{
_coinAverageSettings.KeyPair = null;
}
await _SettingsRepository.WaitSettingsChanged<RatesSetting>(Cancellation);
}
}
}

@ -37,6 +37,7 @@ using BTCPayServer.Authentication;
using Microsoft.Extensions.Caching.Memory;
using BTCPayServer.Logging;
using BTCPayServer.HostedServices;
using Meziantou.AspNetCore.BundleTagHelpers;
namespace BTCPayServer.Hosting
{
@ -108,6 +109,8 @@ namespace BTCPayServer.Hosting
services.AddSingleton<BTCPayServerEnvironment>();
services.TryAddSingleton<TokenRepository>();
services.TryAddSingleton<EventAggregator>();
services.TryAddSingleton<CoinAverageSettings>();
services.TryAddSingleton<ICoinAverageAuthenticator, CoinAverageSettingsAuthenticator>();
services.TryAddSingleton<ApplicationDbContextFactory>(o =>
{
var opts = o.GetRequiredService<BTCPayServerOptions>();
@ -125,13 +128,15 @@ namespace BTCPayServer.Hosting
}
return dbContext;
});
services.TryAddSingleton<Payments.Lightning.LightningClientFactory>();
services.TryAddSingleton<BTCPayNetworkProvider>(o =>
{
var opts = o.GetRequiredService<BTCPayServerOptions>();
return new BTCPayNetworkProvider(opts.ChainType);
return opts.NetworkProvider;
});
services.TryAddSingleton<LanguageService>();
services.TryAddSingleton<NBXplorerDashboard>();
services.TryAddSingleton<StoreRepository>();
services.TryAddSingleton<BTCPayWalletProvider>();
@ -142,20 +147,29 @@ namespace BTCPayServer.Hosting
BlockTarget = 20
});
services.AddSingleton<CssThemeManager>();
services.AddSingleton<IHostedService, CssThemeManagerHostedService>();
services.AddSingleton<Payments.IPaymentMethodHandler<DerivationStrategy>, Payments.Bitcoin.BitcoinLikePaymentHandler>();
services.AddSingleton<IHostedService, Payments.Bitcoin.NBXplorerListener>();
services.AddSingleton<Payments.IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>, Payments.Lightning.LightningLikePaymentHandler>();
services.AddSingleton<IHostedService, Payments.Lightning.LightningListener>();
services.AddSingleton<IHostedService, NBXplorerWaiters>();
services.AddSingleton<IHostedService, NBXplorerListener>();
services.AddSingleton<IHostedService, InvoiceNotificationManager>();
services.AddSingleton<IHostedService, InvoiceWatcher>();
services.AddSingleton<IHostedService, RatesHostedService>();
services.TryAddSingleton<ExplorerClientProvider>();
services.TryAddSingleton<Bitpay>(o =>
{
if (o.GetRequiredService<BTCPayServerOptions>().ChainType == ChainType.Main)
if (o.GetRequiredService<BTCPayServerOptions>().NetworkType == NetworkType.Mainnet)
return new Bitpay(new Key(), new Uri("https://bitpay.com/"));
else
return new Bitpay(new Key(), new Uri("https://test.bitpay.com/"));
});
services.TryAddSingleton<IRateProviderFactory, CachedDefaultRateProviderFactory>();
services.TryAddSingleton<IRateProviderFactory, BTCPayRateProviderFactory>();
services.TryAddScoped<IHttpContextAccessor, HttpContextAccessor>();
services.TryAddSingleton<IAuthorizationHandler, OwnStoreHandler>();
@ -166,17 +180,29 @@ namespace BTCPayServer.Hosting
services.AddAuthorization(o =>
{
o.AddPolicy("CanAccessStore", builder =>
o.AddPolicy(StorePolicies.CanAccessStores, builder =>
{
builder.AddRequirements(new OwnStoreAuthorizationRequirement());
});
o.AddPolicy("OwnStore", builder =>
o.AddPolicy(StorePolicies.OwnStore, builder =>
{
builder.AddRequirements(new OwnStoreAuthorizationRequirement("Owner"));
builder.AddRequirements(new OwnStoreAuthorizationRequirement(StoreRoles.Owner));
});
});
// bundling
services.AddBundles();
services.AddTransient<BundleOptions>(provider =>
{
var opts = provider.GetRequiredService<BTCPayServerOptions>();
var bundle = new BundleOptions();
bundle.UseMinifiedFiles = opts.BundleJsCss;
bundle.AppendVersion = true;
return bundle;
});
return services;
}
@ -192,7 +218,7 @@ namespace BTCPayServer.Hosting
}
app.UseMiddleware<BTCPayMiddleware>();
return app;
return app;
}
static void Retry(Action act)

@ -22,20 +22,29 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Http.Extensions;
using BTCPayServer.Controllers;
using System.Net.WebSockets;
using System.Security.Claims;
using BTCPayServer.Services;
using NBitpayClient;
using Newtonsoft.Json.Linq;
using BTCPayServer.Services.Stores;
namespace BTCPayServer.Hosting
{
public class BTCPayMiddleware
{
TokenRepository _TokenRepository;
StoreRepository _StoreRepository;
RequestDelegate _Next;
BTCPayServerOptions _Options;
public BTCPayMiddleware(RequestDelegate next,
TokenRepository tokenRepo,
StoreRepository storeRepo,
BTCPayServerOptions options)
{
_TokenRepository = tokenRepo ?? throw new ArgumentNullException(nameof(tokenRepo));
_StoreRepository = storeRepo;
_Next = next ?? throw new ArgumentNullException(nameof(next));
_Options = options ?? throw new ArgumentNullException(nameof(options));
}
@ -44,44 +53,52 @@ namespace BTCPayServer.Hosting
public async Task Invoke(HttpContext httpContext)
{
RewriteHostIfNeeded(httpContext);
httpContext.Request.Headers.TryGetValue("x-signature", out StringValues values);
var sig = values.FirstOrDefault();
httpContext.Request.Headers.TryGetValue("x-identity", out values);
var id = values.FirstOrDefault();
if (!string.IsNullOrEmpty(sig) && !string.IsNullOrEmpty(id))
{
httpContext.Request.EnableRewind();
string body = string.Empty;
if (httpContext.Request.ContentLength != 0 && httpContext.Request.Body != null)
{
using (StreamReader reader = new StreamReader(httpContext.Request.Body, Encoding.UTF8, true, 1024, true))
{
body = reader.ReadToEnd();
}
httpContext.Request.Body.Position = 0;
}
var url = httpContext.Request.GetEncodedUrl();
try
{
var key = new PubKey(id);
if (BitIdExtensions.CheckBitIDSignature(key, sig, url, body))
{
var bitid = new BitIdentity(key);
httpContext.User = new GenericPrincipal(bitid, new string[0]);
Logs.PayServer.LogDebug($"BitId signature check success for SIN {bitid.SIN}");
}
}
catch (FormatException) { }
if (!(httpContext.User.Identity is BitIdentity))
Logs.PayServer.LogDebug("BitId signature check failed");
}
try
{
var bitpayAuth = GetBitpayAuth(httpContext, out bool isBitpayAuth);
var isBitpayAPI = IsBitpayAPI(httpContext, isBitpayAuth);
httpContext.SetIsBitpayAPI(isBitpayAPI);
if (isBitpayAPI)
{
string storeId = null;
var failedAuth = false;
if (!string.IsNullOrEmpty(bitpayAuth.Signature) && !string.IsNullOrEmpty(bitpayAuth.Id))
{
storeId = await CheckBitId(httpContext, bitpayAuth.Signature, bitpayAuth.Id);
if (!httpContext.User.Claims.Any(c => c.Type == Claims.SIN))
{
Logs.PayServer.LogDebug("BitId signature check failed");
failedAuth = true;
}
}
else if (!string.IsNullOrEmpty(bitpayAuth.Authorization))
{
storeId = await CheckLegacyAPIKey(httpContext, bitpayAuth.Authorization);
if (storeId == null)
{
Logs.PayServer.LogDebug("API key check failed");
failedAuth = true;
}
}
if (storeId != null)
{
var identity = ((ClaimsIdentity)httpContext.User.Identity);
identity.AddClaim(new Claim(Claims.OwnStore, storeId));
var store = await _StoreRepository.FindStore(storeId);
httpContext.SetStoreData(store);
}
else if (failedAuth)
{
throw new BitpayHttpException(401, "Can't access to store");
}
}
await _Next(httpContext);
}
catch (WebSocketException)
{ }
catch (UnauthorizedAccessException ex)
{
await HandleBitpayHttpException(httpContext, new BitpayHttpException(401, ex.Message));
@ -97,6 +114,55 @@ namespace BTCPayServer.Hosting
}
}
private static (string Signature, String Id, String Authorization) GetBitpayAuth(HttpContext httpContext, out bool hasBitpayAuth)
{
httpContext.Request.Headers.TryGetValue("x-signature", out StringValues values);
var sig = values.FirstOrDefault();
httpContext.Request.Headers.TryGetValue("x-identity", out values);
var id = values.FirstOrDefault();
httpContext.Request.Headers.TryGetValue("Authorization", out values);
var auth = values.FirstOrDefault();
hasBitpayAuth = auth != null || (sig != null && id != null);
return (sig, id, auth);
}
private bool IsBitpayAPI(HttpContext httpContext, bool bitpayAuth)
{
if (!httpContext.Request.Path.HasValue)
return false;
var path = httpContext.Request.Path.Value;
if (
bitpayAuth &&
path == "/invoices" &&
httpContext.Request.Method == "POST" &&
(httpContext.Request.ContentType ?? string.Empty).StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
return true;
if (
bitpayAuth &&
path == "/invoices" &&
httpContext.Request.Method == "GET")
return true;
if (
bitpayAuth &&
path.StartsWith("/invoices/", StringComparison.OrdinalIgnoreCase) &&
httpContext.Request.Method == "GET")
return true;
if (path.Equals("/rates", StringComparison.OrdinalIgnoreCase) &&
httpContext.Request.Method == "GET")
return true;
if (
path.Equals("/tokens", StringComparison.Ordinal) &&
( httpContext.Request.Method == "GET" || httpContext.Request.Method == "POST"))
return true;
return false;
}
private void RewriteHostIfNeeded(HttpContext httpContext)
{
string reverseProxyScheme = null;
@ -129,7 +195,7 @@ namespace BTCPayServer.Hosting
httpContext.Request.Scheme = reverseProxyScheme;
}
else
{
{
httpContext.Request.Scheme = _Options.ExternalUrl.Scheme;
}
if (_Options.ExternalUrl.IsDefaultPort)
@ -189,5 +255,109 @@ namespace BTCPayServer.Hosting
await writer.FlushAsync();
}
}
private async Task<string> CheckBitId(HttpContext httpContext, string sig, string id)
{
httpContext.Request.EnableRewind();
string storeId = null;
string body = string.Empty;
if (httpContext.Request.ContentLength != 0 && httpContext.Request.Body != null)
{
using (StreamReader reader = new StreamReader(httpContext.Request.Body, Encoding.UTF8, true, 1024, true))
{
body = reader.ReadToEnd();
}
httpContext.Request.Body.Position = 0;
}
var url = httpContext.Request.GetEncodedUrl();
try
{
var key = new PubKey(id);
if (BitIdExtensions.CheckBitIDSignature(key, sig, url, body))
{
var sin = key.GetBitIDSIN();
var identity = ((ClaimsIdentity)httpContext.User.Identity);
identity.AddClaim(new Claim(Claims.SIN, sin));
string token = null;
if (httpContext.Request.Query.TryGetValue("token", out var tokenValues))
{
token = tokenValues[0];
}
if (token == null && !String.IsNullOrEmpty(body) && httpContext.Request.Method == "POST")
{
try
{
token = JObject.Parse(body)?.Property("token")?.Value?.Value<string>();
}
catch { }
}
if (token != null)
{
var bitToken = await GetTokenPermissionAsync(sin, token);
if (bitToken == null)
{
return null;
}
storeId = bitToken.StoreId;
}
}
}
catch (FormatException) { }
return storeId;
}
private async Task<string> CheckLegacyAPIKey(HttpContext httpContext, string auth)
{
var splitted = auth.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (splitted.Length != 2 || !splitted[0].Equals("Basic", StringComparison.OrdinalIgnoreCase))
{
return null;
}
string apiKey = null;
try
{
apiKey = Encoders.ASCII.EncodeData(Encoders.Base64.DecodeData(splitted[1]));
}
catch
{
return null;
}
return await _TokenRepository.GetStoreIdFromAPIKey(apiKey);
}
private async Task<BitTokenEntity> GetTokenPermissionAsync(string sin, string expectedToken)
{
var actualTokens = (await _TokenRepository.GetTokens(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.Merchant} for SIN {sin}");
return null;
}
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;
}
}
}

@ -39,6 +39,7 @@ using Microsoft.ApplicationInsights.AspNetCore.Extensions;
using Microsoft.AspNetCore.Mvc.Cors.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using System.Net;
using Meziantou.AspNetCore.BundleTagHelpers;
namespace BTCPayServer.Hosting
{
@ -75,8 +76,6 @@ namespace BTCPayServer.Hosting
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
// Big hack, tests fails because Hangfire fail at initializing at the second test run
AddHangfireFix(services);
services.AddBTCPayServer();
services.AddMvc(o =>
{
@ -92,6 +91,24 @@ namespace BTCPayServer.Hosting
options.Password.RequireUppercase = false;
});
services.AddHangfire((o) =>
{
var scope = AspNetCoreJobActivator.Current.BeginScope(null);
var options = (ApplicationDbContextFactory)scope.Resolve(typeof(ApplicationDbContextFactory));
options.ConfigureHangfireBuilder(o);
});
services.AddCors(o =>
{
o.AddPolicy("BitpayAPI", b =>
{
b.AllowAnyMethod().AllowAnyHeader().AllowAnyOrigin();
});
});
services.Configure<IOptions<ApplicationInsightsServiceOptions>>(o =>
{
o.Value.DeveloperMode = _Env.IsDevelopment();
});
// Needed to debug U2F for ledger support
//services.Configure<KestrelServerOptions>(kestrel =>
//{
@ -102,51 +119,29 @@ namespace BTCPayServer.Hosting
//});
}
// Big hack, tests fails if only call AddHangfire because Hangfire fail at initializing at the second test run
private void AddHangfireFix(IServiceCollection services)
{
Action<IGlobalConfiguration> configuration = o =>
{
var scope = AspNetCoreJobActivator.Current.BeginScope(null);
var options = (ApplicationDbContextFactory)scope.Resolve(typeof(ApplicationDbContextFactory));
options.ConfigureHangfireBuilder(o);
};
ServiceCollectionDescriptorExtensions.TryAddSingleton<Action<IGlobalConfiguration>>(services, (IServiceProvider serviceProvider) => new Action<IGlobalConfiguration>((config) =>
{
ILoggerFactory service = ServiceProviderServiceExtensions.GetService<ILoggerFactory>(serviceProvider);
if (service != null)
{
Hangfire.GlobalConfigurationExtensions.UseLogProvider<AspNetCoreLogProvider>(config, new AspNetCoreLogProvider(service));
}
IServiceScopeFactory service2 = ServiceProviderServiceExtensions.GetService<IServiceScopeFactory>(serviceProvider);
if (service2 != null)
{
Hangfire.GlobalConfigurationExtensions.UseActivator<AspNetCoreJobActivator>(config, new AspNetCoreJobActivator(service2));
}
configuration(config);
}));
services.AddHangfire(configuration);
services.AddCors(o =>
{
o.AddPolicy("BitpayAPI", b =>
{
b.AllowAnyMethod().AllowAnyHeader().AllowAnyOrigin();
});
});
services.Configure<IOptions<ApplicationInsightsServiceOptions>>(o =>
{
o.Value.DeveloperMode = _Env.IsDevelopment();
});
}
public void Configure(
IApplicationBuilder app,
IHostingEnvironment env,
IServiceProvider prov,
BTCPayServerOptions options,
ILoggerFactory loggerFactory)
{
Logs.Configure(loggerFactory);
Logs.Configuration.LogInformation($"Root Path: {options.RootPath}");
if (options.RootPath.Equals("/", StringComparison.OrdinalIgnoreCase))
{
ConfigureCore(app, env, prov, loggerFactory, options);
}
else
{
app.Map(options.RootPath, appChild =>
{
ConfigureCore(appChild, env, prov, loggerFactory, options);
});
}
}
private static void ConfigureCore(IApplicationBuilder app, IHostingEnvironment env, IServiceProvider prov, ILoggerFactory loggerFactory, BTCPayServerOptions options)
{
if (env.IsDevelopment())
{
@ -154,9 +149,6 @@ namespace BTCPayServer.Hosting
app.UseBrowserLink();
}
Logs.Configure(loggerFactory);
//App insight do not that by itself...
loggerFactory.AddApplicationInsights(prov, LogLevel.Information);
@ -164,7 +156,11 @@ namespace BTCPayServer.Hosting
app.UseStaticFiles();
app.UseAuthentication();
app.UseHangfireServer();
app.UseHangfireDashboard("/hangfire", new DashboardOptions() { Authorization = new[] { new NeedRole(Roles.ServerAdmin) } });
app.UseHangfireDashboard("/hangfire", new DashboardOptions()
{
AppPath = options.GetRootUri(),
Authorization = new[] { new NeedRole(Roles.ServerAdmin) }
});
app.UseWebSockets();
app.UseMvc(routes =>
{

@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Reflection;
using Newtonsoft.Json;
using NBitcoin.JsonConverters;
namespace BTCPayServer.JsonConverters
{
public class CurrencyValueJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(CurrencyValue).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
try
{
return reader.TokenType == JsonToken.Null ? null :
CurrencyValue.TryParse((string)reader.Value, out var result) ? result :
throw new JsonObjectException("Invalid Currency value", reader);
}
catch (InvalidCastException)
{
throw new JsonObjectException("Invalid Currency value", reader);
}
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if(value != null)
writer.WriteValue(value.ToString());
}
}
}

@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json;
using System.Reflection;
using BTCPayServer.Payments.Lightning;
using NBitcoin.JsonConverters;
using System.Globalization;
namespace BTCPayServer.JsonConverters
{
public class LightMoneyJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(LightMoneyJsonConverter).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
}
Type longType = typeof(long).GetTypeInfo();
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
try
{
return reader.TokenType == JsonToken.Null ? null :
reader.TokenType == JsonToken.Integer ?
longType.IsAssignableFrom(reader.ValueType) ? new LightMoney((long)reader.Value)
: new LightMoney(long.MaxValue) :
reader.TokenType == JsonToken.String ? new LightMoney(long.Parse((string)reader.Value, CultureInfo.InvariantCulture))
: null;
}
catch (InvalidCastException)
{
throw new JsonObjectException("Money amount should be in millisatoshi", reader);
}
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
writer.WriteValue(((LightMoney)value).MilliSatoshi);
}
}
}

@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Reflection;
using Newtonsoft.Json;
using NBitcoin.JsonConverters;
namespace BTCPayServer.JsonConverters
{
public class UriJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(Uri).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
try
{
return reader.TokenType == JsonToken.Null ? null :
Uri.TryCreate((string)reader.Value, UriKind.Absolute, out var result) ? result :
throw new JsonObjectException("Invalid Currency value", reader);
}
catch (InvalidCastException)
{
throw new JsonObjectException("Invalid Currency value", reader);
}
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value != null)
writer.WriteValue(((Uri)value).AbsoluteUri);
}
}
}

@ -6,6 +6,7 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace BTCPayServer.Logging
@ -333,7 +334,8 @@ namespace BTCPayServer.Logging
_outputTask = Task.Factory.StartNew(
ProcessLogQueue,
this,
TaskCreationOptions.LongRunning);
default(CancellationToken),
TaskCreationOptions.LongRunning, TaskScheduler.Default);
}
public virtual void EnqueueMessage(LogMessageEntry message)

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

@ -0,0 +1,45 @@
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;
namespace BTCPayServer.Migrations
{
public partial class appdata : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Apps",
columns: table => new
{
Id = table.Column<string>(nullable: false),
AppType = table.Column<string>(nullable: true),
Created = table.Column<DateTimeOffset>(nullable: false),
Name = table.Column<string>(nullable: true),
Settings = table.Column<string>(nullable: true),
StoreDataId = table.Column<string>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Apps", x => x.Id);
table.ForeignKey(
name: "FK_Apps_Stores_StoreDataId",
column: x => x.StoreDataId,
principalTable: "Stores",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
name: "IX_Apps_StoreDataId",
table: "Apps",
column: "StoreDataId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Apps");
}
}
}

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

@ -0,0 +1,35 @@
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;
namespace BTCPayServer.Migrations
{
public partial class legacyapikey : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "ApiKeys",
columns: table => new
{
Id = table.Column<string>(maxLength: 50, nullable: false),
StoreId = table.Column<string>(maxLength: 50, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_ApiKeys", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_ApiKeys_StoreId",
table: "ApiKeys",
column: "StoreId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ApiKeys");
}
}
}

@ -18,7 +18,7 @@ namespace BTCPayServer.Migrations
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.0.1-rtm-125");
.HasAnnotation("ProductVersion", "2.0.2-rtm-10011");
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
@ -36,6 +36,44 @@ namespace BTCPayServer.Migrations
b.ToTable("AddressInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasMaxLength(50);
b.Property<string>("StoreId")
.HasMaxLength(50);
b.HasKey("Id");
b.HasIndex("StoreId");
b.ToTable("ApiKeys");
});
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("AppType");
b.Property<DateTimeOffset>("Created");
b.Property<string>("Name");
b.Property<string>("Settings");
b.Property<string>("StoreDataId");
b.HasKey("Id");
b.HasIndex("StoreDataId");
b.ToTable("Apps");
});
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
{
b.Property<string>("InvoiceDataId");
@ -407,6 +445,13 @@ namespace BTCPayServer.Migrations
.HasForeignKey("InvoiceDataId");
});
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("Apps")
.HasForeignKey("StoreDataId");
});
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData")

@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Services.Apps;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace BTCPayServer.Models.AppViewModels
{
public class CreateAppViewModel
{
public CreateAppViewModel()
{
SetApps();
}
class Format
{
public string Name { get; set; }
public string Value { get; set; }
}
[Required]
[MaxLength(50)]
[MinLength(1)]
public string Name { get; set; }
[Display(Name = "Store")]
public string SelectedStore { get; set; }
public void SetStores(StoreData[] stores)
{
var defaultStore = stores[0].Id;
var choices = stores.Select(o => new Format() { Name = o.StoreName, Value = o.Id }).ToArray();
var chosen = choices.FirstOrDefault(f => f.Value == defaultStore) ?? choices.FirstOrDefault();
Stores = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
SelectedStore = chosen.Value;
}
public SelectList Stores { get; set; }
[Display(Name = "App type")]
public string SelectedAppType { get; set; }
public SelectList AppTypes { get; set; }
void SetApps()
{
var defaultAppType = AppType.PointOfSale.ToString();
var choices = typeof(AppType).GetEnumNames().Select(o => new Format() { Name = o, Value = o }).ToArray();
var chosen = choices.FirstOrDefault(f => f.Value == defaultAppType) ?? choices.FirstOrDefault();
AppTypes = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
SelectedAppType = chosen.Value;
}
}
}

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models.AppViewModels
{
public class ListAppsViewModel
{
public class ListAppViewModel
{
public string Id { get; set; }
public string StoreName { get; set; }
public string StoreId { get; set; }
public string AppName { get; set; }
public string AppType { get; set; }
public bool IsOwner { get; set; }
public string UpdateAction { get { return "Update" + AppType; } }
public string ViewAction { get { return "View" + AppType; } }
}
public ListAppViewModel[] Apps { get; set; }
}
}

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models.AppViewModels
{
public class UpdatePointOfSaleViewModel
{
[Required]
[MaxLength(30)]
public string Title { get; set; }
[Required]
[MaxLength(5)]
public string Currency { get; set; }
[Required]
[MaxLength(5000)]
public string Template { get; set; }
[Display(Name = "User can input custom amount")]
public bool ShowCustomAmount { get; set; }
}
}

@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models.AppViewModels
{
public class ViewPointOfSaleViewModel
{
public class Item
{
public class ItemPrice
{
public string Formatted { get; set; }
public decimal Value { get; set; }
}
public string Id { get; set; }
public ItemPrice Price { get; set; }
public string Title { get; set; }
}
public bool ShowCustomAmount { get; set; }
public string Step { get; set; }
public string Title { get; set; }
public Item[] Items { get; set; }
}
}

@ -42,7 +42,6 @@ namespace BTCPayServer.Models
{
//"url":"https://test.bitpay.com/invoice?id=9saCHtp1zyPcNoi3rDdBu8"
[JsonProperty("url")]
[Obsolete("Use CryptoInfo.Url instead")]
public string Url
{
get; set;

@ -12,17 +12,23 @@ namespace BTCPayServer.Models.InvoicingModels
{
public class CryptoPayment
{
public string CryptoCode { get; set; }
public string PaymentMethod { get; set; }
public string Due { get; set; }
public string Paid { get; set; }
public string Address { get; internal set; }
public string Rate { get; internal set; }
public string PaymentUrl { get; internal set; }
}
public class AddressModel
{
public string PaymentMethod { get; set; }
public string Destination { get; set; }
public bool Current { get; set; }
}
public class Payment
{
public string CryptoCode { get; set; }
public int Confirmations
public string Crypto { get; set; }
public string Confirmations
{
get; set;
}
@ -66,7 +72,13 @@ namespace BTCPayServer.Models.InvoicingModels
get; set;
} = new List<CryptoPayment>();
public List<Payment> Payments { get; set; } = new List<Payment>();
public List<Payment> OnChainPayments { get; set; } = new List<Payment>();
public List<OffChainPayment> OffChainPayments { get; set; } = new List<OffChainPayment>();
public class OffChainPayment
{
public string Crypto { get; set; }
public string BOLT11 { get; set; }
}
public string Status
{
@ -126,7 +138,7 @@ namespace BTCPayServer.Models.InvoicingModels
get;
internal set;
}
public HistoricalAddressInvoiceData[] Addresses { get; set; }
public AddressModel[] Addresses { get; set; }
public DateTimeOffset MonitoringDate { get; internal set; }
public List<Data.InvoiceEventData> Events { get; internal set; }
}

@ -33,11 +33,13 @@ namespace BTCPayServer.Models.InvoicingModels
public class InvoiceModel
{
public DateTimeOffset Date
public string Date
{
get; set;
}
public string OrderId { get; set; }
public string RedirectUrl { get; set; }
public string InvoiceId
{
get; set;

@ -9,17 +9,22 @@ namespace BTCPayServer.Models.InvoicingModels
{
public class AvailableCrypto
{
public string CryptoCode { get; set; }
public string PaymentMethodId { get; set; }
public string CryptoImage { get; set; }
public string Link { get; set; }
}
public string CustomCSSLink { get; set; }
public string CustomLogoLink { get; set; }
public string DefaultLang { get; set; }
public List<AvailableCrypto> AvailableCryptos { get; set; } = new List<AvailableCrypto>();
public bool IsLightning { get; set; }
public string CryptoCode { get; set; }
public string ServerUrl { get; set; }
public string InvoiceId { get; set; }
public string BtcAddress { get; set; }
public string BtcDue { get; set; }
public string CustomerEmail { get; set; }
public bool RequiresRefundEmail { get; set; }
public int ExpirationSeconds { get; set; }
public string Status { get; set; }
public string MerchantRefLink { get; set; }
@ -32,13 +37,20 @@ namespace BTCPayServer.Models.InvoicingModels
public string Rate { get; set; }
public string OrderAmount { get; set; }
public string InvoiceBitcoinUrl { get; set; }
public string InvoiceBitcoinUrlQR { get; set; }
public int TxCount { get; set; }
public string BtcPaid { get; set; }
public string StoreEmail { get; set; }
public string OrderId { get; set; }
public string CryptoImage { get; set; }
public string NetworkFeeDescription { get; internal set; }
public decimal NetworkFee { get; set; }
public bool IsMultiCurrency { get; set; }
public int MaxTimeMinutes { get; internal set; }
public string PaymentType { get; internal set; }
public string PaymentMethodId { get; internal set; }
public bool AllowCoinConversion { get; set; }
public string PeerInfo { get; set; }
}
}

@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
namespace BTCPayServer.Models.ServerViewModels
{
public class RatesViewModel
{
[Display(Name = "Bitcoin average api keys")]
public string PublicKey { get; set; }
public string PrivateKey { get; set; }
[Display(Name = "Cache the rates for ... minutes")]
[Range(0, 60)]
public int CacheMinutes { get; set; }
public GetRateLimitsResponse RateLimits { get; internal set; }
}
}

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