Compare commits

..

216 Commits

Author SHA1 Message Date
1d243910ae bump 2018-03-18 15:29:21 +09:00
e624649cd8 Merge branch 'lepipele-dev-shapeshift' 2018-03-18 15:28:42 +09:00
e6ca07e9b5 Merge remote-tracking branch 'source/master' into dev-shapeshift 2018-03-18 00:42:55 -05:00
b3d6435772 Custom text in Conversion tab in case of BTC LN payment 2018-03-18 00:41:16 -05:00
806474c8c6 Allow account selection of the ledger 2018-03-18 14:15:23 +09:00
1524fb4499 Merge remote-tracking branch 'source/master' into dev-shapeshift 2018-03-17 23:50:11 -05:00
14b70ff35e Rendering of Conversion tab on Invoice if enabled in store settings 2018-03-17 23:49:09 -05:00
c36a900627 Store setting for allowing conversion through Shapeshift 2018-03-17 23:48:06 -05:00
d3befb5b86 Fixing slider style when there are only two tabs 2018-03-17 23:36:32 -05:00
7f0ce1f802 Changing text to clarify usage 2018-03-17 23:28:39 -05:00
8342ad9175 Remove reference to browser mode of ledger 2018-03-18 12:58:14 +09:00
acb2407654 Fix bug: Paying a lightning invoice might miss 1 satoshi due to rounding error 2018-03-18 02:26:33 +09:00
b8a4f0c012 fix tests 2018-03-18 01:59:16 +09:00
57bb3b231c Add linux script for manual testing 2018-03-17 19:40:23 +09:00
e5d626e0fd Remove useless stuff in command line for tests 2018-03-17 19:35:37 +09:00
e2c4c913ff Remove hard coded container names in test docker-compose 2018-03-17 19:33:36 +09:00
09f97915d6 Fix charge listener bug, and decouple charge from clightning in test docker compose 2018-03-17 19:26:30 +09:00
81328b2667 Update charge in tests and fix two build time warnings 2018-03-17 17:49:42 +09:00
0d8affc68d Remove dependency on Eclair for tests 2018-03-17 17:02:47 +09:00
da77d278fb Adding Shapeshift button in new Altcoins tab 2018-03-16 23:46:39 -05:00
5e9f6f3542 Refactoring jquery logic for tab switching
Need to be more general to incorporate third tab
2018-03-16 23:15:01 -05:00
f337470f09 Adding styles for third tab - altcoin payments 2018-03-16 23:14:13 -05:00
636224d0c8 Checkout html and js cleanup 2018-03-16 22:46:30 -05:00
b28b3ef4ff Fix: Invoice can't be paid in lightning anymore if lightning server sent error 2018-03-14 20:10:04 +09:00
9e2e102ec4 Fix bug making it impossible to remove LTC xpub 2018-03-14 19:32:24 +09:00
9e16b83202 Testing Shapeshift integration 2018-03-13 12:20:22 -05:00
cbd40d49c1 bump 2018-03-13 15:56:17 +09:00
1d051648b7 Merge pull request from lepipele/dev-lepi
Bugfixing loading spinner when switching currency
2018-03-13 15:41:22 +09:00
49cf804914 bump 2018-03-13 15:39:52 +09:00
0f6ad75536 Remove internal exception thrown by NBitcoin 2018-03-13 15:28:39 +09:00
56eea18b2d Bugfixing loading spinner when switching currency
Moving it to buttons so it directly interacts with actions and doesn't break form states
2018-03-13 00:34:26 -05:00
b3698846c6 Improve UX of invoice list and invoice details 2018-03-13 09:13:16 +09:00
6806d96baa Listen to all derivation schemes 2018-03-12 19:02:03 +09:00
dc3b3077c2 Add text align for rate in invoice detail page 2018-03-12 11:02:02 +09:00
936ae64ca3 Allow connection via non https lightning charge node through localhost or 127.0.0.1 2018-03-11 15:14:05 +09:00
3a0a5dbd7f Accept all success HTTP code for invoice callbacks 2018-03-07 14:22:02 -05:00
ed4430ae7d bump 2018-03-07 07:49:46 -05:00
9a0e4e35d9 Merge pull request from lepipele/dev-lepi
Tweaking Checkout page so that it works properly in IE
2018-03-07 07:48:52 -05:00
5715dd2058 Disabling AJAX caching that messes up checkout in IE
Ref: https://stackoverflow.com/questions/4303829/how-to-prevent-a-jquery-ajax-request-from-caching-in-internet-explorer
2018-03-06 22:04:03 -06:00
da4c132f9d Adding Vue.js binding attributes 2018-03-06 22:02:34 -06:00
303a617f9e Improve invoice.cshtml display if offchain payment is present 2018-03-06 16:37:25 -05:00
3116ec9cb8 Fix bitcoin logo for internet explorer, update nbxplorer 2018-03-06 10:41:41 -05:00
b3f4eab075 fix doc 2018-03-06 09:40:21 -05:00
c98593b47b add dependencies to README 2018-03-06 09:35:20 -05:00
2c49d61682 shebang and fix line ending 2018-03-06 09:23:08 -05:00
21f5c94cff update readme 2018-03-06 09:18:00 -05:00
834ba4afab chmod +x scripts 2018-03-06 09:15:09 -05:00
d690cd6b7b Add build and run scripts 2018-03-06 09:14:45 -05:00
937bd07daa add doc 2018-03-05 10:58:27 -05:00
559b822111 fix broken expiration screen on checkout 2018-03-03 20:33:52 -05:00
7afb4c6b11 bump 2018-03-03 15:08:11 -05:00
1c98a3a33d Rename currency selection with "Pay With" 2018-03-03 15:07:34 -05:00
3320eb284e Merge branch 'lepipele-dev-lepi' 2018-03-03 15:06:58 -05:00
919fb60558 Hiding currency selection when invoice paid 2018-03-03 00:52:49 -06:00
7796589105 Extracting public methods in core.js 2018-03-03 00:41:52 -06:00
de6d3198ff Bundling JS and CSS files for Checkout.cshtml
Now we'll finally have versioning so when those JS/CSS files update, clients will properly request new bundle
2018-03-03 00:32:51 -06:00
f1e971d047 Refactoring core.js in preparation for bundling
Moving Vue registration to body for quick update of page
Removing defer dependancy for core.js
2018-03-03 00:32:04 -06:00
acd98aad32 Showing loader for better UX when switching currencies 2018-03-03 00:11:08 -06:00
b0c810398c Moving currency selection to order details
This way state transitions of form are now properly preserved
2018-03-02 23:49:51 -06:00
03a0044745 Currency selection moved to top of the form 2018-03-02 23:42:17 -06:00
15684efdce Update README.md 2018-03-02 15:04:00 -05:00
b67a962d12 Make sure the txrelayfee is correctly set 2018-03-02 14:16:16 -05:00
339cedadf7 Save a call to nbxplorer.GetStatus, update NBXplorer 2018-03-02 14:03:47 -05:00
e19d730fb7 Merge pull request from practicalswift/typos
Fix typos
2018-03-03 02:43:16 +09:00
649497e54f Fix typos 2018-03-01 15:11:30 +01:00
40eee0924a bump 2018-03-01 11:48:55 +09:00
bbf5fb3c30 show port if failing to connect to lightning node 2018-03-01 10:31:01 +09:00
346cdf2431 show block gap if lightning node is not synched 2018-02-28 23:12:09 +09:00
030cb09af8 Fix bug of address parsing for lightning 2018-02-28 22:56:12 +09:00
9f734349da Prettify date on the invoice list, and add orderid 2018-02-28 19:03:23 +09:00
2d5a861df0 Update to work with 0.16.0 2018-02-28 17:01:10 +09:00
061f428a54 fix bundling 2018-02-27 17:29:57 +09:00
9a539fd350 Remove default profile for launchSettings 2018-02-27 17:19:37 +09:00
4149fe10e9 Removing unused bundle.css & bundle.min.css 2018-02-27 17:03:51 +09:00
1014083160 Preserving bundles directory required for build 2018-02-27 17:03:49 +09:00
dfa3167c18 Removing generated bundles from source control 2018-02-27 17:03:45 +09:00
7e09efb9a3 Adding BundleJsCss as property on BtcPayServerOptions 2018-02-27 17:03:41 +09:00
fb736c0d0f Moving AddBundles to BtcPayServerServices 2018-02-27 17:03:39 +09:00
e4a7263e9b Turning JS/CSS bundling on 2018-02-27 17:03:38 +09:00
c52926f2b0 Using min versions of JS and CSS files 2018-02-27 17:03:37 +09:00
b6138b36be Restoring Unobtrusive Jquery validation 2018-02-27 17:03:29 +09:00
04bce3ae00 Bundling of CSS/JS files that's configurable in launchSettings.json
If you set BTCPAY_BUDNLEJSCSS to true it'll bundle all JS/CSS files into one

Ref: https://github.com/btcpayserver/btcpayserver/issues/47
2018-02-27 16:44:28 +09:00
68ca162dd3 Cleaning up JS/CSS references on Checkout page 2018-02-27 16:39:15 +09:00
100bb02cd5 fix logo size in copy part of checkout 2018-02-26 23:21:35 +09:00
856249d52c Update test sdk 2018-02-26 19:06:02 +09:00
309d6fdfe0 Can configure an internallightningnode to make things easier 2018-02-26 18:58:02 +09:00
f289420364 update image 2018-02-26 17:07:19 +09:00
f05e85de5f fix typo 2018-02-26 16:16:15 +09:00
4138849546 Better logo and warning 2018-02-26 16:13:10 +09:00
7052e4e1dc adjust layout of UpdateStore 2018-02-26 15:40:49 +09:00
ffb3e4f1fb Add logo for lightning 2018-02-26 15:33:03 +09:00
297834be66 Tell to users that using lightning is reckless 2018-02-26 15:16:17 +09:00
5924f1730c Poll for charge invoice 2018-02-26 14:52:08 +09:00
adc6bea4dc make tests bit more resilient 2018-02-26 13:36:00 +09:00
ef431f688f Make ChargeListener use only one websocket connection per url 2018-02-26 13:29:23 +09:00
c8923af573 Lightning Network support implementation 2018-02-26 00:48:12 +09:00
3d33ecf397 make IsAvailable async 2018-02-23 16:09:15 +09:00
82d38da18e FAQ 2018-02-23 15:31:19 +09:00
200e259b82 Add lightning dependencies to tests and docker-compose 2018-02-23 15:21:42 +09:00
2d1f4e5e0a update NBitcoin 2018-02-21 14:19:11 +09:00
7fe64612ad bump nbxplorer 2018-02-21 11:56:30 +09:00
5e452a679e simplify code 2018-02-20 14:23:50 +09:00
10be8aec82 bump 2018-02-20 12:46:40 +09:00
0e1a1fd2cd Remove dependencies in StoreController to on chain payment specific stuff 2018-02-20 12:45:04 +09:00
3f07010de8 Rename IPaymentMethodFactory to ISupportedPaymentMethod 2018-02-20 10:44:39 +09:00
2e45c8b190 Isolate PaymentMethodId in its own class, generalise DerivationStrategy 2018-02-19 23:13:23 +09:00
b4f4401cdc remove unused code, remove derivationscheme specific logic from InvoiceEntity 2018-02-19 22:41:47 +09:00
a6b92a0dd5 Fix build 2018-02-19 18:58:58 +09:00
2f3238c65e Use decimal for calculations instead of Money, and round due amount at ceil satoshi 2018-02-19 18:54:21 +09:00
65f5a38b4a bump 2018-02-19 15:14:07 +09:00
271cbf682f fix casing 2018-02-19 15:13:45 +09:00
a634593903 Big refactoring renaming cryptoData => PaymentMethod 2018-02-19 15:09:05 +09:00
af94de93d1 Add some comments 2018-02-19 11:31:34 +09:00
35f669aa15 Isolating code of on chain specific payment in its own folder 2018-02-19 11:06:08 +09:00
4795bd8108 Add some sanity check, make sure to use CrytpoDataId everywhere 2018-02-19 03:35:19 +09:00
29aed99fd1 prevent a crash if the new property DerivationStrategies is notset at invoice level 2018-02-19 02:56:44 +09:00
aa4519ac30 Big refactoring for supporting new type of payment 2018-02-19 02:38:03 +09:00
752133b01c fix bug 2018-02-18 20:37:42 +09:00
fe0c21ba08 Make sure that IPN sent for the send invoice are sent one at a time 2018-02-18 16:09:09 +09:00
90904a6b5e bump 2018-02-18 02:42:38 +09:00
8e3f7ea68d do not block next invoices if one invoice fail processing 2018-02-18 02:40:53 +09:00
a239104a28 fix uppercase 2018-02-18 02:36:11 +09:00
3bc232e1da Further isolate bitcoin related stuff inside BitcoinLikePaymentData 2018-02-18 02:35:02 +09:00
a1ee09cd85 Further abstract payment data by encapsulating bitcoin related logic into BitcoinLikePaymentData 2018-02-18 02:19:35 +09:00
b898cc030c general code cleanup + add analyzers 2018-02-17 13:18:16 +09:00
0602353dd2 fix bug happening if only btc is supported 2018-02-17 01:55:38 +09:00
9d406923ae make sure the waitingInvoices tasks are done 2018-02-17 01:43:43 +09:00
aa8565e3cc forgot remove dev time stuff 2018-02-17 01:35:30 +09:00
5de330b1f9 Refactoring to keep coin logic out of InvoiceWatcher 2018-02-17 01:34:40 +09:00
66597aed46 hide websocket exceptions 2018-02-15 16:17:27 +09:00
3071826f06 bump 2018-02-15 15:17:41 +09:00
c3684eb064 BTCPayWallet should be singleton per cryptcode 2018-02-15 15:17:12 +09:00
335dd9e66d bump 2018-02-15 14:44:35 +09:00
cd1611dbcd make sure to not spam too much NBXplorer 2018-02-15 14:44:08 +09:00
c17793aca9 do not freeze the stores page 2018-02-15 13:33:29 +09:00
01d898b618 Caching GetCoins 2018-02-15 13:02:12 +09:00
17069c311b Remove transaction cache 2018-02-15 12:42:48 +09:00
921d072942 fix electrum format for add derivation 2018-02-15 11:47:45 +09:00
6181e8b3e4 Refactor the code to prepare the group to support of another hardware wallet 2018-02-13 16:57:40 +09:00
93fc12bb2e fix typo 2018-02-13 15:28:22 +09:00
8e73c1a2f0 error message if not using segwit 2018-02-13 13:22:37 +09:00
e97c15578d bump nbxplorer 2018-02-13 12:42:03 +09:00
fd4f4e6aff better feedback if forgot to activate browser support 2018-02-13 12:20:56 +09:00
cedf8f75e8 Small UI adjustements 2018-02-13 11:41:21 +09:00
cd0a650df4 Ledger wallet support 2018-02-13 03:27:36 +09:00
6d6b9e2ba6 disable test not passing because of bitpay 2018-02-10 22:24:50 +09:00
fd915fdc5c fix test 2018-02-10 22:21:52 +09:00
59be813fe9 bump 2018-02-10 22:10:36 +09:00
465fbdd47f Fix bug which can happen if parsing of CoinAverage decimal is on another culture 2018-02-10 22:03:33 +09:00
f220abb716 Make the address verification step mandatory 2018-02-07 21:59:16 +09:00
db46ca87d7 do not share cache between long and short profile 2018-02-01 21:34:07 +01:00
d873a1a545 Set a longer timeout for the cache for /rates, update NBXPlorer, bump 2018-02-01 21:24:13 +01:00
a464a8702b Merge pull request from Eskyee/patch-2
bootstrap.min.css deleted last line
2018-01-26 07:02:35 +01:00
63722b932a Merge pull request from Eskyee/patch-1
bootstrap.css deleted last line
2018-01-26 07:02:22 +01:00
698b3c46cd bootstrap.min.css deleted last line
deleted this line
/*# sourceMappingURL=bootstrap.min.css.map */
because of a debug error in browser's
[Error] Failed to load resource: the server responded with a status of 404
(Not Found) (bootstrap.min.css.map, line 0)
https://btcpay-server-testnet.azurewebsites.net/vendor/bootstrap/css/bootstrap.min.css.map
2018-01-26 01:19:58 +00:00
df81051d07 bootstrap.css deleted last line
deleted this line 
/*# sourceMappingURL=bootstrap.css.map */
because of a debug error in browser's
[Error] Failed to load resource: the server responded with a status of 404 
(Not Found) (bootstrap.css.map, line 0)
https://btcpay-server-testnet.azurewebsites.net/vendor/bootstrap/css/bootstrap.css.map
2018-01-26 01:12:52 +00:00
ac70a77361 Fix with paidOver + paidLate 2018-01-24 10:37:23 +01:00
59a2432af9 Better invoice loop, fix javascript 2018-01-20 14:09:57 +09:00
ea4fa8d5d4 Mock rate provider 2018-01-20 12:30:22 +09:00
ade3eff75c unwrap rates in api/rates 2018-01-20 12:11:24 +09:00
db2a2a2b6c Fix expiration message on checkout page 2018-01-20 00:33:37 +09:00
579dcb5af8 Fix confirmation message when changing altcoin derivation scheme 2018-01-20 00:29:20 +09:00
69247dee8a Fix api/rates allow to scope by cyrptoCode and storeId 2018-01-19 18:11:43 +09:00
7b9541b8e9 Do not crash if some of the altcoins are unavailable 2018-01-19 17:39:15 +09:00
a12e4d7f64 fix typo 2018-01-19 17:14:27 +09:00
897da9b07a Better explanation for the price source 2018-01-19 17:13:29 +09:00
293525d480 do not query the rate source if the preferred exchange did not changed 2018-01-19 16:19:13 +09:00
198e810355 Store can customize rate source 2018-01-19 16:00:20 +09:00
fe25e00c94 Fix https://github.com/btcpayserver/btcpayserver/issues/38 2018-01-19 10:52:44 +09:00
8b129ab2e5 Merge pull request from lepipele/dev-lepi
Resolving problems with Vue console warnings
2018-01-19 01:41:01 +09:00
770bed54d1 bump 2018-01-19 00:52:38 +09:00
774817d4ac Add transaction speed on the invoice page 2018-01-19 00:52:17 +09:00
b8068b2ae8 Vue ignoring custom HTML5 elements
Ref: https://github.com/btcpayserver/btcpayserver/issues/34#issuecomment-358541767
2018-01-18 09:48:21 -06:00
3007a6bbc8 Upgrading Vue and linking production (min) version 2018-01-18 09:47:39 -06:00
1c0c8fece2 Change default speed to medium 2018-01-19 00:37:00 +09:00
c52eee47f0 bump 2018-01-19 00:13:40 +09:00
f88c98b9d9 fix block explorer link for mainnet 2018-01-18 23:57:41 +09:00
b0e9e10f7e Add extended notifications 2018-01-18 20:56:55 +09:00
39d47e33f6 Fix https://github.com/btcpayserver/btcpayserver/issues/31 2018-01-18 18:53:11 +09:00
4b7b6c6327 debug 2018-01-18 18:33:26 +09:00
a59edc5e8c bump 2018-01-18 18:12:44 +09:00
5ba322ea6a Add debug messages 2018-01-18 18:12:01 +09:00
b47b4b10cb should fix https://github.com/btcpayserver/btcpayserver/issues/31 2018-01-18 17:21:29 +09:00
c88f391935 Merge branch 'master' of https://github.com/btcpayserver/btcpayserver 2018-01-18 12:45:55 +09:00
26d3178e93 Fix expiration transitioning with a delay 2018-01-18 12:45:39 +09:00
1ad27c7827 Merge pull request from thijstriemstra/patch-1
fix typos
2018-01-18 11:48:05 +09:00
4200a8eed5 add c# comment 2018-01-17 17:45:57 +01:00
daceb7af8e happy new year 2018-01-17 17:43:57 +01:00
f703b53bce fix typos 2018-01-17 17:42:06 +01:00
2762224f0f Fix parsing bug zpub LTC mainnet 2018-01-17 19:39:15 +09:00
86fc64d184 Bump 2018-01-17 16:34:24 +09:00
726cd6fd49 Add badge if not on mainnet in the top bar 2018-01-17 16:34:01 +09:00
be1c4666e0 resize videos 2018-01-17 16:28:09 +09:00
b75dfc4191 Merge branch 'lepipele-dev-lepi' 2018-01-17 16:23:21 +09:00
97815f8daf Merge branch 'dev-lepi' of https://github.com/lepipele/btcpayserver into lepipele-dev-lepi 2018-01-17 16:18:54 +09:00
af16e1db69 update docker test 2018-01-17 16:17:27 +09:00
5f6913b3a2 Can tweak the rate at store level 2018-01-17 15:59:31 +09:00
2b31af80cb Can configure invoice expiration 2018-01-17 15:14:53 +09:00
3f9889d374 Update docker, remove tx cache, use new nbxplorer 2018-01-17 15:02:53 +09:00
f8189c64a4 Non blocking modal that shows sync progress
Ref: https://forkbitpay.slack.com/archives/C7M093Z55/p1515557792000053
2018-01-16 10:37:06 -06:00
c9b5f89f17 Make sure BTCPayServer exit cleanly in case of crash during startup, see https://github.com/aspnet/Hosting/issues/1194 2018-01-15 14:42:51 +09:00
ecb82f2cc9 do not send IPN if not set 2018-01-15 00:22:40 +09:00
f340c6eb7f Add more events to invoice 2018-01-14 22:06:06 +09:00
ba0e080816 Invoices has events recorded 2018-01-14 21:48:23 +09:00
bb3d107309 Fix legacy rate not being correct for alt only payment 2018-01-14 15:26:14 +09:00
8517b222bf Add redirect url to the invoice page 2018-01-14 15:01:09 +09:00
aed32204b5 Fix crashing ManageController 2018-01-14 14:52:15 +09:00
6b4eeff3f1 add tests, do not returns bitcoinAddress field if not supported by the invoice 2018-01-13 22:01:09 +09:00
e3cc589ebb fix nbxplorer 2018-01-13 21:07:04 +09:00
4a152e8ffc fix build 2018-01-13 17:32:08 +09:00
d54a9474d1 Fixing exception thrown when invoice is paid and supporting only LTC 2018-01-13 17:23:09 +09:00
98472211fc bump 2018-01-13 12:55:05 +09:00
099c9fa1f9 Fix balance calculation when there is unconfirmed tx 2018-01-13 12:53:56 +09:00
5226b77ffc Fix bug happening when removing data of nbxplorer 2018-01-13 02:28:23 +09:00
290779ee39 bump 2018-01-13 01:48:43 +09:00
4f39a8060c Fixing bug of uncorrectly unassigned addresses 2018-01-13 01:48:19 +09:00
165 changed files with 24536 additions and 12713 deletions
.gitattributes.gitignore
BTCPayServer.Tests
BTCPayServer
BTCPayNetwork.csBTCPayNetworkProvider.Bitcoin.csBTCPayNetworkProvider.Litecoin.csBTCPayNetworkProvider.csBTCPayServer.csprojBTCPayServer.ruleset
Configuration
Controllers
Data
DerivationStrategy.cs
Eclair
EventAggregator.cs
Events
ExplorerClientProvider.csExtensions.cs
HostedServices
Hosting
JsonConverters
Logging
Migrations
Models
MultiValueDictionary.cs
Payments
Program.cs
Properties
SearchString.cs
Services
Views
bundleconfig.json
wwwroot
DockerfileLICENSEREADME.mdbtcpayserver.slnbuild.ps1build.shglobal.jsonrun.ps1run.sh

17
.gitattributes vendored Normal file

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

4
.gitignore vendored

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

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

