Compare commits

...

225 Commits

Author SHA1 Message Date
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
201 changed files with 21852 additions and 29340 deletions
.gitignore
BTCPayServer.Tests
BTCPayServer
BTCPayNetwork.csBTCPayNetworkProvider.Bitcoin.csBTCPayNetworkProvider.Dogecoin.csBTCPayNetworkProvider.Litecoin.csBTCPayNetworkProvider.csBTCPayServer.csproj
Configuration
Controllers
CurrencyValue.cs
Data
DerivationSchemeParser.csExtensions.cs
HostedServices
Hosting
JsonConverters
Migrations
Models
MultiValueDictionary.cs
Payments
SearchString.cs
Services
SynchronizationContextRemover.cs
Views
bundleconfig.json
wwwroot
publish-docker.ps1

2
.gitignore vendored

@ -291,3 +291,5 @@ __pycache__/
# Bundling JS/CSS
BTCPayServer/wwwroot/bundles/*
!BTCPayServer/wwwroot/bundles/.gitignore
.vscode

@ -8,7 +8,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.6.1" />
<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>

@ -47,7 +47,7 @@ namespace BTCPayServer.Tests
}
public Uri LTCNBXplorerUri { get; set; }
public Uri ServerUri
{
get;
@ -65,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);
@ -101,12 +104,15 @@ namespace BTCPayServer.Tests
.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)
@ -120,9 +126,8 @@ namespace BTCPayServer.Tests
.Build();
_Host.Start();
InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository));
((LightningLikePaymentHandler)_Host.Services.GetService(typeof(IPaymentMethodHandler<LightningSupportedPaymentMethod>))).SkipP2PTest = !InContainer;
}
public string HostName
{
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"]

@ -34,22 +34,12 @@ namespace BTCPayServer.Tests
public ServerTester(string scope)
{
_Directory = scope;
}
public bool Dockerized
{
get; set;
}
public void Start()
{
if (Directory.Exists(_Directory))
Utils.DeleteDirectory(_Directory);
if (!Directory.Exists(_Directory))
Directory.CreateDirectory(_Directory);
NetworkProvider = new BTCPayNetworkProvider(ChainType.Regtest);
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);
@ -72,6 +62,15 @@ namespace BTCPayServer.Tests
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
{
get; set;
}
public void Start()
{
PayTester.Start();
}

@ -54,6 +54,11 @@ namespace BTCPayServer.Tests
return CreateStoreAsync().GetAwaiter().GetResult();
}
public T GetController<T>() where T : Controller
{
return parent.PayTester.GetController<T>(UserId);
}
public async Task<StoresController> CreateStoreAsync()
{
var store = parent.PayTester.GetController<UserStoresController>(UserId);
@ -82,7 +87,6 @@ namespace BTCPayServer.Tests
await store.AddDerivationScheme(StoreId, new DerivationSchemeViewModel()
{
DerivationSchemeFormat = "BTCPay",
DerivationScheme = DerivationScheme.ToString(),
Confirmation = true
}, cryptoCode);
@ -128,7 +132,8 @@ namespace BTCPayServer.Tests
{
Url = connectionType == LightningConnectionType.Charge ? parent.MerchantCharge.Client.Uri.AbsoluteUri :
connectionType == LightningConnectionType.CLightning ? parent.MerchantLightningD.Address.AbsoluteUri
: throw new NotSupportedException(connectionType.ToString())
: throw new NotSupportedException(connectionType.ToString()),
SkipPortTest = true
}, "save", "BTC");
if (storeController.ModelState.ErrorCount != 0)
Assert.False(true, storeController.ModelState.FirstOrDefault().Value.Errors[0].ErrorMessage);

@ -30,6 +30,8 @@ using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.HostedServices;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Services.Apps;
namespace BTCPayServer.Tests
{
@ -596,8 +598,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]
@ -614,12 +626,54 @@ namespace BTCPayServer.Tests
}
}
[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();
@ -806,6 +860,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, "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()
{
@ -831,13 +1064,13 @@ namespace BTCPayServer.Tests
{
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();
@ -962,6 +1195,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()
{
@ -983,7 +1236,7 @@ namespace BTCPayServer.Tests
private static bool IsMapped(Invoice invoice, ApplicationDbContext ctx)
{
var h = BitcoinAddress.Create(invoice.BitcoinAddress).ScriptPubKey.Hash.ToString();
var h = BitcoinAddress.Create(invoice.BitcoinAddress, Network.RegTest).ScriptPubKey.Hash.ToString();
return ctx.AddressInvoices.FirstOrDefault(i => i.InvoiceDataId == invoice.Id && i.GetAddress() == h) != null;
}

@ -46,7 +46,7 @@ services:
- lightning-charged
nbxplorer:
image: nicolasdorier/nbxplorer:1.0.1.24
image: nicolasdorier/nbxplorer:1.0.2.0
ports:
- "32838:32838"
expose:
@ -89,7 +89,7 @@ services:
- "bitcoin_datadir:/data"
customer_lightningd:
image: nicolasdorier/clightning
image: nicolasdorier/clightning:0.0.0.3
environment:
EXPOSE_TCP: "true"
LIGHTNINGD_OPT: |
@ -110,11 +110,10 @@ services:
- bitcoind
lightning-charged:
image: shesek/lightning-charge:0.3.5
image: shesek/lightning-charge:0.3.9
environment:
NETWORK: regtest
API_TOKEN: foiewnccewuify
SKIP_BITCOIND: 1
BITCOIND_RPCCONNECT: bitcoind
volumes:
- "bitcoin_datadir:/etc/bitcoin"
@ -130,7 +129,7 @@ services:
- merchant_lightningd
merchant_lightningd:
image: nicolasdorier/clightning
image: nicolasdorier/clightning:0.0.0.5-dev
environment:
EXPOSE_TCP: "true"
LIGHTNINGD_OPT: |
@ -139,6 +138,7 @@ services:
ipaddr=merchant_lightningd
network=regtest
log-level=debug
dev-broadcast-interval=1000
ports:
- "30993:9835" # api port
expose:

@ -14,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
@ -50,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
@ -72,6 +66,12 @@ namespace BTCPayServer
public override string ToString()
{
return CryptoCode;
}
}
internal KeyPath GetRootKeyPath()
{
return new KeyPath(NBitcoinNetwork.Consensus.SupportSegwit ? "49'" : "44'")
.Derive(CoinType);
}
}
}

@ -14,21 +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",
LightningImagePath = "imlegacy/btc-lightning.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NBXplorerNetworkProvider.ChainType),
CoinType = NBXplorerNetworkProvider.ChainType == ChainType.Main ? new KeyPath("0'") : new KeyPath("1'")
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,22 +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",
LightningImagePath = "imlegacy/ltc-lightning.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NBXplorerNetworkProvider.ChainType),
CoinType = NBXplorerNetworkProvider.ChainType == ChainType.Main ? new KeyPath("2'") : new KeyPath("3'")
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("2'") : new KeyPath("1'")
});
}
}

@ -27,8 +27,8 @@ namespace BTCPayServer
BTCPayNetworkProvider(BTCPayNetworkProvider filtered, string[] cryptoCodes)
{
ChainType = filtered.ChainType;
_NBXplorerNetworkProvider = new NBXplorerNetworkProvider(filtered.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)
@ -40,13 +40,14 @@ namespace BTCPayServer
}
}
public ChainType ChainType { get; set; }
public BTCPayNetworkProvider(ChainType chainType)
public NetworkType NetworkType { get; private set; }
public BTCPayNetworkProvider(NetworkType networkType)
{
_NBXplorerNetworkProvider = new NBXplorerNetworkProvider(chainType);
ChainType = chainType;
_NBXplorerNetworkProvider = new NBXplorerNetworkProvider(networkType);
NetworkType = networkType;
InitBitcoin();
InitLitecoin();
InitDogecoin();
}
/// <summary>

@ -2,21 +2,25 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<Version>1.0.1.59</Version>
<Version>1.0.1.92</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>
@ -26,19 +30,19 @@
<EmbeddedResource Include="Currencies.txt" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BuildBundlerMinifier" Version="2.6.362" />
<PackageReference Include="Hangfire" Version="1.6.17" />
<PackageReference Include="BuildBundlerMinifier" Version="2.6.375" />
<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.35" />
<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="Microsoft.NetCore.Analyzers" Version="2.6.0" />
<PackageReference Include="NBitcoin" Version="4.0.0.65" />
<PackageReference Include="NBitcoin" Version="4.1.1" />
<PackageReference Include="NBitpayClient" Version="1.0.0.18" />
<PackageReference Include="DBreeze" Version="1.87.0" />
<PackageReference Include="NBXplorer.Client" Version="1.0.1.16" />
<PackageReference Include="NBXplorer.Client" Version="1.0.2" />
<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" />
@ -49,8 +53,9 @@
</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.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>
@ -60,13 +65,8 @@
</ItemGroup>
<ItemGroup>
<None Include="wwwroot\js\checkout\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" />

@ -23,7 +23,7 @@ namespace BTCPayServer.Configuration
public class BTCPayServerOptions
{
public ChainType ChainType
public NetworkType NetworkType
{
get; set;
}
@ -51,15 +51,15 @@ 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());
NetworkProvider = new BTCPayNetworkProvider(ChainType).Filter(supportedChains.ToArray());
NetworkProvider = new BTCPayNetworkProvider(NetworkType).Filter(supportedChains.ToArray());
foreach (var chain in supportedChains)
{
if (NetworkProvider.GetNetwork(chain) == null)
@ -92,11 +92,15 @@ namespace BTCPayServer.Configuration
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; }
@ -115,5 +119,14 @@ namespace BTCPayServer.Configuration
get;
set;
}
internal string GetRootUri()
{
if (ExternalUrl == null)
return null;
UriBuilder builder = new UriBuilder(ExternalUrl);
builder.Path = RootPath;
return builder.ToString();
}
}
}

@ -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,6 +31,9 @@ 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();
@ -38,8 +41,6 @@ namespace BTCPayServer.Configuration
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);
app.Option("--bundlejscss", $"Bundle javascript and css files for better performance (default: true)", CommandOptionType.SingleValue);
return app;
}
@ -47,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;
@ -68,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)
@ -78,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");
@ -101,7 +103,7 @@ 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}");
@ -115,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);
}
}
}

@ -0,0 +1,198 @@
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";
}
public string Title { get; set; }
public string Currency { get; set; }
public string Template { 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, 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,
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>();
return View(new ViewPointOfSaleViewModel()
{
Title = settings.Title,
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, string choiceKey)
{
var app = await GetApp(appId, AppType.PointOfSale);
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
var choices = Parse(settings.Template, settings.Currency);
var choice = choices.FirstOrDefault(c => c.Id == choiceKey);
if (choice == null)
return NotFound();
var store = await GetStore(app);
var invoice = await _InvoiceController.CreateInvoiceCore(new NBitpayClient.Invoice()
{
ItemDesc = choice.Title,
Currency = settings.Currency,
Price = (double)choice.Price.Value,
}, 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);
}
}
}

@ -87,8 +87,8 @@ namespace BTCPayServer.Controllers
StartDate = dateStart,
OrderId = orderId,
ItemCode = itemCode,
Status = status,
StoreId = store.Id
Status = status == null ? null : new[] { status },
StoreId = new[] { store.Id }
};

@ -21,6 +21,7 @@ using System.Threading;
using BTCPayServer.Events;
using NBXplorer;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
namespace BTCPayServer.Controllers
{
@ -84,54 +85,66 @@ namespace BTCPayServer.Controllers
model.CryptoPayments.Add(cryptoPayment);
}
var payments = invoice
var onChainPayments = invoice
.GetPayments()
.Where(p => p.GetPaymentMethodId().PaymentType == PaymentTypes.BTCLike)
.Select(async payment =>
.Select<PaymentEntity, Task<object>>(async payment =>
{
var paymentData = (Payments.Bitcoin.BitcoinLikePaymentData)payment.GetCryptoPaymentData();
var m = new InvoiceDetailsModel.Payment();
var paymentNetwork = _NetworkProvider.GetNetwork(payment.GetCryptoCode());
m.PaymentMethod = ToString(payment.GetPaymentMethodId());
m.DepositAddress = paymentData.Output.ScriptPubKey.GetDestinationAddress(paymentNetwork.NBitcoinNetwork);
int confirmationCount = 0;
if ( (paymentData.ConfirmationCount < paymentNetwork.MaxTrackedConfirmation && payment.Accounted)
&& (paymentData.Legacy || invoice.MonitoringExpiration < DateTimeOffset.UtcNow)) // The confirmation count in the paymentData is not up to date
var paymentData = payment.GetCryptoPaymentData();
if (paymentData is Payments.Bitcoin.BitcoinLikePaymentData onChainPaymentData)
{
confirmationCount = (await ((ExplorerClientProvider)_ServiceProvider.GetService(typeof(ExplorerClientProvider))).GetExplorerClient(payment.GetCryptoCode())?.GetTransactionAsync(paymentData.Outpoint.Hash))?.Confirmations ?? 0;
paymentData.ConfirmationCount = confirmationCount;
payment.SetCryptoPaymentData(paymentData);
await _InvoiceRepository.UpdatePayments(new List<PaymentEntity> { payment });
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
{
confirmationCount = paymentData.ConfirmationCount;
var lightningPaymentData = (Payments.Lightning.LightningLikePaymentData)paymentData;
return new InvoiceDetailsModel.OffChainPayment()
{
Crypto = paymentNetwork.CryptoCode,
BOLT11 = lightningPaymentData.BOLT11
};
}
if (confirmationCount >= paymentNetwork.MaxTrackedConfirmation)
{
m.Confirmations = "At least " + (paymentNetwork.MaxTrackedConfirmation);
}
else
{
m.Confirmations = confirmationCount.ToString(CultureInfo.InvariantCulture);
}
m.TransactionId = paymentData.Outpoint.Hash.ToString();
m.ReceivedTime = payment.ReceivedTime;
m.TransactionLink = string.Format(CultureInfo.InvariantCulture, paymentNetwork.BlockExplorerLink, m.TransactionId);
m.Replaced = !payment.Accounted;
return m;
})
.ToArray();
await Task.WhenAll(payments);
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.Payments = payments.Select(p => p.GetAwaiter().GetResult()).ToList();
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);
}
@ -208,14 +221,18 @@ namespace BTCPayServer.Controllers
{
CryptoCode = network.CryptoCode,
PaymentMethodId = paymentMethodId.ToString(),
IsLightning = paymentMethodId.PaymentType == PaymentTypes.LightningLike,
ServerUrl = HttpContext.Request.GetAbsoluteRoot(),
OrderId = invoice.OrderId,
InvoiceId = invoice.Id,
DefaultLang = storeBlob.DefaultLang ?? "en",
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,
@ -226,6 +243,7 @@ namespace BTCPayServer.Controllers
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(),
@ -233,7 +251,8 @@ namespace BTCPayServer.Controllers
BtcPaid = accounting.Paid.ToString(),
Status = invoice.Status,
CryptoImage = "/" + GetImage(paymentMethodId, network),
NetworkFeeDescription = $"{accounting.TxRequired} transaction{(accounting.TxRequired > 1 ? "s" : "")} x {paymentMethodDetails.GetTxFee()} {network.CryptoCode}",
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)
@ -246,12 +265,8 @@ namespace BTCPayServer.Controllers
.ToList()
};
var isMultiCurrency = invoice.GetPayments().Select(p => p.GetPaymentMethodId()).Concat(new[] { paymentMethod.GetId() }).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;
}
@ -270,17 +285,6 @@ namespace BTCPayServer.Controllers
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(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();
}
[HttpGet]
[Route("i/{invoiceId}/status")]
[Route("i/{invoiceId}/{paymentMethodId}/status")]
@ -353,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);
@ -363,15 +367,15 @@ 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 = Prettify(invoice.InvoiceTime),
Date = (DateTimeOffset.UtcNow - invoice.InvoiceTime).Prettify() + " ago",
InvoiceId = invoice.Id,
OrderId = invoice.OrderId ?? string.Empty,
RedirectUrl = invoice.RedirectURL ?? string.Empty,
@ -384,30 +388,6 @@ namespace BTCPayServer.Controllers
return View(model);
}
private string Prettify(DateTimeOffset invoiceTime)
{
var ago = DateTime.UtcNow - invoiceTime;
if (ago.TotalMinutes < 1)
{
return $"{(int)ago.TotalSeconds} second{Plural((int)ago.TotalSeconds)} ago";
}
if (ago.TotalHours < 1)
{
return $"{(int)ago.TotalMinutes} minute{Plural((int)ago.TotalMinutes)} ago";
}
if (ago.Days < 1)
{
return $"{(int)ago.TotalHours} hour{Plural((int)ago.TotalHours)} ago";
}
return $"{(int)ago.TotalDays} day{Plural((int)ago.TotalDays)} ago";
}
private string Plural(int totalDays)
{
return totalDays > 1 ? "s" : string.Empty;
}
[HttpGet]
[Route("invoices/create")]
[Authorize(AuthenticationSchemes = "Identity.Application")]
@ -438,14 +418,17 @@ namespace BTCPayServer.Controllers
StatusMessage = null;
if (store.Role != StoreRoles.Owner)
{
StatusMessage = "Error: You need to be owner of this store to create an invoice";
}
if (store.GetSupportedPaymentMethods(_NetworkProvider).Count() == 0)
{
StatusMessage = "Error: You need to configure the derivation scheme in order to create an invoice";
ModelState.AddModelError(nameof(model.StoreId), "You need to be owner of this store to create an invoice");
return View(model);
}
if(StatusMessage != null)
if (store.GetSupportedPaymentMethods(_NetworkProvider).Count() == 0)
{
ModelState.AddModelError(nameof(model.StoreId), "You need to configure the derivation scheme in order to create an invoice");
return View(model);
}
if (StatusMessage != null)
{
return RedirectToAction(nameof(StoresController.UpdateStore), "Stores", new
{

@ -79,33 +79,10 @@ namespace BTCPayServer.Controllers
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl)
{
var supportedPaymentMethods = store.GetSupportedPaymentMethods(_NetworkProvider)
.Select(c =>
(Handler: (IPaymentMethodHandler)_ServiceProvider.GetService(typeof(IPaymentMethodHandler<>).MakeGenericType(c.GetType())),
SupportedPaymentMethod: c,
Network: _NetworkProvider.GetNetwork(c.PaymentId.CryptoCode),
IsAvailable: Task.FromResult(false)))
.Where(c => c.Network != null)
.Select(c =>
{
c.IsAvailable = c.Handler.IsAvailable(c.SupportedPaymentMethod, c.Network);
return c;
})
.ToList();
foreach(var supportedPaymentMethod in supportedPaymentMethods.ToList())
{
if(!await supportedPaymentMethod.IsAvailable)
{
supportedPaymentMethods.Remove(supportedPaymentMethod);
}
}
if (supportedPaymentMethods.Count == 0)
throw new BitpayHttpException(400, "No derivation strategy are available now for this store");
var entity = new InvoiceEntity
{
InvoiceTime = DateTimeOffset.UtcNow
};
entity.SetSupportedPaymentMethods(supportedPaymentMethods.Select(s => s.SupportedPaymentMethod));
var storeBlob = store.GetStoreBlob();
Uri notificationUri = Uri.IsWellFormedUriString(invoice.NotificationURL, UriKind.Absolute) ? new Uri(invoice.NotificationURL, UriKind.Absolute) : null;
@ -133,33 +110,54 @@ namespace BTCPayServer.Controllers
entity.Status = "new";
entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy);
var methods = supportedPaymentMethods
.Select(async o =>
{
var rate = await storeBlob.ApplyRateRules(o.Network, _RateProviders.GetRateProvider(o.Network, false)).GetRateAsync(invoice.Currency);
PaymentMethod paymentMethod = new PaymentMethod();
paymentMethod.ParentEntity = entity;
paymentMethod.SetId(o.SupportedPaymentMethod.PaymentId);
paymentMethod.Rate = rate;
var paymentDetails = await o.Handler.CreatePaymentMethodDetails(o.SupportedPaymentMethod, paymentMethod, o.Network);
if (storeBlob.NetworkFeeDisabled)
paymentDetails.SetNoTxFee();
paymentMethod.SetPaymentMethodDetails(paymentDetails);
#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;
});
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 method in methods)
foreach (var o in supportedPaymentMethods)
{
paymentMethods.Add(await method);
try
{
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()})");
}
}
if (supported.Count == 0)
{
StringBuilder errors = new StringBuilder();
errors.AppendLine("No payment method available for this store");
foreach (var error in paymentMethodErrors)
{
errors.AppendLine(error);
}
throw new BitpayHttpException(400, errors.ToString());
}
entity.SetSupportedPaymentMethods(supported);
entity.SetPaymentMethods(paymentMethods);
#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);
@ -167,7 +165,7 @@ namespace BTCPayServer.Controllers
{
var btc = _NetworkProvider.BTC;
var feeProvider = ((IFeeProviderFactory)_ServiceProvider.GetService(typeof(IFeeProviderFactory))).CreateFeeProvider(btc);
var rateProvider = storeBlob.ApplyRateRules(btc, _RateProviders.GetRateProvider(btc, false));
var rateProvider = _RateProviders.GetRateProvider(btc, storeBlob.GetRateRules());
if (feeProvider != null && rateProvider != null)
{
var gettingFee = feeProvider.GetFeeRateAsync();
@ -177,15 +175,74 @@ namespace BTCPayServer.Controllers
}
#pragma warning restore CS0618
}
entity.SetPaymentMethods(paymentMethods);
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)
{
@ -221,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));

@ -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,88 @@ namespace BTCPayServer.Controllers
{
private UserManager<ApplicationUser> _UserManager;
SettingsRepository _SettingsRepository;
private IRateProviderFactory _RateProviderFactory;
private CssThemeManager _CssThemeManager;
public ServerController(UserManager<ApplicationUser> userManager, SettingsRepository settingsRepository)
public ServerController(UserManager<ApplicationUser> userManager,
IRateProviderFactory rateProviderFactory,
SettingsRepository settingsRepository,
CssThemeManager cssThemeManager)
{
_UserManager = userManager;
_SettingsRepository = settingsRepository;
_RateProviderFactory = rateProviderFactory;
_CssThemeManager = cssThemeManager;
}
[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")]
@ -52,6 +132,7 @@ namespace BTCPayServer.Controllers
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);
}
@ -72,7 +153,7 @@ namespace BTCPayServer.Controllers
var isAdmin = IsAdmin(roles);
bool updated = false;
if(isAdmin != viewModel.IsAdmin)
if (isAdmin != viewModel.IsAdmin)
{
if (viewModel.IsAdmin)
await _UserManager.AddToRoleAsync(user, Roles.ServerAdmin);
@ -80,7 +161,7 @@ namespace BTCPayServer.Controllers
await _UserManager.RemoveFromRoleAsync(user, Roles.ServerAdmin);
updated = true;
}
if(updated)
if (updated)
{
viewModel.StatusMessage = "User successfully updated";
}
@ -138,7 +219,25 @@ 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);
// TODO: remove controller/class-level property and have only reference to
// CssThemeManager here in this method
_CssThemeManager.Update(settings);
TempData["StatusMessage"] = "Theme settings updated successfully";
return View(settings);
}

@ -26,10 +26,16 @@ namespace BTCPayServer.Controllers
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);
}
@ -63,6 +69,7 @@ namespace BTCPayServer.Controllers
{
return NotFound();
}
vm.RootKeyPath = network.GetRootKeyPath();
var wallet = _WalletProvider.GetWallet(network);
if (wallet == null)
{
@ -75,7 +82,7 @@ namespace BTCPayServer.Controllers
{
if (!string.IsNullOrEmpty(vm.DerivationScheme))
{
strategy = ParseDerivationStrategy(vm.DerivationScheme, vm.DerivationSchemeFormat, network);
strategy = ParseDerivationStrategy(vm.DerivationScheme, null, network);
vm.DerivationScheme = strategy.ToString();
}
}
@ -86,8 +93,38 @@ namespace BTCPayServer.Controllers
return View(vm);
}
if (!vm.Confirmation && strategy != null)
return ShowAddresses(vm, strategy);
if (vm.Confirmation || strategy == null)
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
{
@ -105,23 +142,24 @@ namespace BTCPayServer.Controllers
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.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(network.NBitcoinNetwork).ToString()));
}
}
vm.Confirmation = true;
return View(vm);
}
}
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
@ -173,7 +211,7 @@ namespace BTCPayServer.Controllers
{
try
{
destinationAddress = BitcoinAddress.Create(destination);
destinationAddress = BitcoinAddress.Create(destination, network.NBitcoinNetwork);
}
catch { }
if (destinationAddress == null)
@ -219,8 +257,7 @@ namespace BTCPayServer.Controllers
}
if (command == "getxpub")
{
var getxpubResult = await hw.GetExtPubKey(network, account); ;
getxpubResult.CoinType = (int)(getxpubResult.KeyPath.Indexes[1] - 0x80000000);
var getxpubResult = await hw.GetExtPubKey(network, account);
result = getxpubResult;
}
if (command == "getinfo")
@ -240,13 +277,13 @@ namespace BTCPayServer.Controllers
if (command == "sendtoaddress")
{
if(!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
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,

@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Mvc;
using BTCPayServer.Payments.Lightning;
using System.Net;
using BTCPayServer.Data;
using System.Threading;
namespace BTCPayServer.Controllers
{
@ -127,13 +128,20 @@ namespace BTCPayServer.Controllers
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);
}
}

@ -5,6 +5,7 @@ using BTCPayServer.HostedServices;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Services;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
using Microsoft.AspNetCore.Authorization;
@ -47,7 +48,8 @@ namespace BTCPayServer.Controllers
ExplorerClientProvider explorerProvider,
IFeeProviderFactory feeRateProvider,
LanguageService langService,
IHostingEnvironment env)
IHostingEnvironment env,
CoinAverageSettings coinAverage)
{
_Dashboard = dashboard;
_Repo = repo;
@ -64,7 +66,9 @@ namespace BTCPayServer.Controllers
_ServiceProvider = serviceProvider;
_BtcpayServerOptions = btcpayServerOptions;
_BTCPayEnv = btcpayEnv;
_CoinAverage = coinAverage;
}
CoinAverageSettings _CoinAverage;
NBXplorerDashboard _Dashboard;
BTCPayServerOptions _BtcpayServerOptions;
BTCPayServerEnvironment _BTCPayEnv;
@ -103,7 +107,7 @@ namespace BTCPayServer.Controllers
private string GetStoreUrl(string storeId)
{
return HttpContext.Request.GetAbsoluteRoot() + "/stores/" + storeId + "/";
}
}
[HttpGet]
[Route("{storeId}/users")]
@ -131,22 +135,22 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> StoreUsers(string storeId, StoreUsersViewModel vm)
{
await FillUsers(storeId, vm);
if(!ModelState.IsValid)
if (!ModelState.IsValid)
{
return View(vm);
}
var user = await _UserManager.FindByEmailAsync(vm.Email);
if(user == null)
if (user == null)
{
ModelState.AddModelError(nameof(vm.Email), "User not found");
return View(vm);
}
if(!StoreRoles.AllRoles.Contains(vm.Role))
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))
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);
@ -183,6 +187,88 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(StoreUsers), new { storeId = storeId, userId = userId });
}
[HttpGet]
[Route("{storeId}/checkout")]
public async Task<IActionResult> CheckoutExperience(string storeId)
{
var store = await _Repo.FindStore(storeId, GetUserId());
if (store == null)
return NotFound();
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]
[Route("{storeId}")]
public async Task<IActionResult> UpdateStore(string storeId)
@ -193,27 +279,24 @@ 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.SetLanguages(_LangService, storeBlob.DefaultLang);
vm.StoreWebsite = store.StoreWebsite;
vm.NetworkFee = !storeBlob.NetworkFeeDisabled;
vm.SpeedPolicy = store.SpeedPolicy;
AddPaymentMethods(store, vm);
vm.StatusMessage = StatusMessage;
vm.MonitoringExpiration = storeBlob.MonitoringExpiration;
vm.InvoiceExpiration = storeBlob.InvoiceExpiration;
vm.RateMultiplier = (double)storeBlob.GetRateMultiplier();
vm.PreferredExchange = storeBlob.PreferredExchange.IsCoinAverage() ? "coinaverage" : storeBlob.PreferredExchange;
vm.AllowCoinConversion = storeBlob.AllowCoinConversion;
vm.LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate;
return View(vm);
}
private void AddPaymentMethods(StoreData store, StoreViewModel vm)
{
var derivationByCryptoCode =
var derivationByCryptoCode =
store
.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<DerivationStrategy>()
@ -248,7 +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);
@ -277,25 +360,16 @@ namespace BTCPayServer.Controllers
store.StoreWebsite = model.StoreWebsite;
}
if (store.GetDefaultCrypto() != model.DefaultCryptoCurrency)
{
needUpdate = true;
store.SetDefaultCrypto(model.DefaultCryptoCurrency);
}
model.SetCryptoCurrencies(_ExplorerProvider, model.DefaultCryptoCurrency);
model.SetLanguages(_LangService, model.DefaultLang);
var blob = store.GetStoreBlob();
blob.NetworkFeeDisabled = !model.NetworkFee;
blob.MonitoringExpiration = model.MonitoringExpiration;
blob.InvoiceExpiration = model.InvoiceExpiration;
blob.DefaultLang = model.DefaultLang;
blob.LightningDescriptionTemplate = model.LightningDescriptionTemplate ?? string.Empty;
bool newExchange = blob.PreferredExchange != model.PreferredExchange;
blob.PreferredExchange = model.PreferredExchange;
blob.SetRateMultiplier(model.RateMultiplier);
blob.AllowCoinConversion = model.AllowCoinConversion;
if (store.SetStoreBlob(blob))
{
@ -304,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);
}
}
@ -327,41 +398,16 @@ namespace BTCPayServer.Controllers
});
}
private DerivationStrategy 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, Array.Empty<string>());
return new[] { ("Coin Average", "coinaverage") }.Concat(_CoinAverage.AvailableExchanges).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 DerivationStrategy(new DerivationStrategyFactory(network.NBitcoinNetwork).Parse(derivationScheme), network);
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]
@ -519,7 +565,7 @@ namespace BTCPayServer.Controllers
if (store == null || pairing == null)
return NotFound();
if(store.Role != StoreRoles.Owner)
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");

@ -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,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;
@ -107,6 +112,10 @@ namespace BTCPayServer.Data
t.StoreDataId
});
builder.Entity<AppData>()
.HasOne(a => a.StoreData);
builder.Entity<UserStore>()
.HasOne(pt => pt.ApplicationUser)
.WithMany(p => p.UserStores)

@ -13,6 +13,7 @@ using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using BTCPayServer.Services.Rates;
using BTCPayServer.Payments;
using BTCPayServer.JsonConverters;
namespace BTCPayServer.Data
{
@ -29,6 +30,11 @@ namespace BTCPayServer.Data
get; set;
}
public List<AppData> Apps
{
get; set;
}
[Obsolete("Use GetDerivationStrategies instead")]
public string DerivationStrategy
{
@ -208,6 +214,7 @@ namespace BTCPayServer.Data
{
InvoiceExpiration = 15;
MonitoringExpiration = 60;
RequiresRefundEmail = true;
}
public bool NetworkFeeDisabled
{
@ -217,6 +224,9 @@ namespace BTCPayServer.Data
{
get; set;
}
public bool RequiresRefundEmail { get; set; }
public string DefaultLang { get; set; }
[DefaultValue(60)]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
@ -254,30 +264,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;
}
}
}

@ -25,11 +25,47 @@ 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;
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++)

@ -0,0 +1,62 @@
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 CssThemeManager(SettingsRepository settingsRepository)
{
Update(settingsRepository);
}
private async void Update(SettingsRepository settingsRepository)
{
var data = (await settingsRepository.GetSettingAsync<ThemeSettings>()) ?? new ThemeSettings();
Update(data);
}
public void Update(ThemeSettings data)
{
UpdateBootstrap(data.BootstrapCssUri);
UpdateCreativeStart(data.CreativeStartCssUri);
}
private string _bootstrapUri = "/vendor/bootstrap4/css/bootstrap.css?v=" + DateTime.Now.Ticks;
public string BootstrapUri
{
get { return _bootstrapUri; }
}
public void UpdateBootstrap(string newUri)
{
if (String.IsNullOrWhiteSpace(newUri))
_bootstrapUri = "/vendor/bootstrap4/css/bootstrap.css?v="+ DateTime.Now.Ticks;
else
_bootstrapUri = newUri;
}
private string _creativeStartUri = "/vendor/bootstrap4-creativestart/creative.css?v=" + DateTime.Now.Ticks;
public string CreativeStartUri
{
get { return _creativeStartUri; }
}
public void UpdateCreativeStart(string newUri)
{
if (String.IsNullOrWhiteSpace(newUri))
_creativeStartUri = "/vendor/bootstrap4-creativestart/creative.css?v=" + DateTime.Now.Ticks;
else
_creativeStartUri = newUri;
}
}
}

@ -209,16 +209,16 @@ namespace BTCPayServer.HostedServices
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);
await Task.Delay(delay, _Cts.Token);
}
Watch(invoiceId);
now = DateTimeOffset.UtcNow;
if (invoice.MonitoringExpiration > now)
delay = invoice.MonitoringExpiration - DateTimeOffset.UtcNow;
if (delay > TimeSpan.Zero)
{
await Task.Delay(invoice.MonitoringExpiration - now, _Cts.Token);
await Task.Delay(delay, _Cts.Token);
}
Watch(invoiceId);
}

@ -184,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)

@ -0,0 +1,106 @@
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 : IHostedService
{
private SettingsRepository _SettingsRepository;
private IRateProviderFactory _RateProviderFactory;
private CoinAverageSettings _coinAverageSettings;
public RatesHostedService(SettingsRepository repo,
CoinAverageSettings coinAverageSettings,
IRateProviderFactory rateProviderFactory)
{
this._SettingsRepository = repo;
_RateProviderFactory = rateProviderFactory;
_coinAverageSettings = coinAverageSettings;
}
CancellationTokenSource _Cts = new CancellationTokenSource();
List<Task> _Tasks = new List<Task>();
public Task StartAsync(CancellationToken cancellationToken)
{
_Tasks.Add(RefreshCoinAverageSupportedExchanges(_Cts.Token));
_Tasks.Add(RefreshCoinAverageSettings(_Cts.Token));
return Task.CompletedTask;
}
async Task Timer(Func<Task> act, CancellationToken cancellation, [CallerMemberName]string caller = null)
{
await new SynchronizationContextRemover();
while (!cancellation.IsCancellationRequested)
{
try
{
await act();
}
catch (OperationCanceledException) when (cancellation.IsCancellationRequested)
{
}
catch (Exception ex)
{
Logs.PayServer.LogWarning(ex, caller + " failed");
try
{
await Task.Delay(TimeSpan.FromMinutes(1), cancellation);
}
catch (OperationCanceledException) when (cancellation.IsCancellationRequested) { }
}
}
}
Task RefreshCoinAverageSupportedExchanges(CancellationToken cancellation)
{
return Timer(async () =>
{
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);
}, cancellation);
}
Task RefreshCoinAverageSettings(CancellationToken cancellation)
{
return Timer(async () =>
{
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);
}, cancellation);
}
public Task StopAsync(CancellationToken cancellationToken)
{
_Cts.Cancel();
return Task.WhenAll(_Tasks.ToArray());
}
}
}

@ -109,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>();
@ -136,6 +138,7 @@ namespace BTCPayServer.Hosting
services.TryAddSingleton<LanguageService>();
services.TryAddSingleton<NBXplorerDashboard>();
services.TryAddSingleton<CssThemeManager>();
services.TryAddSingleton<StoreRepository>();
services.TryAddSingleton<BTCPayWalletProvider>();
services.TryAddSingleton<CurrencyNameTable>();
@ -154,16 +157,17 @@ namespace BTCPayServer.Hosting
services.AddSingleton<IHostedService, NBXplorerWaiters>();
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>();

@ -76,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 =>
{
@ -93,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 =>
//{
@ -103,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())
{
@ -155,9 +149,6 @@ namespace BTCPayServer.Hosting
app.UseBrowserLink();
}
Logs.Configure(loggerFactory);
//App insight do not that by itself...
loggerFactory.AddApplicationInsights(prov, LogLevel.Information);
@ -165,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,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);
}
}
}

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

@ -1,9 +1,12 @@
// <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
@ -33,6 +36,28 @@ namespace BTCPayServer.Migrations
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");
@ -404,6 +429,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,21 @@
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; }
}
}

@ -0,0 +1,24 @@
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 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;

@ -27,7 +27,7 @@ namespace BTCPayServer.Models.InvoicingModels
}
public class Payment
{
public string PaymentMethod { get; set; }
public string Crypto { get; set; }
public string Confirmations
{
get; set;
@ -72,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
{

@ -13,15 +13,18 @@ namespace BTCPayServer.Models.InvoicingModels
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; }
@ -41,11 +44,13 @@ namespace BTCPayServer.Models.InvoicingModels
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; }
}
}

@ -0,0 +1,68 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace BTCPayServer.Models.StoreViewModels
{
public class CheckoutExperienceViewModel
{
class Format
{
public string Name { get; set; }
public string Value { get; set; }
}
public SelectList CryptoCurrencies { get; set; }
public SelectList Languages { get; set; }
[Display(Name = "Default crypto currency on checkout")]
public string DefaultCryptoCurrency { get; set; }
[Display(Name = "Default language on checkout")]
public string DefaultLang { get; set; }
[Display(Name = "Allow conversion through third party (Shapeshift, Changelly...)")]
public bool AllowCoinConversion
{
get; set;
}
[Display(Name = "Do not propose lightning payment if value of the invoice is above...")]
[MaxLength(20)]
public string LightningMaxValue { get; set; }
[Display(Name = "Requires a refund email")]
public bool RequiresRefundEmail
{
get; set;
}
[Display(Name = "Do not propose on chain payment if the value of the invoice is below...")]
[MaxLength(20)]
public string OnChainMinValue { get; set; }
[Display(Name = "Link to a custom CSS stylesheet")]
[Url]
public string CustomCSS { get; set; }
[Display(Name = "Link to a custom logo")]
[Url]
public string CustomLogo { get; set; }
public void SetCryptoCurrencies(ExplorerClientProvider explorerProvider, string defaultCrypto)
{
var choices = explorerProvider.GetAll().Select(o => new Format() { Name = o.Item1.CryptoCode, Value = o.Item1.CryptoCode }).ToArray();
var chosen = choices.FirstOrDefault(f => f.Value == defaultCrypto) ?? choices.FirstOrDefault();
CryptoCurrencies = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
DefaultCryptoCurrency = chosen.Name;
}
public void SetLanguages(LanguageService langService, string defaultLang)
{
defaultLang = defaultLang ?? "en-US";
var choices = langService.GetLanguages().Select(o => new Format() { Name = o.DisplayName, Value = o.Code }).ToArray();
var chosen = choices.FirstOrDefault(f => f.Value == defaultLang) ?? choices.FirstOrDefault();
Languages = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
DefaultLang = chosen.Value;
}
}
}

@ -4,25 +4,14 @@ using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Rendering;
using NBitcoin;
namespace BTCPayServer.Models.StoreViewModels
{
public class DerivationSchemeViewModel
{
class Format
{
public string Name { get; set; }
public string Value { get; set; }
}
public DerivationSchemeViewModel()
{
var btcPay = new Format { Name = "BTCPay", Value = "BTCPay" };
DerivationSchemeFormat = btcPay.Value;
DerivationSchemeFormats = new SelectList(new Format[]
{
btcPay,
new Format { Name = "Electrum", Value = "Electrum" },
}, nameof(btcPay.Value), nameof(btcPay.Name), btcPay);
}
public string DerivationScheme
{
@ -34,18 +23,13 @@ namespace BTCPayServer.Models.StoreViewModels
get; set;
} = new List<(string KeyPath, string Address)>();
[Display(Name = "Derivation Scheme format")]
public string DerivationSchemeFormat
{
get;
set;
}
public string CryptoCode { get; set; }
[Display(Name = "Hint address")]
public string HintAddress { get; set; }
public bool Confirmation { get; set; }
public SelectList DerivationSchemeFormats { get; set; }
public string ServerUrl { get; set; }
public string StatusMessage { get; internal set; }
public KeyPath RootKeyPath { get; set; }
}
}

@ -23,5 +23,6 @@ namespace BTCPayServer.Models.StoreViewModels
}
public string StatusMessage { get; set; }
public string InternalLightningNode { get; internal set; }
public bool SkipPortTest { get; set; }
}
}

@ -12,16 +12,17 @@ namespace BTCPayServer.Models.StoreViewModels
{
public class StoreViewModel
{
public class DerivationScheme
{
public string Crypto { get; set; }
public string Value { get; set; }
}
class Format
{
public string Name { get; set; }
public string Value { get; set; }
}
public class DerivationScheme
{
public string Crypto { get; set; }
public string Value { get; set; }
}
public StoreViewModel()
{
@ -48,6 +49,17 @@ namespace BTCPayServer.Models.StoreViewModels
public List<StoreViewModel.DerivationScheme> DerivationSchemes { get; set; } = new List<StoreViewModel.DerivationScheme>();
public void SetExchangeRates((String DisplayName, String Name)[] supportedList, string preferredExchange)
{
var defaultStore = preferredExchange ?? "coinaverage";
var choices = supportedList.Select(o => new Format() { Name = o.DisplayName, Value = o.Name }).ToArray();
var chosen = choices.FirstOrDefault(f => f.Value == defaultStore) ?? choices.FirstOrDefault();
Exchanges = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
PreferredExchange = chosen.Value;
}
public SelectList Exchanges { get; set; }
[Display(Name = "Preferred price source (eg. bitfinex, bitstamp...)")]
public string PreferredExchange { get; set; }
@ -68,7 +80,7 @@ namespace BTCPayServer.Models.StoreViewModels
}
[Display(Name = "Invoice expires if the full amount has not been paid after ... minutes")]
[Range(1, 60 * 24 * 31)]
[Range(1, 60 * 24 * 24)]
public int InvoiceExpiration
{
get;
@ -76,7 +88,7 @@ namespace BTCPayServer.Models.StoreViewModels
}
[Display(Name = "Payment invalid if transactions fails to confirm ... minutes after invoice expiration")]
[Range(10, 60 * 24 * 31)]
[Range(10, 60 * 24 * 24)]
public int MonitoringExpiration
{
get;
@ -95,23 +107,8 @@ namespace BTCPayServer.Models.StoreViewModels
get; set;
}
[Display(Name = "Allow conversion through third party (Shapeshift, Changelly...)")]
public bool AllowCoinConversion
{
get; set;
}
public string StatusMessage
{
get; set;
}
public SelectList CryptoCurrencies { get; set; }
public SelectList Languages { get; set; }
[Display(Name = "Default crypto currency on checkout")]
public string DefaultCryptoCurrency { get; set; }
[Display(Name = "Default language on checkout")]
public string DefaultLang { get; set; }
[Display(Name = "Description template of the lightning invoice")]
public string LightningDescriptionTemplate { get; set; }
public class LightningNode
{
@ -122,22 +119,5 @@ namespace BTCPayServer.Models.StoreViewModels
{
get; set;
} = new List<LightningNode>();
public void SetCryptoCurrencies(ExplorerClientProvider explorerProvider, string defaultCrypto)
{
var choices = explorerProvider.GetAll().Select(o => new Format() { Name = o.Item1.CryptoCode, Value = o.Item1.CryptoCode }).ToArray();
var chosen = choices.FirstOrDefault(f => f.Value == defaultCrypto) ?? choices.FirstOrDefault();
CryptoCurrencies = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
DefaultCryptoCurrency = chosen.Name;
}
public void SetLanguages(LanguageService langService, string defaultLang)
{
defaultLang = defaultLang ?? "en-US";
var choices = langService.GetLanguages().Select(o => new Format() { Name = o.DisplayName, Value = o.Code }).ToArray();
var chosen = choices.FirstOrDefault(f => f.Value == defaultLang) ?? choices.FirstOrDefault();
Languages = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
DefaultLang = chosen.Value;
}
}
}

@ -1111,4 +1111,17 @@ namespace BTCPayServer
#endregion
}
}
public static class MultiValueDictionaryExtensions
{
public static MultiValueDictionary<TKey, TValue> ToMultiValueDictionary<TInput, TKey, TValue>(this IEnumerable<TInput> collection, Func<TInput, TKey> keySelector, Func<TInput, TValue> valueSelector)
{
var dictionary = new MultiValueDictionary<TKey, TValue>();
foreach(var item in collection)
{
dictionary.Add(keySelector(item), valueSelector(item));
}
return dictionary;
}
}
}

@ -27,8 +27,10 @@ namespace BTCPayServer.Payments.Bitcoin
_WalletProvider = walletProvider;
}
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(DerivationStrategy supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network)
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(DerivationStrategy supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network)
{
if (!_ExplorerProvider.IsAvailable(network))
throw new PaymentMethodUnavailableException($"Full node not available");
var getFeeRate = _FeeRateProviderFactory.CreateFeeProvider(network).GetFeeRateAsync();
var getAddress = _WalletProvider.GetWallet(network).ReserveAddressAsync(supportedPaymentMethod.DerivationStrategyBase);
Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod onchainMethod = new Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod();
@ -37,10 +39,5 @@ namespace BTCPayServer.Payments.Bitcoin
onchainMethod.DepositAddress = (await getAddress).ToString();
return onchainMethod;
}
public override Task<bool> IsAvailable(DerivationStrategy supportedPaymentMethod, BTCPayNetwork network)
{
return Task.FromResult(_ExplorerProvider.IsAvailable(network));
}
}
}

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Services.Invoices;
namespace BTCPayServer.Payments
@ -11,52 +12,33 @@ namespace BTCPayServer.Payments
/// </summary>
public interface IPaymentMethodHandler
{
/// <summary>
/// Returns true if the dependencies for a specific payment method are satisfied.
/// </summary>
/// <param name="supportedPaymentMethod"></param>
/// <param name="network"></param>
/// <returns>true if this payment method is available</returns>
Task<bool> IsAvailable(ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network);
/// <summary>
/// Create needed to track payments of this invoice
/// </summary>
/// <param name="supportedPaymentMethod"></param>
/// <param name="paymentMethod"></param>
/// <param name="store"></param>
/// <param name="network"></param>
/// <returns></returns>
Task<IPaymentMethodDetails> CreatePaymentMethodDetails(ISupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network);
Task<IPaymentMethodDetails> CreatePaymentMethodDetails(ISupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network);
}
public interface IPaymentMethodHandler<T> : IPaymentMethodHandler where T : ISupportedPaymentMethod
{
Task<bool> IsAvailable(T supportedPaymentMethod, BTCPayNetwork network);
Task<IPaymentMethodDetails> CreatePaymentMethodDetails(T supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network);
Task<IPaymentMethodDetails> CreatePaymentMethodDetails(T supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network);
}
public abstract class PaymentMethodHandlerBase<T> : IPaymentMethodHandler<T> where T : ISupportedPaymentMethod
{
public abstract Task<IPaymentMethodDetails> CreatePaymentMethodDetails(T supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network);
public abstract Task<IPaymentMethodDetails> CreatePaymentMethodDetails(T supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network);
Task<IPaymentMethodDetails> IPaymentMethodHandler.CreatePaymentMethodDetails(ISupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network)
Task<IPaymentMethodDetails> IPaymentMethodHandler.CreatePaymentMethodDetails(ISupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network)
{
if (supportedPaymentMethod is T method)
{
return CreatePaymentMethodDetails(method, paymentMethod, network);
return CreatePaymentMethodDetails(method, paymentMethod, store, network);
}
throw new NotSupportedException("Invalid supportedPaymentMethod");
}
public abstract Task<bool> IsAvailable(T supportedPaymentMethod, BTCPayNetwork network);
Task<bool> IPaymentMethodHandler.IsAvailable(ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
{
if(supportedPaymentMethod is T method)
{
return IsAvailable(method, network);
}
return Task.FromResult(false);
}
}
}

@ -7,7 +7,7 @@ using Newtonsoft.Json;
namespace BTCPayServer.Payments.Lightning.CLightning
{
public class CreateInvoiceResponse
public class CLightningInvoice
{
[JsonConverter(typeof(NBitcoin.JsonConverters.UInt256JsonConverter))]
[JsonProperty("payment_hash")]

@ -7,7 +7,6 @@ using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Payments.Lightning.Charge;
using Mono.Unix;
using NBitcoin;
using NBitcoin.RPC;
@ -42,9 +41,9 @@ namespace BTCPayServer.Payments.Lightning.CLightning
Network = network;
}
public Task<GetInfoResponse> GetInfoAsync(CancellationToken cancellation = default(CancellationToken))
public Task<Charge.GetInfoResponse> GetInfoAsync(CancellationToken cancellation = default(CancellationToken))
{
return SendCommandAsync<GetInfoResponse>("getinfo", cancellation: cancellation);
return SendCommandAsync<Charge.GetInfoResponse>("getinfo", cancellation: cancellation);
}
public Task SendAsync(string bolt11)
@ -168,24 +167,24 @@ namespace BTCPayServer.Payments.Lightning.CLightning
async Task<LightningInvoice> ILightningInvoiceClient.GetInvoice(string invoiceId, CancellationToken cancellation)
{
var invoices = await SendCommandAsync<ChargeInvoice[]>("listinvoices", new[] { invoiceId }, false, true, cancellation);
var invoices = await SendCommandAsync<CLightningInvoice[]>("listinvoices", new[] { invoiceId }, false, true, cancellation);
if (invoices.Length == 0)
return null;
return ChargeClient.ToLightningInvoice(invoices[0]);
return ToLightningInvoice(invoices[0]);
}
static NBitcoin.DataEncoders.DataEncoder InvoiceIdEncoder = NBitcoin.DataEncoders.Encoders.Base58;
async Task<LightningInvoice> ILightningInvoiceClient.CreateInvoice(LightMoney amount, TimeSpan expiry, CancellationToken cancellation)
async Task<LightningInvoice> ILightningInvoiceClient.CreateInvoice(LightMoney amount, string description, TimeSpan expiry, CancellationToken cancellation)
{
var id = InvoiceIdEncoder.EncodeData(RandomUtils.GetBytes(20));
var invoice = await SendCommandAsync<CreateInvoiceResponse>("invoice", new object[] { amount.MilliSatoshi, id, "" }, cancellation: cancellation);
var invoice = await SendCommandAsync<CLightningInvoice>("invoice", new object[] { amount.MilliSatoshi, id, description ?? "", Math.Max(0, (int)expiry.TotalSeconds) }, cancellation: cancellation);
invoice.Label = id;
invoice.MilliSatoshi = amount;
invoice.Status = "unpaid";
return ToLightningInvoice(invoice);
}
private static LightningInvoice ToLightningInvoice(CreateInvoiceResponse invoice)
private static LightningInvoice ToLightningInvoice(CLightningInvoice invoice)
{
return new LightningInvoice()
{
@ -204,9 +203,9 @@ namespace BTCPayServer.Payments.Lightning.CLightning
long lastInvoiceIndex = 99999999999;
async Task<LightningInvoice> ILightningListenInvoiceSession.WaitInvoice(CancellationToken cancellation)
{
var chargeInvoice = await SendCommandAsync<CreateInvoiceResponse>("waitanyinvoice", new object[] { lastInvoiceIndex }, cancellation: cancellation);
lastInvoiceIndex = chargeInvoice.PayIndex.Value;
return ToLightningInvoice(chargeInvoice);
var invoice = await SendCommandAsync<CLightningInvoice>("waitanyinvoice", new object[] { lastInvoiceIndex }, cancellation: cancellation);
lastInvoiceIndex = invoice.PayIndex.Value;
return ToLightningInvoice(invoice);
}
async Task<LightningNodeInformation> ILightningInvoiceClient.GetInfo(CancellationToken cancellation)

@ -48,8 +48,10 @@ namespace BTCPayServer.Payments.Lightning.Charge
{
var message = CreateMessage(HttpMethod.Post, "invoice");
Dictionary<string, string> parameters = new Dictionary<string, string>();
parameters.Add("msatoshi", request.Amont.MilliSatoshi.ToString(CultureInfo.InvariantCulture));
parameters.Add("msatoshi", request.Amount.MilliSatoshi.ToString(CultureInfo.InvariantCulture));
parameters.Add("expiry", ((int)request.Expiry.TotalSeconds).ToString(CultureInfo.InvariantCulture));
if(request.Description != null)
parameters.Add("description", request.Description);
message.Content = new FormUrlEncodedContent(parameters);
var result = await _Client.SendAsync(message, cancellation);
result.EnsureSuccessStatusCode();
@ -130,6 +132,8 @@ namespace BTCPayServer.Payments.Lightning.Charge
async Task<LightningInvoice> ILightningInvoiceClient.GetInvoice(string invoiceId, CancellationToken cancellation)
{
var invoice = await GetInvoice(invoiceId, cancellation);
if (invoice == null)
return null;
return ChargeClient.ToLightningInvoice(invoice);
}
@ -150,9 +154,9 @@ namespace BTCPayServer.Payments.Lightning.Charge
};
}
async Task<LightningInvoice> ILightningInvoiceClient.CreateInvoice(LightMoney amount, TimeSpan expiry, CancellationToken cancellation)
async Task<LightningInvoice> ILightningInvoiceClient.CreateInvoice(LightMoney amount, string description, TimeSpan expiry, CancellationToken cancellation)
{
var invoice = await CreateInvoiceAsync(new CreateInvoiceRequest() { Amont = amount, Expiry = expiry });
var invoice = await CreateInvoiceAsync(new CreateInvoiceRequest() { Amount = amount, Expiry = expiry, Description = description ?? "" });
return new LightningInvoice() { Id = invoice.Id, Amount = amount, BOLT11 = invoice.PayReq, Status = "unpaid" };
}

@ -7,7 +7,8 @@ namespace BTCPayServer.Payments.Lightning.Charge
{
public class CreateInvoiceRequest
{
public LightMoney Amont { get; set; }
public LightMoney Amount { get; set; }
public TimeSpan Expiry { get; set; }
public string Description { get; set; }
}
}

@ -29,7 +29,7 @@ namespace BTCPayServer.Payments.Lightning
public interface ILightningInvoiceClient
{
Task<LightningInvoice> GetInvoice(string invoiceId, CancellationToken cancellation = default(CancellationToken));
Task<LightningInvoice> CreateInvoice(LightMoney amount, TimeSpan expiry, CancellationToken cancellation = default(CancellationToken));
Task<LightningInvoice> CreateInvoice(LightMoney amount, string description, TimeSpan expiry, CancellationToken cancellation = default(CancellationToken));
Task<ILightningListenInvoiceSession> Listen(CancellationToken cancellation = default(CancellationToken));
Task<LightningNodeInformation> GetInfo(CancellationToken cancellation = default(CancellationToken));
}

@ -5,6 +5,7 @@ using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.Payments.Lightning.Charge;
using BTCPayServer.Payments.Lightning.CLightning;
@ -23,42 +24,44 @@ namespace BTCPayServer.Payments.Lightning
_LightningClientFactory = lightningClientFactory;
_Dashboard = dashboard;
}
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(LightningSupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network)
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(LightningSupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network)
{
var storeBlob = store.GetStoreBlob();
var test = Test(supportedPaymentMethod, network);
var invoice = paymentMethod.ParentEntity;
var due = Extensions.RoundUp(invoice.ProductInformation.Price / paymentMethod.Rate, 8);
var client = _LightningClientFactory.CreateClient(supportedPaymentMethod, network);
var expiry = invoice.ExpirationTime - DateTimeOffset.UtcNow;
if (expiry < TimeSpan.Zero)
expiry = TimeSpan.FromSeconds(1);
var lightningInvoice = await client.CreateInvoice(new LightMoney(due, LightMoneyUnit.BTC), expiry);
LightningInvoice lightningInvoice = null;
try
{
string description = storeBlob.LightningDescriptionTemplate;
description = description.Replace("{StoreName}", store.StoreName ?? "", StringComparison.OrdinalIgnoreCase)
.Replace("{ItemDescription}", invoice.ProductInformation.ItemDesc ?? "", StringComparison.OrdinalIgnoreCase)
.Replace("{OrderId}", invoice.OrderId ?? "", StringComparison.OrdinalIgnoreCase);
lightningInvoice = await client.CreateInvoice(new LightMoney(due, LightMoneyUnit.BTC), description, expiry);
}
catch (Exception ex)
{
throw new PaymentMethodUnavailableException($"Impossible to create lightning invoice ({ex.Message})", ex);
}
var nodeInfo = await test;
return new LightningLikePaymentMethodDetails()
{
BOLT11 = lightningInvoice.BOLT11,
InvoiceId = lightningInvoice.Id
InvoiceId = lightningInvoice.Id,
NodeInfo = nodeInfo.ToString()
};
}
public async override Task<bool> IsAvailable(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
{
try
{
await Test(supportedPaymentMethod, network);
return true;
}
catch { return false; }
}
/// <summary>
/// Used for testing
/// </summary>
public bool SkipP2PTest { get; set; }
public async Task<NodeInfo> Test(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
{
if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
throw new Exception($"Full node not available");
throw new PaymentMethodUnavailableException($"Full node not available");
var cts = new CancellationTokenSource(5000);
var client = _LightningClientFactory.CreateClient(supportedPaymentMethod, network);
LightningNodeInformation info = null;
@ -68,64 +71,53 @@ namespace BTCPayServer.Payments.Lightning
}
catch (OperationCanceledException) when (cts.IsCancellationRequested)
{
throw new Exception($"The lightning node did not replied in a timely maner");
throw new PaymentMethodUnavailableException($"The lightning node did not replied in a timely maner");
}
catch (Exception ex)
{
throw new Exception($"Error while connecting to the API ({ex.Message})");
throw new PaymentMethodUnavailableException($"Error while connecting to the API ({ex.Message})");
}
if(info.Address == null)
if (info.Address == null)
{
throw new Exception($"No lightning node public address has been configured");
throw new PaymentMethodUnavailableException($"No lightning node public address has been configured");
}
var blocksGap = Math.Abs(info.BlockHeight - summary.Status.ChainHeight);
if (blocksGap > 10)
{
throw new Exception($"The lightning is not synched ({blocksGap} blocks)");
throw new PaymentMethodUnavailableException($"The lightning is not synched ({blocksGap} blocks)");
}
try
{
if(!SkipP2PTest)
await TestConnection(info.Address, info.P2PPort, cts.Token);
}
catch (Exception ex)
{
throw new Exception($"Error while connecting to the lightning node via {info.Address}:{info.P2PPort} ({ex.Message})");
}
return new NodeInfo(info.NodeId, info.Address, info.P2PPort);
}
private async Task<bool> TestConnection(string addressStr, int port, CancellationToken cancellation)
public async Task TestConnection(NodeInfo nodeInfo, CancellationToken cancellation)
{
IPAddress address = null;
try
{
address = IPAddress.Parse(addressStr);
}
catch
{
IPAddress address = null;
try
{
address = (await Dns.GetHostAddressesAsync(addressStr)).FirstOrDefault();
address = IPAddress.Parse(nodeInfo.Host);
}
catch { }
}
if (address == null)
throw new Exception($"DNS did not resolved {addressStr}");
using (var tcp = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp))
{
try
catch
{
await WithTimeout(tcp.ConnectAsync(new IPEndPoint(address, port)), cancellation);
address = (await Dns.GetHostAddressesAsync(nodeInfo.Host)).FirstOrDefault();
}
if (address == null)
throw new PaymentMethodUnavailableException($"DNS did not resolved {nodeInfo.Host}");
using (var tcp = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp))
{
await WithTimeout(tcp.ConnectAsync(new IPEndPoint(address, nodeInfo.Port)), cancellation);
}
catch { return false; }
}
return true;
catch (Exception ex)
{
throw new PaymentMethodUnavailableException($"Error while connecting to the lightning node via {nodeInfo.Host}:{nodeInfo.Port} ({ex.Message})");
}
}
static Task WithTimeout(Task task, CancellationToken token)

@ -9,6 +9,7 @@ namespace BTCPayServer.Payments.Lightning
{
public string BOLT11 { get; set; }
public string InvoiceId { get; set; }
public string NodeInfo { get; set; }
public string GetPaymentDestination()
{

@ -52,9 +52,20 @@ namespace BTCPayServer.Payments.Lightning
_ListenPoller = new Timer(async s =>
{
await Task.WhenAll((await _InvoiceRepository.GetPendingInvoices())
.Select(async invoiceId => await EnsureListening(invoiceId, true))
.ToArray());
try
{
await Task.WhenAll((await _InvoiceRepository.GetPendingInvoices())
.Select(async invoiceId => await EnsureListening(invoiceId, true))
.ToArray());
}
catch (AggregateException ex)
{
Logs.PayServer.LogError(ex.InnerException ?? ex.InnerExceptions.FirstOrDefault(), $"Lightning: Uncaught error");
}
catch (Exception ex)
{
Logs.PayServer.LogError(ex, $"Lightning: Uncaught error");
}
}, null, 0, (int)PollInterval.TotalMilliseconds);
leases.Add(_ListenPoller);
return Task.CompletedTask;
@ -90,7 +101,16 @@ namespace BTCPayServer.Payments.Lightning
if (poll)
{
var charge = _LightningClientFactory.CreateClient(lightningSupportedMethod, network);
var chargeInvoice = await charge.GetInvoice(lightningMethod.InvoiceId);
LightningInvoice chargeInvoice = null;
try
{
chargeInvoice = await charge.GetInvoice(lightningMethod.InvoiceId);
}
catch(Exception ex)
{
Logs.PayServer.LogError(ex, $"{lightningSupportedMethod.CryptoCode} (Lightning): Can't connect to the lightning server");
continue;
}
if (chargeInvoice == null)
continue;
if (chargeInvoice.Status == "paid")

@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Payments
{
public class PaymentMethodUnavailableException : Exception
{
public PaymentMethodUnavailableException(string message) : base(message)
{
}
public PaymentMethodUnavailableException(string message, Exception inner) : base(message, inner)
{
}
}
}

@ -21,7 +21,7 @@ namespace BTCPayServer
.Select(t => t.Split(new char[] { ':' }, StringSplitOptions.RemoveEmptyEntries))
.Where(kv => kv.Length == 2)
.Select(kv => new KeyValuePair<string, string>(kv[0].ToLowerInvariant(), kv[1]))
.ToDictionary(o => o.Key, o => o.Value);
.ToMultiValueDictionary(o => o.Key, o => o.Value);
foreach(var filter in splitted)
{
@ -38,8 +38,8 @@ namespace BTCPayServer
get;
private set;
}
public Dictionary<string, string> Filters { get; private set; }
public MultiValueDictionary<string, string> Filters { get; private set; }
public override string ToString()
{

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Services.Apps
{
public enum AppType
{
PointOfSale
}
}

@ -6,6 +6,7 @@ using System.Reflection;
using System.Threading.Tasks;
using System.Text;
using NBXplorer;
using NBitcoin;
namespace BTCPayServer.Services
{
@ -20,13 +21,13 @@ namespace BTCPayServer.Services
Build = "Release";
#endif
Environment = env;
ChainType = provider.NBXplorerNetworkProvider.ChainType;
NetworkType = provider.NetworkType;
}
public IHostingEnvironment Environment
{
get; set;
}
public ChainType ChainType { get; set; }
public NetworkType NetworkType { get; set; }
public string Version
{
get; set;
@ -40,7 +41,7 @@ namespace BTCPayServer.Services
{
get
{
return ChainType == ChainType.Regtest && Environment.IsDevelopment();
return NetworkType == NetworkType.Regtest && Environment.IsDevelopment();
}
}
public override string ToString()

@ -1,4 +1,5 @@
using System;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
using System.Net.WebSockets;
@ -82,12 +83,13 @@ namespace BTCPayServer.Services
if (network == null)
throw new ArgumentNullException(nameof(network));
var path = new KeyPath("49'").Derive(network.CoinType).Derive(account, true);
var segwit = network.NBitcoinNetwork.Consensus.SupportSegwit;
var path = network.GetRootKeyPath().Derive(account, true);
var pubkey = await GetExtPubKey(_Ledger, network, path, false);
var derivation = new DerivationStrategyFactory(network.NBitcoinNetwork).CreateDirectDerivationStrategy(pubkey, new DerivationStrategyOptions()
{
P2SH = true,
Legacy = false
P2SH = segwit,
Legacy = !segwit
});
return new GetXPubResult() { ExtPubKey = derivation.ToString(), KeyPath = path };
}
@ -99,7 +101,7 @@ namespace BTCPayServer.Services
var pubKey = await ledger.GetWalletPubKeyAsync(account);
if (pubKey.Address.Network != network.NBitcoinNetwork)
{
if (network.DefaultSettings.ChainType == NBXplorer.ChainType.Main)
if (network.NBitcoinNetwork.NetworkType == NetworkType.Mainnet)
throw new Exception($"The opened ledger app should be for {network.NBitcoinNetwork.Name}, not for {pubKey.Address.Network}");
}
var fingerprint = onlyChaincode ? new byte[4] : (await ledger.GetWalletPubKeyAsync(account.Parent)).UncompressedPublicKey.Compress().Hash.ToBytes().Take(4).ToArray();
@ -125,9 +127,13 @@ namespace BTCPayServer.Services
private static async Task<KeyPath> GetKeyPath(LedgerClient ledger, BTCPayNetwork network, DirectDerivationStrategy directStrategy)
{
List<KeyPath> derivations = new List<KeyPath>();
if(network.NBitcoinNetwork.Consensus.SupportSegwit)
derivations.Add(new KeyPath("49'"));
derivations.Add(new KeyPath("44'"));
KeyPath foundKeyPath = null;
foreach (var account in
new[] { new KeyPath("49'"), new KeyPath("44'") }
derivations
.Select(purpose => purpose.Derive(network.CoinType))
.SelectMany(coinType => Enumerable.Range(0, 5).Select(i => coinType.Derive(i, true))))
{
@ -235,6 +241,5 @@ namespace BTCPayServer.Services
public string ExtPubKey { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.KeyPathJsonConverter))]
public KeyPath KeyPath { get; set; }
public int CoinType { get; internal set; }
}
}

@ -242,7 +242,7 @@ namespace BTCPayServer.Services.Invoices
#pragma warning disable CS0618
public List<PaymentEntity> GetPayments()
{
return Payments.ToList();
return Payments?.ToList() ?? new List<PaymentEntity>();
}
public List<PaymentEntity> GetPayments(string cryptoCode)
{
@ -375,7 +375,8 @@ namespace BTCPayServer.Services.Invoices
BIP21 = $"{scheme}:{cryptoInfo.Address}?amount={cryptoInfo.Due}",
};
}
if (info.GetId().PaymentType == PaymentTypes.LightningLike)
var paymentId = info.GetId();
if (paymentId.PaymentType == PaymentTypes.LightningLike)
{
cryptoInfo.PaymentUrls = new NBitpayClient.InvoicePaymentUrls()
{
@ -383,7 +384,7 @@ namespace BTCPayServer.Services.Invoices
};
}
#pragma warning disable CS0618
if (info.CryptoCode == "BTC")
if (info.CryptoCode == "BTC" && paymentId.PaymentType == PaymentTypes.BTCLike)
{
dto.Url = cryptoInfo.Url;
dto.BTCPrice = cryptoInfo.Price;

@ -101,7 +101,7 @@ namespace BTCPayServer.Services.Invoices
}
}
public async Task<InvoiceEntity> CreateInvoiceAsync(string storeId, InvoiceEntity invoice, BTCPayNetworkProvider networkProvider)
public async Task<InvoiceEntity> CreateInvoiceAsync(string storeId, InvoiceEntity invoice, IEnumerable<string> creationLogs, BTCPayNetworkProvider networkProvider)
{
List<string> textSearch = new List<string>();
invoice = Clone(invoice, null);
@ -146,6 +146,17 @@ namespace BTCPayServer.Services.Invoices
textSearch.Add(paymentMethod.Calculate().TotalDue.ToString());
}
context.PendingInvoices.Add(new PendingInvoiceData() { Id = invoice.Id });
foreach(var log in creationLogs)
{
context.InvoiceEvents.Add(new InvoiceEventData()
{
InvoiceDataId = invoice.Id,
Message = log,
Timestamp = invoice.InvoiceTime,
UniqueId = Encoders.Hex.EncodeData(RandomUtils.GetBytes(10))
});
}
await context.SaveChangesAsync().ConfigureAwait(false);
}
@ -388,9 +399,10 @@ namespace BTCPayServer.Services.Invoices
query = query.Where(i => i.Id == queryObject.InvoiceId);
}
if (!string.IsNullOrEmpty(queryObject.StoreId))
if (queryObject.StoreId != null && queryObject.StoreId.Length > 0)
{
query = query.Where(i => i.StoreDataId == queryObject.StoreId);
var stores = queryObject.StoreId.ToHashSet();
query = query.Where(i => stores.Contains(i.StoreDataId));
}
if (queryObject.UserId != null)
@ -418,8 +430,11 @@ namespace BTCPayServer.Services.Invoices
if (queryObject.OrderId != null)
query = query.Where(i => i.OrderId == queryObject.OrderId);
if (queryObject.Status != null)
query = query.Where(i => i.Status == queryObject.Status);
if (queryObject.Status != null && queryObject.Status.Length > 0)
{
var statusSet = queryObject.Status.ToHashSet();
query = query.Where(i => statusSet.Contains(i.Status));
}
query = query.OrderByDescending(q => q.Created);
@ -557,7 +572,7 @@ namespace BTCPayServer.Services.Invoices
public class InvoiceQuery
{
public string StoreId
public string[] StoreId
{
get; set;
}
@ -599,7 +614,7 @@ namespace BTCPayServer.Services.Invoices
get; set;
}
public string Status
public string[] Status
{
get; set;
}

@ -23,11 +23,14 @@ namespace BTCPayServer.Services
{
new Language("en-US", "English"),
new Language("de-DE", "Deutsch"),
//new Language("ja-JP", "日本語"),
new Language("ja-JP", "日本語"),
new Language("fr-FR", "Français"),
//new Language("es-ES", "Spanish"),
new Language("es-ES", "Spanish"),
new Language("pt-PT", "Portuguese"),
new Language("pt-BR", "Portuguese (Brazil)"),
new Language("nl-NL", "Dutch"),
new Language("cs-CZ", "Česky"),
new Language("is-IS", "Íslenska"),
};
}
}

@ -0,0 +1,95 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
namespace BTCPayServer.Services.Rates
{
public class BTCPayRateProviderFactory : IRateProviderFactory
{
IMemoryCache _Cache;
private IOptions<MemoryCacheOptions> _CacheOptions;
public IMemoryCache Cache
{
get
{
return _Cache;
}
}
public BTCPayRateProviderFactory(IOptions<MemoryCacheOptions> cacheOptions, IServiceProvider serviceProvider)
{
if (cacheOptions == null)
throw new ArgumentNullException(nameof(cacheOptions));
_Cache = new MemoryCache(cacheOptions);
_CacheOptions = cacheOptions;
// We use 15 min because of limits with free version of bitcoinaverage
CacheSpan = TimeSpan.FromMinutes(15.0);
this.serviceProvider = serviceProvider;
}
IServiceProvider serviceProvider;
TimeSpan _CacheSpan;
public TimeSpan CacheSpan
{
get
{
return _CacheSpan;
}
set
{
_CacheSpan = value;
InvalidateCache();
}
}
public void InvalidateCache()
{
_Cache = new MemoryCache(_CacheOptions);
}
public IRateProvider GetRateProvider(BTCPayNetwork network, RateRules rules)
{
rules = rules ?? new RateRules();
var rateProvider = GetDefaultRateProvider(network);
if (!rules.PreferredExchange.IsCoinAverage())
{
rateProvider = CreateExchangeRateProvider(network, rules.PreferredExchange);
}
rateProvider = CreateCachedRateProvider(network, rateProvider, rules.PreferredExchange);
return new TweakRateProvider(network, rateProvider, rules);
}
private IRateProvider CreateExchangeRateProvider(BTCPayNetwork network, string exchange)
{
List<IRateProvider> providers = new List<IRateProvider>();
if(exchange == "quadrigacx")
{
providers.Add(new QuadrigacxRateProvider(network.CryptoCode));
}
var coinAverage = new CoinAverageRateProviderDescription(network.CryptoCode).CreateRateProvider(serviceProvider);
coinAverage.Exchange = exchange;
providers.Add(coinAverage);
return new FallbackRateProvider(providers.ToArray());
}
private CachedRateProvider CreateCachedRateProvider(BTCPayNetwork network, IRateProvider rateProvider, string additionalScope)
{
return new CachedRateProvider(network.CryptoCode, rateProvider, _Cache) { CacheSpan = CacheSpan, AdditionalScope = additionalScope };
}
private IRateProvider GetDefaultRateProvider(BTCPayNetwork network)
{
if(network.DefaultRateProvider == null)
{
throw new RateUnavailableException(network.CryptoCode);
}
return network.DefaultRateProvider.CreateRateProvider(serviceProvider);
}
}
}

@ -4,9 +4,17 @@ using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using NBitcoin;
namespace BTCPayServer.Services.Rates
{
public class BitpayRateProviderDescription : RateProviderDescription
{
public IRateProvider CreateRateProvider(IServiceProvider serviceProvider)
{
return new BitpayRateProvider(new Bitpay(new Key(), new Uri("https://bitpay.com/")));
}
}
public class BitpayRateProvider : IRateProvider
{
Bitpay _Bitpay;

@ -1,40 +0,0 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
namespace BTCPayServer.Services.Rates
{
public class CachedDefaultRateProviderFactory : IRateProviderFactory
{
IMemoryCache _Cache;
ConcurrentDictionary<string, IRateProvider> _Providers = new ConcurrentDictionary<string, IRateProvider>();
ConcurrentDictionary<string, IRateProvider> _LongCacheProviders = new ConcurrentDictionary<string, IRateProvider>();
public IMemoryCache Cache
{
get
{
return _Cache;
}
}
public CachedDefaultRateProviderFactory(IMemoryCache cache)
{
if (cache == null)
throw new ArgumentNullException(nameof(cache));
_Cache = cache;
}
public IRateProvider RateProvider { get; set; }
public TimeSpan CacheSpan { get; set; } = TimeSpan.FromMinutes(1.0);
public TimeSpan LongCacheSpan { get; set; } = TimeSpan.FromMinutes(15.0);
public IRateProvider GetRateProvider(BTCPayNetwork network, bool longCache)
{
return (longCache ? _LongCacheProviders : _Providers).GetOrAdd(network.CryptoCode, new CachedRateProvider(network.CryptoCode, RateProvider ?? network.DefaultRateProvider, _Cache) { CacheSpan = longCache ? LongCacheSpan : CacheSpan, AdditionalScope = longCache ? "LONG" : "SHORT" });
}
}
}

@ -1,11 +1,15 @@
using Newtonsoft.Json;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using System.ComponentModel;
namespace BTCPayServer.Services.Rates
{
@ -16,6 +20,57 @@ namespace BTCPayServer.Services.Rates
}
}
public class CoinAverageRateProviderDescription : RateProviderDescription
{
public CoinAverageRateProviderDescription(string crypto)
{
CryptoCode = crypto;
}
public string CryptoCode { get; set; }
public CoinAverageRateProvider CreateRateProvider(IServiceProvider serviceProvider)
{
return new CoinAverageRateProvider(CryptoCode)
{
Authenticator = serviceProvider.GetService<ICoinAverageAuthenticator>()
};
}
IRateProvider RateProviderDescription.CreateRateProvider(IServiceProvider serviceProvider)
{
return CreateRateProvider(serviceProvider);
}
}
public class GetExchangeTickersResponse
{
public class Exchange
{
public string Name { get; set; }
[JsonProperty("display_name")]
public string DisplayName { get; set; }
public string[] Symbols { get; set; }
}
public bool Success { get; set; }
public Exchange[] Exchanges { get; set; }
}
public class RatesSetting
{
public string PublicKey { get; set; }
public string PrivateKey { get; set; }
[DefaultValue(15)]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
public int CacheInMinutes { get; set; } = 15;
}
public interface ICoinAverageAuthenticator
{
Task AddHeader(HttpRequestMessage message);
}
public class CoinAverageRateProvider : IRateProvider
{
static HttpClient _Client = new HttpClient();
@ -41,22 +96,27 @@ namespace BTCPayServer.Services.Rates
private decimal GetRate(Dictionary<string, decimal> rates, string currency)
{
if (currency == "BTC")
return 1.0m;
if (rates.TryGetValue(currency, out decimal result))
return result;
throw new RateUnavailableException(currency);
}
public ICoinAverageAuthenticator Authenticator { get; set; }
private async Task<Dictionary<string, decimal>> GetRatesCore()
{
HttpResponseMessage resp = null;
if (Exchange == null)
string url = Exchange == null ? $"https://apiv2.bitcoinaverage.com/indices/{Market}/ticker/short"
: $"https://apiv2.bitcoinaverage.com/exchanges/{Exchange}";
var request = new HttpRequestMessage(HttpMethod.Get, url);
var auth = Authenticator;
if (auth != null)
{
resp = await _Client.GetAsync($"https://apiv2.bitcoinaverage.com/indices/{Market}/ticker/short");
}
else
{
resp = await _Client.GetAsync($"https://apiv2.bitcoinaverage.com/exchanges/{Exchange}");
await auth.AddHeader(request);
}
var resp = await _Client.SendAsync(request);
using (resp)
{
@ -97,5 +157,82 @@ namespace BTCPayServer.Services.Rates
Value = o.Value
}).ToList();
}
public async Task TestAuthAsync()
{
var request = new HttpRequestMessage(HttpMethod.Get, "https://apiv2.bitcoinaverage.com/blockchain/tx_price/BTCUSD/8a3b4394ba811a9e2b0bbf3cc56888d053ea21909299b2703cdc35e156c860ff");
var auth = Authenticator;
if (auth != null)
{
await auth.AddHeader(request);
}
var resp = await _Client.SendAsync(request);
resp.EnsureSuccessStatusCode();
}
public async Task<GetRateLimitsResponse> GetRateLimitsAsync()
{
var request = new HttpRequestMessage(HttpMethod.Get, "https://apiv2.bitcoinaverage.com/info/ratelimits");
var auth = Authenticator;
if (auth != null)
{
await auth.AddHeader(request);
}
var resp = await _Client.SendAsync(request);
resp.EnsureSuccessStatusCode();
var jobj = JObject.Parse(await resp.Content.ReadAsStringAsync());
var response = new GetRateLimitsResponse();
response.CounterReset = TimeSpan.FromSeconds(jobj["counter_reset"].Value<int>());
var totalPeriod = jobj["total_period"].Value<string>();
if (totalPeriod == "24h")
{
response.TotalPeriod = TimeSpan.FromHours(24);
}
else if (totalPeriod == "30d")
{
response.TotalPeriod = TimeSpan.FromDays(30);
}
else
{
response.TotalPeriod = TimeSpan.FromSeconds(jobj["total_period"].Value<int>());
}
response.RequestsLeft = jobj["requests_left"].Value<int>();
response.RequestsPerPeriod = jobj["requests_per_period"].Value<int>();
return response;
}
public async Task<GetExchangeTickersResponse> GetExchangeTickersAsync()
{
var request = new HttpRequestMessage(HttpMethod.Get, "https://apiv2.bitcoinaverage.com/symbols/exchanges/ticker");
var auth = Authenticator;
if (auth != null)
{
await auth.AddHeader(request);
}
var resp = await _Client.SendAsync(request);
resp.EnsureSuccessStatusCode();
var jobj = JObject.Parse(await resp.Content.ReadAsStringAsync());
var response = new GetExchangeTickersResponse();
response.Success = jobj["success"].Value<bool>();
var exchanges = (JObject)jobj["exchanges"];
response.Exchanges = exchanges
.Properties()
.Select(p =>
{
var exchange = JsonConvert.DeserializeObject<GetExchangeTickersResponse.Exchange>(p.Value.ToString());
exchange.Name = p.Name;
return exchange;
})
.ToArray();
return response;
}
}
public class GetRateLimitsResponse
{
public TimeSpan CounterReset { get; set; }
public int RequestsLeft { get; set; }
public int RequestsPerPeriod { get; set; }
public TimeSpan TotalPeriod { get; set; }
}
}

@ -0,0 +1,119 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace BTCPayServer.Services.Rates
{
public class CoinAverageSettingsAuthenticator : ICoinAverageAuthenticator
{
CoinAverageSettings _Settings;
public CoinAverageSettingsAuthenticator(CoinAverageSettings settings)
{
_Settings = settings;
}
public Task AddHeader(HttpRequestMessage message)
{
return _Settings.AddHeader(message);
}
}
public class CoinAverageSettings : ICoinAverageAuthenticator
{
private static readonly DateTime _epochUtc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
public (String PublicKey, String PrivateKey)? KeyPair { get; set; }
public (String DisplayName, String Name)[] AvailableExchanges { get; set; } = Array.Empty<(String DisplayName, String Name)>();
public CoinAverageSettings()
{
//GENERATED BY:
//StringBuilder b = new StringBuilder();
//b.AppendLine("_coinAverageSettings.AvailableExchanges = new[] {");
//foreach (var availableExchange in _coinAverageSettings.AvailableExchanges)
//{
// b.AppendLine($"(DisplayName: \"{availableExchange.DisplayName}\", Name: \"{availableExchange.Name}\"),");
//}
//b.AppendLine("}.ToArray()");
AvailableExchanges = new[] {
(DisplayName: "BitBargain", Name: "bitbargain"),
(DisplayName: "Tidex", Name: "tidex"),
(DisplayName: "LocalBitcoins", Name: "localbitcoins"),
(DisplayName: "EtherDelta", Name: "etherdelta"),
(DisplayName: "Kraken", Name: "kraken"),
(DisplayName: "BitBay", Name: "bitbay"),
(DisplayName: "Independent Reserve", Name: "independentreserve"),
(DisplayName: "Exmoney", Name: "exmoney"),
(DisplayName: "Bitcoin.co.id", Name: "bitcoin_co_id"),
(DisplayName: "Huobi", Name: "huobi"),
(DisplayName: "GDAX", Name: "gdax"),
(DisplayName: "Coincheck", Name: "coincheck"),
(DisplayName: "Bittylicious", Name: "bittylicious"),
(DisplayName: "Gemini", Name: "gemini"),
(DisplayName: "QuadrigaCX", Name: "quadrigacx"),
(DisplayName: "Bit2C", Name: "bit2c"),
(DisplayName: "Luno", Name: "luno"),
(DisplayName: "Negocie Coins", Name: "negociecoins"),
(DisplayName: "FYB-SE", Name: "fybse"),
(DisplayName: "Hitbtc", Name: "hitbtc"),
(DisplayName: "Bitex.la", Name: "bitex"),
(DisplayName: "Korbit", Name: "korbit"),
(DisplayName: "itBit", Name: "itbit"),
(DisplayName: "Okex", Name: "okex"),
(DisplayName: "Bitsquare", Name: "bitsquare"),
(DisplayName: "Bitfinex", Name: "bitfinex"),
(DisplayName: "CoinMate", Name: "coinmate"),
(DisplayName: "Bitstamp", Name: "bitstamp"),
(DisplayName: "Cryptonit", Name: "cryptonit"),
(DisplayName: "Foxbit", Name: "foxbit"),
(DisplayName: "QuickBitcoin", Name: "quickbitcoin"),
(DisplayName: "Poloniex", Name: "poloniex"),
(DisplayName: "Bit-Z", Name: "bitz"),
(DisplayName: "Liqui", Name: "liqui"),
(DisplayName: "BitKonan", Name: "bitkonan"),
(DisplayName: "Kucoin", Name: "kucoin"),
(DisplayName: "Binance", Name: "binance"),
(DisplayName: "Rock Trading", Name: "rocktrading"),
(DisplayName: "Mercado Bitcoin", Name: "mercado"),
(DisplayName: "Coinsecure", Name: "coinsecure"),
(DisplayName: "Coinfloor", Name: "coinfloor"),
(DisplayName: "bitFlyer", Name: "bitflyer"),
(DisplayName: "BTCTurk", Name: "btcturk"),
(DisplayName: "Bittrex", Name: "bittrex"),
(DisplayName: "CampBX", Name: "campbx"),
(DisplayName: "Zaif", Name: "zaif"),
(DisplayName: "FYB-SG", Name: "fybsg"),
(DisplayName: "Quoine", Name: "quoine"),
(DisplayName: "BTC Markets", Name: "btcmarkets"),
(DisplayName: "Bitso", Name: "bitso"),
}.ToArray();
}
public Task AddHeader(HttpRequestMessage message)
{
var signature = GetCoinAverageSignature();
if (signature != null)
{
message.Headers.Add("X-signature", signature);
}
return Task.CompletedTask;
}
public string GetCoinAverageSignature()
{
var keyPair = KeyPair;
if (!keyPair.HasValue)
return null;
if (string.IsNullOrEmpty(keyPair.Value.PublicKey) || string.IsNullOrEmpty(keyPair.Value.PrivateKey))
return null;
var timestamp = (int)((DateTime.UtcNow - _epochUtc).TotalSeconds);
var payload = timestamp + "." + keyPair.Value.PublicKey;
var digestValueBytes = new HMACSHA256(Encoding.ASCII.GetBytes(keyPair.Value.PrivateKey)).ComputeHash(Encoding.ASCII.GetBytes(payload));
var digestValueHex = NBitcoin.DataEncoders.Encoders.Hex.EncodeData(digestValueBytes);
return payload + "." + digestValueHex;
}
}
}

@ -5,8 +5,24 @@ using System.Threading.Tasks;
namespace BTCPayServer.Services.Rates
{
public class FallbackRateProviderDescription : RateProviderDescription
{
public FallbackRateProviderDescription(RateProviderDescription[] rateProviders)
{
RateProviders = rateProviders;
}
public RateProviderDescription[] RateProviders { get; set; }
public IRateProvider CreateRateProvider(IServiceProvider serviceProvider)
{
return new FallbackRateProvider(RateProviders.Select(r => r.CreateRateProvider(serviceProvider)).ToArray());
}
}
public class FallbackRateProvider : IRateProvider
{
IRateProvider[] _Providers;
public FallbackRateProvider(IRateProvider[] providers)
{

@ -1,12 +1,40 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Data;
namespace BTCPayServer.Services.Rates
{
public class RateRules : IEnumerable<RateRule>
{
private List<RateRule> rateRules;
public RateRules()
{
rateRules = new List<RateRule>();
}
public RateRules(List<RateRule> rateRules)
{
this.rateRules = rateRules?.ToList() ?? new List<RateRule>();
}
public string PreferredExchange { get; set; }
public IEnumerator<RateRule> GetEnumerator()
{
return rateRules.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
public interface IRateProviderFactory
{
IRateProvider GetRateProvider(BTCPayNetwork network, bool longCache);
IRateProvider GetRateProvider(BTCPayNetwork network, RateRules rules);
TimeSpan CacheSpan { get; set; }
void InvalidateCache();
}
}

@ -14,14 +14,21 @@ namespace BTCPayServer.Services.Rates
}
public TimeSpan CacheSpan { get; set; }
public void AddMock(MockRateProvider mock)
{
_Mocks.Add(mock);
}
public IRateProvider GetRateProvider(BTCPayNetwork network, bool longCache)
public IRateProvider GetRateProvider(BTCPayNetwork network, RateRules rules)
{
return _Mocks.FirstOrDefault(m => m.CryptoCode == network.CryptoCode);
}
public void InvalidateCache()
{
}
}
public class MockRateProvider : IRateProvider
{

@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.Rates
{
public class QuadrigacxRateProvider : IRateProvider
{
public QuadrigacxRateProvider(string crypto)
{
CryptoCode = crypto;
}
public string CryptoCode { get; set; }
static HttpClient _Client = new HttpClient();
public async Task<decimal> GetRateAsync(string currency)
{
return await GetRatesAsyncCore(CryptoCode, currency);
}
private async Task<decimal> GetRatesAsyncCore(string cryptoCode, string currency)
{
var response = await _Client.GetAsync($"https://api.quadrigacx.com/v2/ticker?book={cryptoCode.ToLowerInvariant()}_{currency.ToLowerInvariant()}");
response.EnsureSuccessStatusCode();
var rates = JObject.Parse(await response.Content.ReadAsStringAsync());
if (!TryToDecimal(rates, out var result))
throw new RateUnavailableException(currency);
return result;
}
private bool TryToDecimal(JObject p, out decimal v)
{
v = 0.0m;
JToken token = p.Property("bid")?.Value;
if (token == null)
return false;
return decimal.TryParse(token.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out v);
}
public async Task<ICollection<Rate>> GetRatesAsync()
{
var response = await _Client.GetAsync($"https://api.quadrigacx.com/v2/ticker?book=all");
response.EnsureSuccessStatusCode();
var rates = JObject.Parse(await response.Content.ReadAsStringAsync());
List<Rate> result = new List<Rate>();
foreach (var prop in rates.Properties())
{
var rate = new Rate();
var splitted = prop.Name.Split('_');
var crypto = splitted[0].ToUpperInvariant();
if (crypto != CryptoCode)
continue;
rate.Currency = splitted[1].ToUpperInvariant();
TryToDecimal((JObject)prop.Value, out var v);
rate.Value = v;
result.Add(rate);
}
return result;
}
}
}

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Services.Rates
{
public interface RateProviderDescription
{
IRateProvider CreateRateProvider(IServiceProvider serviceProvider);
}
}

@ -10,9 +10,9 @@ namespace BTCPayServer.Services.Rates
{
private BTCPayNetwork network;
private IRateProvider rateProvider;
private List<RateRule> rateRules;
private RateRules rateRules;
public TweakRateProvider(BTCPayNetwork network, IRateProvider rateProvider, List<RateRule> rateRules)
public TweakRateProvider(BTCPayNetwork network, IRateProvider rateProvider, RateRules rateRules)
{
if (network == null)
throw new ArgumentNullException(nameof(network));

@ -8,6 +8,7 @@ using Microsoft.EntityFrameworkCore;
using BTCPayServer.Models;
using Microsoft.EntityFrameworkCore.Infrastructure.Internal;
using Newtonsoft.Json;
using System.Threading;
namespace BTCPayServer.Services
{
@ -51,6 +52,22 @@ namespace BTCPayServer.Services
await ctx.SaveChangesAsync();
}
}
IReadOnlyCollection<TaskCompletionSource<bool>> value;
lock (_Subscriptions)
{
if(_Subscriptions.TryGetValue(typeof(T), out value))
{
_Subscriptions.Remove(typeof(T));
}
}
if(value != null)
{
foreach(var v in value)
{
v.TrySetResult(true);
}
}
}
private T Deserialize<T>(string value)
@ -62,5 +79,35 @@ namespace BTCPayServer.Services
{
return JsonConvert.SerializeObject(obj);
}
MultiValueDictionary<Type, TaskCompletionSource<bool>> _Subscriptions = new MultiValueDictionary<Type, TaskCompletionSource<bool>>();
public async Task WaitSettingsChanged<T>(CancellationToken cancellation)
{
var tcs = new TaskCompletionSource<bool>();
using (cancellation.Register(() =>
{
try
{
tcs.TrySetCanceled();
}
catch { }
}))
{
lock (_Subscriptions)
{
_Subscriptions.Add(typeof(T), tcs);
}
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
tcs.Task.ContinueWith(_ =>
{
lock (_Subscriptions)
{
_Subscriptions.Remove(typeof(T), tcs);
}
}, TaskScheduler.Default);
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
await tcs.Task;
}
}
}
}

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace BTCPayServer.Services
{
public class ThemeSettings
{
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
public string BootstrapCssUri { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
public string CreativeStartCssUri { get; set; }
}
}

@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
namespace BTCPayServer
{
public struct SynchronizationContextRemover : INotifyCompletion
{
public bool IsCompleted => SynchronizationContext.Current == null;
public void OnCompleted(Action continuation)
{
var prev = SynchronizationContext.Current;
try
{
SynchronizationContext.SetSynchronizationContext(null);
continuation();
}
finally
{
SynchronizationContext.SetSynchronizationContext(prev);
}
}
public SynchronizationContextRemover GetAwaiter()
{
return this;
}
public void GetResult()
{
}
}
}

@ -22,7 +22,7 @@
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-default">Register</button>
<button type="submit" class="btn btn-primary">Register</button>
</form>
</div>
</div>

@ -3,23 +3,37 @@
ViewData["Title"] = "Forgot your password?";
}
<h2>@ViewData["Title"]</h2>
<h4>Enter your email.</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="ForgotPassword" method="post">
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="Email"></label>
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger"></span>
<section>
<div class="container">
<div class="row">
<div class="col-lg-12 text-center">
@Html.Partial("_StatusMessage", TempData["StatusMessage"])
</div>
<button type="submit" class="btn btn-default">Submit</button>
</form>
</div>
<div class="row">
<div class="col-lg-12 text-center">
<h2 class="section-heading">@ViewData["Title"]</h2>
<hr class="primary">
</div>
<div class="col-md-4">
<form asp-action="ForgotPassword" method="post">
<h4>Start password reset</h4>
<hr />
<p>
We all forget passwords every now and then. Just provide email address tied to your account and we'll start the process of helping you recover your account.
</p>
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="Email"></label>
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
</div>
</div>
</div>
</section>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
}

@ -1,8 +1,22 @@
@{
ViewData["Title"] = "Forgot password confirmation";
ViewData["Title"] = "Email sent!";
}
<h2>@ViewData["Title"]</h2>
<h2></h2>
<p>
Please check your email to reset your password.
</p>
<section>
<div class="container">
<div class="row">
<div class="col-md-4">
<h4>@ViewData["Title"]</h4>
<hr />
<p>
Please check your email to reset your password.
</p>
</div>
</div>
</div>
</section>

@ -41,7 +41,7 @@
</div>
</div>
<div class="form-group">
<button type="submit" class="btn btn-default">Log in</button>
<button type="submit" class="btn btn-primary">Log in</button>
</div>
<div class="form-group">
<p>
@ -76,7 +76,7 @@
<p>
@foreach(var provider in loginProviders)
{
<button type="submit" class="btn btn-default" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.Name</button>
<button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.Name</button>
}
</p>
</div>

@ -31,7 +31,7 @@
</div>
</div>
<div class="form-group">
<button type="submit" class="btn btn-default">Log in</button>
<button type="submit" class="btn btn-primary">Log in</button>
</div>
</form>
</div>

@ -18,7 +18,7 @@
<input asp-for="RecoveryCode" class="form-control" autocomplete="off" />
<span asp-validation-for="RecoveryCode" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-default">Log in</button>
<button type="submit" class="btn btn-primary">Log in</button>
</form>
</div>
</div>

@ -35,7 +35,7 @@
<input asp-for="ConfirmPassword" class="form-control" />
<span asp-validation-for="ConfirmPassword" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-default">Register</button>
<button type="submit" class="btn btn-primary">Register</button>
</form>
</div>
</div>

@ -26,7 +26,7 @@
<input asp-for="ConfirmPassword" class="form-control" />
<span asp-validation-for="ConfirmPassword" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-default">Reset</button>
<button type="submit" class="btn btn-primary">Reset</button>
</form>
</div>
</div>

@ -0,0 +1,38 @@
@model CreateAppViewModel
@{
ViewData["Title"] = "Create a new app";
}
<section>
<div class="container">
<div class="row">
<div class="col-lg-12 text-center">
<h2 class="section-heading">@ViewData["Title"]</h2>
<hr class="primary">
</div>
</div>
<div class="row">
<div class="col-lg-12">
<form asp-action="CreateApp">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Name" class="control-label"></label>*
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="SelectedAppType" class="control-label"></label>*
<select asp-for="SelectedAppType" asp-items="Model.AppTypes" class="form-control"></select>
</div>
<div class="form-group">
<label asp-for="SelectedStore" class="control-label"></label>*
<select asp-for="SelectedStore" asp-items="Model.Stores" class="form-control"></select>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
<a asp-action="ListApps">Back to the app list</a>
</div>
</div>
</div>
</section>

@ -0,0 +1,64 @@
@model ListAppsViewModel
@{
ViewData["Title"] = "Stores";
}
<section>
<div class="container">
<div class="row">
<div class="col-lg-12 text-center">
@Html.Partial("_StatusMessage", TempData["TempDataProperty-StatusMessage"])
</div>
</div>
<div class="row">
<div class="col-lg-12 text-center">
<h2 class="section-heading">@ViewData["Title"]</h2>
<hr class="primary">
<p>Create and manage apps.</p>
</div>
</div>
<div class="row">
<a asp-action="CreateApp" class="btn btn-primary" role="button"><span class="fa fa-plus"></span> Create a new app</a>
<table class="table table-sm table-responsive-md">
<thead>
<tr>
<th>Store</th>
<th>Name</th>
<th>App type</th>
<th style="text-align:right">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var app in Model.Apps)
{
<tr>
<td>
@if (app.IsOwner)
{
<span><a asp-action="UpdateStore" asp-controller="Stores" asp-route-storeId="@app.StoreId">@app.StoreName</a></span>
}
else
{
<span>@app.StoreName</span>
}
</td>
<td>@app.AppName</td>
<td>@app.AppType</td>
<td style="text-align:right">
@if (app.IsOwner)
{
<a asp-action="@app.UpdateAction" asp-controller="Apps" asp-route-appId="@app.Id">Settings</a><span> - </span>
}
<a asp-action="@app.ViewAction" asp-controller="Apps" asp-route-appId="@app.Id">View</a><span> - </span>
<a asp-action="DeleteApp" asp-route-appId="@app.Id">Remove</a>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</section>

@ -0,0 +1,45 @@
@model UpdatePointOfSaleViewModel
@{
ViewData["Title"] = "Update Point of Sale";
}
<section>
<div class="container">
<div class="row">
<div class="col-lg-12 text-center">
<h2 class="section-heading">@ViewData["Title"]</h2>
<hr class="primary">
</div>
</div>
<div class="row">
<div class="col-lg-12 text-center">
@Html.Partial("_StatusMessage", TempData["TempDataProperty-StatusMessage"])
</div>
</div>
<div class="row">
<div class="col-lg-12">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Title" class="control-label"></label>*
<input asp-for="Title" class="form-control" />
<span asp-validation-for="Title" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Currency" class="control-label"></label>*
<input asp-for="Currency" class="form-control" />
<span asp-validation-for="Currency" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Template" class="control-label"></label>*
<textarea asp-for="Template" rows="20" cols="40" class="form-control"></textarea>
<span asp-validation-for="Template" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" class="btn btn-primary" />
</div>
</form>
<a asp-action="ListApps">Back to the app list</a>
</div>
</div>
</div>
</section>

@ -0,0 +1,48 @@
@model ViewPointOfSaleViewModel
@{
ViewData["Title"] = Model.Title;
Layout = null;
}
<!DOCTYPE html>
<html class="h-100">
<head>
<title>@Model.Title</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="apple-mobile-web-app-capable" content="yes">
<link rel="stylesheet" href="~/vendor/bootstrap4/css/bootstrap.css" />
</head>
<body class="h-100">
<div class="container d-flex h-100">
<div class="justify-content-center align-self-center text-center mx-auto" style="margin: auto;">
<h1 class="mb-4">@Model.Title</h1>
<form method="post">
<div class="row">
@foreach (var item in Model.Items)
{
<div class="col-sm-4 mb-3">
<h3>@item.Title</h3>
<button type="submit" name="choiceKey" class="btn btn-primary" value="@item.Id">Buy for @item.Price.Formatted</button>
</div>
}
</div>
</form>
@*<div class="row mt-4">
<div class="col-md-4 offset-md-4 col-sm-6 offset-sm-3">
<h3>Something else</h3>
<form data-buy>
<div class="input-group">
<input class="form-control" type="number" step="0.00001" name="amount" placeholder="undefined (optional)"><div class="input-group-append">
<button class="btn btn-primary" type="submit">Pay</button>
</div>
</div>
</form>
</div>
</div>*@
</div>
</div>
<script src="~/vendor/jquery/jquery.js"></script>
<script src="~/vendor/bootstrap4/js/bootstrap.js"></script>
</body>
</html>

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