@ -84,6 +84,8 @@ namespace BTCPayServer.Tests
config.AppendLine($"ltc.explorer.url={LTCNBXplorerUri.AbsoluteUri}");
config.AppendLine($"ltc.explorer.cookiefile=0");
config.AppendLine($"internallightningnode={IntegratedLightning.AbsoluteUri}");
if (Postgres != null)
config.AppendLine($"postgres=" + Postgres);
var confPath = Path.Combine(chainDirectory, "settings.config");
@ -91,13 +93,18 @@ namespace BTCPayServer.Tests
ServerUri = new Uri("http://" + HostName + ":" + Port + "/");
Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development");
var conf = new DefaultConfiguration() { Logger = Logs.LogProvider.CreateLogger("Console") }.CreateConfiguration(new[] { "--datadir", _Directory, "--conf", confPath });
_Host = new WebHostBuilder()
.UseConfiguration(conf)
.ConfigureServices(s =>
{
s.AddSingleton<IRateProvider>(new MockRateProvider(new Rate("USD", 5000m)));
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);
s.AddLogging(l =>
{
l.SetMinimumLevel(LogLevel.Information)
@ -119,6 +126,7 @@ namespace BTCPayServer.Tests
internal set;
}
public InvoiceRepository InvoiceRepository { get; private set; }
public Uri IntegratedLightning { get; internal set; }
public T GetService<T>()
{

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

@ -1,4 +1,4 @@
FROM microsoft/dotnet:2.0.0-sdk
FROM microsoft/dotnet:2.0.5-sdk-2.1.4
WORKDIR /app
# caches restore result by copying csproj file separately
COPY BTCPayServer.Tests/BTCPayServer.Tests.csproj BTCPayServer.Tests/BTCPayServer.Tests.csproj

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

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

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

@ -26,7 +26,7 @@ docker-compose down
If you want to stop, and remove all existing data
```
docker-compose down -v
docker-compose down --v
```
You can run the tests inside a container by running
@ -35,14 +35,50 @@ You can run the tests inside a container by running
docker-compose run --rm tests
```
## Send commands to bitcoind
## How to manually test payments
### Using the test bitcoin-cli
You can call bitcoin-cli inside the container with `docker exec`, for example, if you want to send `0.23111090` to `mohu16LH66ptoWGEL1GtP6KHTBJYXMWhEf`:
```
docker exec -ti btcpayserver_dev_bitcoind bitcoin-cli -regtest -conf="/data/bitcoin.conf" -datadir="/data" sendtoaddress "mohu16LH66ptoWGEL1GtP6KHTBJYXMWhEf" 0.23111090
./docker-bitcoin-cli.sh sendtoaddress "mohu16LH66ptoWGEL1GtP6KHTBJYXMWhEf" 0.23111090
```
If you are using Powershell:
```
.\docker-bitcoin-cli.ps1 sendtoaddress "mohu16LH66ptoWGEL1GtP6KHTBJYXMWhEf" 0.23111090
```
### Using the test litecoin-cli
Same as bitcoin-cli, but with `.\docker-litecoin-cli.ps1` and `.\docker-litecoin-cli.sh` instead.
### Using the test lightning-cli
If you are using Linux:
```
./docker-customer-lightning-cli.sh pay lnbcrt100u1pd2e6uspp5ajnadvhazjrz55twd5k6yeg9u87wpw0q2fdr7g960yl5asv5fmnqdq9d3hkccqpxmedyrk0ehw5ueqx5e0r4qrrv74cewddfcvsxaawqz7634cmjj39sqwy5tvhz0hasktkk6t9pqfdh3edmf3z09zst5y7khv3rvxh8ctqqw6mwhh
```
If you are using Powershell:
```
.\docker-customer-lightning-cli.ps1 pay lnbcrt100u1pd2e6uspp5ajnadvhazjrz55twd5k6yeg9u87wpw0q2fdr7g960yl5asv5fmnqdq9d3hkccqpxmedyrk0ehw5ueqx5e0r4qrrv74cewddfcvsxaawqz7634cmjj39sqwy5tvhz0hasktkk6t9pqfdh3edmf3z09zst5y7khv3rvxh8ctqqw6mwhh
```
If you get this message:
```
{ "code" : 205, "message" : "Could not find a route", "data" : { "getroute_tries" : 1, "sendpay_tries" : 0 } }
```
Please, run the test `CanSetLightningServer`, this will establish a channel between the customer and the merchant, then, retry.
## FAQ
`docker-compose up dev` failed or tests are not passing, what should I do?
1. Run `docker-compose down --v` (this will reset your test environment)
2. Run `docker-compose pull` (this will ensure you have the lastest images)
3. Run again with `docker-compose up dev`
If you still have issues, try to restart docker.

@ -17,7 +17,8 @@ using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using BTCPayServer.Eclair;
using System.Globalization;
using BTCPayServer.Payments.Lightning.CLightning.RPC;
namespace BTCPayServer.Tests
{
@ -54,49 +55,99 @@ namespace BTCPayServer.Tests
ExplorerClient = new ExplorerClient(NetworkProvider.GetNetwork("BTC").NBXplorerNetwork, new Uri(GetEnvironment("TESTS_BTCNBXPLORERURL", "http://127.0.0.1:32838/")));
LTCExplorerClient = new ExplorerClient(NetworkProvider.GetNetwork("LTC").NBXplorerNetwork, new Uri(GetEnvironment("TESTS_LTCNBXPLORERURL", "http://127.0.0.1:32838/")));
var btc = NetworkProvider.GetNetwork("BTC").NBitcoinNetwork;
CustomerLightningD = new CLightningRPCClient(new Uri(GetEnvironment("TEST_CUSTOMERLIGHTNINGD", "http://127.0.0.1:30992/")), btc);
MerchantCharge = new ChargeTester(this, "TEST_MERCHANTCHARGE", "http://api-token:foiewnccewuify@127.0.0.1:54938/", "merchant_lightningd", btc);
PayTester = new BTCPayServerTester(Path.Combine(_Directory, "pay"))
{
NBXplorerUri = ExplorerClient.Address,
LTCNBXplorerUri = LTCExplorerClient.Address,
Postgres = GetEnvironment("TESTS_POSTGRES", "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver")
Postgres = GetEnvironment("TESTS_POSTGRES", "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver"),
IntegratedLightning = MerchantCharge.Client.Uri
};
PayTester.Port = int.Parse(GetEnvironment("TESTS_PORT", Utils.FreeTcpPort().ToString()));
PayTester.Port = int.Parse(GetEnvironment("TESTS_PORT", Utils.FreeTcpPort().ToString(CultureInfo.InvariantCulture)), CultureInfo.InvariantCulture);
PayTester.HostName = GetEnvironment("TESTS_HOSTNAME", "127.0.0.1");
PayTester.Start();
MerchantEclair = new EclairTester(this, "TEST_ECLAIR1", "http://127.0.0.1:30992/", "eclair1");
CustomerEclair = new EclairTester(this, "TEST_ECLAIR2", "http://127.0.0.1:30993/", "eclair2");
}
/// <summary>
/// This will setup a channel going from customer to merchant
/// Connect a customer LN node to the merchant LN node
/// </summary>
public void PrepareLightning()
{
PrepareLightningAsync().GetAwaiter().GetResult();
}
/// <summary>
/// Connect a customer LN node to the merchant LN node
/// </summary>
/// <returns></returns>
public async Task PrepareLightningAsync()
{
// Activate segwit
var blockCount = ExplorerNode.GetBlockCountAsync();
// Fetch node info, but that in cache
var merchant = MerchantEclair.GetNodeInfoAsync();
var customer = CustomerEclair.GetNodeInfoAsync();
var channels = CustomerEclair.RPC.ChannelsAsync();
var connect = CustomerEclair.RPC.ConnectAsync(merchant.Result);
await Task.WhenAll(blockCount, merchant, customer, channels, connect);
// Mine until segwit is activated
if (blockCount.Result <= 432)
while (true)
{
ExplorerNode.Generate(433 - blockCount.Result);
var channel = (await CustomerLightningD.ListPeersAsync())
.SelectMany(p => p.Channels)
.FirstOrDefault();
switch (channel?.State)
{
case null:
var merchantInfo = await WaitLNSynched();
var clightning = new NodeInfo(merchantInfo.Id, MerchantCharge.P2PHost, merchantInfo.Port);
await CustomerLightningD.ConnectAsync(clightning);
var address = await CustomerLightningD.NewAddressAsync();
await ExplorerNode.SendToAddressAsync(address, Money.Coins(0.2m));
ExplorerNode.Generate(1);
await WaitLNSynched();
await Task.Delay(1000);
await CustomerLightningD.FundChannelAsync(clightning, Money.Satoshis(16777215));
break;
case "CHANNELD_AWAITING_LOCKIN":
ExplorerNode.Generate(1);
await WaitLNSynched();
break;
case "CHANNELD_NORMAL":
return;
default:
throw new NotSupportedException(channel?.State ?? "");
}
}
}
public EclairTester MerchantEclair { get; set; }
public EclairTester CustomerEclair { get; set; }
private async Task<Payments.Lightning.CLightning.GetInfoResponse> WaitLNSynched()
{
while (true)
{
var merchantInfo = await MerchantCharge.Client.GetInfoAsync();
var blockCount = await ExplorerNode.GetBlockCountAsync();
if (merchantInfo.BlockHeight != blockCount)
{
await Task.Delay(1000);
}
else
{
return merchantInfo;
}
}
}
public void SendLightningPayment(Invoice invoice)
{
SendLightningPaymentAsync(invoice).GetAwaiter().GetResult();
}
public async Task SendLightningPaymentAsync(Invoice invoice)
{
var bolt11 = invoice.CryptoInfo.Where(o => o.PaymentUrls.BOLT11 != null).First().PaymentUrls.BOLT11;
bolt11 = bolt11.Replace("lightning:", "", StringComparison.OrdinalIgnoreCase);
await CustomerLightningD.SendAsync(bolt11);
}
public CLightningRPCClient CustomerLightningD { get; set; }
public ChargeTester MerchantCharge { get; private set; }
internal string GetEnvironment(string variable, string defaultValue)
{

@ -46,29 +46,16 @@ namespace BTCPayServer.Tests
Assert.IsType<ViewResult>(await store.RequestPairing(pairingCode.ToString()));
await store.Pair(pairingCode.ToString(), StoreId);
}
public StoresController CreateStore(string cryptoCode = "BTC")
public StoresController CreateStore()
{
return CreateStoreAsync(cryptoCode).GetAwaiter().GetResult();
return CreateStoreAsync().GetAwaiter().GetResult();
}
public async Task<StoresController> CreateStoreAsync(string cryptoCode = "BTC")
public async Task<StoresController> CreateStoreAsync()
{
SupportedNetwork = parent.NetworkProvider.GetNetwork(cryptoCode);
ExtKey = new ExtKey().GetWif(SupportedNetwork.NBitcoinNetwork);
var store = parent.PayTester.GetController<StoresController>(UserId);
await store.CreateStore(new CreateStoreViewModel() { Name = "Test Store" });
StoreId = store.CreatedStoreId;
DerivationScheme = new DerivationStrategyFactory(SupportedNetwork.NBitcoinNetwork).Parse(ExtKey.Neuter().ToString() + "-[legacy]");
await store.UpdateStore(StoreId, new StoreViewModel()
{
SpeedPolicy = SpeedPolicy.MediumSpeed
});
await store.AddDerivationScheme(StoreId, new DerivationSchemeViewModel()
{
CryptoCurrency = "BTC",
DerivationSchemeFormat = "BTCPay",
DerivationScheme = DerivationScheme.ToString(),
}, "Save");
return store;
}
@ -78,17 +65,23 @@ namespace BTCPayServer.Tests
{
RegisterDerivationSchemeAsync(crytoCode).GetAwaiter().GetResult();
}
public async Task RegisterDerivationSchemeAsync(string crytoCode)
public async Task RegisterDerivationSchemeAsync(string cryptoCode)
{
SupportedNetwork = parent.NetworkProvider.GetNetwork(cryptoCode);
var store = parent.PayTester.GetController<StoresController>(UserId);
var networkProvider = parent.PayTester.GetService<BTCPayNetworkProvider>();
var derivation = new DerivationStrategyFactory(networkProvider.GetNetwork(crytoCode).NBitcoinNetwork).Parse(ExtKey.Neuter().ToString() + "-[legacy]");
ExtKey = new ExtKey().GetWif(SupportedNetwork.NBitcoinNetwork);
DerivationScheme = new DerivationStrategyFactory(SupportedNetwork.NBitcoinNetwork).Parse(ExtKey.Neuter().ToString() + "-[legacy]");
var vm = (StoreViewModel)((ViewResult)await store.UpdateStore(StoreId)).Model;
vm.SpeedPolicy = SpeedPolicy.MediumSpeed;
await store.UpdateStore(StoreId, vm);
await store.AddDerivationScheme(StoreId, new DerivationSchemeViewModel()
{
CryptoCurrency = crytoCode,
DerivationSchemeFormat = crytoCode,
DerivationScheme = derivation.ToString(),
}, "Save");
CryptoCurrency = cryptoCode,
DerivationSchemeFormat = "BTCPay",
DerivationScheme = DerivationScheme.ToString(),
Confirmation = true
});
}
public DerivationStrategyBase DerivationScheme { get; set; }
@ -118,5 +111,20 @@ namespace BTCPayServer.Tests
{
get; set;
}
public void RegisterLightningNode(string cryptoCode)
{
RegisterLightningNodeAsync(cryptoCode).GetAwaiter().GetResult();
}
public async Task RegisterLightningNodeAsync(string cryptoCode)
{
var storeController = parent.PayTester.GetController<StoresController>(UserId);
await storeController.AddLightningNode(StoreId, new LightningNodeViewModel()
{
CryptoCurrency = "BTC",
Url = parent.MerchantCharge.Client.Uri.AbsoluteUri
}, "save");
}
}
}

@ -22,8 +22,14 @@ using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using BTCPayServer.Services.Rates;
using Microsoft.Extensions.Caching.Memory;
using BTCPayServer.Eclair;
using System.Collections.Generic;
using BTCPayServer.Models.StoreViewModels;
using System.Threading.Tasks;
using System.Globalization;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.HostedServices;
using BTCPayServer.Payments.Lightning;
namespace BTCPayServer.Tests
{
@ -35,6 +41,63 @@ namespace BTCPayServer.Tests
Logs.LogProvider = new XUnitLogProvider(helper);
}
[Fact]
public void CanCalculateCryptoDue2()
{
var dummy = new Key().PubKey.GetAddress(Network.RegTest).ToString();
#pragma warning disable CS0618
InvoiceEntity invoiceEntity = new InvoiceEntity();
invoiceEntity.Payments = new System.Collections.Generic.List<PaymentEntity>();
invoiceEntity.ProductInformation = new ProductInformation() { Price = 100 };
PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary();
paymentMethods.Add(new PaymentMethod()
{
CryptoCode = "BTC",
Rate = 10513.44m,
}.SetPaymentMethodDetails(new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod()
{
TxFee = Money.Coins(0.00000100m),
DepositAddress = dummy
}));
paymentMethods.Add(new PaymentMethod()
{
CryptoCode = "LTC",
Rate = 216.79m
}.SetPaymentMethodDetails(new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod()
{
TxFee = Money.Coins(0.00010000m),
DepositAddress = dummy
}));
invoiceEntity.SetPaymentMethods(paymentMethods);
var btc = invoiceEntity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null);
var accounting = btc.Calculate();
invoiceEntity.Payments.Add(new PaymentEntity() { Accounted = true, CryptoCode = "BTC" }.SetCryptoPaymentData(new BitcoinLikePaymentData()
{
Output = new TxOut() { Value = Money.Coins(0.00151263m) }
}));
accounting = btc.Calculate();
invoiceEntity.Payments.Add(new PaymentEntity() { Accounted = true, CryptoCode = "BTC" }.SetCryptoPaymentData(new BitcoinLikePaymentData()
{
Output = new TxOut() { Value = accounting.Due }
}));
accounting = btc.Calculate();
Assert.Equal(Money.Zero, accounting.Due);
Assert.Equal(Money.Zero, accounting.DueUncapped);
var ltc = invoiceEntity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike), null);
accounting = ltc.Calculate();
Assert.Equal(Money.Zero, accounting.Due);
// LTC might have over paid due to BTC paying above what it should (round 1 satoshi up)
Assert.True(accounting.DueUncapped < Money.Zero);
var paymentMethod = InvoiceWatcher.GetNearestClearedPayment(paymentMethods, out var accounting2, null);
Assert.Equal(btc.CryptoCode, paymentMethod.CryptoCode);
#pragma warning restore CS0618
}
[Fact]
public void CanCalculateCryptoDue()
{
@ -47,79 +110,81 @@ namespace BTCPayServer.Tests
entity.ProductInformation = new ProductInformation() { Price = 5000 };
// Some check that handling legacy stuff does not break things
var cryptoData = entity.GetCryptoData("BTC", null, true);
cryptoData.Calculate();
Assert.NotNull(cryptoData);
Assert.Null(entity.GetCryptoData("BTC", null, false));
entity.SetCryptoData(new CryptoData() { ParentEntity = entity, Rate = entity.Rate, CryptoCode = "BTC", TxFee = entity.TxFee });
Assert.NotNull(entity.GetCryptoData("BTC", null, false));
Assert.NotNull(entity.GetCryptoData("BTC", null, true));
var paymentMethod = entity.GetPaymentMethods(null, true).TryGet("BTC", PaymentTypes.BTCLike);
paymentMethod.Calculate();
Assert.NotNull(paymentMethod);
Assert.Null(entity.GetPaymentMethods(null, false).TryGet("BTC", PaymentTypes.BTCLike));
entity.SetPaymentMethod(new PaymentMethod() { ParentEntity = entity, Rate = entity.Rate, CryptoCode = "BTC", TxFee = entity.TxFee });
Assert.NotNull(entity.GetPaymentMethods(null, false).TryGet("BTC", PaymentTypes.BTCLike));
Assert.NotNull(entity.GetPaymentMethods(null, true).TryGet("BTC", PaymentTypes.BTCLike));
////////////////////
var accounting = cryptoData.Calculate();
var accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(1.1m), accounting.Due);
Assert.Equal(Money.Coins(1.1m), accounting.TotalDue);
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.5m), new Key()), Accounted = true });
accounting = cryptoData.Calculate();
accounting = paymentMethod.Calculate();
//Since we need to spend one more txout, it should be 1.1 - 0,5 + 0.1
Assert.Equal(Money.Coins(0.7m), accounting.Due);
Assert.Equal(Money.Coins(1.2m), accounting.TotalDue);
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()), Accounted = true });
accounting = cryptoData.Calculate();
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(0.6m), accounting.Due);
Assert.Equal(Money.Coins(1.3m), accounting.TotalDue);
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.6m), new Key()), Accounted = true });
accounting = cryptoData.Calculate();
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Zero, accounting.Due);
Assert.Equal(Money.Coins(1.3m), accounting.TotalDue);
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()), Accounted = true });
accounting = cryptoData.Calculate();
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Zero, accounting.Due);
Assert.Equal(Money.Coins(1.3m), accounting.TotalDue);
entity = new InvoiceEntity();
entity.ProductInformation = new ProductInformation() { Price = 5000 };
entity.SetCryptoData(new System.Collections.Generic.Dictionary<string, CryptoData>(new KeyValuePair<string, CryptoData>[] {
new KeyValuePair<string,CryptoData>("BTC", new CryptoData()
{
Rate = 1000,
TxFee = Money.Coins(0.1m)
}),
new KeyValuePair<string,CryptoData>("LTC", new CryptoData()
{
Rate = 500,
TxFee = Money.Coins(0.01m)
})
}));
PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary();
paymentMethods.Add(new PaymentMethod()
{
CryptoCode = "BTC",
Rate = 1000,
TxFee = Money.Coins(0.1m)
});
paymentMethods.Add(new PaymentMethod()
{
CryptoCode = "LTC",
Rate = 500,
TxFee = Money.Coins(0.01m)
});
entity.SetPaymentMethods(paymentMethods);
entity.Payments = new List<PaymentEntity>();
cryptoData = entity.GetCryptoData("BTC", null);
accounting = cryptoData.Calculate();
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null);
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(5.1m), accounting.Due);
cryptoData = entity.GetCryptoData("LTC", null);
accounting = cryptoData.Calculate();
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike), null);
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(10.01m), accounting.TotalDue);
entity.Payments.Add(new PaymentEntity() { CryptoCode = "BTC", Output = new TxOut(Money.Coins(1.0m), new Key()), Accounted = true });
cryptoData = entity.GetCryptoData("BTC", null);
accounting = cryptoData.Calculate();
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null);
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(4.2m), accounting.Due);
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
Assert.Equal(Money.Coins(1.0m), accounting.Paid);
Assert.Equal(Money.Coins(5.2m), accounting.TotalDue);
Assert.Equal(2, accounting.TxCount);
Assert.Equal(2, accounting.TxRequired);
cryptoData = entity.GetCryptoData("LTC", null);
accounting = cryptoData.Calculate();
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike), null);
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(10.01m + 0.1m * 2 - 2.0m /* 8.21m */), accounting.Due);
Assert.Equal(Money.Coins(0.0m), accounting.CryptoPaid);
Assert.Equal(Money.Coins(2.0m), accounting.Paid);
@ -128,44 +193,43 @@ namespace BTCPayServer.Tests
entity.Payments.Add(new PaymentEntity() { CryptoCode = "LTC", Output = new TxOut(Money.Coins(1.0m), new Key()), Accounted = true });
cryptoData = entity.GetCryptoData("BTC", null);
accounting = cryptoData.Calculate();
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null);
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(4.2m - 0.5m + 0.01m / 2), accounting.Due);
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
Assert.Equal(Money.Coins(1.5m), accounting.Paid);
Assert.Equal(Money.Coins(5.2m + 0.01m / 2), accounting.TotalDue); // The fee for LTC added
Assert.Equal(2, accounting.TxCount);
Assert.Equal(2, accounting.TxRequired);
cryptoData = entity.GetCryptoData("LTC", null);
accounting = cryptoData.Calculate();
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike), null);
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(8.21m - 1.0m + 0.01m), accounting.Due);
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
Assert.Equal(Money.Coins(3.0m), accounting.Paid);
Assert.Equal(Money.Coins(10.01m + 0.1m * 2 + 0.01m), accounting.TotalDue);
Assert.Equal(2, accounting.TxCount);
Assert.Equal(2, accounting.TxRequired);
var remaining = Money.Coins(4.2m - 0.5m + 0.01m / 2);
entity.Payments.Add(new PaymentEntity() { CryptoCode = "BTC", Output = new TxOut(remaining, new Key()), Accounted = true });
cryptoData = entity.GetCryptoData("BTC", null);
accounting = cryptoData.Calculate();
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null);
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Zero, accounting.Due);
Assert.Equal(Money.Coins(1.0m) + remaining, accounting.CryptoPaid);
Assert.Equal(Money.Coins(1.5m) + remaining, accounting.Paid);
Assert.Equal(Money.Coins(5.2m + 0.01m / 2), accounting.TotalDue);
Assert.Equal(accounting.Paid, accounting.TotalDue);
Assert.Equal(2, accounting.TxCount);
Assert.Equal(2, accounting.TxRequired);
cryptoData = entity.GetCryptoData("LTC", null);
accounting = cryptoData.Calculate();
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike), null);
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Zero, accounting.Due);
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
Assert.Equal(Money.Coins(3.0m) + remaining * 2, accounting.Paid);
// Paying 2 BTC fee, LTC fee removed because fully paid
Assert.Equal(Money.Coins(10.01m + 0.1m * 2 + 0.1m * 2 /* + 0.01m no need to pay this fee anymore */), accounting.TotalDue);
Assert.Equal(1, accounting.TxCount);
Assert.Equal(1, accounting.TxRequired);
Assert.Equal(accounting.Paid, accounting.TotalDue);
#pragma warning restore CS0618
}
@ -177,6 +241,7 @@ namespace BTCPayServer.Tests
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Buyer = new Buyer() { email = "test@fwf.com" },
@ -222,18 +287,100 @@ namespace BTCPayServer.Tests
{
var light = LightMoney.MilliSatoshis(1);
Assert.Equal("0.00000000001", light.ToString());
light = LightMoney.MilliSatoshis(200000);
Assert.Equal(200m, light.ToDecimal(LightMoneyUnit.Satoshi));
Assert.Equal(0.00000001m * 200m, light.ToDecimal(LightMoneyUnit.BTC));
}
//[Fact]
//public void CanSendLightningPayment()
//{
[Fact]
public void CanSetLightningServer()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
var storeController = tester.PayTester.GetController<StoresController>(user.UserId);
Assert.IsType<ViewResult>(storeController.UpdateStore(user.StoreId).GetAwaiter().GetResult());
Assert.IsType<ViewResult>(storeController.AddLightningNode(user.StoreId).GetAwaiter().GetResult());
// using (var tester = ServerTester.Create())
// {
// tester.Start();
// tester.PrepareLightning();
// }
//}
var testResult = storeController.AddLightningNode(user.StoreId, new LightningNodeViewModel()
{
CryptoCurrency = "BTC",
Url = tester.MerchantCharge.Client.Uri.AbsoluteUri
}, "test").GetAwaiter().GetResult();
Assert.DoesNotContain("Error", ((LightningNodeViewModel)Assert.IsType<ViewResult>(testResult).Model).StatusMessage, StringComparison.OrdinalIgnoreCase);
Assert.True(storeController.ModelState.IsValid);
Assert.IsType<RedirectToActionResult>(storeController.AddLightningNode(user.StoreId, new LightningNodeViewModel()
{
CryptoCurrency = "BTC",
Url = tester.MerchantCharge.Client.Uri.AbsoluteUri
}, "save").GetAwaiter().GetResult());
var storeVm = Assert.IsType<Models.StoreViewModels.StoreViewModel>(Assert.IsType<ViewResult>(storeController.UpdateStore(user.StoreId).GetAwaiter().GetResult()).Model);
Assert.Single(storeVm.LightningNodes);
}
}
[Fact]
public void CanSendLightningPayment()
{
using (var tester = ServerTester.Create())
{
tester.Start();
tester.PrepareLightning();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterLightningNode("BTC");
user.RegisterDerivationScheme("BTC");
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 0.01,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description"
});
tester.SendLightningPayment(invoice);
Eventually(() =>
{
var localInvoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal("complete", localInvoice.Status);
Assert.Equal("False", localInvoice.ExceptionStatus.ToString());
});
Task.WaitAll(Enumerable.Range(0, 5)
.Select(_ => CanSendLightningPaymentCore(tester, user))
.ToArray());
}
}
async Task CanSendLightningPaymentCore(ServerTester tester, TestAccount user)
{
await Task.Delay(TimeSpan.FromSeconds(RandomUtils.GetUInt32() % 5));
var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice()
{
Price = 0.01,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description"
});
await tester.SendLightningPaymentAsync(invoice);
await EventuallyAsync(async () =>
{
var localInvoice = await user.BitPay.GetInvoiceAsync(invoice.Id);
Assert.Equal("complete", localInvoice.Status);
Assert.Equal("False", localInvoice.ExceptionStatus.ToString());
});
}
[Fact]
public void CanUseServerInitiatedPairingCode()
@ -270,6 +417,7 @@ namespace BTCPayServer.Tests
tester.Start();
var acc = tester.NewAccount();
acc.GrantAccess();
acc.RegisterDerivationScheme("BTC");
var invoice = acc.BitPay.CreateInvoice(new Invoice()
{
Price = 5.0,
@ -278,7 +426,8 @@ namespace BTCPayServer.Tests
OrderId = "orderId",
NotificationURL = callbackServer.GetUri().AbsoluteUri,
ItemDesc = "Some description",
FullNotifications = true
FullNotifications = true,
ExtendedNotifications = true
});
BitcoinUrlBuilder url = new BitcoinUrlBuilder(invoice.PaymentUrls.BIP21);
tester.ExplorerNode.SendToAddress(url.Address, url.Amount);
@ -309,7 +458,7 @@ namespace BTCPayServer.Tests
pairingCode = acc.BitPay.RequestClientAuthorization("test1", Facade.Merchant);
var store2 = acc.CreateStore();
store2.Pair(pairingCode.ToString(), store2.CreatedStoreId).GetAwaiter().GetResult();
Assert.Contains(nameof(PairingResult.ReusedKey), store2.StatusMessage);
Assert.Contains(nameof(PairingResult.ReusedKey), store2.StatusMessage, StringComparison.CurrentCultureIgnoreCase);
}
}
@ -321,17 +470,17 @@ namespace BTCPayServer.Tests
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5000.0,
Currency = "USD"
}, Facade.Merchant);
var payment1 = Money.Coins(0.04m);
var payment2 = Money.Coins(0.08m);
var payment1 = invoice.BtcDue + Money.Coins(0.0001m);
var payment2 = invoice.BtcDue;
var tx1 = new uint256(tester.ExplorerNode.SendCommand("sendtoaddress", new object[]
{
invoice.BitcoinAddress.ToString(),
invoice.BitcoinAddress,
payment1.ToString(),
null, //comment
null, //comment_to
@ -344,6 +493,8 @@ namespace BTCPayServer.Tests
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal(payment1, invoice.BtcPaid);
Assert.Equal("paid", invoice.Status);
Assert.Equal("paidOver", invoice.ExceptionStatus.ToString());
invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, user.SupportedNetwork.NBitcoinNetwork);
});
@ -352,18 +503,17 @@ namespace BTCPayServer.Tests
{
input.ScriptSig = Script.Empty; //Strip signatures
}
var change = tx.Outputs.First(o => o.Value != payment1);
var output = tx.Outputs.First(o => o.Value == payment1);
output.Value = payment2;
output.ScriptPubKey = invoiceAddress.ScriptPubKey;
change.Value -= (payment2 - payment1) * 2; //Add more fees
var replaced = tester.ExplorerNode.SignRawTransaction(tx);
tester.ExplorerNode.SendRawTransaction(replaced);
var test = tester.ExplorerClient.Sync(user.DerivationScheme, null);
var test = tester.ExplorerClient.GetUTXOs(user.DerivationScheme, null);
Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal(payment2, invoice.BtcPaid);
Assert.Equal("False", invoice.ExceptionStatus.ToString());
});
}
}
@ -388,10 +538,114 @@ namespace BTCPayServer.Tests
var user = tester.NewAccount();
Assert.False(user.BitPay.TestAccess(Facade.Merchant));
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
Assert.True(user.BitPay.TestAccess(Facade.Merchant));
}
}
[Fact]
public void CanTweakRate()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
// First we try payment with a merchant having only BTC
var invoice1 = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5000.0,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
var storeController = tester.PayTester.GetController<StoresController>(user.UserId);
var vm = (StoreViewModel)((ViewResult)storeController.UpdateStore(user.StoreId).Result).Model;
Assert.Equal(1.0, vm.RateMultiplier);
vm.RateMultiplier = 0.5;
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);
Assert.True(invoice2.BtcPrice.Almost(invoice1.BtcPrice * 2, 0.00001m));
}
}
[Fact]
public void CanHaveLTCOnlyStore()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("LTC");
// First we try payment with a merchant having only BTC
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 500,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
Assert.Single(invoice.CryptoInfo);
Assert.Equal("LTC", invoice.CryptoInfo[0].CryptoCode);
var cashCow = tester.LTCExplorerNode;
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
var firstPayment = Money.Coins(0.1m);
cashCow.SendToAddress(invoiceAddress, firstPayment);
Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal(firstPayment, invoice.CryptoInfo[0].Paid);
});
Assert.Single(invoice.CryptoInfo); // Only BTC should be presented
var controller = tester.PayTester.GetController<InvoiceController>(null);
var checkout = (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id, null).GetAwaiter().GetResult()).Value;
Assert.Single(checkout.AvailableCryptos);
Assert.Equal("LTC", checkout.CryptoCode);
//////////////////////
// Despite it is called BitcoinAddress it should be LTC because BTC is not available
Assert.Null(invoice.BitcoinAddress);
Assert.NotEqual(1.0, invoice.Rate);
Assert.NotEqual(invoice.BtcDue, invoice.CryptoInfo[0].Due); // Should be BTC rate
cashCow.SendToAddress(invoiceAddress, invoice.CryptoInfo[0].Due);
Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal("paid", invoice.Status);
checkout = (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id, null).GetAwaiter().GetResult()).Value;
Assert.Equal("paid", checkout.Status);
});
}
}
[Fact]
public void CanPayWithTwoCurrencies()
{
@ -400,7 +654,7 @@ namespace BTCPayServer.Tests
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
// First we try payment with a merchant having only BTC
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
@ -413,6 +667,7 @@ namespace BTCPayServer.Tests
}, Facade.Merchant);
var cashCow = tester.ExplorerNode;
cashCow.Generate(2); // get some money in case
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
var firstPayment = Money.Coins(0.04m);
cashCow.SendToAddress(invoiceAddress, firstPayment);
@ -447,6 +702,7 @@ namespace BTCPayServer.Tests
invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
firstPayment = Money.Coins(0.04m);
cashCow.SendToAddress(invoiceAddress, firstPayment);
Logs.Tester.LogInformation("First payment sent to " + invoiceAddress);
Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
@ -457,10 +713,10 @@ namespace BTCPayServer.Tests
var ltcCryptoInfo = invoice.CryptoInfo.FirstOrDefault(c => c.CryptoCode == "LTC");
Assert.NotNull(ltcCryptoInfo);
invoiceAddress = BitcoinAddress.Create(ltcCryptoInfo.Address, cashCow.Network);
var secondPayment = Money.Coins(decimal.Parse(ltcCryptoInfo.Due));
var secondPayment = Money.Coins(decimal.Parse(ltcCryptoInfo.Due, CultureInfo.InvariantCulture));
cashCow.Generate(2); // LTC is not worth a lot, so just to make sure we have money...
cashCow.SendToAddress(invoiceAddress, secondPayment);
Logs.Tester.LogInformation("Second payment sent to " + invoiceAddress);
Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
@ -487,6 +743,7 @@ namespace BTCPayServer.Tests
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5000.0,
@ -498,7 +755,7 @@ namespace BTCPayServer.Tests
}, Facade.Merchant);
var repo = tester.PayTester.GetService<InvoiceRepository>();
var ctx = tester.PayTester.GetService<ApplicationDbContextFactory>().CreateContext();
Assert.Equal(0, invoice.CryptoInfo[0].TxCount);
Eventually(() =>
{
var textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery()
@ -521,11 +778,11 @@ namespace BTCPayServer.Tests
Assert.Equal("new", invoice.Status);
Assert.False((bool)((JValue)invoice.ExceptionStatus).Value);
Assert.Single(user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime));
Assert.Empty(user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime + TimeSpan.FromDays(1)));
Assert.Single(user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5)));
Assert.Single(user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5), invoice.InvoiceTime.DateTime));
Assert.Empty(user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5), invoice.InvoiceTime.DateTime - TimeSpan.FromDays(1)));
Assert.Single(user.BitPay.GetInvoices(invoice.InvoiceTime.UtcDateTime));
Assert.Empty(user.BitPay.GetInvoices(invoice.InvoiceTime.UtcDateTime + TimeSpan.FromDays(2)));
Assert.Single(user.BitPay.GetInvoices(invoice.InvoiceTime.UtcDateTime - TimeSpan.FromDays(5)));
Assert.Single(user.BitPay.GetInvoices(invoice.InvoiceTime.UtcDateTime - TimeSpan.FromDays(5), invoice.InvoiceTime.DateTime + TimeSpan.FromDays(1.0)));
Assert.Empty(user.BitPay.GetInvoices(invoice.InvoiceTime.UtcDateTime - TimeSpan.FromDays(5), invoice.InvoiceTime.DateTime - TimeSpan.FromDays(1)));
var firstPayment = Money.Coins(0.04m);
@ -554,14 +811,15 @@ namespace BTCPayServer.Tests
Assert.Equal(firstPayment, localInvoice.BtcPaid);
txFee = localInvoice.BtcDue - invoice.BtcDue;
Assert.Equal("paidPartial", localInvoice.ExceptionStatus.ToString());
Assert.Equal(1, localInvoice.CryptoInfo[0].TxCount);
Assert.NotEqual(localInvoice.BitcoinAddress, invoice.BitcoinAddress); //New address
Assert.True(IsMapped(invoice, ctx));
Assert.True(IsMapped(localInvoice, ctx));
invoiceEntity = repo.GetInvoice(null, invoice.Id, true).GetAwaiter().GetResult();
var historical1 = invoiceEntity.HistoricalAddresses.FirstOrDefault(h => h.GetAddress() == invoice.BitcoinAddress.ToString());
var historical1 = invoiceEntity.HistoricalAddresses.FirstOrDefault(h => h.GetAddress() == invoice.BitcoinAddress);
Assert.NotNull(historical1.UnAssigned);
var historical2 = invoiceEntity.HistoricalAddresses.FirstOrDefault(h => h.GetAddress() == localInvoice.BitcoinAddress.ToString());
var historical2 = invoiceEntity.HistoricalAddresses.FirstOrDefault(h => h.GetAddress() == localInvoice.BitcoinAddress);
Assert.Null(historical2.UnAssigned);
invoiceAddress = BitcoinAddress.Create(localInvoice.BitcoinAddress, cashCow.Network);
secondPayment = localInvoice.BtcDue;
@ -573,6 +831,7 @@ namespace BTCPayServer.Tests
{
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("paid", localInvoice.Status);
Assert.Equal(2, localInvoice.CryptoInfo[0].TxCount);
Assert.Equal(firstPayment + secondPayment, localInvoice.BtcPaid);
Assert.Equal(Money.Zero, localInvoice.BtcDue);
Assert.Equal(localInvoice.BitcoinAddress, invoiceAddress.ToString()); //no new address generated
@ -645,17 +904,21 @@ namespace BTCPayServer.Tests
var b = cached.GetRateAsync("JPY").GetAwaiter().GetResult();
//Manually check that cache get hit after 10 sec
var c = cached.GetRateAsync("JPY").GetAwaiter().GetResult();
var bitstamp = new CoinAverageRateProvider("BTC") { Exchange = "bitstamp" };
var bitstampRate = bitstamp.GetRateAsync("USD").GetAwaiter().GetResult();
Assert.Throws<RateUnavailableException>(() => bitstamp.GetRateAsync("XXXXX").GetAwaiter().GetResult());
}
private static bool IsMapped(Invoice invoice, ApplicationDbContext ctx)
{
var h = BitcoinAddress.Create(invoice.BitcoinAddress).ScriptPubKey.Hash;
return ctx.AddressInvoices.FirstOrDefault(i => i.InvoiceDataId == invoice.Id && i.GetHash() == h) != null;
var h = BitcoinAddress.Create(invoice.BitcoinAddress).ScriptPubKey.Hash.ToString();
return ctx.AddressInvoices.FirstOrDefault(i => i.InvoiceDataId == invoice.Id && i.GetAddress() == h) != null;
}
private void Eventually(Action act)
{
CancellationTokenSource cts = new CancellationTokenSource(10000);
CancellationTokenSource cts = new CancellationTokenSource(20000);
while (true)
{
try
@ -669,5 +932,22 @@ namespace BTCPayServer.Tests
}
}
}
private async Task EventuallyAsync(Func<Task> act)
{
CancellationTokenSource cts = new CancellationTokenSource(20000);
while (true)
{
try
{
await act();
break;
}
catch (XunitException) when (!cts.Token.IsCancellationRequested)
{
await Task.Delay(500);
}
}
}
}
}

@ -13,25 +13,27 @@ namespace BTCPayServer.Tests
{
// Unit test that generates temorary checkout Bitpay page
// https://forkbitpay.slack.com/archives/C7M093Z55/p1508293682000217
[Fact]
public void BitpayCheckout()
{
var key = new Key(Encoders.Hex.DecodeData("7b70a06f35562873e3dcb46005ed0fe78e1991ad906e56adaaafa40ba861e056"));
var url = new Uri("https://test.bitpay.com/");
var btcpay = new Bitpay(key, url);
var invoice = btcpay.CreateInvoice(new Invoice()
{
Price = 5.0,
Currency = "USD",
PosData = "posData",
OrderId = "cdfd8a5f-6928-4c3b-ba9b-ddf438029e73",
ItemDesc = "Hello from the otherside"
}, Facade.Merchant);
// Testnet of Bitpay down
//[Fact]
//public void BitpayCheckout()
//{
// var key = new Key(Encoders.Hex.DecodeData("7b70a06f35562873e3dcb46005ed0fe78e1991ad906e56adaaafa40ba861e056"));
// var url = new Uri("https://test.bitpay.com/");
// var btcpay = new Bitpay(key, url);
// var invoice = btcpay.CreateInvoice(new Invoice()
// {
// go to invoice.Url
Console.WriteLine(invoice.Url);
}
// Price = 5.0,
// Currency = "USD",
// PosData = "posData",
// OrderId = "cdfd8a5f-6928-4c3b-ba9b-ddf438029e73",
// ItemDesc = "Hello from the otherside"
// }, Facade.Merchant);
// // go to invoice.Url
// Console.WriteLine(invoice.Url);
//}
// Generating Extended public key to use on http://localhost:14142/stores/{storeId}
[Fact]

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

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

@ -1,7 +1,7 @@
version: "3"
# Run `docker-compose up dev` for bootstrapping your development environment
# Doing so will expose eclair API, NBXplorer, Bitcoind RPC and postgres port to the host so that tests can Run,
# Doing so will expose NBXplorer, Bitcoind RPC and postgres port to the host so that tests can Run,
# The Visual Studio launch setting `Docker-Regtest` is configured to use this environment.
services:
@ -17,17 +17,18 @@ services:
TESTS_POSTGRES: User ID=postgres;Host=postgres;Port=5432;Database=btcpayserver
TESTS_PORT: 80
TESTS_HOSTNAME: tests
TEST_CUSTOMERLIGHTNINGD: http://customer_lightningd:9835/
TEST_MERCHANTCHARGE: http://api-token:foiewnccewuify@lightning-charged:9112/
expose:
- "80"
links:
- nbxplorer
- postgres
- dev
extra_hosts:
- "tests:127.0.0.1"
# The dev container is not actually used, it is just handy to run `docker-compose up dev` to start all services
dev:
image: nicolasdorier/docker-bitcoin:0.15.0.1
image: nicolasdorier/docker-bitcoin:0.16.0
environment:
BITCOIN_EXTRA_ARGS: |
regtest=1
@ -35,9 +36,12 @@ services:
links:
- nbxplorer
- postgres
- customer_lightningd
- merchant_lightningd
- lightning-charged
nbxplorer:
image: nicolasdorier/nbxplorer:1.0.0.60
image: nicolasdorier/nbxplorer:1.0.1.22
ports:
- "32838:32838"
expose:
@ -61,8 +65,7 @@ services:
- litecoind
bitcoind:
container_name: btcpayserver_dev_bitcoind
image: nicolasdorier/docker-bitcoin:0.15.0.1
image: nicolasdorier/docker-bitcoin:0.16.0
environment:
BITCOIN_EXTRA_ARGS: |
rpcuser=ceiwHEbqWI83
@ -77,9 +80,68 @@ services:
expose:
- "43782" # RPC
- "39388" # P2P
volumes:
- "bitcoin_datadir:/data"
customer_lightningd:
image: nicolasdorier/clightning
environment:
LIGHTNINGD_OPT: |
bitcoin-datadir=/etc/bitcoin
bitcoin-rpcconnect=bitcoind
network=regtest
log-level=debug
ports:
- "30992:9835" # api port
expose:
- "9735" # server port
- "9835" # api port
volumes:
- "bitcoin_datadir:/etc/bitcoin"
- "customer_lightningd_datadir:/root/.lightning"
links:
- bitcoind
lightning-charged:
image: shesek/lightning-charge:0.3.5
environment:
NETWORK: regtest
API_TOKEN: foiewnccewuify
SKIP_BITCOIND: 1
BITCOIND_RPCCONNECT: bitcoind
volumes:
- "bitcoin_datadir:/etc/bitcoin"
- "lightning_charge_datadir:/data"
- "merchant_lightningd_datadir:/etc/lightning"
expose:
- "9112" # Charge
- "9735" # Lightning
ports:
- "54938:9112" # Charge
links:
- bitcoind
- merchant_lightningd
merchant_lightningd:
image: nicolasdorier/clightning
environment:
LIGHTNINGD_OPT: |
bitcoin-datadir=/etc/bitcoin
bitcoin-rpcconnect=bitcoind
network=regtest
log-level=debug
ports:
- "30993:9835" # api port
expose:
- "9735" # server port
- "9835" # api port
volumes:
- "bitcoin_datadir:/etc/bitcoin"
- "merchant_lightningd_datadir:/root/.lightning"
links:
- bitcoind
litecoind:
container_name: btcpayserver_dev_litecoind
image: nicolasdorier/docker-litecoin:0.14.2
environment:
BITCOIN_EXTRA_ARGS: |
@ -102,3 +164,9 @@ services:
- "39372:5432"
expose:
- "5432"
volumes:
bitcoin_datadir:
customer_lightningd_datadir:
merchant_lightningd_datadir:
lightning_charge_datadir:

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

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

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

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

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using NBitcoin;
using NBXplorer;
@ -61,13 +62,17 @@ namespace BTCPayServer
}
public string CryptoImagePath { get; set; }
public string LightningImagePath { get; set; }
public NBXplorer.NBXplorerNetwork NBXplorerNetwork { get; set; }
public BTCPayDefaultSettings DefaultSettings { get; set; }
public KeyPath CoinType { get; internal set; }
public int MaxTrackedConfirmation { get; internal set; } = 6;
public string CLightningNetworkName { get; internal set; }
public override string ToString()
{
return CryptoCode;
}
}
}
}

@ -20,13 +20,18 @@ namespace BTCPayServer
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
BlockExplorerLink = "https://testnet.smartbit.com.au/tx/{0}",
BlockExplorerLink = NBXplorerNetworkProvider.ChainType == ChainType.Main ? "https://www.smartbit.com.au/tx/{0}" : "https://testnet.smartbit.com.au/tx/{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "bitcoin",
DefaultRateProvider = btcRate,
CryptoImagePath = "imlegacy/bitcoin-symbol.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NBXplorerNetworkProvider.ChainType)
LightningImagePath = "imlegacy/btc-lightning.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NBXplorerNetworkProvider.ChainType),
CoinType = NBXplorerNetworkProvider.ChainType == ChainType.Main ? new KeyPath("0'") : new KeyPath("1'"),
CLightningNetworkName = ChainType == ChainType.Main ? "bitcoin" :
ChainType == ChainType.Test ? "testnet" :
ChainType == ChainType.Regtest ? "regtest" : null
});
}
}

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
using NBitcoin;
using NBXplorer;
namespace BTCPayServer
@ -18,13 +19,17 @@ namespace BTCPayServer
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
BlockExplorerLink = "https://live.blockcypher.com/ltc/tx/{0}/",
BlockExplorerLink = NBXplorerNetworkProvider.ChainType == ChainType.Main ? "https://live.blockcypher.com/ltc/tx/{0}/" : "http://explorer.litecointools.com/tx/{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "litecoin",
DefaultRateProvider = ltcRate,
CryptoImagePath = "imlegacy/litecoin-symbol.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NBXplorerNetworkProvider.ChainType)
LightningImagePath = "imlegacy/ltc-lightning.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NBXplorerNetworkProvider.ChainType),
CoinType = NBXplorerNetworkProvider.ChainType == ChainType.Main ? new KeyPath("2'") : new KeyPath("3'"),
CLightningNetworkName = ChainType == ChainType.Main ? "litecoin" :
ChainType == ChainType.Test ? "litecoin-testnet" : null
});
}
}

@ -25,13 +25,40 @@ namespace BTCPayServer
}
}
BTCPayNetworkProvider(BTCPayNetworkProvider filtered, string[] cryptoCodes)
{
ChainType = filtered.ChainType;
_NBXplorerNetworkProvider = new NBXplorerNetworkProvider(filtered.ChainType);
_Networks = new Dictionary<string, BTCPayNetwork>();
cryptoCodes = cryptoCodes.Select(c => c.ToUpperInvariant()).ToArray();
foreach (var network in filtered._Networks)
{
if(cryptoCodes.Contains(network.Key))
{
_Networks.Add(network.Key, network.Value);
}
}
}
public ChainType ChainType { get; set; }
public BTCPayNetworkProvider(ChainType chainType)
{
_NBXplorerNetworkProvider = new NBXplorerNetworkProvider(chainType);
ChainType = chainType;
InitBitcoin();
InitLitecoin();
}
/// <summary>
/// Keep only the specified crypto
/// </summary>
/// <param name="cryptoCodes">Crypto to support</param>
/// <returns></returns>
public BTCPayNetworkProvider Filter(string[] cryptoCodes)
{
return new BTCPayNetworkProvider(this, cryptoCodes);
}
[Obsolete("To use only for legacy stuff")]
public BTCPayNetwork BTC
{
@ -43,7 +70,7 @@ namespace BTCPayServer
public void Add(BTCPayNetwork network)
{
_Networks.Add(network.CryptoCode, network);
_Networks.Add(network.CryptoCode.ToUpperInvariant(), network);
}
public IEnumerable<BTCPayNetwork> GetAll()
@ -51,6 +78,11 @@ namespace BTCPayServer
return _Networks.Values.ToArray();
}
public bool Support(string cryptoCode)
{
return _Networks.ContainsKey(cryptoCode.ToUpperInvariant());
}
public BTCPayNetwork GetNetwork(string cryptoCode)
{
_Networks.TryGetValue(cryptoCode.ToUpperInvariant(), out BTCPayNetwork network);

@ -2,13 +2,18 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<Version>1.0.0.73</Version>
<Version>1.0.1.52</Version>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Build\dockerfiles\**" />
<Compile Remove="wwwroot\bundles\jqueryvalidate\**" />
<Content Remove="Build\dockerfiles\**" />
<Content Remove="wwwroot\bundles\jqueryvalidate\**" />
<EmbeddedResource Remove="Build\dockerfiles\**" />
<EmbeddedResource Remove="wwwroot\bundles\jqueryvalidate\**" />
<None Remove="Build\dockerfiles\**" />
<None Remove="wwwroot\bundles\jqueryvalidate\**" />
</ItemGroup>
<ItemGroup>
<None Remove="Currencies.txt" />
@ -17,26 +22,31 @@
<EmbeddedResource Include="Currencies.txt" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BuildBundlerMinifier" Version="2.6.362" />
<PackageReference Include="Hangfire" Version="1.6.17" />
<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="Microsoft.CodeAnalysis.CSharp" Version="2.6.1" />
<PackageReference Include="Meziantou.AspNetCore.BundleTagHelpers" Version="1.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
<PackageReference Include="NBitcoin" Version="4.0.0.51" />
<PackageReference Include="NBitpayClient" Version="1.0.0.16" />
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="2.6.0" />
<PackageReference Include="NBitcoin" Version="4.0.0.65" />
<PackageReference Include="NBitpayClient" Version="1.0.0.18" />
<PackageReference Include="DBreeze" Version="1.87.0" />
<PackageReference Include="NBXplorer.Client" Version="1.0.0.33" />
<PackageReference Include="NBXplorer.Client" Version="1.0.1.16" />
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.1" />
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.2" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.13" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.0.1" />
<PackageReference Include="System.ValueTuple" Version="4.4.0" />
<PackageReference Include="System.Xml.XmlSerializer" Version="4.0.11" />
<PackageReference Include="Text.Analyzers" Version="2.6.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.3" />
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.1" PrivateAssets="All" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.0.1" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
@ -98,5 +108,12 @@
<ItemGroup>
<Folder Include="Build\" />
<Folder Include="wwwroot\vendor\clipboard.js\" />
</ItemGroup>
<ItemGroup>
<None Update="devtest.pfx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

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

@ -58,28 +58,34 @@ namespace BTCPayServer.Configuration
var supportedChains = conf.GetOrDefault<string>("chains", "btc")
.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(t => t.ToUpperInvariant());
var validChains = new List<string>();
foreach (var net in new BTCPayNetworkProvider(ChainType).GetAll())
NetworkProvider = new BTCPayNetworkProvider(ChainType).Filter(supportedChains.ToArray());
foreach (var chain in supportedChains)
{
if (supportedChains.Contains(net.CryptoCode))
{
validChains.Add(net.CryptoCode);
NBXplorerConnectionSetting setting = new NBXplorerConnectionSetting();
setting.CryptoCode = net.CryptoCode;
setting.ExplorerUri = conf.GetOrDefault<Uri>($"{net.CryptoCode}.explorer.url", net.NBXplorerNetwork.DefaultSettings.DefaultUrl);
setting.CookieFile = conf.GetOrDefault<string>($"{net.CryptoCode}.explorer.cookiefile", net.NBXplorerNetwork.DefaultSettings.DefaultCookieFile);
NBXplorerConnectionSettings.Add(setting);
}
if (NetworkProvider.GetNetwork(chain) == null)
throw new ConfigException($"Invalid chains \"{chain}\"");
}
var validChains = new List<string>();
foreach (var net in NetworkProvider.GetAll())
{
NBXplorerConnectionSetting setting = new NBXplorerConnectionSetting();
setting.CryptoCode = net.CryptoCode;
setting.ExplorerUri = conf.GetOrDefault<Uri>($"{net.CryptoCode}.explorer.url", net.NBXplorerNetwork.DefaultSettings.DefaultUrl);
setting.CookieFile = conf.GetOrDefault<string>($"{net.CryptoCode}.explorer.cookiefile", net.NBXplorerNetwork.DefaultSettings.DefaultCookieFile);
NBXplorerConnectionSettings.Add(setting);
}
var invalidChains = String.Join(',', supportedChains.Where(s => !validChains.Contains(s)).ToArray());
if(!string.IsNullOrEmpty(invalidChains))
throw new ConfigException($"Invalid chains {invalidChains}");
Logs.Configuration.LogInformation("Supported chains: " + String.Join(',', supportedChains.ToArray()));
PostgresConnectionString = conf.GetOrDefault<string>("postgres", null);
BundleJsCss = conf.GetOrDefault<bool>("bundlejscss", true);
ExternalUrl = conf.GetOrDefault<Uri>("externalurl", null);
InternalLightningNode = conf.GetOrDefault<Uri>("internallightningnode", null);
}
public Uri InternalLightningNode { get; set; }
public BTCPayNetworkProvider NetworkProvider { get; set; }
public string PostgresConnectionString
{
get;
@ -90,5 +96,10 @@ namespace BTCPayServer.Configuration
get;
set;
}
public bool BundleJsCss
{
get;
set;
}
}
}

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

@ -38,6 +38,8 @@ namespace BTCPayServer.Configuration
app.Option($"--{crypto}explorercookiefile", $"Path to the cookie file (default: {network.NBXplorerNetwork.DefaultSettings.DefaultCookieFile})", 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("--internallightningnode", $"An internal lightning node which can be used without https requirement and easily configured by the admin (default: empty)", CommandOptionType.SingleValue);
app.Option("--bundlejscss", $"Bundle javascript and css files for better performance (default: true)", CommandOptionType.SingleValue);
return app;
}

@ -15,6 +15,7 @@ using BTCPayServer.Models.AccountViewModels;
using BTCPayServer.Services;
using BTCPayServer.Services.Mails;
using BTCPayServer.Services.Stores;
using BTCPayServer.Logging;
namespace BTCPayServer.Controllers
{
@ -25,10 +26,10 @@ namespace BTCPayServer.Controllers
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly IEmailSender _emailSender;
private readonly ILogger _logger;
StoreRepository storeRepository;
RoleManager<IdentityRole> _RoleManager;
SettingsRepository _SettingsRepository;
ILogger _logger;
public AccountController(
UserManager<ApplicationUser> userManager,
@ -36,16 +37,15 @@ namespace BTCPayServer.Controllers
StoreRepository storeRepository,
SignInManager<ApplicationUser> signInManager,
IEmailSender emailSender,
SettingsRepository settingsRepository,
ILogger<AccountController> logger)
SettingsRepository settingsRepository)
{
this.storeRepository = storeRepository;
_userManager = userManager;
_signInManager = signInManager;
_emailSender = emailSender;
_logger = logger;
_RoleManager = roleManager;
_SettingsRepository = settingsRepository;
_logger = Logs.PayServer;
}
[TempData]
@ -150,7 +150,7 @@ namespace BTCPayServer.Controllers
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var authenticatorCode = model.TwoFactorCode.Replace(" ", string.Empty).Replace("-", string.Empty);
var authenticatorCode = model.TwoFactorCode.Replace(" ", string.Empty, StringComparison.InvariantCulture).Replace("-", string.Empty, StringComparison.InvariantCulture);
var result = await _signInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, rememberMe, model.RememberMachine);
@ -204,7 +204,7 @@ namespace BTCPayServer.Controllers
throw new ApplicationException($"Unable to load two-factor authentication user.");
}
var recoveryCode = model.RecoveryCode.Replace(" ", string.Empty);
var recoveryCode = model.RecoveryCode.Replace(" ", string.Empty, StringComparison.InvariantCulture);
var result = await _signInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode);
@ -259,12 +259,10 @@ namespace BTCPayServer.Controllers
var result = await _userManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
_logger.LogInformation("User created a new account with password.");
var admin = await _userManager.GetUsersInRoleAsync(Roles.ServerAdmin);
Logs.PayServer.LogInformation($"A new user just registered {user.Email} {(admin.Count == 0 ? "(admin)" : "")}");
if (admin.Count == 0)
{
_logger.LogInformation("Admin created.");
await _RoleManager.CreateAsync(new IdentityRole(Roles.ServerAdmin));
await _userManager.AddToRoleAsync(user, Roles.ServerAdmin);
}
@ -291,7 +289,7 @@ namespace BTCPayServer.Controllers
return View(model);
}
/// <summary>
/// <summary>
/// Test property
/// </summary>
public string RegisteredUserId

@ -9,13 +9,15 @@ using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Payments;
namespace BTCPayServer.Controllers
{
public partial class InvoiceController
{
[HttpGet]
[Route("i/{invoiceId}")]
[Route("i/{invoiceId}/{cryptoCode?}")]
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest")]
public async Task<IActionResult> GetInvoiceRequest(string invoiceId, string cryptoCode = null)
{
@ -23,11 +25,12 @@ namespace BTCPayServer.Controllers
cryptoCode = "BTC";
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
var network = _NetworkProvider.GetNetwork(cryptoCode);
if (invoice == null || invoice.IsExpired() || network == null || !invoice.Support(network))
var paymentMethodId = new PaymentMethodId(cryptoCode, Payments.PaymentTypes.BTCLike);
if (invoice == null || invoice.IsExpired() || network == null || !invoice.Support(paymentMethodId))
return NotFound();
var dto = invoice.EntityToDTO(_NetworkProvider);
var cryptoData = dto.CryptoInfo.First(c => c.CryptoCode.Equals(cryptoCode, StringComparison.OrdinalIgnoreCase));
var paymentMethod = dto.CryptoInfo.First(c => c.GetpaymentMethodId() == paymentMethodId);
PaymentRequest request = new PaymentRequest
{
DetailsVersion = 1
@ -35,7 +38,7 @@ namespace BTCPayServer.Controllers
request.Details.Expires = invoice.ExpirationTime;
request.Details.Memo = invoice.ProductInformation.ItemDesc;
request.Details.Network = network.NBitcoinNetwork;
request.Details.Outputs.Add(new PaymentOutput() { Amount = cryptoData.Due, Script = BitcoinAddress.Create(cryptoData.Address, network.NBitcoinNetwork).ScriptPubKey });
request.Details.Outputs.Add(new PaymentOutput() { Amount = paymentMethod.Due, Script = BitcoinAddress.Create(paymentMethod.Address, network.NBitcoinNetwork).ScriptPubKey });
request.Details.MerchantData = Encoding.UTF8.GetBytes(invoice.Id);
request.Details.Time = DateTimeOffset.UtcNow;
request.Details.PaymentUrl = new Uri(invoice.ServerUrl.WithTrailingSlash() + ($"i/{invoice.Id}"), UriKind.Absolute);
@ -69,7 +72,7 @@ namespace BTCPayServer.Controllers
if (cryptoCode == null)
cryptoCode = "BTC";
var network = _NetworkProvider.GetNetwork(cryptoCode);
if (network == null || invoice == null || invoice.IsExpired() || !invoice.Support(network))
if (network == null || invoice == null || invoice.IsExpired() || !invoice.Support(new PaymentMethodId(cryptoCode, Payments.PaymentTypes.BTCLike)))
return NotFound();
var wallet = _WalletProvider.GetWallet(network);

@ -20,6 +20,7 @@ using System.Net.WebSockets;
using System.Threading;
using BTCPayServer.Events;
using NBXplorer;
using BTCPayServer.Payments;
namespace BTCPayServer.Controllers
{
@ -33,7 +34,8 @@ namespace BTCPayServer.Controllers
{
UserId = GetUserId(),
InvoiceId = invoiceId,
IncludeAddresses = true
IncludeAddresses = true,
IncludeEvents = true
})).FirstOrDefault();
if (invoice == null)
return NotFound();
@ -47,6 +49,7 @@ namespace BTCPayServer.Controllers
StoreLink = Url.Action(nameof(StoresController.UpdateStore), "Stores", new { storeId = store.Id }),
Id = invoice.Id,
Status = invoice.Status,
TransactionSpeed = invoice.SpeedPolicy == SpeedPolicy.HighSpeed ? "high" : invoice.SpeedPolicy == SpeedPolicy.MediumSpeed ? "medium" : "low",
RefundEmail = invoice.RefundMail,
CreatedDate = invoice.InvoiceTime,
ExpirationDate = invoice.ExpirationTime,
@ -55,131 +58,190 @@ namespace BTCPayServer.Controllers
BuyerInformation = invoice.BuyerInformation,
Fiat = FormatCurrency((decimal)dto.Price, dto.Currency),
NotificationUrl = invoice.NotificationURL,
RedirectUrl = invoice.RedirectURL,
ProductInformation = invoice.ProductInformation,
StatusException = invoice.ExceptionStatus
StatusException = invoice.ExceptionStatus,
Events = invoice.Events
};
foreach (var data in invoice.GetCryptoData(null))
foreach (var data in invoice.GetPaymentMethods(null))
{
var cryptoInfo = dto.CryptoInfo.First(o => o.CryptoCode.Equals(data.Key, StringComparison.OrdinalIgnoreCase));
var accounting = data.Value.Calculate();
var paymentNetwork = _NetworkProvider.GetNetwork(data.Key);
var cryptoInfo = dto.CryptoInfo.First(o => o.GetpaymentMethodId() == data.GetId());
var accounting = data.Calculate();
var paymentMethodId = data.GetId();
var cryptoPayment = new InvoiceDetailsModel.CryptoPayment();
cryptoPayment.CryptoCode = paymentNetwork.CryptoCode;
cryptoPayment.Due = accounting.Due.ToString() + $" {paymentNetwork.CryptoCode}";
cryptoPayment.Paid = accounting.CryptoPaid.ToString() + $" {paymentNetwork.CryptoCode}";
cryptoPayment.Address = data.Value.DepositAddress.ToString();
cryptoPayment.Rate = FormatCurrency(data.Value);
cryptoPayment.PaymentMethod = ToString(paymentMethodId);
cryptoPayment.Due = accounting.Due.ToString() + $" {paymentMethodId.CryptoCode}";
cryptoPayment.Paid = accounting.CryptoPaid.ToString() + $" {paymentMethodId.CryptoCode}";
var onchainMethod = data.GetPaymentMethodDetails() as Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod;
if(onchainMethod != null)
{
cryptoPayment.Address = onchainMethod.DepositAddress;
}
cryptoPayment.Rate = FormatCurrency(data);
cryptoPayment.PaymentUrl = cryptoInfo.PaymentUrls.BIP21;
model.CryptoPayments.Add(cryptoPayment);
}
var payments = invoice
.GetPayments()
.Where(p => p.GetPaymentMethodId().PaymentType == PaymentTypes.BTCLike)
.Select(async payment =>
{
var paymentData = (Payments.Bitcoin.BitcoinLikePaymentData)payment.GetCryptoPaymentData();
var m = new InvoiceDetailsModel.Payment();
var paymentNetwork = _NetworkProvider.GetNetwork(payment.GetCryptoCode());
m.CryptoCode = payment.GetCryptoCode();
m.DepositAddress = payment.GetScriptPubKey().GetDestinationAddress(paymentNetwork.NBitcoinNetwork);
m.Confirmations = (await _ExplorerClients.GetExplorerClient(payment.GetCryptoCode())?.GetTransactionAsync(payment.Outpoint.Hash))?.Confirmations ?? 0;
m.TransactionId = payment.Outpoint.Hash.ToString();
m.PaymentMethod = ToString(payment.GetPaymentMethodId());
m.DepositAddress = paymentData.Output.ScriptPubKey.GetDestinationAddress(paymentNetwork.NBitcoinNetwork);
int confirmationCount = 0;
if(paymentData.Legacy) // The confirmation count in the paymentData is not up to date
{
confirmationCount = (await ((ExplorerClientProvider)_ServiceProvider.GetService(typeof(ExplorerClientProvider))).GetExplorerClient(payment.GetCryptoCode())?.GetTransactionAsync(paymentData.Outpoint.Hash))?.Confirmations ?? 0;
}
else
{
confirmationCount = paymentData.ConfirmationCount;
}
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(paymentNetwork.BlockExplorerLink, m.TransactionId);
m.TransactionLink = string.Format(CultureInfo.InvariantCulture, paymentNetwork.BlockExplorerLink, m.TransactionId);
m.Replaced = !payment.Accounted;
return m;
})
.ToArray();
await Task.WhenAll(payments);
model.Addresses = invoice.HistoricalAddresses;
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.StatusMessage = StatusMessage;
return View(model);
}
private string ToString(PaymentMethodId paymentMethodId)
{
var type = paymentMethodId.PaymentType.ToString();
switch (paymentMethodId.PaymentType)
{
case PaymentTypes.BTCLike:
type = "On-Chain";
break;
case PaymentTypes.LightningLike:
type = "Off-Chain";
break;
}
return $"{paymentMethodId.CryptoCode} ({type})";
}
[HttpGet]
[Route("i/{invoiceId}")]
[Route("i/{invoiceId}/{cryptoCode}")]
[Route("i/{invoiceId}/{paymentMethodId}")]
[Route("invoice")]
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest", false)]
[XFrameOptionsAttribute(null)]
public async Task<IActionResult> Checkout(string invoiceId, string id = null, string cryptoCode = null)
public async Task<IActionResult> Checkout(string invoiceId, string id = null, string paymentMethodId = null)
{
//Keep compatibility with Bitpay
invoiceId = invoiceId ?? id;
id = invoiceId;
////
var model = await GetInvoiceModel(invoiceId, cryptoCode);
var model = await GetInvoiceModel(invoiceId, paymentMethodId);
if (model == null)
return NotFound();
return View(nameof(Checkout), model);
}
private async Task<PaymentModel> GetInvoiceModel(string invoiceId, string cryptoCode)
private async Task<PaymentModel> GetInvoiceModel(string invoiceId, string paymentMethodIdStr)
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
if (invoice == null)
return null;
var store = await _StoreRepository.FindStore(invoice.StoreId);
bool isDefaultCrypto = false;
if (cryptoCode == null)
{
cryptoCode = store.GetDefaultCrypto();
if (paymentMethodIdStr == null)
{
paymentMethodIdStr = store.GetDefaultCrypto();
isDefaultCrypto = true;
}
var network = _NetworkProvider.GetNetwork(cryptoCode);
var paymentMethodId = PaymentMethodId.Parse(paymentMethodIdStr);
var network = _NetworkProvider.GetNetwork(paymentMethodId.CryptoCode);
if (invoice == null || network == null)
return null;
if(!invoice.Support(network))
if (!invoice.Support(paymentMethodId))
{
if(!isDefaultCrypto)
return null;
network = invoice.GetCryptoData(_NetworkProvider).First().Value.Network;
var paymentMethodTemp = invoice.GetPaymentMethods(_NetworkProvider).First();
network = paymentMethodTemp.Network;
paymentMethodId = paymentMethodTemp.GetId();
}
var cryptoData = invoice.GetCryptoData(network, _NetworkProvider);
var paymentMethod = invoice.GetPaymentMethod(paymentMethodId, _NetworkProvider);
var paymentMethodDetails = paymentMethod.GetPaymentMethodDetails();
var dto = invoice.EntityToDTO(_NetworkProvider);
var cryptoInfo = dto.CryptoInfo.First(o => o.CryptoCode == network.CryptoCode);
var cryptoInfo = dto.CryptoInfo.First(o => o.GetpaymentMethodId() == paymentMethodId);
var currency = invoice.ProductInformation.Currency;
var accounting = cryptoData.Calculate();
var accounting = paymentMethod.Calculate();
var model = new PaymentModel()
{
CryptoCode = network.CryptoCode,
PaymentMethodId = paymentMethodId.ToString(),
ServerUrl = HttpContext.Request.GetAbsoluteRoot(),
OrderId = invoice.OrderId,
InvoiceId = invoice.Id,
BtcAddress = cryptoData.DepositAddress,
BtcAddress = paymentMethodDetails.GetPaymentDestination(),
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ToString(),
BtcDue = accounting.Due.ToString(),
CustomerEmail = invoice.RefundMail,
ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds),
MaxTimeSeconds = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalSeconds,
MaxTimeMinutes = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalMinutes,
ItemDesc = invoice.ProductInformation.ItemDesc,
Rate = FormatCurrency(cryptoData),
Rate = FormatCurrency(paymentMethod),
MerchantRefLink = invoice.RedirectURL ?? "/",
StoreName = store.StoreName,
InvoiceBitcoinUrl = cryptoInfo.PaymentUrls.BIP21,
TxCount = accounting.TxCount,
InvoiceBitcoinUrl = paymentMethodId.PaymentType == PaymentTypes.BTCLike ? cryptoInfo.PaymentUrls.BIP21 :
paymentMethodId.PaymentType == PaymentTypes.LightningLike ? cryptoInfo.PaymentUrls.BOLT11 :
throw new NotSupportedException(),
InvoiceBitcoinUrlQR = paymentMethodId.PaymentType == PaymentTypes.BTCLike ? cryptoInfo.PaymentUrls.BIP21 :
paymentMethodId.PaymentType == PaymentTypes.LightningLike ? cryptoInfo.PaymentUrls.BOLT11.ToUpperInvariant() :
throw new NotSupportedException(),
TxCount = accounting.TxRequired,
BtcPaid = accounting.Paid.ToString(),
Status = invoice.Status,
CryptoImage = "/" + Url.Content(network.CryptoImagePath),
NetworkFeeDescription = $"{accounting.TxCount} transaction{(accounting.TxCount > 1 ? "s" : "")} x {cryptoData.TxFee} {network.CryptoCode}",
AvailableCryptos = invoice.GetCryptoData(_NetworkProvider)
.Where(i => i.Value.Network != null)
CryptoImage = "/" + GetImage(paymentMethodId, network),
NetworkFeeDescription = $"{accounting.TxRequired} transaction{(accounting.TxRequired > 1 ? "s" : "")} x {paymentMethodDetails.GetTxFee()} {network.CryptoCode}",
AllowCoinConversion = store.GetStoreBlob().AllowCoinConversion,
AvailableCryptos = invoice.GetPaymentMethods(_NetworkProvider)
.Where(i => i.Network != null)
.Select(kv=> new PaymentModel.AvailableCrypto()
{
CryptoCode = kv.Key,
CryptoImage = "/" + kv.Value.Network.CryptoImagePath,
Link = Url.Action(nameof(Checkout), new { invoiceId = invoiceId, cryptoCode = kv.Key })
PaymentMethodId = kv.GetId().ToString(),
CryptoImage = "/" + GetImage(kv.GetId(), kv.Network),
Link = Url.Action(nameof(Checkout), new { invoiceId = invoiceId, paymentMethodId = kv.GetId().ToString() })
}).Where(c => c.CryptoImage != "/")
.ToList()
};
var isMultiCurrency = invoice.GetPayments().Select(p=>p.GetCryptoCode()).Concat(new[] { network.CryptoCode }).Distinct().Count() > 1;
var isMultiCurrency = invoice.GetPayments().Select(p=>p.GetPaymentMethodId()).Concat(new[] { paymentMethod.GetId() }).Distinct().Count() > 1;
if (isMultiCurrency)
model.NetworkFeeDescription = $"{accounting.NetworkFee} {network.CryptoCode}";
@ -188,10 +250,15 @@ namespace BTCPayServer.Controllers
return model;
}
private string FormatCurrency(CryptoData cryptoData)
private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network)
{
string currency = cryptoData.ParentEntity.ProductInformation.Currency;
return FormatCurrency(cryptoData.Rate, currency);
return (paymentMethodId.PaymentType == PaymentTypes.BTCLike ? Url.Content(network.CryptoImagePath) : Url.Content(network.LightningImagePath));
}
private string FormatCurrency(PaymentMethod paymentMethod)
{
string currency = paymentMethod.ParentEntity.ProductInformation.Currency;
return FormatCurrency(paymentMethod.Rate, currency);
}
public string FormatCurrency(decimal price, string currency)
{
@ -202,19 +269,19 @@ namespace BTCPayServer.Controllers
{
StringBuilder builder = new StringBuilder();
if (expiration.Days >= 1)
builder.Append(expiration.Days.ToString());
builder.Append(expiration.Days.ToString(CultureInfo.InvariantCulture));
if (expiration.Hours >= 1)
builder.Append(expiration.Hours.ToString("00"));
builder.Append($"{expiration.Minutes.ToString("00")}:{expiration.Seconds.ToString("00")}");
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}/{cryptoCode}/status")]
public async Task<IActionResult> GetStatus(string invoiceId, string cryptoCode)
[Route("i/{invoiceId}/{paymentMethodId}/status")]
public async Task<IActionResult> GetStatus(string invoiceId, string paymentMethodId = null)
{
var model = await GetInvoiceModel(invoiceId, cryptoCode);
var model = await GetInvoiceModel(invoiceId, paymentMethodId);
if (model == null)
return NotFound();
return Json(model);
@ -234,8 +301,8 @@ namespace BTCPayServer.Controllers
try
{
leases.Add(_EventAggregator.Subscribe<Events.InvoiceDataChangedEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
leases.Add(_EventAggregator.Subscribe<Events.InvoicePaymentEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
leases.Add(_EventAggregator.Subscribe<Events.InvoiceStatusChangedEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
leases.Add(_EventAggregator.Subscribe<Events.InvoiceNewAddressEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
leases.Add(_EventAggregator.Subscribe<Events.InvoiceEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
while (true)
{
var message = await webSocket.ReceiveAsync(DummyBuffer, default(CancellationToken));
@ -246,7 +313,7 @@ namespace BTCPayServer.Controllers
finally
{
leases.Dispose();
await CloseSocket(webSocket);
await webSocket.CloseSocket();
}
return new EmptyResult();
}
@ -265,21 +332,6 @@ namespace BTCPayServer.Controllers
catch { try { webSocket.Dispose(); } catch { } }
}
private static async Task CloseSocket(WebSocket webSocket)
{
try
{
if (webSocket.State == WebSocketState.Open)
{
CancellationTokenSource cts = new CancellationTokenSource();
cts.CancelAfter(5000);
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", cts.Token);
}
}
catch { }
finally { try { webSocket.Dispose(); } catch { } }
}
[HttpPost]
[Route("i/{invoiceId}/UpdateCustomer")]
public async Task<IActionResult> UpdateCustomer(string invoiceId, [FromBody]UpdateCustomerModel data)
@ -314,8 +366,10 @@ namespace BTCPayServer.Controllers
model.Invoices.Add(new InvoiceModel()
{
Status = invoice.Status,
Date = invoice.InvoiceTime,
Date = Prettify(invoice.InvoiceTime),
InvoiceId = invoice.Id,
OrderId = invoice.OrderId ?? string.Empty,
RedirectUrl = invoice.RedirectURL ?? string.Empty,
AmountCurrency = $"{invoice.ProductInformation.Price.ToString(CultureInfo.InvariantCulture)} {invoice.ProductInformation.Currency}"
});
}
@ -325,6 +379,30 @@ 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")]
@ -352,7 +430,7 @@ namespace BTCPayServer.Controllers
return View(model);
}
var store = await _StoreRepository.FindStore(model.StoreId, GetUserId());
if (store.GetDerivationStrategies(_NetworkProvider).Count() == 0)
if (store.GetSupportedPaymentMethods(_NetworkProvider).Count() == 0)
{
StatusMessage = "Error: You need to configure the derivation scheme in order to create an invoice";
return RedirectToAction(nameof(StoresController.UpdateStore), "Stores", new
@ -411,6 +489,7 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> InvalidatePaidInvoice(string invoiceId)
{
await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoiceId);
_EventAggregator.Publish(new InvoiceEvent(invoiceId, 1008, "invoice_markedInvalid"));
return RedirectToAction(nameof(ListInvoices));
}

@ -39,62 +39,80 @@ using Microsoft.AspNetCore.Mvc.Routing;
using NBXplorer.DerivationStrategy;
using NBXplorer;
using BTCPayServer.HostedServices;
using BTCPayServer.Payments;
namespace BTCPayServer.Controllers
{
public partial class InvoiceController : Controller
{
InvoiceRepository _InvoiceRepository;
BTCPayWalletProvider _WalletProvider;
IRateProviderFactory _RateProviders;
StoreRepository _StoreRepository;
UserManager<ApplicationUser> _UserManager;
IFeeProviderFactory _FeeProviderFactory;
private CurrencyNameTable _CurrencyNameTable;
EventAggregator _EventAggregator;
BTCPayNetworkProvider _NetworkProvider;
ExplorerClientProvider _ExplorerClients;
public InvoiceController(InvoiceRepository invoiceRepository,
private readonly BTCPayWalletProvider _WalletProvider;
IServiceProvider _ServiceProvider;
public InvoiceController(
IServiceProvider serviceProvider,
InvoiceRepository invoiceRepository,
CurrencyNameTable currencyNameTable,
UserManager<ApplicationUser> userManager,
BTCPayWalletProvider walletProvider,
IRateProviderFactory rateProviders,
StoreRepository storeRepository,
EventAggregator eventAggregator,
BTCPayNetworkProvider networkProvider,
ExplorerClientProvider explorerClientProviders,
IFeeProviderFactory feeProviderFactory)
BTCPayWalletProvider walletProvider,
BTCPayNetworkProvider networkProvider)
{
_ExplorerClients = explorerClientProviders;
_ServiceProvider = serviceProvider;
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
_StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository));
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
_WalletProvider = walletProvider ?? throw new ArgumentNullException(nameof(walletProvider));
_RateProviders = rateProviders ?? throw new ArgumentNullException(nameof(rateProviders));
_UserManager = userManager;
_FeeProviderFactory = feeProviderFactory ?? throw new ArgumentNullException(nameof(feeProviderFactory));
_EventAggregator = eventAggregator;
_NetworkProvider = networkProvider;
_WalletProvider = walletProvider;
}
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl, double expiryMinutes = 15)
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl)
{
var derivationStrategies = store.GetDerivationStrategies(_NetworkProvider).ToList();
if (derivationStrategies.Count == 0)
throw new BitpayHttpException(400, "This store has not configured the derivation strategy");
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.SetDerivationStrategies(derivationStrategies);
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;
if (notificationUri == null || (notificationUri.Scheme != "http" && notificationUri.Scheme != "https")) //TODO: Filer non routable addresses ?
notificationUri = null;
EmailAddressAttribute emailValidator = new EmailAddressAttribute();
entity.ExpirationTime = entity.InvoiceTime.AddMinutes(expiryMinutes);
entity.ExpirationTime = entity.InvoiceTime.AddMinutes(storeBlob.InvoiceExpiration);
entity.MonitoringExpiration = entity.ExpirationTime + TimeSpan.FromMinutes(storeBlob.MonitoringExpiration);
entity.OrderId = invoice.OrderId;
entity.ServerUrl = serverUrl;
@ -115,80 +133,65 @@ namespace BTCPayServer.Controllers
entity.Status = "new";
entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy);
var queries = derivationStrategies
.Select(derivationStrategy => (Wallet: _WalletProvider.GetWallet(derivationStrategy.Network),
DerivationStrategy: derivationStrategy.DerivationStrategyBase,
Network: derivationStrategy.Network,
RateProvider: _RateProviders.GetRateProvider(derivationStrategy.Network),
FeeRateProvider: _FeeProviderFactory.CreateFeeProvider(derivationStrategy.Network)))
.Where(_ => _.Wallet != null &&
_.FeeRateProvider != null &&
_.RateProvider != null)
.Select(_ =>
{
return new
var methods = supportedPaymentMethods
.Select(async o =>
{
network = _.Network,
getFeeRate = _.FeeRateProvider.GetFeeRateAsync(),
getRate = _.RateProvider.GetRateAsync(invoice.Currency),
getAddress = _.Wallet.ReserveAddressAsync(_.DerivationStrategy)
};
});
bool legacyBTCisSet = false;
var cryptoDatas = new Dictionary<string, CryptoData>();
foreach (var q in queries)
{
CryptoData cryptoData = new CryptoData();
cryptoData.CryptoCode = q.network.CryptoCode;
cryptoData.FeeRate = (await q.getFeeRate);
cryptoData.TxFee = GetTxFee(storeBlob, cryptoData.FeeRate); // assume price for 100 bytes
cryptoData.Rate = await q.getRate;
cryptoData.DepositAddress = (await q.getAddress).ToString();
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 (q.network.IsBTC)
{
legacyBTCisSet = true;
entity.TxFee = cryptoData.TxFee;
entity.Rate = cryptoData.Rate;
entity.DepositAddress = cryptoData.DepositAddress;
}
if (paymentMethod.GetId().IsBTCOnChain)
{
entity.TxFee = paymentMethod.TxFee;
entity.Rate = paymentMethod.Rate;
entity.DepositAddress = paymentMethod.DepositAddress;
}
#pragma warning restore CS0618
cryptoDatas.Add(cryptoData.CryptoCode, cryptoData);
}
if (!legacyBTCisSet)
return paymentMethod;
});
var paymentMethods = new PaymentMethodDictionary();
foreach (var method in methods)
{
// Legacy Bitpay clients expect information for BTC information, even if the store do not support it
paymentMethods.Add(await method);
}
#pragma warning disable CS0618
// Legacy Bitpay clients expect information for BTC information, even if the store do not support it
var legacyBTCisSet = paymentMethods.Any(p => p.GetId().IsBTCOnChain);
if (!legacyBTCisSet && _NetworkProvider.BTC != null)
{
var btc = _NetworkProvider.BTC;
var feeProvider = _FeeProviderFactory.CreateFeeProvider(btc);
var rateProvider = _RateProviders.GetRateProvider(btc);
var feeProvider = ((IFeeProviderFactory)_ServiceProvider.GetService(typeof(IFeeProviderFactory))).CreateFeeProvider(btc);
var rateProvider = storeBlob.ApplyRateRules(btc, _RateProviders.GetRateProvider(btc, false));
if (feeProvider != null && rateProvider != null)
{
var gettingFee = feeProvider.GetFeeRateAsync();
var gettingRate = rateProvider.GetRateAsync("BTC");
var gettingRate = rateProvider.GetRateAsync(invoice.Currency);
entity.TxFee = GetTxFee(storeBlob, await gettingFee);
entity.Rate = await gettingRate;
}
// So users does not crash if they check depositAddress is not set
entity.DepositAddress = cryptoDatas.First().Value.DepositAddress;
#pragma warning restore CS0618
}
entity.SetCryptoData(cryptoDatas);
entity.SetPaymentMethods(paymentMethods);
entity.PosData = invoice.PosData;
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, _NetworkProvider);
_EventAggregator.Publish(new Events.InvoiceCreatedEvent(entity.Id));
_EventAggregator.Publish(new Events.InvoiceEvent(entity, 1001, "invoice_created"));
var resp = entity.EntityToDTO(_NetworkProvider);
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
}
#pragma warning disable CS0618
private static Money GetTxFee(StoreBlob storeBlob, FeeRate feeRate)
{
return storeBlob.NetworkFeeDisabled ? Money.Zero : feeRate.GetFee(100);
}
#pragma warning restore CS0618
private SpeedPolicy ParseSpeedPolicy(string transactionSpeed, SpeedPolicy defaultPolicy)
{

@ -20,6 +20,7 @@ using NBitcoin;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
using BTCPayServer.Services.Mails;
using System.Globalization;
namespace BTCPayServer.Controllers
{
@ -33,7 +34,6 @@ namespace BTCPayServer.Controllers
private readonly ILogger _logger;
private readonly UrlEncoder _urlEncoder;
TokenRepository _TokenRepository;
private readonly BTCPayWallet _Wallet;
IHostingEnvironment _Env;
StoreRepository _StoreRepository;
@ -47,7 +47,7 @@ namespace BTCPayServer.Controllers
ILogger<ManageController> logger,
UrlEncoder urlEncoder,
TokenRepository tokenRepository,
BTCPayWallet wallet,
BTCPayWalletProvider walletProvider,
StoreRepository storeRepository,
IHostingEnvironment env)
{
@ -57,7 +57,6 @@ namespace BTCPayServer.Controllers
_logger = logger;
_urlEncoder = urlEncoder;
_TokenRepository = tokenRepository;
_Wallet = wallet;
_Env = env;
_StoreRepository = storeRepository;
}
@ -369,7 +368,7 @@ namespace BTCPayServer.Controllers
if (!user.TwoFactorEnabled)
{
throw new ApplicationException($"Unexpected error occured disabling 2FA for user with ID '{user.Id}'.");
throw new ApplicationException($"Unexpected error occurred disabling 2FA for user with ID '{user.Id}'.");
}
return View(nameof(Disable2fa));
@ -388,7 +387,7 @@ namespace BTCPayServer.Controllers
var disable2faResult = await _userManager.SetTwoFactorEnabledAsync(user, false);
if (!disable2faResult.Succeeded)
{
throw new ApplicationException($"Unexpected error occured disabling 2FA for user with ID '{user.Id}'.");
throw new ApplicationException($"Unexpected error occurred disabling 2FA for user with ID '{user.Id}'.");
}
_logger.LogInformation("User with ID {UserId} has disabled 2fa.", user.Id);
@ -436,7 +435,7 @@ namespace BTCPayServer.Controllers
}
// Strip spaces and hypens
var verificationCode = model.Code.Replace(" ", string.Empty).Replace("-", string.Empty);
var verificationCode = model.Code.Replace(" ", string.Empty, StringComparison.InvariantCulture).Replace("-", string.Empty, StringComparison.InvariantCulture);
var is2faTokenValid = await _userManager.VerifyTwoFactorTokenAsync(
user, _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode);
@ -526,7 +525,7 @@ namespace BTCPayServer.Controllers
private string GenerateQrCodeUri(string email, string unformattedKey)
{
return string.Format(
return string.Format(CultureInfo.InvariantCulture,
AuthenicatorUriFormat,
_urlEncoder.Encode("BTCPayServer"),
_urlEncoder.Encode(email),

@ -7,34 +7,68 @@ using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Filters;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
namespace BTCPayServer.Controllers
{
public class RateController : Controller
{
IRateProvider _RateProvider;
IRateProviderFactory _RateProviderFactory;
BTCPayNetworkProvider _NetworkProvider;
CurrencyNameTable _CurrencyNameTable;
public RateController(IRateProvider rateProvider, CurrencyNameTable currencyNameTable)
StoreRepository _StoreRepo;
public RateController(
IRateProviderFactory rateProviderFactory,
BTCPayNetworkProvider networkProvider,
StoreRepository storeRepo,
CurrencyNameTable currencyNameTable)
{
_RateProvider = rateProvider ?? throw new ArgumentNullException(nameof(rateProvider));
_RateProviderFactory = rateProviderFactory ?? throw new ArgumentNullException(nameof(rateProviderFactory));
_NetworkProvider = networkProvider;
_StoreRepo = storeRepo;
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
}
[Route("rates")]
[HttpGet]
[BitpayAPIConstraint]
public async Task<DataWrapper<NBitpayClient.Rate[]>> GetRates()
public async Task<IActionResult> GetRates(string cryptoCode = null, string storeId = null)
{
var allRates = (await _RateProvider.GetRatesAsync());
return new DataWrapper<NBitpayClient.Rate[]>
(allRates.Select(r =>
var result = await GetRates2(cryptoCode, storeId);
var rates = (result as JsonResult)?.Value as NBitpayClient.Rate[];
if(rates == null)
return result;
return Json(new DataWrapper<NBitpayClient.Rate[]>(rates));
}
[Route("api/rates")]
[HttpGet]
public async Task<IActionResult> GetRates2(string cryptoCode = null, string storeId = null)
{
cryptoCode = cryptoCode ?? "BTC";
var network = _NetworkProvider.GetNetwork(cryptoCode);
if (network == null)
return NotFound();
var rateProvider = _RateProviderFactory.GetRateProvider(network, true);
if (rateProvider == null)
return NotFound();
if (storeId != null)
{
var store = await _StoreRepo.FindStore(storeId);
if (store == null)
return NotFound();
rateProvider = store.GetStoreBlob().ApplyRateRules(network, rateProvider);
}
var allRates = (await rateProvider.GetRatesAsync());
return Json(allRates.Select(r =>
new NBitpayClient.Rate()
{
Code = r.Currency,
Name = _CurrencyNameTable.GetCurrencyData(r.Currency)?.Name,
Value = r.Value
}).Where(n => n.Name != null).ToArray());
}
}
}

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

@ -0,0 +1,135 @@
using System;
using Microsoft.Extensions.DependencyInjection;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning.CLightning;
using Microsoft.AspNetCore.Mvc;
using BTCPayServer.Payments.Lightning;
namespace BTCPayServer.Controllers
{
public partial class StoresController
{
[HttpGet]
[Route("{storeId}/lightning")]
public async Task<IActionResult> AddLightningNode(string storeId, string selectedCrypto = null)
{
selectedCrypto = selectedCrypto ?? "BTC";
var store = await _Repo.FindStore(storeId, GetUserId());
if (store == null)
return NotFound();
LightningNodeViewModel vm = new LightningNodeViewModel();
vm.SetCryptoCurrencies(_NetworkProvider, selectedCrypto);
vm.InternalLightningNode = GetInternalLightningNodeIfAuthorized();
return View(vm);
}
[HttpPost]
[Route("{storeId}/lightning")]
public async Task<IActionResult> AddLightningNode(string storeId, LightningNodeViewModel vm, string command)
{
var store = await _Repo.FindStore(storeId, GetUserId());
if (store == null)
return NotFound();
var network = vm.CryptoCurrency == null ? null : _ExplorerProvider.GetNetwork(vm.CryptoCurrency);
vm.SetCryptoCurrencies(_NetworkProvider, vm.CryptoCurrency);
vm.InternalLightningNode = GetInternalLightningNodeIfAuthorized();
if (network == null || network.CLightningNetworkName == null)
{
ModelState.AddModelError(nameof(vm.CryptoCurrency), "Invalid network");
return View(vm);
}
PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.LightningLike);
Payments.Lightning.LightningSupportedPaymentMethod paymentMethod = null;
if (!string.IsNullOrEmpty(vm.Url))
{
Uri uri;
if (!Uri.TryCreate(vm.Url, UriKind.Absolute, out uri))
{
ModelState.AddModelError(nameof(vm.Url), "Invalid URL");
return View(vm);
}
var domain = GetDomain(uri.AbsoluteUri);
if (uri.Scheme != "https" && domain != "127.0.0.1" && domain != "localhost")
{
var internalNode = GetInternalLightningNodeIfAuthorized();
if (internalNode == null || GetDomain(internalNode) != domain)
{
ModelState.AddModelError(nameof(vm.Url), "The url must be HTTPS");
return View(vm);
}
}
if (!CanUseInternalLightning() && GetDomain(_BtcpayServerOptions.InternalLightningNode.AbsoluteUri) == GetDomain(uri.AbsoluteUri))
{
ModelState.AddModelError(nameof(vm.Url), "Unauthorized url");
return View(vm);
}
if (string.IsNullOrEmpty(uri.UserInfo) || uri.UserInfo.Split(':').Length != 2)
{
ModelState.AddModelError(nameof(vm.Url), "The url is missing user and password");
return View(vm);
}
paymentMethod = new Payments.Lightning.LightningSupportedPaymentMethod()
{
CryptoCode = paymentMethodId.CryptoCode
};
paymentMethod.SetLightningChargeUrl(uri);
}
if (command == "save")
{
store.SetSupportedPaymentMethod(paymentMethodId, paymentMethod);
await _Repo.UpdateStore(store);
StatusMessage = $"Lightning node modified ({network.CryptoCode})";
return RedirectToAction(nameof(UpdateStore), new { storeId = storeId });
}
else // if(command == "test")
{
if (paymentMethod == null)
{
ModelState.AddModelError(nameof(vm.Url), "Missing url parameter");
return View(vm);
}
var handler = (LightningLikePaymentHandler)_ServiceProvider.GetRequiredService<IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>>();
try
{
await handler.Test(paymentMethod, network);
}
catch (Exception ex)
{
vm.StatusMessage = $"Error: {ex.Message}";
return View(vm);
}
vm.StatusMessage = "Connection to the lightning node succeed";
return View(vm);
}
}
private string GetInternalLightningNodeIfAuthorized()
{
if (_BtcpayServerOptions.InternalLightningNode != null &&
CanUseInternalLightning())
{
return _BtcpayServerOptions.InternalLightningNode.AbsoluteUri;
}
return null;
}
private bool CanUseInternalLightning()
{
return (_BTCPayEnv.IsDevelopping || User.IsInRole(Roles.ServerAdmin));
}
string GetDomain(string uri)
{
return new UriBuilder(uri).Host;
}
}
}

@ -1,22 +1,26 @@
using BTCPayServer.Authentication;
using BTCPayServer.Configuration;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Services;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.Extensions.Options;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBitpayClient;
using NBXplorer.DerivationStrategy;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace BTCPayServer.Controllers
@ -25,9 +29,14 @@ namespace BTCPayServer.Controllers
[Authorize(AuthenticationSchemes = "Identity.Application")]
[Authorize(Policy = "CanAccessStore")]
[AutoValidateAntiforgeryToken]
public class StoresController : Controller
public partial class StoresController : Controller
{
public StoresController(
NBXplorerDashboard dashboard,
IServiceProvider serviceProvider,
BTCPayServerOptions btcpayServerOptions,
BTCPayServerEnvironment btcpayEnv,
IOptions<MvcJsonOptions> mvcJsonOptions,
StoreRepository repo,
TokenRepository tokenRepo,
UserManager<ApplicationUser> userManager,
@ -35,8 +44,10 @@ namespace BTCPayServer.Controllers
BTCPayWalletProvider walletProvider,
BTCPayNetworkProvider networkProvider,
ExplorerClientProvider explorerProvider,
IFeeProviderFactory feeRateProvider,
IHostingEnvironment env)
{
_Dashboard = dashboard;
_Repo = repo;
_TokenRepository = tokenRepo;
_UserManager = userManager;
@ -45,9 +56,20 @@ namespace BTCPayServer.Controllers
_Env = env;
_NetworkProvider = networkProvider;
_ExplorerProvider = explorerProvider;
_MvcJsonOptions = mvcJsonOptions.Value;
_FeeRateProvider = feeRateProvider;
_ServiceProvider = serviceProvider;
_BtcpayServerOptions = btcpayServerOptions;
_BTCPayEnv = btcpayEnv;
}
NBXplorerDashboard _Dashboard;
BTCPayServerOptions _BtcpayServerOptions;
BTCPayServerEnvironment _BTCPayEnv;
IServiceProvider _ServiceProvider;
BTCPayNetworkProvider _NetworkProvider;
private ExplorerClientProvider _ExplorerProvider;
private MvcJsonOptions _MvcJsonOptions;
private IFeeProviderFactory _FeeRateProvider;
BTCPayWalletProvider _WalletProvider;
AccessTokenController _TokenController;
StoreRepository _Repo;
@ -87,6 +109,24 @@ namespace BTCPayServer.Controllers
get; set;
}
[HttpGet]
[Route("{storeId}/wallet")]
public async Task<IActionResult> Wallet(string storeId)
{
var store = await _Repo.FindStore(storeId, GetUserId());
if (store == null)
return NotFound();
WalletModel model = new WalletModel();
model.ServerUrl = GetStoreUrl(storeId);
model.SetCryptoCurrencies(_ExplorerProvider, store.GetDefaultCrypto());
return View(model);
}
private string GetStoreUrl(string storeId)
{
return HttpContext.Request.GetAbsoluteRoot() + "/stores/" + storeId + "/";
}
[HttpGet]
public async Task<IActionResult> ListStores()
{
@ -94,11 +134,12 @@ namespace BTCPayServer.Controllers
result.StatusMessage = StatusMessage;
var stores = await _Repo.GetStoresByUserId(GetUserId());
var balances = stores
.Select(s => s.GetDerivationStrategies(_NetworkProvider)
.Select(d => (Wallet: _WalletProvider.GetWallet(d.Network),
DerivationStrategy: d.DerivationStrategyBase))
.Select(s => s.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<DerivationStrategy>()
.Select(d => ((Wallet: _WalletProvider.GetWallet(d.Network),
DerivationStrategy: d.DerivationStrategyBase)))
.Where(_ => _.Wallet != null)
.Select(async _ => (await _.Wallet.GetBalance(_.DerivationStrategy)).ToString() + " " + _.Wallet.Network.CryptoCode))
.Select(async _ => (await GetBalanceString(_)) + " " + _.Wallet.Network.CryptoCode))
.ToArray();
await Task.WhenAll(balances.SelectMany(_ => _));
@ -116,6 +157,21 @@ namespace BTCPayServer.Controllers
return View(result);
}
private static async Task<string> GetBalanceString((BTCPayWallet Wallet, DerivationStrategyBase DerivationStrategy) _)
{
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)))
{
try
{
return (await _.Wallet.GetBalance(_.DerivationStrategy, cts.Token)).ToString();
}
catch
{
return "--";
}
}
}
[HttpGet]
[Route("{storeId}/delete")]
public async Task<IActionResult> DeleteStore(string storeId)
@ -160,109 +216,38 @@ namespace BTCPayServer.Controllers
vm.StoreWebsite = store.StoreWebsite;
vm.NetworkFee = !storeBlob.NetworkFeeDisabled;
vm.SpeedPolicy = store.SpeedPolicy;
AddDerivationSchemes(store, vm);
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;
return View(vm);
}
private void AddDerivationSchemes(StoreData store, StoreViewModel vm)
private void AddPaymentMethods(StoreData store, StoreViewModel vm)
{
var strategies = store
.GetDerivationStrategies(_NetworkProvider)
.ToDictionary(s => s.Network.CryptoCode);
foreach (var explorerProvider in _ExplorerProvider.GetAll())
foreach(var strategy in store
.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<DerivationStrategy>())
{
if (strategies.TryGetValue(explorerProvider.Item1.CryptoCode, out DerivationStrategy strat))
vm.DerivationSchemes.Add(new StoreViewModel.DerivationScheme()
{
vm.DerivationSchemes.Add(new StoreViewModel.DerivationScheme()
{
Crypto = explorerProvider.Item1.CryptoCode,
Value = strat.DerivationStrategyBase.ToString()
});
}
}
}
[HttpGet]
[Route("{storeId}/derivations")]
public async Task<IActionResult> AddDerivationScheme(string storeId, string selectedScheme = null)
{
selectedScheme = selectedScheme ?? "BTC";
var store = await _Repo.FindStore(storeId, GetUserId());
if (store == null)
return NotFound();
DerivationSchemeViewModel vm = new DerivationSchemeViewModel();
vm.SetCryptoCurrencies(_ExplorerProvider, selectedScheme);
return View(vm);
}
[HttpPost]
[Route("{storeId}/derivations")]
public async Task<IActionResult> AddDerivationScheme(string storeId, DerivationSchemeViewModel vm, string command, string selectedScheme = null)
{
selectedScheme = selectedScheme ?? "BTC";
var store = await _Repo.FindStore(storeId, GetUserId());
if (store == null)
return NotFound();
var network = vm.CryptoCurrency == null ? null : _ExplorerProvider.GetNetwork(vm.CryptoCurrency);
vm.SetCryptoCurrencies(_ExplorerProvider, selectedScheme);
if (network == null)
{
ModelState.AddModelError(nameof(vm.CryptoCurrency), "Invalid network");
return View(vm);
}
var wallet = _WalletProvider.GetWallet(network);
if (wallet == null)
{
ModelState.AddModelError(nameof(vm.CryptoCurrency), "Invalid network");
return View(vm);
Crypto = strategy.PaymentId.CryptoCode,
Value = strategy.DerivationStrategyBase.ToString()
});
}
if (command == "Save")
foreach(var lightning in store
.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<Payments.Lightning.LightningSupportedPaymentMethod>())
{
try
vm.LightningNodes.Add(new StoreViewModel.LightningNode()
{
if (!string.IsNullOrEmpty(vm.DerivationScheme))
{
var strategy = ParseDerivationStrategy(vm.DerivationScheme, vm.DerivationSchemeFormat, network);
await wallet.TrackAsync(strategy);
vm.DerivationScheme = strategy.ToString();
}
store.SetDerivationStrategy(network, vm.DerivationScheme);
}
catch
{
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme");
return View(vm);
}
await _Repo.UpdateStore(store);
StatusMessage = $"Derivation scheme for {vm.CryptoCurrency} has been modified.";
return RedirectToAction(nameof(UpdateStore), new { storeId = storeId });
}
else
{
if (!string.IsNullOrEmpty(vm.DerivationScheme))
{
try
{
var scheme = ParseDerivationStrategy(vm.DerivationScheme, vm.DerivationSchemeFormat, network);
var line = scheme.GetLineFor(DerivationFeature.Deposit);
for (int i = 0; i < 10; i++)
{
var address = line.Derive((uint)i);
vm.AddressSamples.Add((line.Path.Derive((uint)i).ToString(), address.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork).ToString()));
}
}
catch
{
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme");
}
}
return View(vm);
CryptoCode = lightning.CryptoCode,
Address = lightning.GetLightningChargeUrl(false).AbsoluteUri
});
}
}
@ -274,10 +259,12 @@ namespace BTCPayServer.Controllers
{
return View(model);
}
if (model.PreferredExchange != null)
model.PreferredExchange = model.PreferredExchange.Trim().ToLowerInvariant();
var store = await _Repo.FindStore(storeId, GetUserId());
if (store == null)
return NotFound();
AddDerivationSchemes(store, model);
AddPaymentMethods(store, model);
bool needUpdate = false;
if (store.SpeedPolicy != model.SpeedPolicy)
@ -306,12 +293,32 @@ namespace BTCPayServer.Controllers
var blob = store.GetStoreBlob();
blob.NetworkFeeDisabled = !model.NetworkFee;
blob.MonitoringExpiration = model.MonitoringExpiration;
blob.InvoiceExpiration = model.InvoiceExpiration;
bool newExchange = blob.PreferredExchange != model.PreferredExchange;
blob.PreferredExchange = model.PreferredExchange;
blob.SetRateMultiplier(model.RateMultiplier);
blob.AllowCoinConversion = model.AllowCoinConversion;
if (store.SetStoreBlob(blob))
{
needUpdate = true;
}
if (!blob.PreferredExchange.IsCoinAverage() && newExchange)
{
using (HttpClient client = new HttpClient())
{
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);
}
}
}
if (needUpdate)
{
await _Repo.UpdateStore(store);
@ -324,7 +331,7 @@ namespace BTCPayServer.Controllers
});
}
private DerivationStrategyBase ParseDerivationStrategy(string derivationScheme, string format, BTCPayNetwork network)
private DerivationStrategy ParseDerivationStrategy(string derivationScheme, string format, BTCPayNetwork network)
{
if (format == "Electrum")
{
@ -338,7 +345,7 @@ namespace BTCPayServer.Controllers
var p2wpkh_p2sh = 0x049d7cb2U;
electrumMapping.Add(p2wpkh_p2sh, new string[] { "p2sh" });
var p2wpkh = 0x4b24746U;
electrumMapping.Add(p2wpkh, new string[] { });
electrumMapping.Add(p2wpkh, Array.Empty<string>());
var data = Encoders.Base58Check.DecodeData(derivationScheme);
if (data.Length < 4)
@ -346,7 +353,7 @@ namespace BTCPayServer.Controllers
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.NBitcoinNetwork == Network.Main ? 0x0488b21eU : 0x043587cf, false);
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];
@ -358,7 +365,7 @@ namespace BTCPayServer.Controllers
}
}
return new DerivationStrategyFactory(network.NBitcoinNetwork).Parse(derivationScheme);
return new DerivationStrategy(new DerivationStrategyFactory(network.NBitcoinNetwork).Parse(derivationScheme), network);
}
[HttpGet]
@ -515,7 +522,7 @@ namespace BTCPayServer.Controllers
var pairingResult = await _TokenRepository.PairWithStoreAsync(pairingCode, store.Id);
if (pairingResult == PairingResult.Complete || pairingResult == PairingResult.Partial)
{
StatusMessage = "Pairing is successfull";
StatusMessage = "Pairing is successful";
if (pairingResult == PairingResult.Partial)
StatusMessage = "Server initiated pairing code: " + pairingCode;
return RedirectToAction(nameof(ListTokens), new

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

@ -26,6 +26,11 @@ namespace BTCPayServer.Data
get; set;
}
public DbSet<InvoiceEventData> InvoiceEvents
{
get; set;
}
public DbSet<HistoricalAddressInvoiceData> HistoricalAddressInvoices
{
get; set;
@ -132,6 +137,15 @@ namespace BTCPayServer.Data
o.InvoiceDataId,
#pragma warning disable CS0618
o.Address
#pragma warning restore CS0618
});
builder.Entity<InvoiceEventData>()
.HasKey(o => new
{
o.InvoiceDataId,
#pragma warning disable CS0618
o.UniqueId
#pragma warning restore CS0618
});
}

@ -27,15 +27,16 @@ namespace BTCPayServer.Data
public string CryptoCode { get; set; }
#pragma warning disable CS0618
public string GetCryptoCode()
public Payments.PaymentMethodId GetPaymentMethodId()
{
return string.IsNullOrEmpty(CryptoCode) ? "BTC" : CryptoCode;
return string.IsNullOrEmpty(CryptoCode) ? new Payments.PaymentMethodId("BTC", Payments.PaymentTypes.BTCLike)
: Payments.PaymentMethodId.Parse(CryptoCode);
}
public string GetAddress()
{
if (Address == null)
return null;
var index = Address.IndexOf("#");
var index = Address.IndexOf("#", StringComparison.InvariantCulture);
if (index == -1)
return Address;
return Address.Substring(0, index);

@ -32,6 +32,11 @@ namespace BTCPayServer.Data
get; set;
}
public List<InvoiceEventData> Events
{
get; set;
}
public List<RefundAddressesData> RefundAddresses
{
get; set;

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

@ -11,6 +11,8 @@ using System.Threading.Tasks;
using System.ComponentModel;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using BTCPayServer.Services.Rates;
using BTCPayServer.Payments;
namespace BTCPayServer.Data
{
@ -40,10 +42,12 @@ namespace BTCPayServer.Data
set;
}
public IEnumerable<DerivationStrategy> GetDerivationStrategies(BTCPayNetworkProvider networks)
public IEnumerable<ISupportedPaymentMethod> GetSupportedPaymentMethods(BTCPayNetworkProvider networks)
{
#pragma warning disable CS0618
bool btcReturned = false;
// Legacy stuff which should go away
if (!string.IsNullOrEmpty(DerivationStrategy))
{
if (networks.BTC != null)
@ -59,54 +63,63 @@ namespace BTCPayServer.Data
JObject strategies = JObject.Parse(DerivationStrategies);
foreach (var strat in strategies.Properties())
{
var network = networks.GetNetwork(strat.Name);
var paymentMethodId = PaymentMethodId.Parse(strat.Name);
var network = networks.GetNetwork(paymentMethodId.CryptoCode);
if (network != null)
{
if (network == networks.BTC && btcReturned)
if (network == networks.BTC && paymentMethodId.PaymentType == PaymentTypes.BTCLike && btcReturned)
continue;
if (strat.Value.Type == JTokenType.Null)
continue;
yield return BTCPayServer.DerivationStrategy.Parse(strat.Value.Value<string>(), network);
yield return PaymentMethodExtensions.Deserialize(paymentMethodId, strat.Value, network);
}
}
}
#pragma warning restore CS0618
}
public void SetDerivationStrategy(BTCPayNetwork network, string derivationScheme)
/// <summary>
/// Set or remove a new supported payment method for the store
/// </summary>
/// <param name="paymentMethodId">The paymentMethodId</param>
/// <param name="supportedPaymentMethod">The payment method, or null to remove</param>
public void SetSupportedPaymentMethod(PaymentMethodId paymentMethodId, ISupportedPaymentMethod supportedPaymentMethod)
{
if (supportedPaymentMethod != null && paymentMethodId != supportedPaymentMethod.PaymentId)
throw new InvalidOperationException("Argument mismatch");
#pragma warning disable CS0618
JObject strategies = string.IsNullOrEmpty(DerivationStrategies) ? new JObject() : JObject.Parse(DerivationStrategies);
bool existing = false;
foreach (var strat in strategies.Properties().ToList())
{
if (strat.Name == network.CryptoCode)
var stratId = PaymentMethodId.Parse(strat.Name);
if (stratId.IsBTCOnChain)
{
if (network.IsBTC)
DerivationStrategy = null;
if (string.IsNullOrEmpty(derivationScheme))
// Legacy stuff which should go away
DerivationStrategy = null;
}
if (stratId == paymentMethodId)
{
if (supportedPaymentMethod == null)
{
strat.Remove();
}
else
{
strat.Value = new JValue(derivationScheme);
strat.Value = PaymentMethodExtensions.Serialize(supportedPaymentMethod);
}
existing = true;
break;
}
}
if (!existing && string.IsNullOrEmpty(derivationScheme))
if(!existing && supportedPaymentMethod == null && paymentMethodId.IsBTCOnChain)
{
if(network.IsBTC)
DerivationStrategy = null;
DerivationStrategy = null;
}
else if(!existing)
strategies.Add(new JProperty(network.CryptoCode, new JValue(derivationScheme)));
// This is deprecated so we don't have to set anymore
//if (network.IsBTC)
// DerivationStrategy = derivationScheme;
else if (!existing && supportedPaymentMethod != null)
strategies.Add(new JProperty(supportedPaymentMethod.PaymentId.ToString(), PaymentMethodExtensions.Serialize(supportedPaymentMethod)));
DerivationStrategies = strategies.ToString();
#pragma warning restore CS0618
}
@ -173,16 +186,37 @@ namespace BTCPayServer.Data
}
}
public class RateRule
{
public RateRule()
{
RuleName = "Multiplier";
}
public string RuleName { get; set; }
public double Multiplier { get; set; }
public decimal Apply(BTCPayNetwork network, decimal rate)
{
return rate * (decimal)Multiplier;
}
}
public class StoreBlob
{
public StoreBlob()
{
InvoiceExpiration = 15;
MonitoringExpiration = 60;
}
public bool NetworkFeeDisabled
{
get; set;
}
public bool AllowCoinConversion
{
get; set;
}
[DefaultValue(60)]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
public int MonitoringExpiration
@ -190,5 +224,59 @@ namespace BTCPayServer.Data
get;
set;
}
[DefaultValue(15)]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
public int InvoiceExpiration
{
get;
set;
}
public void SetRateMultiplier(double rate)
{
RateRules = new List<RateRule>();
RateRules.Add(new RateRule() { Multiplier = rate });
}
public decimal GetRateMultiplier()
{
decimal rate = 1.0m;
if (RateRules == null || RateRules.Count == 0)
return rate;
foreach (var rule in RateRules)
{
rate = rule.Apply(null, rate);
}
return rate;
}
public List<RateRule> RateRules { get; set; } = new List<RateRule>();
public string PreferredExchange { get; set; }
public IRateProvider ApplyRateRules(BTCPayNetwork network, IRateProvider rateProvider)
{
if (!PreferredExchange.IsCoinAverage())
{
// 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
});
}
}
if (RateRules == null || RateRules.Count == 0)
return rateProvider;
return new TweakRateProvider(network, rateProvider, RateRules.ToList());
}
}
}

@ -2,17 +2,19 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Payments;
using BTCPayServer.Services.Invoices;
using NBitcoin;
using NBXplorer.DerivationStrategy;
namespace BTCPayServer
{
public class DerivationStrategy
public class DerivationStrategy : ISupportedPaymentMethod
{
private DerivationStrategyBase _DerivationStrategy;
private BTCPayNetwork _Network;
DerivationStrategy(DerivationStrategyBase result, BTCPayNetwork network)
public DerivationStrategy(DerivationStrategyBase result, BTCPayNetwork network)
{
this._DerivationStrategy = result;
this._Network = network;
@ -32,6 +34,8 @@ namespace BTCPayServer
public DerivationStrategyBase DerivationStrategyBase { get { return this._DerivationStrategy; } }
public PaymentMethodId PaymentId => new PaymentMethodId(Network.CryptoCode, PaymentTypes.BTCLike);
public override string ToString()
{
return _DerivationStrategy.ToString();

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

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

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

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

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

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

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

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

@ -2,15 +2,15 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Invoices;
namespace BTCPayServer.Events
{
public class InvoicePaymentEvent
public class InvoiceNeedUpdateEvent
{
public InvoicePaymentEvent(string invoiceId)
public InvoiceNeedUpdateEvent(string invoiceId)
{
if (invoiceId == null)
throw new ArgumentNullException(nameof(invoiceId));
InvoiceId = invoiceId;
}
@ -18,7 +18,7 @@ namespace BTCPayServer.Events
public override string ToString()
{
return $"Invoice {InvoiceId} received a payment";
return string.Empty;
}
}
}

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

@ -1,30 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Invoices;
namespace BTCPayServer.Events
{
public class InvoiceStatusChangedEvent
{
public InvoiceStatusChangedEvent()
{
}
public InvoiceStatusChangedEvent(InvoiceEntity invoice, string newState)
{
OldState = invoice.Status;
InvoiceId = invoice.Id;
NewState = newState;
}
public string InvoiceId { get; set; }
public string OldState { get; set; }
public string NewState { get; set; }
public override string ToString()
{
return $"Invoice {InvoiceId} changed status: {OldState} => {NewState}";
}
}
}

@ -5,18 +5,16 @@ using System.Threading.Tasks;
namespace BTCPayServer.Events
{
public class InvoiceCreatedEvent
public class InvoiceStopWatchedEvent
{
public InvoiceCreatedEvent(string id)
public InvoiceStopWatchedEvent(string invoiceId)
{
InvoiceId = id;
this.InvoiceId = invoiceId;
}
public string InvoiceId { get; set; }
public override string ToString()
{
return $"Invoice {InvoiceId} created";
return $"Invoice {InvoiceId} is not monitored anymore.";
}
}
}

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

@ -6,6 +6,7 @@ using System.Threading.Tasks;
using BTCPayServer.Configuration;
using BTCPayServer.Logging;
using NBXplorer;
using BTCPayServer.HostedServices;
namespace BTCPayServer
{
@ -15,9 +16,10 @@ namespace BTCPayServer
BTCPayServerOptions _Options;
public BTCPayNetworkProvider NetworkProviders => _NetworkProviders;
public ExplorerClientProvider(BTCPayNetworkProvider networkProviders, BTCPayServerOptions options)
NBXplorerDashboard _Dashboard;
public ExplorerClientProvider(BTCPayNetworkProvider networkProviders, BTCPayServerOptions options, NBXplorerDashboard dashboard)
{
_Dashboard = dashboard;
_NetworkProviders = networkProviders;
_Options = options;
@ -38,11 +40,15 @@ namespace BTCPayServer
private static ExplorerClient CreateExplorerClient(BTCPayNetwork n, Uri uri, string cookieFile)
{
var explorer = new ExplorerClient(n.NBXplorerNetwork, uri);
if (cookieFile == null || !explorer.SetCookieAuth(cookieFile))
if (cookieFile == null)
{
Logs.Configuration.LogWarning($"{n.CryptoCode}: Not using cookie authentication");
explorer.SetNoAuth();
}
if(!explorer.SetCookieAuth(cookieFile))
{
Logs.Configuration.LogWarning($"{n.CryptoCode}: Using cookie auth against NBXplorer, but {cookieFile} is not found");
}
return explorer;
}
@ -64,6 +70,16 @@ namespace BTCPayServer
return GetExplorerClient(network.CryptoCode);
}
public bool IsAvailable(BTCPayNetwork network)
{
return IsAvailable(network.CryptoCode);
}
public bool IsAvailable(string cryptoCode)
{
return _Clients.ContainsKey(cryptoCode) && _Dashboard.IsFullySynched(cryptoCode, out var unused);
}
public BTCPayNetwork GetNetwork(string cryptoCode)
{
var network = _NetworkProviders.GetNetwork(cryptoCode);

@ -19,16 +19,59 @@ using System.Linq;
using System.Threading;
using BTCPayServer.Services.Wallets;
using System.IO;
using BTCPayServer.Logging;
using Microsoft.Extensions.Logging;
using System.Net.WebSockets;
using BTCPayServer.Services.Invoices;
using NBitpayClient;
using BTCPayServer.Payments;
namespace BTCPayServer
{
public static class Extensions
{
public static decimal RoundUp(decimal value, int precision)
{
for (int i = 0; i < precision; i++)
{
value = value * 10m;
}
value = Math.Ceiling(value);
for (int i = 0; i < precision; i++)
{
value = value / 10m;
}
return value;
}
public static PaymentMethodId GetpaymentMethodId(this InvoiceCryptoInfo info)
{
return new PaymentMethodId(info.CryptoCode, Enum.Parse<PaymentTypes>(info.PaymentType));
}
public static async Task CloseSocket(this WebSocket webSocket)
{
try
{
if (webSocket.State == WebSocketState.Open)
{
CancellationTokenSource cts = new CancellationTokenSource();
cts.CancelAfter(5000);
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", cts.Token);
}
}
catch { }
finally { try { webSocket.Dispose(); } catch { } }
}
public static bool SupportDropColumn(this Microsoft.EntityFrameworkCore.Migrations.Migration migration, string activeProvider)
{
return activeProvider != "Microsoft.EntityFrameworkCore.Sqlite";
}
public static bool IsCoinAverage(this string exchangeName)
{
string[] coinAverages = new[] { "coinaverage", "bitcoinaverage" };
return String.IsNullOrWhiteSpace(exchangeName) ? true : coinAverages.Contains(exchangeName, StringComparer.OrdinalIgnoreCase) ? true : false;
}
public static async Task<Dictionary<uint256, TransactionResult>> GetTransactions(this BTCPayWallet client, uint256[] hashes, CancellationToken cts = default(CancellationToken))
{
hashes = hashes.Distinct().ToArray();
@ -40,7 +83,7 @@ namespace BTCPayServer
}
public static string WithTrailingSlash(this string str)
{
if (str.EndsWith("/"))
if (str.EndsWith("/", StringComparison.InvariantCulture))
return str;
return str + "/";
}

@ -19,6 +19,7 @@ using Microsoft.Extensions.Hosting;
using BTCPayServer.Events;
using NBXplorer;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Payments;
namespace BTCPayServer.HostedServices
{
@ -37,6 +38,9 @@ namespace BTCPayServer.HostedServices
{
get; set;
}
public int? EventCode { get; set; }
public string Message { get; set; }
}
public ILogger Logger
@ -63,18 +67,33 @@ namespace BTCPayServer.HostedServices
_NetworkProvider = networkProvider;
}
async Task Notify(InvoiceEntity invoice)
async Task Notify(InvoiceEntity invoice, int? eventCode = null, string name = null)
{
CancellationTokenSource cts = new CancellationTokenSource(10000);
try
{
await SendNotification(invoice, cts.Token);
if (string.IsNullOrEmpty(invoice.NotificationURL))
return;
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id, eventCode, name));
var response = await SendNotification(invoice, eventCode, name, cts.Token);
response.EnsureSuccessStatusCode();
return;
}
catch // It fails, it is OK because we try with hangfire after
catch (OperationCanceledException) when (cts.IsCancellationRequested)
{
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id, eventCode, name)
{
Error = "Timeout"
});
}
var invoiceStr = NBitcoin.JsonConverters.Serializer.ToString(new ScheduledJob() { TryCount = 0, Invoice = invoice });
catch (Exception ex) // It fails, it is OK because we try with hangfire after
{
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id, eventCode, name)
{
Error = ex.Message
});
}
var invoiceStr = NBitcoin.JsonConverters.Serializer.ToString(new ScheduledJob() { TryCount = 0, Invoice = invoice, EventCode = eventCode, Message = name });
if (!string.IsNullOrEmpty(invoice.NotificationURL))
_JobClient.Schedule(() => NotifyHttp(invoiceStr), TimeSpan.Zero);
}
@ -93,14 +112,45 @@ namespace BTCPayServer.HostedServices
CancellationTokenSource cts = new CancellationTokenSource(10000);
try
{
HttpResponseMessage response = await SendNotification(job.Invoice, cts.Token);
reschedule = response.StatusCode != System.Net.HttpStatusCode.OK;
HttpResponseMessage response = await SendNotification(job.Invoice, job.EventCode, job.Message, cts.Token);
reschedule = !response.IsSuccessStatusCode;
Logger.LogInformation("Job " + jobId + " returned " + response.StatusCode);
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
{
Error = reschedule ? $"Unexpected return code: {(int)response.StatusCode}" : null
});
}
catch (Exception ex)
catch (OperationCanceledException) when (cts.IsCancellationRequested)
{
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
{
Error = "Timeout"
});
reschedule = true;
Logger.LogInformation("Job " + jobId + " threw exception " + ex.Message);
Logger.LogInformation("Job " + jobId + " timed out");
}
catch (Exception ex) // It fails, it is OK because we try with hangfire after
{
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
{
Error = ex.Message
});
reschedule = true;
List<string> messages = new List<string>();
while (ex != null)
{
messages.Add(ex.Message);
ex = ex.InnerException;
}
string message = String.Join(',', messages.ToArray());
Logger.LogInformation("Job " + jobId + " threw exception " + message);
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
{
Error = $"Unexpected error: {message}"
});
}
finally { cts.Dispose(); _Executing.TryRemove(jobId, out jobId); }
@ -115,8 +165,23 @@ namespace BTCPayServer.HostedServices
}
}
public class InvoicePaymentNotificationEvent
{
[JsonProperty("code")]
public int Code { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
}
public class InvoicePaymentNotificationEventWrapper
{
[JsonProperty("event")]
public InvoicePaymentNotificationEvent Event { get; set; }
[JsonProperty("data")]
public InvoicePaymentNotification Data { get; set; }
}
Encoding UTF8 = new UTF8Encoding(false);
private async Task<HttpResponseMessage> SendNotification(InvoiceEntity invoice, CancellationToken cancellation)
private async Task<HttpResponseMessage> SendNotification(InvoiceEntity invoice, int? eventCode, string name, CancellationToken cancellation)
{
var request = new HttpRequestMessage();
request.Method = HttpMethod.Post;
@ -138,8 +203,8 @@ namespace BTCPayServer.HostedServices
// We keep backward compatibility with bitpay by passing BTC info to the notification
// we don't pass other info, as it is a bad idea to use IPN data for logic processing (can be faked)
var btcCryptoInfo = dto.CryptoInfo.FirstOrDefault(c => c.CryptoCode == "BTC");
if(btcCryptoInfo != null)
var btcCryptoInfo = dto.CryptoInfo.FirstOrDefault(c => c.GetpaymentMethodId() == new PaymentMethodId("BTC", Payments.PaymentTypes.BTCLike));
if (btcCryptoInfo != null)
{
#pragma warning disable CS0618
notification.Rate = (double)dto.Rate;
@ -149,12 +214,84 @@ namespace BTCPayServer.HostedServices
notification.BTCPrice = dto.BTCPrice;
#pragma warning restore CS0618
}
string notificationString = null;
if (eventCode.HasValue)
{
var wrapper = new InvoicePaymentNotificationEventWrapper();
wrapper.Data = notification;
wrapper.Event = new InvoicePaymentNotificationEvent() { Code = eventCode.Value, Name = name };
notificationString = JsonConvert.SerializeObject(wrapper);
}
else
{
notificationString = JsonConvert.SerializeObject(notification);
}
request.RequestUri = new Uri(invoice.NotificationURL, UriKind.Absolute);
request.Content = new StringContent(JsonConvert.SerializeObject(notification), UTF8, "application/json");
var response = await _Client.SendAsync(request, cancellation);
request.Content = new StringContent(notificationString, UTF8, "application/json");
var response = await Enqueue(invoice.Id, async () => await _Client.SendAsync(request, cancellation));
return response;
}
Dictionary<string, Task> _SendingRequestsByInvoiceId = new Dictionary<string, Task>();
/// <summary>
/// Will make sure only one callback is called at once on the same invoiceId
/// </summary>
/// <param name="id"></param>
/// <param name="sendRequest"></param>
/// <returns></returns>
private async Task<T> Enqueue<T>(string id, Func<Task<T>> sendRequest)
{
Task<T> sending = null;
lock (_SendingRequestsByInvoiceId)
{
if (_SendingRequestsByInvoiceId.TryGetValue(id, out var executing))
{
var completion = new TaskCompletionSource<T>();
sending = completion.Task;
_SendingRequestsByInvoiceId.Remove(id);
_SendingRequestsByInvoiceId.Add(id, sending);
executing.ContinueWith(_ =>
{
sendRequest()
.ContinueWith(t =>
{
if(t.Status == TaskStatus.RanToCompletion)
{
completion.TrySetResult(t.Result);
}
if(t.Status == TaskStatus.Faulted)
{
completion.TrySetException(t.Exception);
}
if(t.Status == TaskStatus.Canceled)
{
completion.TrySetCanceled();
}
}, TaskScheduler.Default);
}, TaskScheduler.Default);
}
else
{
sending = sendRequest();
_SendingRequestsByInvoiceId.Add(id, sending);
}
sending.ContinueWith(o =>
{
lock (_SendingRequestsByInvoiceId)
{
_SendingRequestsByInvoiceId.TryGetValue(id, out var executing2);
if(executing2 == sending)
_SendingRequestsByInvoiceId.Remove(id);
}
}, TaskScheduler.Default);
}
return await sending;
}
int MaxTry = 6;
private static string GetHttpJobId(InvoiceEntity invoice)
@ -165,29 +302,60 @@ namespace BTCPayServer.HostedServices
CompositeDisposable leases = new CompositeDisposable();
public Task StartAsync(CancellationToken cancellationToken)
{
leases.Add(_EventAggregator.Subscribe<InvoiceStatusChangedEvent>(async e =>
leases.Add(_EventAggregator.Subscribe<InvoiceEvent>(async e =>
{
var invoice = await _InvoiceRepository.GetInvoice(null, e.InvoiceId);
await SaveEvent(invoice.Id, e);
// we need to use the status in the event and not in the invoice. The invoice might now be in another status.
if (invoice.FullNotifications)
{
if (e.NewState == "expired" ||
e.NewState == "paid" ||
e.NewState == "invalid" ||
e.NewState == "complete"
if (e.Name == "invoice_expired" ||
e.Name == "invoice_paidInFull" ||
e.Name == "invoice_failedToConfirm" ||
e.Name == "invoice_markedInvalid" ||
e.Name == "invoice_failedToConfirm" ||
e.Name == "invoice_completed"
)
await Notify(invoice);
}
if(e.NewState == "confirmed")
if (e.Name == "invoice_confirmed")
{
await Notify(invoice);
}
if (invoice.ExtendedNotifications)
{
await Notify(invoice, e.EventCode, e.Name);
}
}));
leases.Add(_EventAggregator.Subscribe<InvoiceDataChangedEvent>(async e =>
{
await SaveEvent(e.InvoiceId, e);
}));
leases.Add(_EventAggregator.Subscribe<InvoiceStopWatchedEvent>(async e =>
{
await SaveEvent(e.InvoiceId, e);
}));
leases.Add(_EventAggregator.Subscribe<InvoiceIPNEvent>(async e =>
{
await SaveEvent(e.InvoiceId, e);
}));
return Task.CompletedTask;
}
private Task SaveEvent(string invoiceId, object evt)
{
return _InvoiceRepository.AddInvoiceEvent(invoiceId, evt);
}
public Task StopAsync(CancellationToken cancellationToken)
{
leases.Dispose();

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

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

@ -41,6 +41,13 @@ namespace BTCPayServer.HostedServices
return _Summaries.All(s => s.Value.Status != null && s.Value.Status.IsFullySynched);
}
public bool IsFullySynched(string cryptoCode, out NBXplorerSummary summary)
{
return _Summaries.TryGetValue(cryptoCode, out summary) &&
summary.Status != null &&
summary.Status.IsFullySynched;
}
public IEnumerable<NBXplorerSummary> GetAll()
{
return _Summaries.Values;
@ -188,6 +195,7 @@ namespace BTCPayServer.HostedServices
_Aggregator.Publish(new NBXplorerErrorEvent(_Network, error));
}
_Dashboard.Publish(_Network, State, status, error);
if (oldState != State)
{
if (State == NBXplorerState.Synching)
@ -200,7 +208,6 @@ namespace BTCPayServer.HostedServices
}
_Aggregator.Publish(new NBXplorerStateChangedEvent(_Network, oldState, State));
}
_Dashboard.Publish(_Network, State, status, error);
return oldState != State;
}

@ -37,6 +37,7 @@ using BTCPayServer.Authentication;
using Microsoft.Extensions.Caching.Memory;
using BTCPayServer.Logging;
using BTCPayServer.HostedServices;
using Meziantou.AspNetCore.BundleTagHelpers;
namespace BTCPayServer.Hosting
{
@ -129,7 +130,7 @@ namespace BTCPayServer.Hosting
services.TryAddSingleton<BTCPayNetworkProvider>(o =>
{
var opts = o.GetRequiredService<BTCPayServerOptions>();
return new BTCPayNetworkProvider(opts.ChainType);
return opts.NetworkProvider;
});
services.TryAddSingleton<NBXplorerDashboard>();
@ -142,10 +143,13 @@ namespace BTCPayServer.Hosting
BlockTarget = 20
});
services.AddSingleton<TransactionCacheProvider>();
services.AddSingleton<Payments.IPaymentMethodHandler<DerivationStrategy>, Payments.Bitcoin.BitcoinLikePaymentHandler>();
services.AddSingleton<IHostedService, Payments.Bitcoin.NBXplorerListener>();
services.AddSingleton<Payments.IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>, Payments.Lightning.LightningLikePaymentHandler>();
services.AddSingleton<IHostedService, Payments.Lightning.ChargeListener>();
services.AddSingleton<IHostedService, NBXplorerWaiters>();
services.AddSingleton<IHostedService, NBXplorerListener>();
services.AddSingleton<IHostedService, InvoiceNotificationManager>();
services.AddSingleton<IHostedService, InvoiceWatcher>();
@ -179,6 +183,18 @@ namespace BTCPayServer.Hosting
});
});
// bundling
services.AddBundles();
services.AddTransient<BundleOptions>(provider =>
{
var opts = provider.GetRequiredService<BTCPayServerOptions>();
var bundle = new BundleOptions();
bundle.UseMinifiedFiles = opts.BundleJsCss;
bundle.AppendVersion = true;
return bundle;
});
return services;
}
@ -194,7 +210,7 @@ namespace BTCPayServer.Hosting
}
app.UseMiddleware<BTCPayMiddleware>();
return app;
return app;
}
static void Retry(Action act)

@ -22,6 +22,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Http.Extensions;
using BTCPayServer.Controllers;
using System.Net.WebSockets;
namespace BTCPayServer.Hosting
{
@ -69,7 +70,7 @@ namespace BTCPayServer.Hosting
if (BitIdExtensions.CheckBitIDSignature(key, sig, url, body))
{
var bitid = new BitIdentity(key);
httpContext.User = new GenericPrincipal(bitid, new string[0]);
httpContext.User = new GenericPrincipal(bitid, Array.Empty<string>());
Logs.PayServer.LogDebug($"BitId signature check success for SIN {bitid.SIN}");
}
}
@ -82,6 +83,8 @@ namespace BTCPayServer.Hosting
{
await _Next(httpContext);
}
catch (WebSocketException)
{ }
catch (UnauthorizedAccessException ex)
{
await HandleBitpayHttpException(httpContext, new BitpayHttpException(401, ex.Message));

@ -6,6 +6,7 @@ using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Mvc;
using NBitpayClient;
@ -36,6 +37,9 @@ using System.Threading;
using Microsoft.Extensions.Options;
using Microsoft.ApplicationInsights.AspNetCore.Extensions;
using Microsoft.AspNetCore.Mvc.Cors.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using System.Net;
using Meziantou.AspNetCore.BundleTagHelpers;
namespace BTCPayServer.Hosting
{
@ -88,6 +92,15 @@ namespace BTCPayServer.Hosting
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
});
// Needed to debug U2F for ledger support
//services.Configure<KestrelServerOptions>(kestrel =>
//{
// kestrel.Listen(IPAddress.Loopback, 5012, l =>
// {
// l.UseHttps("devtest.pfx", "toto");
// });
//});
}
// Big hack, tests fails if only call AddHangfire because Hangfire fail at initializing at the second test run

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

@ -6,13 +6,18 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace BTCPayServer.Logging
{
public class CustomConsoleLogProvider : ILoggerProvider
{
ConsoleLoggerProcessor _Processor = new ConsoleLoggerProcessor();
ConsoleLoggerProcessor _Processor;
public CustomConsoleLogProvider(ConsoleLoggerProcessor processor)
{
_Processor = processor;
}
public ILogger CreateLogger(string categoryName)
{
return new CustomConsoleLogger(categoryName, (a, b) => true, false, _Processor);
@ -329,7 +334,8 @@ namespace BTCPayServer.Logging
_outputTask = Task.Factory.StartNew(
ProcessLogQueue,
this,
TaskCreationOptions.LongRunning);
default(CancellationToken),
TaskCreationOptions.LongRunning, TaskScheduler.Default);
}
public virtual void EnqueueMessage(LogMessageEntry message)

@ -0,0 +1,508 @@
// <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("20180114123253_events")]
partial class events
{
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.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.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,38 @@
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;
namespace BTCPayServer.Migrations
{
public partial class events : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "InvoiceEvents",
columns: table => new
{
InvoiceDataId = table.Column<string>(nullable: false),
UniqueId = table.Column<string>(nullable: false),
Message = table.Column<string>(nullable: true),
Timestamp = table.Column<DateTimeOffset>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_InvoiceEvents", x => new { x.InvoiceDataId, x.UniqueId });
table.ForeignKey(
name: "FK_InvoiceEvents_Invoices_InvoiceDataId",
column: x => x.InvoiceDataId,
principalTable: "Invoices",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "InvoiceEvents");
}
}
}

@ -1,12 +1,9 @@
// <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
@ -81,6 +78,21 @@ namespace BTCPayServer.Migrations
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")
@ -407,6 +419,14 @@ namespace BTCPayServer.Migrations
.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")

@ -12,17 +12,23 @@ namespace BTCPayServer.Models.InvoicingModels
{
public class CryptoPayment
{
public string CryptoCode { get; set; }
public string PaymentMethod { get; set; }
public string Due { get; set; }
public string Paid { get; set; }
public string Address { get; internal set; }
public string Rate { get; internal set; }
public string PaymentUrl { get; internal set; }
}
public class AddressModel
{
public string PaymentMethod { get; set; }
public string Destination { get; set; }
public bool Current { get; set; }
}
public class Payment
{
public string CryptoCode { get; set; }
public int Confirmations
public string PaymentMethod { get; set; }
public string Confirmations
{
get; set;
}
@ -97,6 +103,8 @@ namespace BTCPayServer.Models.InvoicingModels
get;
set;
}
public string TransactionSpeed { get; set; }
public object StoreName
{
get;
@ -112,6 +120,8 @@ namespace BTCPayServer.Models.InvoicingModels
get;
internal set;
}
public string RedirectUrl { get; set; }
public string Fiat
{
get;
@ -122,7 +132,8 @@ namespace BTCPayServer.Models.InvoicingModels
get;
internal set;
}
public HistoricalAddressInvoiceData[] Addresses { get; set; }
public AddressModel[] Addresses { get; set; }
public DateTimeOffset MonitoringDate { get; internal set; }
public List<Data.InvoiceEventData> Events { get; internal set; }
}
}

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

@ -9,7 +9,7 @@ namespace BTCPayServer.Models.InvoicingModels
{
public class AvailableCrypto
{
public string CryptoCode { get; set; }
public string PaymentMethodId { get; set; }
public string CryptoImage { get; set; }
public string Link { get; set; }
}
@ -32,6 +32,7 @@ namespace BTCPayServer.Models.InvoicingModels
public string Rate { get; set; }
public string OrderAmount { get; set; }
public string InvoiceBitcoinUrl { get; set; }
public string InvoiceBitcoinUrlQR { get; set; }
public int TxCount { get; set; }
public string BtcPaid { get; set; }
public string StoreEmail { get; set; }
@ -39,5 +40,10 @@ namespace BTCPayServer.Models.InvoicingModels
public string OrderId { get; set; }
public string CryptoImage { get; set; }
public string NetworkFeeDescription { get; internal set; }
public int MaxTimeMinutes { get; internal set; }
public string PaymentType { get; internal set; }
public string PaymentMethodId { get; internal set; }
public bool AllowCoinConversion { get; set; }
}
}

@ -48,10 +48,13 @@ namespace BTCPayServer.Models.StoreViewModels
set;
}
public bool Confirmation { get; set; }
public SelectList CryptoCurrencies { get; set; }
public SelectList DerivationSchemeFormats { get; set; }
public string ServerUrl { get; set; }
public void SetCryptoCurrencies(ExplorerClientProvider explorerProvider, string selectedScheme)
{
var choices = explorerProvider.GetAll().Select(o => new Format() { Name = o.Item1.CryptoCode, Value = o.Item1.CryptoCode }).ToArray();

@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace BTCPayServer.Models.StoreViewModels
{
public class LightningNodeViewModel
{
class Format
{
public string Name { get; set; }
public string Value { get; set; }
}
[Display(Name = "Lightning charge url")]
public string Url
{
get;
set;
}
[Display(Name = "Crypto currency")]
public string CryptoCurrency
{
get;
set;
}
public SelectList CryptoCurrencies { get; set; }
public string StatusMessage { get; set; }
public string InternalLightningNode { get; internal set; }
public void SetCryptoCurrencies(BTCPayNetworkProvider networkProvider, string selectedScheme)
{
var choices = networkProvider.GetAll()
.Where(n => n.CLightningNetworkName != null)
.Select(o => new Format() { Name = o.CryptoCode, Value = o.CryptoCode }).ToArray();
var chosen = choices.FirstOrDefault(f => f.Name == selectedScheme) ?? choices.FirstOrDefault();
CryptoCurrencies = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
CryptoCurrency = chosen.Name;
}
}
}

@ -47,7 +47,34 @@ namespace BTCPayServer.Models.StoreViewModels
public List<StoreViewModel.DerivationScheme> DerivationSchemes { get; set; } = new List<StoreViewModel.DerivationScheme>();
[Display(Name = "Payment invalid if transactions fails to confirm after ... minutes")]
[Display(Name = "Preferred price source (eg. bitfinex, bitstamp...)")]
public string PreferredExchange { get; set; }
public string RateSource
{
get
{
return PreferredExchange.IsCoinAverage() ? "https://apiv2.bitcoinaverage.com/indices/global/ticker/short" : $"https://apiv2.bitcoinaverage.com/exchanges/{PreferredExchange}";
}
}
[Display(Name = "Multiply the original rate by ...")]
[Range(0.01, 10.0)]
public double RateMultiplier
{
get;
set;
}
[Display(Name = "Invoice expires if the full amount has not been paid after ... minutes")]
[Range(1, 60 * 24 * 31)]
public int InvoiceExpiration
{
get;
set;
}
[Display(Name = "Payment invalid if transactions fails to confirm ... minutes after invoice expiration")]
[Range(10, 60 * 24 * 31)]
public int MonitoringExpiration
{
@ -67,6 +94,12 @@ 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;
@ -76,6 +109,16 @@ namespace BTCPayServer.Models.StoreViewModels
[Display(Name = "Default crypto currency on checkout")]
public string DefaultCryptoCurrency { get; set; }
public class LightningNode
{
public string CryptoCode { get; set; }
public string Address { get; set; }
}
public List<LightningNode> LightningNodes
{
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();

@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace BTCPayServer.Models.StoreViewModels
{
public class WalletModel
{
public string ServerUrl { get; set; }
public SelectList CryptoCurrencies { get; set; }
[Display(Name = "Crypto currency")]
public string CryptoCurrency
{
get;
set;
}
class Format
{
public string Name { get; set; }
public string Value { get; set; }
}
public void SetCryptoCurrencies(ExplorerClientProvider explorerProvider, string selectedScheme)
{
var choices = explorerProvider.GetAll().Select(o => new Format() { Name = o.Item1.CryptoCode, Value = o.Item1.CryptoCode }).ToArray();
var chosen = choices.FirstOrDefault(f => f.Name == selectedScheme) ?? choices.FirstOrDefault();
CryptoCurrencies = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
CryptoCurrency = chosen.Name;
}
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Invoices;
using NBitcoin;
using Newtonsoft.Json;
namespace BTCPayServer.Payments.Bitcoin
{
public class BitcoinLikeOnChainPaymentMethod : IPaymentMethodDetails
{
public PaymentTypes GetPaymentType()
{
return PaymentTypes.BTCLike;
}
public string GetPaymentDestination()
{
return DepositAddress;
}
public decimal GetTxFee()
{
return TxFee.ToDecimal(MoneyUnit.BTC);
}
public void SetNoTxFee()
{
TxFee = Money.Zero;
}
public void SetPaymentDestination(string newPaymentDestination)
{
DepositAddress = newPaymentDestination;
}
// Those properties are JsonIgnore because their data is inside CryptoData class for legacy reason
[JsonIgnore]
public FeeRate FeeRate { get; set; }
[JsonIgnore]
public Money TxFee { get; set; }
[JsonIgnore]
public String DepositAddress { get; set; }
public BitcoinAddress GetDepositAddress(Network network)
{
return string.IsNullOrEmpty(DepositAddress) ? null : BitcoinAddress.Create(DepositAddress, network);
}
///////////////////////////////////////////////////////////////////////////////////////
}
}

@ -0,0 +1,78 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Invoices;
using NBitcoin;
using Newtonsoft.Json;
namespace BTCPayServer.Payments.Bitcoin
{
public class BitcoinLikePaymentData : CryptoPaymentData
{
public PaymentTypes GetPaymentType()
{
return PaymentTypes.BTCLike;
}
public BitcoinLikePaymentData()
{
}
public BitcoinLikePaymentData(Coin coin, bool rbf)
{
Outpoint = coin.Outpoint;
Output = coin.TxOut;
ConfirmationCount = 0;
RBF = rbf;
}
[JsonIgnore]
public OutPoint Outpoint { get; set; }
[JsonIgnore]
public TxOut Output { get; set; }
public int ConfirmationCount { get; set; }
public bool RBF { get; set; }
/// <summary>
/// This is set to true if the payment was created before CryptoPaymentData existed in BTCPayServer
/// </summary>
public bool Legacy { get; set; }
public string GetPaymentId()
{
return Outpoint.ToString();
}
public string[] GetSearchTerms()
{
return new[] { Outpoint.Hash.ToString() };
}
public decimal GetValue()
{
return Output.Value.ToDecimal(MoneyUnit.BTC);
}
public bool PaymentCompleted(PaymentEntity entity, BTCPayNetwork network)
{
return ConfirmationCount >= network.MaxTrackedConfirmation;
}
public bool PaymentConfirmed(PaymentEntity entity, SpeedPolicy speedPolicy, BTCPayNetwork network)
{
if (speedPolicy == SpeedPolicy.HighSpeed)
{
return ConfirmationCount >= 1 || !RBF;
}
else if (speedPolicy == SpeedPolicy.MediumSpeed)
{
return ConfirmationCount >= 1;
}
else if (speedPolicy == SpeedPolicy.LowSpeed)
{
return ConfirmationCount >= 6;
}
return false;
}
}
}

@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using NBitcoin;
namespace BTCPayServer.Payments.Bitcoin
{
public class BitcoinLikePaymentHandler : PaymentMethodHandlerBase<DerivationStrategy>
{
ExplorerClientProvider _ExplorerProvider;
private IFeeProviderFactory _FeeRateProviderFactory;
private Services.Wallets.BTCPayWalletProvider _WalletProvider;
public BitcoinLikePaymentHandler(ExplorerClientProvider provider,
IFeeProviderFactory feeRateProviderFactory,
Services.Wallets.BTCPayWalletProvider walletProvider)
{
if (provider == null)
throw new ArgumentNullException(nameof(provider));
_ExplorerProvider = provider;
this._FeeRateProviderFactory = feeRateProviderFactory;
_WalletProvider = walletProvider;
}
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(DerivationStrategy supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network)
{
var getFeeRate = _FeeRateProviderFactory.CreateFeeProvider(network).GetFeeRateAsync();
var getAddress = _WalletProvider.GetWallet(network).ReserveAddressAsync(supportedPaymentMethod.DerivationStrategyBase);
Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod onchainMethod = new Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod();
onchainMethod.FeeRate = await getFeeRate;
onchainMethod.TxFee = onchainMethod.FeeRate.GetFee(100); // assume price for 100 bytes
onchainMethod.DepositAddress = (await getAddress).ToString();
return onchainMethod;
}
public override Task<bool> IsAvailable(DerivationStrategy supportedPaymentMethod, BTCPayNetwork network)
{
return Task.FromResult(_ExplorerProvider.IsAvailable(network));
}
}
}

@ -0,0 +1,377 @@
using System;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Logging;
using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using NBXplorer;
using System.Collections.Concurrent;
using NBXplorer.DerivationStrategy;
using BTCPayServer.Events;
using BTCPayServer.Services;
using BTCPayServer.Services.Wallets;
using NBitcoin;
using NBXplorer.Models;
using BTCPayServer.Payments;
using BTCPayServer.HostedServices;
namespace BTCPayServer.Payments.Bitcoin
{
/// <summary>
/// This class listener NBXplorer instances to detect incoming on-chain, bitcoin like payment
/// </summary>
public class NBXplorerListener : IHostedService
{
EventAggregator _Aggregator;
ExplorerClientProvider _ExplorerClients;
IApplicationLifetime _Lifetime;
InvoiceRepository _InvoiceRepository;
private TaskCompletionSource<bool> _RunningTask;
private CancellationTokenSource _Cts;
BTCPayWalletProvider _Wallets;
BTCPayNetworkProvider _NetworkProvider;
public NBXplorerListener(ExplorerClientProvider explorerClients,
BTCPayWalletProvider wallets,
InvoiceRepository invoiceRepository,
BTCPayNetworkProvider networkProvider,
EventAggregator aggregator, IApplicationLifetime lifetime)
{
PollInterval = TimeSpan.FromMinutes(1.0);
_Wallets = wallets;
_InvoiceRepository = invoiceRepository;
_ExplorerClients = explorerClients;
_Aggregator = aggregator;
_Lifetime = lifetime;
_NetworkProvider = networkProvider;
}
CompositeDisposable leases = new CompositeDisposable();
ConcurrentDictionary<string, NotificationSession> _SessionsByCryptoCode = new ConcurrentDictionary<string, NotificationSession>();
private Timer _ListenPoller;
TimeSpan _PollInterval;
public TimeSpan PollInterval
{
get
{
return _PollInterval;
}
set
{
_PollInterval = value;
if (_ListenPoller != null)
{
_ListenPoller.Change(0, (int)value.TotalMilliseconds);
}
}
}
public Task StartAsync(CancellationToken cancellationToken)
{
_RunningTask = new TaskCompletionSource<bool>();
_Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
leases.Add(_Aggregator.Subscribe<Events.NBXplorerStateChangedEvent>(async nbxplorerEvent =>
{
if (nbxplorerEvent.NewState == NBXplorerState.Ready)
{
var wallet = _Wallets.GetWallet(nbxplorerEvent.Network);
if (_Wallets.IsAvailable(wallet.Network))
{
await Listen(wallet);
}
}
}));
_ListenPoller = new Timer(async s =>
{
foreach (var wallet in _Wallets.GetWallets())
{
if (_Wallets.IsAvailable(wallet.Network))
{
await Listen(wallet);
}
}
}, null, 0, (int)PollInterval.TotalMilliseconds);
leases.Add(_ListenPoller);
return Task.CompletedTask;
}
private async Task Listen(BTCPayWallet wallet)
{
var network = wallet.Network;
bool cleanup = false;
try
{
if (_SessionsByCryptoCode.ContainsKey(network.CryptoCode))
return;
var client = _ExplorerClients.GetExplorerClient(network);
if (client == null)
return;
if (_Cts.IsCancellationRequested)
return;
var session = await client.CreateNotificationSessionAsync(_Cts.Token).ConfigureAwait(false);
if (!_SessionsByCryptoCode.TryAdd(network.CryptoCode, session))
{
await session.DisposeAsync();
return;
}
cleanup = true;
using (session)
{
await session.ListenNewBlockAsync(_Cts.Token).ConfigureAwait(false);
await session.ListenAllDerivationSchemesAsync(cancellation: _Cts.Token).ConfigureAwait(false);
Logs.PayServer.LogInformation($"{network.CryptoCode}: Checking if any pending invoice got paid while offline...");
int paymentCount = await FindPaymentViaPolling(wallet, network);
Logs.PayServer.LogInformation($"{network.CryptoCode}: {paymentCount} payments happened while offline");
Logs.PayServer.LogInformation($"Connected to WebSocket of NBXplorer ({network.CryptoCode})");
while (!_Cts.IsCancellationRequested)
{
var newEvent = await session.NextEventAsync(_Cts.Token).ConfigureAwait(false);
switch (newEvent)
{
case NBXplorer.Models.NewBlockEvent evt:
await Task.WhenAll((await _InvoiceRepository.GetPendingInvoices())
.Select(invoiceId => UpdatePaymentStates(wallet, invoiceId))
.ToArray());
_Aggregator.Publish(new Events.NewBlockEvent() { CryptoCode = evt.CryptoCode });
break;
case NBXplorer.Models.NewTransactionEvent evt:
wallet.InvalidateCache(evt.DerivationStrategy);
foreach (var output in evt.Outputs)
{
foreach (var txCoin in evt.TransactionData.Transaction.Outputs.AsCoins()
.Where(o => o.ScriptPubKey == output.ScriptPubKey)
.Select(o => output.Redeem == null ? o : o.ToScriptCoin(output.Redeem)))
{
var invoice = await _InvoiceRepository.GetInvoiceFromScriptPubKey(output.ScriptPubKey, network.CryptoCode);
if (invoice != null)
{
var paymentData = new BitcoinLikePaymentData(txCoin, evt.TransactionData.Transaction.RBF);
var alreadyExist = GetAllBitcoinPaymentData(invoice).Where(c => c.GetPaymentId() == paymentData.GetPaymentId()).Any();
if (!alreadyExist)
{
var payment = await _InvoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, paymentData, network.CryptoCode);
if(payment != null)
await ReceivedPayment(wallet, invoice.Id, payment, evt.DerivationStrategy);
}
else
{
await UpdatePaymentStates(wallet, invoice.Id);
}
}
}
}
break;
default:
Logs.PayServer.LogWarning("Received unknown message from NBXplorer");
break;
}
}
}
}
catch when (_Cts.IsCancellationRequested) { }
catch (Exception ex)
{
Logs.PayServer.LogError(ex, $"Error while connecting to WebSocket of NBXplorer ({network.CryptoCode})");
}
finally
{
if (cleanup)
{
Logs.PayServer.LogInformation($"Disconnected from WebSocket of NBXplorer ({network.CryptoCode})");
_SessionsByCryptoCode.TryRemove(network.CryptoCode, out NotificationSession unused);
if (_SessionsByCryptoCode.Count == 0 && _Cts.IsCancellationRequested)
{
_RunningTask.TrySetResult(true);
}
}
}
}
IEnumerable<BitcoinLikePaymentData> GetAllBitcoinPaymentData(InvoiceEntity invoice)
{
return invoice.GetPayments()
.Where(p => p.GetPaymentMethodId().PaymentType == PaymentTypes.BTCLike)
.Select(p => (BitcoinLikePaymentData)p.GetCryptoPaymentData());
}
async Task<InvoiceEntity> UpdatePaymentStates(BTCPayWallet wallet, string invoiceId)
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, false);
List<PaymentEntity> updatedPaymentEntities = new List<PaymentEntity>();
var transactions = await wallet.GetTransactions(GetAllBitcoinPaymentData(invoice)
.Select(p => p.Outpoint.Hash)
.ToArray());
var conflicts = GetConflicts(transactions.Select(t => t.Value));
foreach (var payment in invoice.GetPayments(wallet.Network))
{
if (payment.GetPaymentMethodId().PaymentType != PaymentTypes.BTCLike)
continue;
var paymentData = (BitcoinLikePaymentData)payment.GetCryptoPaymentData();
if (!transactions.TryGetValue(paymentData.Outpoint.Hash, out TransactionResult tx))
continue;
var txId = tx.Transaction.GetHash();
var txConflict = conflicts.GetConflict(txId);
var accounted = txConflict == null || txConflict.IsWinner(txId);
bool updated = false;
if (accounted != payment.Accounted)
{
updated = true;
payment.Accounted = accounted;
}
if (paymentData.ConfirmationCount != tx.Confirmations)
{
if (wallet.Network.MaxTrackedConfirmation >= paymentData.ConfirmationCount)
{
paymentData.ConfirmationCount = tx.Confirmations;
payment.SetCryptoPaymentData(paymentData);
updated = true;
}
}
if (updated)
updatedPaymentEntities.Add(payment);
}
await _InvoiceRepository.UpdatePayments(updatedPaymentEntities);
if (updatedPaymentEntities.Count != 0)
_Aggregator.Publish(new Events.InvoiceNeedUpdateEvent(invoice.Id));
return invoice;
}
class TransactionConflict
{
public Dictionary<uint256, TransactionResult> Transactions { get; set; } = new Dictionary<uint256, TransactionResult>();
uint256 _Winner;
public bool IsWinner(uint256 txId)
{
if (_Winner == null)
{
var confirmed = Transactions.FirstOrDefault(t => t.Value.Confirmations >= 1);
if (!confirmed.Equals(default(KeyValuePair<uint256, TransactionResult>)))
{
_Winner = confirmed.Key;
}
else
{
// Take the most recent (bitcoin node would not forward a conflict without a successful RBF)
_Winner = Transactions
.OrderByDescending(t => t.Value.Timestamp)
.First()
.Key;
}
}
return _Winner == txId;
}
}
class TransactionConflicts : List<TransactionConflict>
{
public TransactionConflicts(IEnumerable<TransactionConflict> collection) : base(collection)
{
}
public TransactionConflict GetConflict(uint256 txId)
{
return this.FirstOrDefault(c => c.Transactions.ContainsKey(txId));
}
}
private TransactionConflicts GetConflicts(IEnumerable<TransactionResult> transactions)
{
Dictionary<OutPoint, TransactionConflict> conflictsByOutpoint = new Dictionary<OutPoint, TransactionConflict>();
foreach (var tx in transactions)
{
var hash = tx.Transaction.GetHash();
foreach (var input in tx.Transaction.Inputs)
{
TransactionConflict conflict = new TransactionConflict();
if (!conflictsByOutpoint.TryAdd(input.PrevOut, conflict))
{
conflict = conflictsByOutpoint[input.PrevOut];
}
if (!conflict.Transactions.ContainsKey(hash))
conflict.Transactions.Add(hash, tx);
}
}
return new TransactionConflicts(conflictsByOutpoint.Where(c => c.Value.Transactions.Count > 1).Select(c => c.Value));
}
private async Task<int> FindPaymentViaPolling(BTCPayWallet wallet, BTCPayNetwork network)
{
int totalPayment = 0;
var invoices = await _InvoiceRepository.GetPendingInvoices();
foreach (var invoiceId in invoices)
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, true);
var alreadyAccounted = GetAllBitcoinPaymentData(invoice).Select(p => p.Outpoint).ToHashSet();
var strategy = GetDerivationStrategy(invoice, network);
if (strategy == null)
continue;
var cryptoId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
if (!invoice.Support(cryptoId))
continue;
var coins = (await wallet.GetUnspentCoins(strategy))
.Where(c => invoice.AvailableAddressHashes.Contains(c.Coin.ScriptPubKey.Hash.ToString() + cryptoId))
.ToArray();
foreach (var coin in coins.Where(c => !alreadyAccounted.Contains(c.Coin.Outpoint)))
{
var transaction = await wallet.GetTransactionAsync(coin.Coin.Outpoint.Hash);
var paymentData = new BitcoinLikePaymentData(coin.Coin, transaction.Transaction.RBF);
var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin.Timestamp, paymentData, network.CryptoCode).ConfigureAwait(false);
alreadyAccounted.Add(coin.Coin.Outpoint);
if (payment != null)
invoice = await ReceivedPayment(wallet, invoice.Id, payment, strategy);
totalPayment++;
}
}
return totalPayment;
}
private DerivationStrategyBase GetDerivationStrategy(InvoiceEntity invoice, BTCPayNetwork network)
{
return invoice.GetSupportedPaymentMethod<DerivationStrategy>(new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike), _ExplorerClients.NetworkProviders)
.Select(d => d.DerivationStrategyBase)
.FirstOrDefault();
}
private async Task<InvoiceEntity> ReceivedPayment(BTCPayWallet wallet, string invoiceId, PaymentEntity payment, DerivationStrategyBase strategy)
{
var paymentData = (BitcoinLikePaymentData)payment.GetCryptoPaymentData();
var invoice = (await UpdatePaymentStates(wallet, invoiceId));
var paymentMethod = invoice.GetPaymentMethod(wallet.Network, PaymentTypes.BTCLike, _ExplorerClients.NetworkProviders);
if (paymentMethod != null &&
paymentMethod.GetPaymentMethodDetails() is BitcoinLikeOnChainPaymentMethod btc &&
btc.GetDepositAddress(wallet.Network.NBitcoinNetwork).ScriptPubKey == paymentData.Output.ScriptPubKey &&
paymentMethod.Calculate().Due > Money.Zero)
{
var address = await wallet.ReserveAddressAsync(strategy);
btc.DepositAddress = address.ToString();
await _InvoiceRepository.NewAddress(invoiceId, btc, wallet.Network);
_Aggregator.Publish(new InvoiceNewAddressEvent(invoiceId, address.ToString(), wallet.Network));
paymentMethod.SetPaymentMethodDetails(btc);
invoice.SetPaymentMethod(paymentMethod);
}
wallet.InvalidateCache(strategy);
_Aggregator.Publish(new InvoiceEvent(invoiceId, 1002, "invoice_receivedPayment"));
return invoice;
}
public Task StopAsync(CancellationToken cancellationToken)
{
leases.Dispose();
_Cts.Cancel();
return Task.WhenAny(_RunningTask.Task, Task.Delay(-1, cancellationToken));
}
}
}

@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NBitcoin;
namespace BTCPayServer.Payments
{
/// <summary>
/// Represent information necessary to track a payment
/// </summary>
public interface IPaymentMethodDetails
{
/// <summary>
/// A string representation of the payment destination
/// </summary>
/// <returns></returns>
string GetPaymentDestination();
PaymentTypes GetPaymentType();
/// <summary>
/// Returns what a merchant would need to pay to cashout this payment
/// </summary>
/// <returns></returns>
decimal GetTxFee();
void SetNoTxFee();
/// <summary>
/// Change the payment destination (internal plumbing)
/// </summary>
/// <param name="newPaymentDestination"></param>
void SetPaymentDestination(string newPaymentDestination);
}
}

@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Invoices;
namespace BTCPayServer.Payments
{
/// <summary>
/// This class customize invoice creation by the creation of payment details for the PaymentMethod during invoice creation
/// </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="network"></param>
/// <returns></returns>
Task<IPaymentMethodDetails> CreatePaymentMethodDetails(ISupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, 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);
}
public abstract class PaymentMethodHandlerBase<T> : IPaymentMethodHandler<T> where T : ISupportedPaymentMethod
{
public abstract Task<IPaymentMethodDetails> CreatePaymentMethodDetails(T supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network);
Task<IPaymentMethodDetails> IPaymentMethodHandler.CreatePaymentMethodDetails(ISupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network)
{
if (supportedPaymentMethod is T method)
{
return CreatePaymentMethodDetails(method, paymentMethod, 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);
}
}
}

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Invoices;
namespace BTCPayServer.Payments
{
/// <summary>
/// This class represent a mode of payment supported by a store.
/// It is stored at the store level and cloned to the invoice during invoice creation.
/// This object will be serialized in database in json
/// </summary>
public interface ISupportedPaymentMethod
{
PaymentMethodId PaymentId { get; }
}
}

@ -0,0 +1,130 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using NBitcoin;
using NBXplorer;
using Newtonsoft.Json;
namespace BTCPayServer.Payments.Lightning.CLightning
{
public class ChargeClient
{
private Uri _Uri;
public Uri Uri
{
get
{
return _Uri;
}
}
private Network _Network;
static HttpClient _Client = new HttpClient();
public ChargeClient(Uri uri, Network network)
{
if (uri == null)
throw new ArgumentNullException(nameof(uri));
if (network == null)
throw new ArgumentNullException(nameof(network));
this._Uri = uri;
this._Network = network;
if (uri.UserInfo == null)
throw new ArgumentException(paramName: nameof(uri), message: "User information not present in uri");
var userInfo = uri.UserInfo.Split(':');
if (userInfo.Length != 2)
throw new ArgumentException(paramName: nameof(uri), message: "User information not present in uri");
Credentials = new NetworkCredential(userInfo[0], userInfo[1]);
}
public async Task<CreateInvoiceResponse> CreateInvoiceAsync(CreateInvoiceRequest request, CancellationToken cancellation = default(CancellationToken))
{
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("expiry", ((int)request.Expiry.TotalSeconds).ToString(CultureInfo.InvariantCulture));
message.Content = new FormUrlEncodedContent(parameters);
var result = await _Client.SendAsync(message, cancellation);
result.EnsureSuccessStatusCode();
var content = await result.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<CreateInvoiceResponse>(content);
}
public async Task<ChargeSession> Listen(CancellationToken cancellation = default(CancellationToken))
{
var socket = new ClientWebSocket();
socket.Options.SetRequestHeader("Authorization", $"Basic {GetBase64Creds()}");
var uri = new UriBuilder(Uri) { UserName = null, Password = null }.Uri.AbsoluteUri;
if (!uri.EndsWith('/'))
uri += "/";
uri += "ws";
uri = ToWebsocketUri(uri);
await socket.ConnectAsync(new Uri(uri), cancellation);
return new ChargeSession(socket);
}
private static string ToWebsocketUri(string uri)
{
if (uri.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
uri = uri.Replace("https://", "wss://", StringComparison.OrdinalIgnoreCase);
if (uri.StartsWith("http://", StringComparison.OrdinalIgnoreCase))
uri = uri.Replace("http://", "ws://", StringComparison.OrdinalIgnoreCase);
return uri;
}
public NetworkCredential Credentials { get; set; }
public GetInfoResponse GetInfo()
{
return GetInfoAsync().GetAwaiter().GetResult();
}
public async Task<ChargeInvoice> GetInvoice(string invoiceId, CancellationToken cancellation = default(CancellationToken))
{
var request = CreateMessage(HttpMethod.Get, $"invoice/{invoiceId}");
var message = await _Client.SendAsync(request, cancellation);
if (message.StatusCode == HttpStatusCode.NotFound)
return null;
message.EnsureSuccessStatusCode();
var content = await message.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<ChargeInvoice>(content);
}
public async Task<GetInfoResponse> GetInfoAsync(CancellationToken cancellation = default(CancellationToken))
{
var request = CreateMessage(HttpMethod.Get, "info");
var message = await _Client.SendAsync(request, cancellation);
message.EnsureSuccessStatusCode();
var content = await message.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<GetInfoResponse>(content);
}
private HttpRequestMessage CreateMessage(HttpMethod method, string path)
{
var uri = GetFullUri(path);
var request = new HttpRequestMessage(method, uri);
request.Headers.Authorization = new AuthenticationHeaderValue("Basic", GetBase64Creds());
return request;
}
private string GetBase64Creds()
{
return Convert.ToBase64String(Encoding.ASCII.GetBytes($"{Credentials.UserName}:{Credentials.Password}"));
}
private Uri GetFullUri(string partialUrl)
{
var uri = _Uri.AbsoluteUri;
if (!uri.EndsWith("/", StringComparison.InvariantCultureIgnoreCase))
uri += "/";
return new Uri(uri + partialUrl);
}
}
}

@ -0,0 +1,124 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using NBXplorer;
using Newtonsoft.Json;
namespace BTCPayServer.Payments.Lightning.CLightning
{
public class ChargeInvoice
{
public string Id { get; set; }
[JsonProperty("msatoshi")]
[JsonConverter(typeof(JsonConverters.LightMoneyJsonConverter))]
public LightMoney MilliSatoshi { get; set; }
[JsonProperty("paid_at")]
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
public DateTimeOffset? PaidAt { get; set; }
[JsonProperty("expires_at")]
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
public DateTimeOffset? ExpiresAt { get; set; }
public string Status { get; set; }
[JsonProperty("payreq")]
public string PaymentRequest { get; set; }
}
public class ChargeSession : IDisposable
{
private ClientWebSocket socket;
const int ORIGINAL_BUFFER_SIZE = 1024 * 5;
const int MAX_BUFFER_SIZE = 1024 * 1024 * 5;
public ChargeSession(ClientWebSocket socket)
{
this.socket = socket;
var buffer = new byte[ORIGINAL_BUFFER_SIZE];
_Buffer = new ArraySegment<byte>(buffer, 0, buffer.Length);
}
ArraySegment<byte> _Buffer;
public async Task<ChargeInvoice> NextEvent(CancellationToken cancellation = default(CancellationToken))
{
var buffer = _Buffer;
var array = _Buffer.Array;
var originalSize = _Buffer.Array.Length;
var newSize = _Buffer.Array.Length;
while (true)
{
var message = await socket.ReceiveAsync(buffer, cancellation);
if (message.MessageType == WebSocketMessageType.Close)
{
await CloseSocketAndThrow(WebSocketCloseStatus.NormalClosure, "Close message received from the peer", cancellation);
break;
}
if (message.MessageType != WebSocketMessageType.Text)
{
await CloseSocketAndThrow(WebSocketCloseStatus.InvalidMessageType, "Only Text is supported", cancellation);
break;
}
if (message.EndOfMessage)
{
buffer = new ArraySegment<byte>(array, 0, buffer.Offset + message.Count);
try
{
var o = ParseMessage(buffer);
if (newSize != originalSize)
{
Array.Resize(ref array, originalSize);
}
return o;
}
catch (Exception ex)
{
await CloseSocketAndThrow(WebSocketCloseStatus.InvalidPayloadData, $"Invalid payload: {ex.Message}", cancellation);
}
}
else
{
if (buffer.Count - message.Count <= 0)
{
newSize *= 2;
if (newSize > MAX_BUFFER_SIZE)
await CloseSocketAndThrow(WebSocketCloseStatus.MessageTooBig, "Message is too big", cancellation);
Array.Resize(ref array, newSize);
buffer = new ArraySegment<byte>(array, buffer.Offset, newSize - buffer.Offset);
}
buffer = buffer.Slice(message.Count, buffer.Count - message.Count);
}
}
throw new InvalidOperationException("Should never happen");
}
UTF8Encoding UTF8 = new UTF8Encoding(false, true);
private ChargeInvoice ParseMessage(ArraySegment<byte> buffer)
{
var str = UTF8.GetString(buffer.Array, 0, buffer.Count);
return JsonConvert.DeserializeObject<ChargeInvoice>(str, new JsonSerializerSettings());
}
private async Task CloseSocketAndThrow(WebSocketCloseStatus status, string description, CancellationToken cancellation)
{
var array = _Buffer.Array;
if (array.Length != ORIGINAL_BUFFER_SIZE)
Array.Resize(ref array, ORIGINAL_BUFFER_SIZE);
await socket.CloseSocket(status, description, cancellation);
throw new WebSocketException($"The socket has been closed ({status}: {description})");
}
public async void Dispose()
{
await this.socket.CloseSocket();
}
public async Task DisposeAsync()
{
await this.socket.CloseSocket();
}
}
}

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Payments.Lightning.CLightning
{
public class CreateInvoiceRequest
{
public LightMoney Amont { get; set; }
public TimeSpan Expiry { get; set; }
}
}

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Payments.Lightning.CLightning
{
public class CreateInvoiceResponse
{
public string PayReq { get; set; }
public string Id { get; set; }
}
}

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Payments.Lightning.CLightning
{
//[{"type":"ipv4","address":"52.166.90.122","port":9735}]
public class GetInfoResponse
{
public class GetInfoAddress
{
public string Type { get; set; }
public string Address { get; set; }
public int Port { get; set; }
}
public string Id { get; set; }
public int Port { get; set; }
public GetInfoAddress[] Address { get; set; }
public string Version { get; set; }
public int BlockHeight { get; set; }
public string Network { get; set; }
}
}

@ -0,0 +1,118 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
using NBitcoin;
using NBitcoin.RPC;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Payments.Lightning.CLightning.RPC
{
public class CLightningRPCClient
{
public Network Network { get; private set; }
public Uri Address { get; private set; }
public CLightningRPCClient(Uri address, Network network)
{
if (address == null)
throw new ArgumentNullException(nameof(address));
if (network == null)
throw new ArgumentNullException(nameof(network));
Address = address;
Network = network;
}
public Task<GetInfoResponse> GetInfoAsync()
{
return SendCommandAsync<GetInfoResponse>("getinfo");
}
public Task SendAsync(string bolt11)
{
return SendCommandAsync<object>("pay", new[] { bolt11 }, true);
}
public async Task<PeerInfo[]> ListPeersAsync()
{
var peers = await SendCommandAsync<PeerInfo[]>("listpeers", isArray: true);
foreach(var peer in peers)
{
peer.Channels = peer.Channels ?? Array.Empty<ChannelInfo>();
}
return peers;
}
public Task FundChannelAsync(NodeInfo nodeInfo, Money money)
{
return SendCommandAsync<object>("fundchannel", new object[] { nodeInfo.NodeId, money.Satoshi }, true);
}
public Task ConnectAsync(NodeInfo nodeInfo)
{
return SendCommandAsync<object>("connect", new[] { $"{nodeInfo.NodeId}@{nodeInfo.Host}:{nodeInfo.Port}" }, true);
}
static Encoding UTF8 = new UTF8Encoding(false);
private async Task<T> SendCommandAsync<T>(string command, object[] parameters = null, bool noReturn = false, bool isArray = false)
{
parameters = parameters ?? Array.Empty<string>();
var domain = Address.DnsSafeHost;
if (!IPAddress.TryParse(domain, out IPAddress address))
{
address = (await Dns.GetHostAddressesAsync(domain)).FirstOrDefault();
if (address == null)
throw new Exception("Host not found");
}
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
await socket.ConnectAsync(new IPEndPoint(address, Address.Port));
using (var networkStream = new NetworkStream(socket))
{
using (var textWriter = new StreamWriter(networkStream, UTF8, 1024 * 10, true))
{
using (var jsonWriter = new JsonTextWriter(textWriter))
{
var req = new JObject();
req.Add("id", 0);
req.Add("method", command);
req.Add("params", new JArray(parameters));
await req.WriteToAsync(jsonWriter);
await jsonWriter.FlushAsync();
}
await textWriter.FlushAsync();
}
await networkStream.FlushAsync();
using (var textReader = new StreamReader(networkStream, UTF8, false, 1024 * 10, true))
{
using (var jsonReader = new JsonTextReader(textReader))
{
var result = await JObject.LoadAsync(jsonReader);
var error = result.Property("error");
if(error != null)
{
throw new Exception(error.Value.ToString());
}
if (noReturn)
return default(T);
if (isArray)
{
return result["result"].Children().First().Children().First().ToObject<T>();
}
return result["result"].ToObject<T>();
}
}
}
}
public async Task<BitcoinAddress> NewAddressAsync()
{
var obj = await SendCommandAsync<JObject>("newaddr");
return BitcoinAddress.Create(obj.Property("address").Value.Value<string>(), Network);
}
}
}

@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Eclair
namespace BTCPayServer.Payments.Lightning.CLightning.RPC
{
public class NodeInfo
{

@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NBitcoin;
using Newtonsoft.Json;
namespace BTCPayServer.Payments.Lightning.CLightning.RPC
{
public class ChannelInfo
{
public string State { get; set; }
public string Owner { get; set; }
[JsonProperty("funding_txid")]
[JsonConverter(typeof(NBitcoin.JsonConverters.UInt256JsonConverter))]
public uint256 FundingTxId { get; set; }
[JsonProperty("msatoshi_to_us")]
[JsonConverter(typeof(JsonConverters.LightMoneyJsonConverter))]
public LightMoney ToUs { get; set; }
[JsonProperty("msatoshi_total")]
[JsonConverter(typeof(JsonConverters.LightMoneyJsonConverter))]
public LightMoney Total { get; set; }
[JsonProperty("dust_limit_satoshis")]
[JsonConverter(typeof(NBitcoin.JsonConverters.MoneyJsonConverter))]
public Money DustLimit { get; set; }
[JsonProperty("max_htlc_value_in_flight_msat")]
[JsonConverter(typeof(JsonConverters.LightMoneyJsonConverter))]
public LightMoney MaxHTLCValueInFlight { get; set; }
[JsonProperty("channel_reserve_satoshis")]
[JsonConverter(typeof(NBitcoin.JsonConverters.MoneyJsonConverter))]
public Money ChannelReserve { get; set; }
[JsonProperty("htlc_minimum_msat")]
[JsonConverter(typeof(JsonConverters.LightMoneyJsonConverter))]
public LightMoney HTLCMinimum { get; set; }
[JsonProperty("to_self_delay")]
public int ToSelfDelay { get; set; }
[JsonProperty("max_accepted_htlcs")]
public int MaxAcceptedHTLCS { get; set; }
public string[] Status { get; set; }
}
public class PeerInfo
{
public string State { get; set; }
public string Id { get; set; }
[JsonProperty("netaddr")]
public string[] NetworkAddresses { get; set; }
public bool Connected { get; set; }
public string Owner { get; set; }
public ChannelInfo[] Channels { get; set; }
}
}

@ -0,0 +1,271 @@
using System;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Events;
using BTCPayServer.Logging;
using BTCPayServer.Payments.Lightning.CLightning;
using BTCPayServer.Services.Invoices;
using Microsoft.Extensions.Hosting;
using NBXplorer;
namespace BTCPayServer.Payments.Lightning
{
public class ChargeListener : IHostedService
{
class ListenedInvoice
{
public LightningLikePaymentMethodDetails PaymentMethodDetails { get; set; }
public LightningSupportedPaymentMethod SupportedPaymentMethod { get; set; }
public PaymentMethod PaymentMethod { get; set; }
public string Uri { get; internal set; }
public BTCPayNetwork Network { get; internal set; }
public string InvoiceId { get; internal set; }
}
EventAggregator _Aggregator;
InvoiceRepository _InvoiceRepository;
BTCPayNetworkProvider _NetworkProvider;
public ChargeListener(EventAggregator aggregator,
InvoiceRepository invoiceRepository,
BTCPayNetworkProvider networkProvider)
{
_Aggregator = aggregator;
_InvoiceRepository = invoiceRepository;
_NetworkProvider = networkProvider;
}
CompositeDisposable leases = new CompositeDisposable();
public Task StartAsync(CancellationToken cancellationToken)
{
leases.Add(_Aggregator.Subscribe<Events.InvoiceEvent>(async inv =>
{
if (inv.Name == "invoice_created")
{
await EnsureListening(inv.InvoiceId, false);
}
}));
_ListenPoller = new Timer(async s =>
{
await Task.WhenAll((await _InvoiceRepository.GetPendingInvoices())
.Select(async invoiceId => await EnsureListening(invoiceId, true))
.ToArray());
}, null, 0, (int)PollInterval.TotalMilliseconds);
leases.Add(_ListenPoller);
return Task.CompletedTask;
}
private async Task EnsureListening(string invoiceId, bool poll)
{
if (Listening(invoiceId))
return;
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
foreach (var paymentMethod in invoice.GetPaymentMethods(_NetworkProvider)
.Where(c => c.GetId().PaymentType == PaymentTypes.LightningLike))
{
var lightningMethod = paymentMethod.GetPaymentMethodDetails() as LightningLikePaymentMethodDetails;
if (lightningMethod == null)
continue;
var lightningSupportedMethod = invoice.GetSupportedPaymentMethod<LightningSupportedPaymentMethod>(_NetworkProvider)
.FirstOrDefault(c => c.CryptoCode == paymentMethod.GetId().CryptoCode);
if (lightningSupportedMethod == null)
continue;
var network = _NetworkProvider.GetNetwork(paymentMethod.GetId().CryptoCode);
var listenedInvoice = new ListenedInvoice()
{
Uri = lightningSupportedMethod.GetLightningChargeUrl(false).AbsoluteUri,
PaymentMethodDetails = lightningMethod,
SupportedPaymentMethod = lightningSupportedMethod,
PaymentMethod = paymentMethod,
Network = network,
InvoiceId = invoice.Id
};
if (poll)
{
var charge = GetChargeClient(lightningSupportedMethod, network);
var chargeInvoice = await charge.GetInvoice(lightningMethod.InvoiceId);
if (chargeInvoice == null)
continue;
if (chargeInvoice.Status == "paid")
await AddPayment(network, chargeInvoice, listenedInvoice);
if (chargeInvoice.Status == "paid" || chargeInvoice.Status == "expired")
continue;
}
StartListening(listenedInvoice);
}
}
TimeSpan _PollInterval = TimeSpan.FromMinutes(1.0);
public TimeSpan PollInterval
{
get
{
return _PollInterval;
}
set
{
_PollInterval = value;
if (_ListenPoller != null)
{
_ListenPoller.Change(0, (int)value.TotalMilliseconds);
}
}
}
CancellationTokenSource _Cts = new CancellationTokenSource();
private async Task Listen(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
{
try
{
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Start listening {supportedPaymentMethod.GetLightningChargeUrl(false)}");
var charge = GetChargeClient(supportedPaymentMethod, network);
var session = await charge.Listen(_Cts.Token);
while (true)
{
var notification = await session.NextEvent(_Cts.Token);
ListenedInvoice listenedInvoice = GetListenedInvoice(notification.Id);
if (listenedInvoice == null)
continue;
if (notification.Id == listenedInvoice.PaymentMethodDetails.InvoiceId &&
notification.PaymentRequest == listenedInvoice.PaymentMethodDetails.BOLT11)
{
if (notification.Status == "paid" && notification.PaidAt.HasValue)
{
await AddPayment(network, notification, listenedInvoice);
if (DoneListening(listenedInvoice))
break;
}
if (notification.Status == "expired")
{
if (DoneListening(listenedInvoice))
break;
}
}
}
}
catch when (_Cts.IsCancellationRequested)
{
}
catch (Exception ex)
{
Logs.PayServer.LogError(ex, $"{supportedPaymentMethod.CryptoCode} (Lightning): Error while contacting {supportedPaymentMethod.GetLightningChargeUrl(false)}");
DoneListening(supportedPaymentMethod.GetLightningChargeUrl(false));
}
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Stop listening {supportedPaymentMethod.GetLightningChargeUrl(false)}");
}
private async Task AddPayment(BTCPayNetwork network, ChargeInvoice notification, ListenedInvoice listenedInvoice)
{
var payment = await _InvoiceRepository.AddPayment(listenedInvoice.InvoiceId, notification.PaidAt.Value, new LightningLikePaymentData()
{
BOLT11 = notification.PaymentRequest,
Amount = notification.MilliSatoshi
}, network.CryptoCode, accounted: true);
if(payment != null)
_Aggregator.Publish(new InvoiceEvent(listenedInvoice.InvoiceId, 1002, "invoice_receivedPayment"));
}
private static ChargeClient GetChargeClient(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
{
return new ChargeClient(supportedPaymentMethod.GetLightningChargeUrl(true), network.NBitcoinNetwork);
}
List<Task> _ListeningLightning = new List<Task>();
MultiValueDictionary<string, ListenedInvoice> _ListenedInvoiceByLightningUrl = new MultiValueDictionary<string, ListenedInvoice>();
Dictionary<string, ListenedInvoice> _ListenedInvoiceByChargeInvoiceId = new Dictionary<string, ListenedInvoice>();
HashSet<string> _InvoiceIds = new HashSet<string>();
private Timer _ListenPoller;
/// <summary>
/// Stop listening an invoice
/// </summary>
/// <param name="listenedInvoice">The invoice to stop listening</param>
/// <returns>true if still need to listen the lightning instance</returns>
bool DoneListening(ListenedInvoice listenedInvoice)
{
lock (_ListenedInvoiceByLightningUrl)
{
_ListenedInvoiceByChargeInvoiceId.Remove(listenedInvoice.PaymentMethodDetails.InvoiceId);
_ListenedInvoiceByLightningUrl.Remove(listenedInvoice.Uri, listenedInvoice);
_InvoiceIds.Remove(listenedInvoice.InvoiceId);
if (!_ListenedInvoiceByLightningUrl.ContainsKey(listenedInvoice.Uri))
{
return true;
}
}
return false;
}
/// <summary>
/// Stop listening all invoices on this server
/// </summary>
/// <param name="uri"></param>
private void DoneListening(Uri uri)
{
lock (_ListenedInvoiceByChargeInvoiceId)
{
foreach (var listenedInvoice in _ListenedInvoiceByLightningUrl[uri.AbsoluteUri])
{
_ListenedInvoiceByChargeInvoiceId.Remove(listenedInvoice.PaymentMethodDetails.InvoiceId);
_InvoiceIds.Remove(listenedInvoice.InvoiceId);
}
_ListenedInvoiceByLightningUrl.Remove(uri.AbsoluteUri);
}
}
bool Listening(string invoiceId)
{
lock (_ListenedInvoiceByLightningUrl)
{
return _InvoiceIds.Contains(invoiceId);
}
}
private ListenedInvoice GetListenedInvoice(string chargeInvoiceId)
{
ListenedInvoice listenedInvoice = null;
lock (_ListenedInvoiceByLightningUrl)
{
_ListenedInvoiceByChargeInvoiceId.TryGetValue(chargeInvoiceId, out listenedInvoice);
}
return listenedInvoice;
}
bool StartListening(ListenedInvoice listenedInvoice)
{
lock (_ListenedInvoiceByLightningUrl)
{
if (_InvoiceIds.Contains(listenedInvoice.InvoiceId))
return false;
if (!_ListenedInvoiceByLightningUrl.ContainsKey(listenedInvoice.Uri))
{
var listen = Listen(listenedInvoice.SupportedPaymentMethod, listenedInvoice.Network);
_ListeningLightning.Add(listen);
}
_ListenedInvoiceByLightningUrl.Add(listenedInvoice.Uri, listenedInvoice);
_ListenedInvoiceByChargeInvoiceId.Add(listenedInvoice.PaymentMethodDetails.InvoiceId, listenedInvoice);
_InvoiceIds.Add(listenedInvoice.InvoiceId);
}
return true;
}
public async Task StopAsync(CancellationToken cancellationToken)
{
leases.Dispose();
_Cts.Cancel();
Task[] listening = null;
lock (_ListenedInvoiceByLightningUrl)
{
listening = _ListeningLightning.ToArray();
}
await Task.WhenAll(listening);
}
}
}

@ -3,8 +3,9 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using NBitcoin;
namespace BTCPayServer.Eclair
namespace BTCPayServer.Payments.Lightning
{
public enum LightMoneyUnit : ulong
{
@ -94,28 +95,31 @@ namespace BTCPayServer.Eclair
return a;
}
public LightMoney(int satoshis)
public LightMoney(int msatoshis)
{
MilliSatoshi = satoshis;
MilliSatoshi = msatoshis;
}
public LightMoney(uint satoshis)
public LightMoney(uint msatoshis)
{
MilliSatoshi = satoshis;
MilliSatoshi = msatoshis;
}
public LightMoney(Money money)
{
MilliSatoshi = checked(money.Satoshi * 1000);
}
public LightMoney(long msatoshis)
{
MilliSatoshi = msatoshis;
}
public LightMoney(long satoshis)
{
MilliSatoshi = satoshis;
}
public LightMoney(ulong satoshis)
public LightMoney(ulong msatoshis)
{
// overflow check.
// ulong.MaxValue is greater than long.MaxValue
checked
{
MilliSatoshi = (long)satoshis;
MilliSatoshi = (long)msatoshis;
}
}
@ -139,7 +143,7 @@ namespace BTCPayServer.Eclair
public IEnumerable<LightMoney> Split(int parts)
{
if (parts <= 0)
throw new ArgumentOutOfRangeException("Parts should be more than 0", "parts");
throw new ArgumentOutOfRangeException(nameof(parts), "Parts should be more than 0");
long remain;
long result = DivRem(_MilliSatoshis, parts, out remain);
@ -171,7 +175,7 @@ namespace BTCPayServer.Eclair
CheckMoneyUnit(unit, "unit");
// overflow safe because (long / int) always fit in decimal
// decimal operations are checked by default
return (decimal)MilliSatoshi / (int)unit;
return (decimal)MilliSatoshi / (ulong)unit;
}
/// <summary>
/// Convert Money to decimal (same as ToUnit)
@ -431,7 +435,7 @@ namespace BTCPayServer.Eclair
/// <returns></returns>
public string ToString(bool fplus, bool trimExcessZero = true)
{
var fmt = string.Format("{{0:{0}{1}B}}",
var fmt = string.Format(CultureInfo.InvariantCulture, "{{0:{0}{1}B}}",
(fplus ? "+" : null),
(trimExcessZero ? "2" : "11"));
return string.Format(BitcoinFormatter.Formatter, fmt, _MilliSatoshis);
@ -479,7 +483,7 @@ namespace BTCPayServer.Eclair
unitToUseInCalc = LightMoneyUnit.BTC;
break;
}
var val = Convert.ToDecimal(arg) / (long)unitToUseInCalc;
var val = Convert.ToDecimal(arg, CultureInfo.InvariantCulture) / (long)unitToUseInCalc;
var zeros = new string('0', decPos);
var rest = new string('#', 11 - decPos);
var fmt = plus && val > 0 ? "+" : string.Empty;

@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.JsonConverters;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services.Invoices;
using Newtonsoft.Json;
namespace BTCPayServer.Payments.Lightning
{
public class LightningLikePaymentData : CryptoPaymentData
{
[JsonConverter(typeof(LightMoneyJsonConverter))]
public LightMoney Amount { get; set; }
public string BOLT11 { get; set; }
public string GetPaymentId()
{
return BOLT11;
}
public PaymentTypes GetPaymentType()
{
return PaymentTypes.LightningLike;
}
public string[] GetSearchTerms()
{
return new[] { BOLT11 };
}
public decimal GetValue()
{
return Amount.ToDecimal(LightMoneyUnit.BTC);
}
public bool PaymentCompleted(PaymentEntity entity, BTCPayNetwork network)
{
return true;
}
public bool PaymentConfirmed(PaymentEntity entity, SpeedPolicy speedPolicy, BTCPayNetwork network)
{
return true;
}
}
}

@ -0,0 +1,137 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.HostedServices;
using BTCPayServer.Payments.Lightning.CLightning;
using BTCPayServer.Services.Invoices;
namespace BTCPayServer.Payments.Lightning
{
public class LightningLikePaymentHandler : PaymentMethodHandlerBase<LightningSupportedPaymentMethod>
{
NBXplorerDashboard _Dashboard;
public LightningLikePaymentHandler(NBXplorerDashboard dashboard)
{
_Dashboard = dashboard;
}
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(LightningSupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network)
{
var invoice = paymentMethod.ParentEntity;
var due = Extensions.RoundUp(invoice.ProductInformation.Price / paymentMethod.Rate, 8);
var client = GetClient(supportedPaymentMethod, network);
var expiry = invoice.ExpirationTime - DateTimeOffset.UtcNow;
var lightningInvoice = await client.CreateInvoiceAsync(new CreateInvoiceRequest()
{
Amont = new LightMoney(due, LightMoneyUnit.BTC),
Expiry = expiry < TimeSpan.Zero ? TimeSpan.FromSeconds(1) : expiry
});
return new LightningLikePaymentMethodDetails()
{
BOLT11 = lightningInvoice.PayReq,
InvoiceId = lightningInvoice.Id
};
}
public async override Task<bool> IsAvailable(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
{
try
{
await Test(supportedPaymentMethod, network);
return true;
}
catch { return false; }
}
public async Task Test(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
{
if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
throw new Exception($"Full node not available");
var cts = new CancellationTokenSource(5000);
var client = GetClient(supportedPaymentMethod, network);
GetInfoResponse info = null;
try
{
info = await client.GetInfoAsync(cts.Token);
}
catch (Exception ex)
{
throw new Exception($"Error while connecting to the lightning charge {client.Uri} ({ex.Message})");
}
var address = info.Address.Select(a=>a.Address).FirstOrDefault();
var port = info.Port;
address = address ?? client.Uri.DnsSafeHost;
if (info.Network != network.CLightningNetworkName)
{
throw new Exception($"Lightning node network {info.Network}, but expected is {network.CLightningNetworkName}");
}
var blocksGap = Math.Abs(info.BlockHeight - summary.Status.ChainHeight);
if (blocksGap > 10)
{
throw new Exception($"The lightning is not synched ({blocksGap} blocks)");
}
try
{
await TestConnection(address, port, cts.Token);
}
catch (Exception ex)
{
throw new Exception($"Error while connecting to the lightning node via {address}:{port} ({ex.Message})");
}
}
private static ChargeClient GetClient(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
{
return new ChargeClient(supportedPaymentMethod.GetLightningChargeUrl(true), network.NBitcoinNetwork);
}
private async Task<bool> TestConnection(string addressStr, int port, CancellationToken cancellation)
{
IPAddress address = null;
try
{
address = IPAddress.Parse(addressStr);
}
catch
{
try
{
address = (await Dns.GetHostAddressesAsync(addressStr)).FirstOrDefault();
}
catch { }
}
if (address == null)
throw new Exception($"DNS did not resolved {addressStr}");
using (var tcp = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp))
{
try
{
await WithTimeout(tcp.ConnectAsync(new IPEndPoint(address, port)), cancellation);
}
catch { return false; }
}
return true;
}
static Task WithTimeout(Task task, CancellationToken token)
{
TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();
var registration = token.Register(() => { try { tcs.TrySetResult(true); } catch { } });
#pragma warning disable CA2008 // Do not create tasks without passing a TaskScheduler
var timeoutTask = tcs.Task;
#pragma warning restore CA2008 // Do not create tasks without passing a TaskScheduler
return Task.WhenAny(task, timeoutTask).Unwrap().ContinueWith(t => registration.Dispose(), TaskScheduler.Default);
}
}
}

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