Compare commits

...

304 Commits

Author SHA1 Message Date
96b7373d88 Update NBitcoin 2018-07-23 18:10:54 +09:00
c2f282f85c Fix rounding bug 2018-07-23 17:59:12 +09:00
7afa726ddc Fix, macaroonfilepath should be read in order to create the qr code 2018-07-23 12:20:11 +09:00
6eb7bbf853 Fix some invoice failing to create because of rounding issues 2018-07-23 12:01:20 +09:00
11a26c940d Do not expose the config secret in URL, and use {net.CryptoCode}.external.lnd.grpc argument 2018-07-23 11:54:33 +09:00
624252e4ad LightningConnectionString can parse gRPC 2018-07-23 11:11:00 +09:00
57bb980526 Update packages 2018-07-23 00:21:40 +09:00
e0c718f4ba Fix wallet alert 2018-07-23 00:21:40 +09:00
c58e015bfb Merge pull request from viacoin/master
Viacoin: add to README
2018-07-22 21:45:28 +09:00
648829644a Add QR code information 2018-07-22 21:28:21 +09:00
1b0f8c7aca Viacoin: add to README 2018-07-22 14:18:31 +02:00
4f8e0b0393 Can get lnd config without being logged 2018-07-22 18:43:11 +09:00
466f65d6cd bump 2018-07-22 18:39:22 +09:00
022b4f115d Expose LND gRPC settings 2018-07-22 18:38:14 +09:00
71f6aaabbd Merge pull request from rockstardev/master
Changing Lightning suffix per suggestion
2018-07-21 22:21:49 +09:00
79b06bce42 Changing Lightning suffix per suggestion 2018-07-20 23:33:54 -05:00
480afebcd9 bump 2018-07-20 15:25:45 +09:00
96721e95a2 Clean unreachable store if user is deleted 2018-07-20 15:24:19 +09:00
883cd41232 Fix bug where store was not properly deleted 2018-07-19 22:46:55 +09:00
3f48a478af Add delete button in update store settings 2018-07-19 22:23:14 +09:00
8d3b45bdec Delete store if no owner 2018-07-19 21:38:55 +09:00
bbd19a96ec Make sure we don't delete store on Sqlite 2018-07-19 21:32:33 +09:00
ce17e3212a Can delete stores 2018-07-19 19:31:17 +09:00
c3ea63c6ce bump 2018-07-19 14:49:47 +09:00
1ee4bd9c92 Fix tests, and make sure Listen does not block for LND 2018-07-19 14:49:30 +09:00
e6bb6619e5 bump 2018-07-19 13:55:12 +09:00
cc29d863d7 Merge pull request from rockstardev/dev-currency-selection
Showing currency name next to icon
2018-07-19 13:54:19 +09:00
8cb2c93abd Adding UTF8 lightning icon to Lightning payments methods 2018-07-18 23:53:00 -05:00
2187e05a10 Renaming BTG image to conform to new naming scheme for onchain/offchain 2018-07-17 23:32:44 -05:00
4c07483383 Merge remote-tracking branch 'source/master' into dev-currency-selection 2018-07-17 23:29:52 -05:00
65916755b6 Bitcoin always first selection in currency list 2018-07-17 23:29:40 -05:00
3cefbc89e4 Conditional display not necessary since whole block is hidden 2018-07-17 23:10:54 -05:00
c40b47b1dd Hover indicator and handling case with only one currency 2018-07-17 22:54:09 -05:00
a2d17bfa7e Closing currency selection dialog once invoice expired or paid 2018-07-17 22:30:02 -05:00
cdf0c6d27d Merge pull request from Vutov/master
Added BTG lightning svg
2018-07-17 22:57:18 +09:00
8154986102 Added BTG lightning svg 2018-07-17 13:43:37 +03:00
203494e809 Tweaking CSS styles and display of payment method selection 2018-07-17 00:14:56 -05:00
d49bbc95af Tweaking display with display name and crypto code 2018-07-16 23:43:52 -05:00
c44132fd35 Using same icon for onchain and offchain per user feedback 2018-07-16 23:29:55 -05:00
c75512303d Getting display names directly from NetworkProvider 2018-07-16 23:25:28 -05:00
97d50df13e bump 2018-07-14 12:46:29 +09:00
0e1ef78af1 Fix auth for listening invoices 2018-07-14 12:45:45 +09:00
464ab30fea Ordering currencies by name 2018-07-13 22:35:34 -05:00
3ee1c05646 Merge pull request from rockstardev/master
Fitting longer wallet addresses, displaying ellipsis for overflow
2018-07-14 12:24:38 +09:00
c54c39926b Fitting longer wallet addresses, displaying ellipsis for overflow
Ref: https://github.com/btcpayserver/btcpayserver/issues/223
2018-07-13 16:45:32 -05:00
33d18a3278 Displaying payment method name during checkout
Ref: https://github.com/btcpayserver/btcpayserver/issues/152
2018-07-13 15:58:59 -05:00
97e564901e Merge pull request from martinbehrens/tweak-help-options
tweaking help option wording
2018-07-14 03:14:18 +09:00
832069dd44 bump 2018-07-14 03:08:45 +09:00
1ac17e96c3 Throw lnd exception if any issue with lnd 2018-07-14 02:56:36 +09:00
d907031ec7 Cancel the infinite delay 2018-07-14 02:41:48 +09:00
4b5af9cb5c tweaking help option wording 2018-07-13 19:00:03 +02:00
0057146fee bump 2018-07-14 01:54:10 +09:00
0c8925d2a2 Correctly dispose the session when listening lightning invoices 2018-07-14 01:45:14 +09:00
b9e5b0d56e Merge pull request from viacoin/master
Viacoin: add support
2018-07-13 23:04:03 +09:00
eb6dbd1247 Merge branch 'master' into master 2018-07-13 15:07:15 +02:00
fe8428b8b0 make sure the LndInvoiceClientSession get disposed, even if it fails at initialization 2018-07-13 19:56:19 +09:00
f2aa15310a Viacoin: add support 2018-07-13 12:53:04 +02:00
1814cb2d6e bump 2018-07-13 19:48:01 +09:00
94a6f20a05 Refactor the LndInvoiceClient which might solve memory leak 2018-07-13 19:45:50 +09:00
22e700a53e Fix NullReferenceException when setting lightning connectionString without externalurl 2018-07-13 15:02:31 +09:00
cd78e559cf Merge pull request from romanornr/master
[Translation Dutch]: improve translation
2018-07-12 23:55:19 +09:00
f0257fb8f7 [Translation Dutch]: improve translation 2018-07-12 12:57:15 +02:00
34cdbf73f0 bump 2018-07-12 18:20:01 +09:00
b291a6d25a removing csp 2018-07-12 18:19:43 +09:00
fa7e974e73 bump 2018-07-12 17:38:43 +09:00
976d9d0cda Add CSP (Disable it if custom theming) 2018-07-12 17:38:21 +09:00
6ea2d9175d hamburger menu should not be black 2018-07-12 16:34:09 +09:00
10ceddc709 ReferrerPolicy 2018-07-12 02:38:08 +09:00
5dd57c8064 X-XSS-Protection 2018-07-12 02:23:54 +09:00
a256dd3277 x-content-type-options=nosniff 2018-07-12 01:43:16 +09:00
5ee9a92f1e Do not use external website for highlightjs 2018-07-12 01:20:44 +09:00
65c7c85c14 Do not put youtube on the front page (doing suspicious ads requests from the website) 2018-07-12 00:55:06 +09:00
27b686095c bump 2018-07-11 22:40:25 +09:00
cd2fef0dab Add a error if the browser access BTCPay with the wrong url 2018-07-11 22:40:10 +09:00
743288fa47 add instruction for the lightning connection string 2018-07-11 19:23:23 +09:00
270ebead49 fix error message 2018-07-11 17:47:29 +09:00
145e3bec83 bump 2018-07-11 16:46:31 +09:00
563e931468 simplify the docker-compose 2018-07-11 10:42:20 +09:00
3113097c4f Update to https, use new dockerfile 2018-07-10 19:33:54 +09:00
cdbbad1694 Fix misleading error when using http on internalNode 2018-07-10 12:58:17 +09:00
c9c2730409 check macaroonfilepath is rooted 2018-07-10 12:51:23 +09:00
310a9a6d59 Remove ctor in LndSwaggerClient 2018-07-10 12:49:25 +09:00
1a1078782e Suppoort macaroonfilepath in connection string 2018-07-10 12:46:04 +09:00
73cb3dc4ee Fix listen loop for LND 2018-07-09 01:08:09 +09:00
9eb36a8b40 Clean the LND listener, and make sure it correctly ends. 2018-07-08 22:20:59 +09:00
6307aa8665 Use SHA256 cert thumprint in connection string, allowInsecure=true 2018-07-08 20:58:37 +09:00
b9e8408db5 Simplify LND implementation 2018-07-08 18:55:48 +09:00
0879895678 Fix tests and rename type=lnd to type=lnd-rest 2018-07-08 15:34:19 +09:00
a4ecf070b0 Merge pull request from rockstardev/master
Handling unlikely state transition from paid to invalid
2018-07-08 12:24:33 +09:00
162d76206e Handling unlikely state transition from paid to invalid
Ref: https://github.com/btcpayserver/btcpayserver/issues/216
2018-07-07 10:38:07 -05:00
5af14ef2ec When creating PoS app, redirect to settings. When updating an app, redirect to List of apps. 2018-07-05 21:11:18 +09:00
7210eebeca Create Store redirect to store settings 2018-07-05 21:05:13 +09:00
25dbf6445f LND Support 2018-07-01 21:45:21 +09:00
0828c60537 Deactivate support for UFO (default rate rules are failing CanGetRateCryptoCurrenciesByDefault ) 2018-07-01 16:11:29 +09:00
34deb17f3d Fix tests 2018-07-01 16:10:17 +09:00
06b02b8691 Fix missing logs 2018-07-01 15:52:11 +09:00
b7abc08c27 Create a new format for LightningConnectionString 2018-07-01 15:45:08 +09:00
399ae2cd9e Fix: If DOGE fee becomes higher that 1 DOGE, the transaction would fail to broadcast 2018-07-01 13:46:28 +09:00
63fe0f6612 Make sure that DOGECOIN pays min amount of fee 2018-06-30 22:05:41 +09:00
42d60ef84b Fix: Could not send money from wallet of a coin without segwit 2018-06-30 21:26:10 +09:00
1784c30787 Merge remote-tracking branch 'source/master' into dev-lndrpc
# Conflicts:
#	BTCPayServer.Tests/UnitTest1.cs
2018-06-26 01:08:01 -05:00
ac8feceaf2 bump 2018-06-26 14:19:54 +09:00
3d8c5195ae Update CLightning and charge 2018-06-26 14:18:47 +09:00
9a5259510b Merge remote-tracking branch 'source/master' into dev-lndrpc 2018-06-25 22:31:42 -05:00
caecb26420 fix typos and sentences referencing Bitcoin 2018-06-25 11:58:07 +09:00
ecc8b3d9ed Fix spelling 2018-06-24 21:51:32 +09:00
d313395751 Show rule evaluation in invoice logs 2018-06-24 21:01:29 +09:00
9e698a8004 Commenting few tricky lines of code 2018-06-23 23:37:58 -05:00
3c4c99ee42 Saving of Macaroon and Tls for LND connection 2018-06-23 23:16:39 -05:00
d34ffc0d9a Refactoring conditional method, separating into two properties 2018-06-23 22:50:50 -05:00
039303bfaa Fixing typos during code review 2018-06-23 22:03:51 -05:00
273cf1adc9 Fix checkout if only one currency is present 2018-06-24 00:53:56 +09:00
5feb520843 Add support for groestlcoin 2018-06-24 00:45:57 +09:00
17e914778d Make sure that lightning payments events are using the state of the invoice when they got issued () 2018-06-21 14:15:36 +09:00
db24ab792f update clightning 2018-06-18 23:07:55 +09:00
42475ec7b7 Switching to lnd image from Docker Hub 2018-06-15 18:28:01 -05:00
4972f0ab7b Labeling issue with rapid testing of lightning payments 2018-06-15 18:27:37 -05:00
07e13747cf Merge remote-tracking branch 'source/master' into dev-lndrpc 2018-06-15 17:21:21 -05:00
2465eb7e36 Revering debug param 2018-06-15 17:20:56 -05:00
4ddcd7a4c8 Will depend on lnd bitcoin.defaultremotedelay=720 param
Ref: https://github.com/lightningnetwork/lnd/pull/788
2018-06-15 17:14:20 -05:00
89d9658e82 Bugfixing amount in invoice data, we need to set Satoshis 2018-06-15 17:12:59 -05:00
66ecb32538 Need param so that funding channels can be opened between LND and CL 2018-06-15 16:29:09 -05:00
a22576da0a Streamlining flow of interaction between test lnd customer / merchant 2018-06-15 15:56:02 -05:00
69bd888bab Refactoring ServerTester so that ClightningRPCClient can use LND 2018-06-15 15:02:40 -05:00
9b540273fc Parsing of node info and returning it for GetInfo 2018-06-15 13:57:39 -05:00
cfd083bed5 Providing port for peer-to-peer connection for local tests 2018-06-15 11:53:53 -05:00
55c9314cdd Reference to lnd docker image updated to point to latest
Also helps with building image locally for debugging
2018-06-15 11:53:34 -05:00
448cc06a11 Merge pull request from ChekaZ/master
Support UFO
2018-06-12 10:43:53 +09:00
0780df4fd7 Support UFO 2018-06-09 17:25:45 +02:00
04174b7431 Fix authentication 2018-06-06 16:02:37 +09:00
b7c58c2083 Fix bug of authentication caused by previous refactoring on authentication 2018-06-06 14:46:41 +09:00
cd75fd6842 bump 2018-06-05 12:53:55 +09:00
370951a3bd make sure postgres DB is created with C locale 2018-06-05 12:51:37 +09:00
2c08b0137b Update NBitpayClient 2018-06-05 12:29:45 +09:00
1eee31e9f1 Fix rate bug: Sometimes coinaverage is sending null bid and ask 2018-06-04 12:01:30 +09:00
01cf579530 Use proper custom authentication handler for bitpay 2018-06-04 12:00:03 +09:00
f72705935a Merge pull request from ChekaZ/master
Support Feathercoin
2018-06-04 01:45:07 +09:00
a29ab6b6b0 Support Feathercoin 2018-06-03 14:30:43 +02:00
4784518235 update link 2018-06-01 13:21:56 +09:00
f8c88bd44f Providing ability to increase lightning timeout for tests/debugging 2018-05-31 16:31:39 -05:00
0d1d0d57f4 Logging Swagger errors for logging and easier debugging 2018-05-31 16:31:19 -05:00
2bd1238668 Rounding TotalSeconds expiry so it doesn't break invoice creation 2018-05-31 16:31:00 -05:00
d1fb51b412 Reactivating LND end to end test 2018-05-31 16:07:59 -05:00
279de1b869 Passing CancelToken and properly parsing invoice response 2018-05-31 15:53:14 -05:00
ce9189caf8 Listen / WaitInvoice for Lnd 2018-05-31 15:08:22 -05:00
431147784e Merge branch 'master' into dev-lndrpc 2018-05-31 12:11:31 -05:00
0697b8bf86 update images 2018-05-31 23:54:03 +09:00
5050b59014 bump 2018-05-31 18:41:33 +09:00
665cf4c3b1 Updating BTCPayServer to .NET Core 2.1 2018-05-31 18:41:03 +09:00
98e81ab0fd Merge branch 'rockstardev-master' 2018-05-29 12:27:55 +09:00
6ce70237fc Merge branch 'master' of github.com:btcpayserver/btcpayserver 2018-05-29 12:27:45 +09:00
4f23fc99a1 Renaming method to better communicate function 2018-05-29 12:27:04 +09:00
d7fccae452 Generalizing TimeSpan to Time Ago, reverting invoice list to ago 2018-05-29 12:27:04 +09:00
d7a5021ed2 Updating Invoice details to show local date time 2018-05-29 12:27:03 +09:00
63ec832667 Support for localizing datetimes base on browser's timezone 2018-05-29 12:27:03 +09:00
8d95b9fa04 Renaming method to better communicate function 2018-05-28 09:36:49 -05:00
b497d1871e Merge pull request from andres-pinilla/patch-3
Updated es.js
2018-05-28 10:50:30 +09:00
c7cd029482 Update es.js
Various strings updated.
2018-05-27 20:36:32 -05:00
68f2cba60d Generalizing TimeSpan to Time Ago, reverting invoice list to ago 2018-05-26 09:42:55 -05:00
5c4200b036 Updating Invoice details to show local date time 2018-05-26 09:35:42 -05:00
bc06114023 Support for localizing datetimes base on browser's timezone 2018-05-26 09:32:20 -05:00
556082c4c9 fix json serialization 2018-05-26 18:29:57 +09:00
6a46d02fc6 Add dummy policies field 2018-05-26 18:26:02 +09:00
d75e5b8b12 Merge pull request from cronos-polis/master
Polis Integration
2018-05-26 14:50:32 +09:00
d293bc3947 Throwing NotImplementedException for Listen / WaitInvoice 2018-05-25 12:19:15 -05:00
e634700913 Changing the way we signal that LightningConnectingString is Lnd 2018-05-25 12:18:47 -05:00
ce81136c88 Adding LndMockTester for passing end to end tests 2018-05-25 10:44:59 -05:00
a97ef2eee8 MinerFee matching Bitpay API 2018-05-25 22:49:49 +09:00
be33ebc168 fix typo 2018-05-25 17:35:01 +09:00
789193a0c8 fix typo 2018-05-25 12:55:29 +09:00
01792cf299 Merge pull request from yashbhutwala/fix_typo
Fix spelling of lightning
2018-05-25 11:41:46 +09:00
ff9265f721 Fix spelling of lightning 2018-05-24 16:26:01 -04:00
8d61314852 Use FullNotification and improve instructions 2018-05-25 00:02:24 +09:00
1ce6ae8727 fix typo 2018-05-24 23:58:24 +09:00
dec5dbc0d2 Ability to pass fields to POS app 2018-05-24 23:54:48 +09:00
4e32dad1ea Can solve inverses at exchange level 2018-05-23 19:29:01 +09:00
127ca7582f Make sure inverse rules have priority 2018-05-23 19:13:12 +09:00
b98993f84b fix typo 2018-05-23 11:16:19 +09:00
e35f074b66 Better label for Rate Multiplier 2018-05-23 02:43:22 +09:00
ba3d13d56c Make sure the rate of the merchant is using the ask of a divided exchange 2018-05-23 02:18:38 +09:00
ead67887ab Enable SSL was ignored 2018-05-23 00:59:50 +09:00
437f27f107 Merge pull request from andres-pinilla/master
Better spanish translation (tu)
2018-05-22 14:11:25 +09:00
8d41a8e98d Better spanish translation (tú) 2018-05-21 23:15:04 -05:00
7e6ab015a6 Fix bug on Error page 2018-05-22 01:05:45 +09:00
f8bc3a5081 bump 2018-05-21 20:47:07 +09:00
dd1a93ee0e Revert "Remove unused Error.cshtml"
This reverts commit 7b2ef9aec2c5f5afbecf1b639995b4a645390928.
2018-05-21 20:44:03 +09:00
093ae39e61 Custom HTTPS certificates accepted for lnd connection 2018-05-20 10:27:49 -05:00
cac58808f0 Renaming file to LndInvoiceClient and commenting Dispose 2018-05-20 10:27:35 -05:00
a063f10778 Checking for nulls during channel opening in tests 2018-05-20 10:27:11 -05:00
3cf3aa63f6 CurrencyNameTable can use fallback 2018-05-20 23:37:18 +09:00
011dd5574f Add a fallback currency format info 2018-05-20 23:22:20 +09:00
365911286b bump 2018-05-20 17:04:03 +09:00
fe5347aa86 Maintaining BitPay compatibility
Ref: https://github.com/btcpayserver/btcpayserver/issues/180
2018-05-20 17:00:54 +09:00
f22c8a72cd Rebase 2018-05-16 11:21:57 -05:00
eeb522fe7d Remove special case for showing crypto currency 2018-05-16 21:19:48 +09:00
f9e40b209a Merge pull request from rockstardev/fiat
Showing exchange rate for cryptos
2018-05-16 21:14:23 +09:00
20635ea3d6 Showing exchange rate for cryptos
Ref: https://github.com/btcpayserver/btcpayserver/pull/170#issuecomment-389462155
2018-05-16 05:46:11 -05:00
6cefd9c3e7 Merge remote-tracking branch 'source/master' into dev-lndrpc 2018-05-16 04:50:46 -05:00
7062705d6f bump 2018-05-16 10:40:48 +09:00
58b994e043 fix tests 2018-05-16 10:40:25 +09:00
640ff36fa2 fix build 2018-05-16 10:26:45 +09:00
39ec5242d7 Bump NBitpayClient 2018-05-16 10:22:43 +09:00
1c50210e61 fix build 2018-05-16 10:22:42 +09:00
a1ffda0151 Merge pull request from Kukks/feature/extended-invoice
[WIP] Feature/extended invoice
2018-05-16 10:21:52 +09:00
fd15348551 Merge pull request from monaco-ex/pr-support-monacoin
Support Monacoin.
2018-05-16 10:21:07 +09:00
989c99c550 Merge pull request from rockstardev/fiat
Display fiat value of invoice during checkout
2018-05-16 10:18:24 +09:00
bcf97b1474 Hiding display of payment-tabs now that we have flex line-items 2018-05-15 15:17:59 -05:00
67abbed66a Removing display of exchange if invoice source amount is in crypto 2018-05-15 15:17:58 -05:00
eb01e91e13 Only show Order Amount in Fiat if Invoice is created with fiat value 2018-05-15 15:17:58 -05:00
12ceb9e0bc Simplifying CSS styles for line-items box
Now that we'll have different box sizes it's not possible to rely on exact height specification
2018-05-15 15:17:57 -05:00
ecf03f90aa Fix UriAttribute bug, and currency formatting crash 2018-05-16 02:24:59 +09:00
1747414a57 update clightning of docker compose 2018-05-16 01:37:20 +09:00
3a02f16c6e Fix bug where exchange name in rate rules were uncorrectly considered a currency 2018-05-16 01:27:15 +09:00
a6ee337ed0 more coverage 2018-05-15 16:25:43 +02:00
559f535257 add some coverage for bitpay fields 2018-05-15 16:18:37 +02:00
2952ccc7fd Merge remote-tracking branch 'origin/master' into feature/extended-invoice 2018-05-15 15:44:51 +02:00
a0243fa569 Lnd support for passing macaroon and tls as hex 2018-05-14 22:18:08 -05:00
789b9168ad Adding Lnd to connection types and supporting parsing 2018-05-14 15:54:44 -05:00
7c29cb62ef Enabling dual support - clients with or without macaroons/tls 2018-05-14 15:05:03 -05:00
f81ca1888d Merge remote-tracking branch 'upstream/master' 2018-05-14 11:23:54 -05:00
ed02e0f4d6 Merge pull request from zeusthealmighty/patch-1
KeyPath Updated
2018-05-14 11:23:08 -05:00
0a83f21af5 KeyPath Updated 2018-05-14 10:04:12 -05:00
23a3c145ed fix run.sh 2018-05-14 22:08:35 +09:00
4184c6c208 Convert in UriAttribute use invariant culture 2018-05-14 21:28:33 +09:00
29c28b1841 Merge pull request from Kukks/bugfix/real-url-validation
use alternative uri validation
2018-05-14 17:31:56 +09:00
de48fb4077 add direct file test cases 2018-05-14 09:34:19 +02:00
bcd79c5882 use alternative uri validation 2018-05-14 09:32:04 +02:00
b8c513aa2b Support Monacoin. 2018-05-14 05:44:17 +00:00
ad67f4ef18 update to use longs 2018-05-13 09:47:42 +02:00
2c0bcfc0ec Merge remote-tracking branch 'btcpayserver/master' into feature/extended-invoice 2018-05-13 08:34:36 +02:00
0ba1072d54 bump 2018-05-13 15:09:38 +09:00
f7fe855274 Do not roundup rates 2018-05-13 15:09:17 +09:00
449738414b Add cryptopia 2018-05-12 19:37:32 +09:00
a34842585d bump 2018-05-12 19:19:40 +09:00
eb882c2c46 Update package 2018-05-12 19:15:54 +09:00
ca65c6bd8f fix 2018-05-12 18:38:43 +09:00
f97173e9e7 Testing invoice payment with Lnd 2018-05-12 00:43:13 -05:00
8fc1b0c856 Ensuring lightning channel is open for testing 2018-05-12 00:23:10 -05:00
cabd7c4e64 Lnd requires zmqpubrawblock setting, and port 9735 for peer connections 2018-05-12 00:19:26 -05:00
f8540dc78c Providing merchant_lnd and customer_lnd for testing 2018-05-11 16:59:24 -05:00
b03d271f85 Refactoring LndClient, enabling passing of Swagger instance 2018-05-11 14:07:46 -05:00
3770adb7d3 Displaying fiat value of invoice's order amount in details 2018-05-11 12:15:26 -05:00
7fdf19ca22 Remove cryptopia from CoinAverage 2018-05-11 11:07:42 -05:00
4e776adb03 Merge remote-tracking branch 'upstream/master' 2018-05-11 11:06:51 -05:00
26db946392 BTCPayRateProviderFactory is responsible for getting the supported exchange list 2018-05-12 00:54:17 +09:00
d102c142b9 Typo 2018-05-11 10:46:49 -05:00
f7989541b9 Change Polis Rates - Add Cryptopia 2018-05-11 10:26:08 -05:00
b7f0ce18b3 Make sure Lightning charge can't hang out the payment 2018-05-12 00:23:25 +09:00
e1dfbfe3b0 Rebase 2018-05-11 10:20:23 -05:00
786d129452 Make sure to not freeze if ligthning does not respond 2018-05-12 00:14:39 +09:00
a37a8e8fcd Merge remote-tracking branch 'btcpayserver/master' into feature/extended-invoice 2018-05-11 16:46:38 +02:00
355989c278 bump 2018-05-11 23:34:42 +09:00
af3dee95de round up rates sent back by the RateProviderFactory 2018-05-11 23:31:50 +09:00
70a6bd6a01 bump 2018-05-11 22:42:29 +09:00
4afb0acc84 does not generate antiforgery 2018-05-11 22:41:11 +09:00
9afc143801 Use decimals and fix invoices 2018-05-11 22:38:31 +09:00
8e4943df65 low-medium speed policy 2018-05-11 22:12:45 +09:00
9b3bd8343d bump nuget packages 2018-05-11 21:33:46 +09:00
ee4f83ddba small fixes 2018-05-11 12:21:25 +02:00
c326998381 bump nbitpayclient dependency to .20 2018-05-11 11:42:13 +02:00
239a011e60 add new properties and change types to decimal 2018-05-11 11:31:21 +02:00
5ffe118159 Merge remote-tracking branch 'btcpayserver/master' into feature/extended-invoice 2018-05-11 11:24:32 +02:00
6f07849e1d Use policies security for controlling access to bitpay api 2018-05-11 17:16:18 +09:00
dbe5c62d11 Merge remote-tracking branch 'btcpayserver/master' into feature/extended-invoice 2018-05-11 10:08:29 +02:00
199db01eaf No need of authentication for GetInvoice API () 2018-05-11 17:05:08 +09:00
a3c46c8f67 Use hangfire in-memory provider until the postgres binding to hangfire get updated. 2018-05-11 15:06:11 +09:00
66a68d6180 Rename LockSubscription, remove the link if not available 2018-05-10 16:02:49 +09:00
be1128a886 Support for displaying fiat value of invoice 2018-05-09 22:39:13 -05:00
d41a5a65a2 Update posgres and clightning in tests 2018-05-10 11:56:46 +09:00
d5cab938ee bump 2018-05-09 14:10:06 +09:00
9dddfb65f0 Add globalization to the alpine package 2018-05-09 14:09:41 +09:00
6bd5976d90 Update SQLite package 2018-05-08 19:17:06 +09:00
b3385bf901 update tests image 2018-05-08 18:09:12 +09:00
bba268b5e2 Upgrade to .NET Core 2.1 2018-05-08 17:57:53 +09:00
70c98b6901 use the theme manager for ViewPointOfSale 2018-05-08 00:19:28 +09:00
2d3b7fea2e update packages 2018-05-07 23:02:55 +09:00
3bdf1c9a00 Merge pull request from Vutov/master
Added BTG Support
2018-05-07 14:16:31 +09:00
a52665ea80 bump 2018-05-07 12:29:56 +09:00
3d943d49e6 Make sure if the default crypto is no available, we don't get error 404 2018-05-07 12:25:50 +09:00
6ca8ba9231 Allow signing on non segwit transactions via the ledger 2018-05-07 12:17:46 +09:00
75d685ae6c fix grammar 2018-05-07 00:24:23 +09:00
7b2ef9aec2 Remove unused Error.cshtml 2018-05-06 22:49:00 +09:00
efe666b284 Fix call to Rates via bitpay API 2018-05-06 22:41:38 +09:00
ca8af5047a Changed DefaultRateRules 2018-05-06 14:59:49 +03:00
cdc0b0d628 Fix crash when creating a token 2018-05-06 19:03:30 +09:00
87e28b70fd cap MinimumTotalDue to 1 satoshi 2018-05-06 13:55:03 +09:00
b96f464e39 Add "unusual:" filter to invoice list 2018-05-06 13:16:39 +09:00
bca68986f3 Added BTG Support 2018-05-05 23:06:09 +03:00
272ac49872 try to better respect event ordering 2018-05-06 02:06:07 +09:00
7c3ddf904c Merge remote-tracking branch 'upstream/master' 2018-05-04 09:47:03 -05:00
cfbcf0947a Switching to using Dockerfile from Docker Hub 2018-05-01 21:12:04 -05:00
fcfba7f5e1 Refactoring connection to Lnd now there is HTTP support 2018-05-01 20:33:43 -05:00
f4f9fabfd3 Building docker compose with our custom lnd 2018-05-01 19:02:57 -05:00
25208915eb Merge remote-tracking branch 'upstream/master' 2018-04-30 10:29:30 -05:00
75f4a39ef2 Adding script to build lnd Docker container for testing
Obviously when we publish to Docker Hub this whole folder is bye-bye
2018-04-29 02:57:08 -05:00
f9f4d93191 Lnd Dockerfile that integrates with BtcPayServer 2018-04-29 02:52:33 -05:00
69050f7a56 Lnd sends some integers as strings, testing invoice creation 2018-04-28 12:49:56 -05:00
1743919cd4 Conversion of LnrpcInvoice into LightningInvoice 2018-04-28 00:39:43 -05:00
131328b42c Foundation integration with Lnd 2018-04-27 23:36:58 -05:00
ad3b605148 Adding ZMQ settings Lnd needs 2018-04-27 23:36:58 -05:00
f32e225fa6 Generating Lnd wrapper using NSwag
https://github.com/lightningnetwork/lnd/blob/master/lnrpc/rpc.swagger.json
2018-04-27 23:36:58 -05:00
52e0845fc5 Merge remote-tracking branch 'upstream/master' 2018-04-21 09:59:46 -05:00
daf1a0a4bc Revert to origin csproj 2018-04-17 11:19:28 -05:00
bc8978182e Add Polis 2018-04-17 11:13:50 -05:00
215 changed files with 19683 additions and 1480 deletions
BTCPayServer.Tests
BTCPayServer
BTCPayNetwork.csBTCPayNetworkProvider.Bitcoin.csBTCPayNetworkProvider.BitcoinGold.csBTCPayNetworkProvider.Dogecoin.csBTCPayNetworkProvider.Feathercoin.csBTCPayNetworkProvider.Groestlcoin.csBTCPayNetworkProvider.Litecoin.csBTCPayNetworkProvider.Monacoin.csBTCPayNetworkProvider.Polis.csBTCPayNetworkProvider.Ufo.csBTCPayNetworkProvider.Viacoin.csBTCPayNetworkProvider.csBTCPayServer.csproj
Configuration
Controllers
CurrencyValue.cs
Data
DerivationSchemeParser.cs
Events
Extensions.cs
Filters
HostedServices
Hosting
Logging
Migrations
Models
Payments
Program.cs
Properties
Rating
Security
Services
Validation
Views
bundleconfig.json
wwwroot
DockerfileNuget.ConfigREADME.mdrun.sh

@ -1,14 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
<TargetFramework>netcoreapp2.1</TargetFramework>
<IsPackable>false</IsPackable>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
<LangVersion>7.2</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.2" />
<PackageReference Include="xunit" Version="2.3.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
</ItemGroup>

@ -120,7 +120,7 @@ namespace BTCPayServer.Tests
.Build();
_Host.Start();
InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository));
StoreRepository = (StoreRepository)_Host.Services.GetService(typeof(StoreRepository));
var rateProvider = (BTCPayRateProviderFactory)_Host.Services.GetService(typeof(BTCPayRateProviderFactory));
rateProvider.DirectProviders.Clear();
@ -129,19 +129,19 @@ namespace BTCPayServer.Tests
{
Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("BTC_USD"),
Value = 5000m
BidAsk = new BidAsk(5000m)
});
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("BTC_CAD"),
Value = 4500m
BidAsk = new BidAsk(4500m)
});
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("LTC_USD"),
Value = 500m
BidAsk = new BidAsk(500m)
});
rateProvider.DirectProviders.Add("coinaverage", coinAverageMock);
}
@ -152,6 +152,7 @@ namespace BTCPayServer.Tests
internal set;
}
public InvoiceRepository InvoiceRepository { get; private set; }
public StoreRepository StoreRepository { get; private set; }
public Uri IntegratedLightning { get; internal set; }
public bool InContainer { get; internal set; }

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

@ -1,4 +1,4 @@
FROM microsoft/dotnet:2.0.6-sdk-2.1.101-stretch
FROM microsoft/dotnet:2.1.300-sdk-alpine3.7
WORKDIR /app
# caches restore result by copying csproj file separately
COPY BTCPayServer.Tests/BTCPayServer.Tests.csproj BTCPayServer.Tests/BTCPayServer.Tests.csproj

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

@ -0,0 +1,246 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Payments.Lightning.Lnd;
using NBitcoin;
using NBitcoin.RPC;
using Xunit;
using Xunit.Abstractions;
using System.Linq;
using System.Threading;
using NBitpayClient;
using System.Globalization;
using Xunit.Sdk;
namespace BTCPayServer.Tests.Lnd
{
// this depends for now on `docker-compose up devlnd`
public class UnitTests
{
private readonly ITestOutputHelper output;
public UnitTests(ITestOutputHelper output)
{
this.output = output;
initializeEnvironment();
MerchantLnd = new LndSwaggerClient(new LndRestSettings(new Uri("https://127.0.0.1:53280")) { AllowInsecure = true });
InvoiceClient = new LndInvoiceClient(MerchantLnd);
CustomerLnd = new LndSwaggerClient(new LndRestSettings(new Uri("https://127.0.0.1:53281")) { AllowInsecure = true });
}
private LndSwaggerClient MerchantLnd { get; set; }
private LndInvoiceClient InvoiceClient { get; set; }
private LndSwaggerClient CustomerLnd { get; set; }
[Fact]
public async Task GetInfo()
{
var res = await InvoiceClient.GetInfo();
output.WriteLine("Result: " + res.ToJson());
}
[Fact]
public async Task CreateInvoice()
{
var res = await InvoiceClient.CreateInvoice(10000, "Hello world", TimeSpan.FromSeconds(3600));
output.WriteLine("Result: " + res.ToJson());
}
[Fact]
public async Task GetInvoice()
{
var createInvoice = await InvoiceClient.CreateInvoice(10000, "Hello world", TimeSpan.FromSeconds(3600));
var getInvoice = await InvoiceClient.GetInvoice(createInvoice.Id);
Assert.Equal(createInvoice.BOLT11, getInvoice.BOLT11);
}
[Fact]
public void Play()
{
var seq = new System.Buffers.ReadOnlySequence<byte>(new ReadOnlyMemory<byte>(new byte[1000]));
var seq2 = seq.Slice(3);
var pos = seq2.GetPosition(0);
}
// integration tests
[Fact]
public async Task TestWaitListenInvoice()
{
var merchantInvoice = await InvoiceClient.CreateInvoice(10000, "Hello world", TimeSpan.FromSeconds(3600));
var merchantInvoice2 = await InvoiceClient.CreateInvoice(10000, "Hello world", TimeSpan.FromSeconds(3600));
var waitToken = default(CancellationToken);
var listener = await InvoiceClient.Listen(waitToken);
var waitTask = listener.WaitInvoice(waitToken);
await EnsureLightningChannelAsync();
var payResponse = await CustomerLnd.SendPaymentSyncAsync(new LnrpcSendRequest
{
Payment_request = merchantInvoice.BOLT11
});
var invoice = await waitTask;
Assert.True(invoice.PaidAt.HasValue);
var waitTask2 = listener.WaitInvoice(waitToken);
payResponse = await CustomerLnd.SendPaymentSyncAsync(new LnrpcSendRequest
{
Payment_request = merchantInvoice2.BOLT11
});
invoice = await waitTask2;
Assert.True(invoice.PaidAt.HasValue);
var waitTask3 = listener.WaitInvoice(waitToken);
await Task.Delay(100);
listener.Dispose();
Assert.Throws<TaskCanceledException>(() => waitTask3.GetAwaiter().GetResult());
}
[Fact]
public async Task CreateLndInvoiceAndPay()
{
var merchantInvoice = await InvoiceClient.CreateInvoice(10000, "Hello world", TimeSpan.FromSeconds(3600));
await EnsureLightningChannelAsync();
await EventuallyAsync(async () =>
{
var payResponse = await CustomerLnd.SendPaymentSyncAsync(new LnrpcSendRequest
{
Payment_request = merchantInvoice.BOLT11
});
var invoice = await InvoiceClient.GetInvoice(merchantInvoice.Id);
Assert.True(invoice.PaidAt.HasValue);
});
}
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);
}
}
}
public async Task<LnrpcChannel> EnsureLightningChannelAsync()
{
var merchantInfo = await WaitLNSynched();
var merchantNodeAddress = new LnrpcLightningAddress
{
Pubkey = merchantInfo.NodeId,
Host = "merchant_lnd:9735"
};
while (true)
{
// if channel is pending generate blocks until confirmed
var pendingResponse = await CustomerLnd.PendingChannelsAsync();
if (pendingResponse.Pending_open_channels?
.Any(a => a.Channel?.Remote_node_pub == merchantNodeAddress.Pubkey) == true)
{
ExplorerNode.Generate(1);
await WaitLNSynched();
continue;
}
// check if channel is established
var chanResponse = await CustomerLnd.ListChannelsAsync(null, null, null, null);
LnrpcChannel channelToMerchant = null;
if (chanResponse != null && chanResponse.Channels != null)
{
channelToMerchant = chanResponse.Channels
.Where(a => a.Remote_pubkey == merchantNodeAddress.Pubkey)
.FirstOrDefault();
}
if (channelToMerchant == null)
{
// create new channel
var isConnected = await CustomerLnd.ListPeersAsync();
if (isConnected.Peers == null ||
!isConnected.Peers.Any(a => a.Pub_key == merchantInfo.NodeId))
{
var connectResp = await CustomerLnd.ConnectPeerAsync(new LnrpcConnectPeerRequest
{
Addr = merchantNodeAddress
});
}
var addressResponse = await CustomerLnd.NewWitnessAddressAsync();
var address = BitcoinAddress.Create(addressResponse.Address, Network.RegTest);
await ExplorerNode.SendToAddressAsync(address, Money.Coins(0.2m));
ExplorerNode.Generate(1);
await WaitLNSynched();
var channelReq = new LnrpcOpenChannelRequest
{
Local_funding_amount = 16777215.ToString(CultureInfo.InvariantCulture),
Node_pubkey_string = merchantInfo.NodeId
};
var channelResp = await CustomerLnd.OpenChannelSyncAsync(channelReq);
}
else
{
// channel exists, return it
ExplorerNode.Generate(1);
await WaitLNSynched();
return channelToMerchant;
}
}
}
private async Task<LightningNodeInformation> WaitLNSynched()
{
while (true)
{
var merchantInfo = await InvoiceClient.GetInfo();
var blockCount = await ExplorerNode.GetBlockCountAsync();
if (merchantInfo.BlockHeight != blockCount)
{
await Task.Delay(500);
}
else
{
return merchantInfo;
}
}
}
//
private void initializeEnvironment()
{
NetworkProvider = new BTCPayNetworkProvider(NetworkType.Regtest);
ExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_BTCRPCCONNECTION", "server=http://127.0.0.1:43782;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork("BTC").NBitcoinNetwork);
}
public BTCPayNetworkProvider NetworkProvider { get; private set; }
public RPCClient ExplorerNode { get; set; }
internal string GetEnvironment(string variable, string defaultValue)
{
var var = Environment.GetEnvironmentVariable(variable);
return String.IsNullOrEmpty(var) ? defaultValue : var;
}
}
}

@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Text;
using BTCPayServer.Rating;
using Xunit;
using System.Globalization;
namespace BTCPayServer.Tests
{
@ -35,9 +36,9 @@ namespace BTCPayServer.Tests
rules.ToString());
var tests = new[]
{
(Pair: "DOGE_USD", Expected: "bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1"),
(Pair: "BTC_USD", Expected: "gdax(BTC_USD)"),
(Pair: "BTC_CAD", Expected: "coinbase(BTC_CAD)"),
(Pair: "DOGE_USD", Expected: "bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1"),
(Pair: "DOGE_CAD", Expected: "bittrex(DOGE_BTC) * coinbase(BTC_CAD) * 1.1"),
(Pair: "LTC_CAD", Expected: "coinaverage(LTC_CAD) * 1.02"),
};
@ -81,9 +82,9 @@ namespace BTCPayServer.Tests
var tests2 = new[]
{
(Pair: "DOGE_USD", Expected: "bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1", ExpectedExchangeRates: "bittrex(DOGE_BTC),gdax(BTC_USD)"),
(Pair: "BTC_USD", Expected: "gdax(BTC_USD)", ExpectedExchangeRates: "gdax(BTC_USD)"),
(Pair: "BTC_CAD", Expected: "coinbase(BTC_CAD)", ExpectedExchangeRates: "coinbase(BTC_CAD)"),
(Pair: "DOGE_USD", Expected: "bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1", ExpectedExchangeRates: "bittrex(DOGE_BTC),gdax(BTC_USD)"),
(Pair: "DOGE_CAD", Expected: "bittrex(DOGE_BTC) * coinbase(BTC_CAD) * 1.1", ExpectedExchangeRates: "bittrex(DOGE_BTC),coinbase(BTC_CAD)"),
(Pair: "LTC_CAD", Expected: "coinaverage(LTC_CAD) * 1.02", ExpectedExchangeRates: "coinaverage(LTC_CAD)"),
};
@ -94,12 +95,12 @@ namespace BTCPayServer.Tests
Assert.Equal(test.ExpectedExchangeRates, string.Join(',', rule.ExchangeRates.OfType<object>().ToArray()));
}
var rule2 = rules.GetRuleFor(CurrencyPair.Parse("DOGE_CAD"));
rule2.ExchangeRates.SetRate("bittrex", CurrencyPair.Parse("DOGE_BTC"), 5000);
rule2.ExchangeRates.SetRate("bittrex", CurrencyPair.Parse("DOGE_BTC"), new BidAsk(5000m));
rule2.Reevaluate();
Assert.True(rule2.HasError);
Assert.Equal("5000 * ERR_RATE_UNAVAILABLE(coinbase, BTC_CAD) * 1.1", rule2.ToString(true));
Assert.Equal("bittrex(DOGE_BTC) * coinbase(BTC_CAD) * 1.1", rule2.ToString(false));
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), 2000.4m);
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), new BidAsk(2000.4m));
rule2.Reevaluate();
Assert.False(rule2.HasError);
Assert.Equal("5000 * 2000.4 * 1.1", rule2.ToString(true));
@ -116,7 +117,7 @@ namespace BTCPayServer.Tests
rule2 = rules.GetRuleFor(CurrencyPair.Parse("DOGE_USD"));
Assert.Equal("(2000 * (-3 + coinbase(BTC_CAD) + 50 - 5)) * 1.1", rule2.ToString());
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), 1000m);
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), new BidAsk(1000m));
Assert.True(rule2.Reevaluate());
Assert.Equal("(2000 * (-3 + 1000 + 50 - 5)) * 1.1", rule2.ToString(true));
Assert.Equal((2000m * (-3m + 1000m + 50m - 5m)) * 1.1m, rule2.Value.Value);
@ -124,11 +125,55 @@ namespace BTCPayServer.Tests
// Test inverse
rule2 = rules.GetRuleFor(CurrencyPair.Parse("USD_DOGE"));
Assert.Equal("(1 / (2000 * (-3 + coinbase(BTC_CAD) + 50 - 5))) * 1.1", rule2.ToString());
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), 1000m);
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), new BidAsk(1000m));
Assert.True(rule2.Reevaluate());
Assert.Equal("(1 / (2000 * (-3 + 1000 + 50 - 5))) * 1.1", rule2.ToString(true));
Assert.Equal(( 1.0m / (2000m * (-3m + 1000m + 50m - 5m))) * 1.1m, rule2.Value.Value);
Assert.Equal((1.0m / (2000m * (-3m + 1000m + 50m - 5m))) * 1.1m, rule2.Value.Value);
////////
// Make sure kraken is not converted to CurrencyPair
builder = new StringBuilder();
builder.AppendLine("BTC_USD = kraken(BTC_USD)");
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
rule2 = rules.GetRuleFor(CurrencyPair.Parse("BTC_USD"));
rule2.ExchangeRates.SetRate("kraken", CurrencyPair.Parse("BTC_USD"), new BidAsk(1000m));
Assert.True(rule2.Reevaluate());
// Make sure can handle pairs
builder = new StringBuilder();
builder.AppendLine("BTC_USD = kraken(BTC_USD)");
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
rule2 = rules.GetRuleFor(CurrencyPair.Parse("BTC_USD"));
rule2.ExchangeRates.SetRate("kraken", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m));
Assert.True(rule2.Reevaluate());
Assert.Equal("(6000, 6100)", rule2.ToString(true));
Assert.Equal(6000m, rule2.Value.Value);
rule2 = rules.GetRuleFor(CurrencyPair.Parse("USD_BTC"));
rule2.ExchangeRates.SetRate("kraken", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m));
Assert.True(rule2.Reevaluate());
Assert.Equal("1 / (6000, 6100)", rule2.ToString(true));
Assert.Equal(1m / 6100m, rule2.Value.Value);
// Make sure the inverse has more priority than X_X or CDNT_X
builder = new StringBuilder();
builder.AppendLine("EUR_CDNT = 10");
builder.AppendLine("CDNT_BTC = CDNT_EUR * EUR_BTC;");
builder.AppendLine("CDNT_X = CDNT_BTC * BTC_X;");
builder.AppendLine("X_X = coinaverage(X_X);");
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
rule2 = rules.GetRuleFor(CurrencyPair.Parse("CDNT_EUR"));
rule2.ExchangeRates.SetRate("coinaverage", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m));
Assert.True(rule2.Reevaluate());
Assert.Equal("1 / 10", rule2.ToString(false));
// Make sure an inverse can be solved on an exchange
builder = new StringBuilder();
builder.AppendLine("X_X = coinaverage(X_X);");
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
rule2 = rules.GetRuleFor(CurrencyPair.Parse("USD_BTC"));
rule2.ExchangeRates.SetRate("coinaverage", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m));
Assert.True(rule2.Reevaluate());
Assert.Equal($"({(1m / 6100m).ToString(CultureInfo.InvariantCulture)}, {(1m / 6000m).ToString(CultureInfo.InvariantCulture)})", rule2.ToString(true));
}
}
}

@ -20,6 +20,8 @@ using System.Threading;
using System.Globalization;
using BTCPayServer.Payments.Lightning.CLightning;
using BTCPayServer.Payments.Lightning.Charge;
using BTCPayServer.Tests.Lnd;
using BTCPayServer.Payments.Lightning;
namespace BTCPayServer.Tests
{
@ -47,10 +49,12 @@ namespace BTCPayServer.Tests
LTCExplorerClient = new ExplorerClient(NetworkProvider.GetNetwork("LTC").NBXplorerNetwork, new Uri(GetEnvironment("TESTS_LTCNBXPLORERURL", "http://127.0.0.1:32838/")));
var btc = NetworkProvider.GetNetwork("BTC").NBitcoinNetwork;
CustomerLightningD = new CLightningRPCClient(new Uri(GetEnvironment("TEST_CUSTOMERLIGHTNINGD", "tcp://127.0.0.1:30992/")), btc);
MerchantLightningD = new CLightningRPCClient(new Uri(GetEnvironment("TEST_MERCHANTLIGHTNINGD", "tcp://127.0.0.1:30993/")), btc);
CustomerLightningD = (CLightningRPCClient)LightningClientFactory.CreateClient(GetEnvironment("TEST_CUSTOMERLIGHTNINGD", "type=clightning;server=tcp://127.0.0.1:30992/"), btc);
MerchantLightningD = (CLightningRPCClient)LightningClientFactory.CreateClient(GetEnvironment("TEST_MERCHANTLIGHTNINGD", "type=clightning;server=tcp://127.0.0.1:30993/"), btc);
MerchantCharge = new ChargeTester(this, "TEST_MERCHANTCHARGE", "http://api-token:foiewnccewuify@127.0.0.1:54938/", "merchant_lightningd", btc);
MerchantCharge = new ChargeTester(this, "TEST_MERCHANTCHARGE", "type=charge;server=http://127.0.0.1:54938/;api-token=foiewnccewuify", "merchant_lightningd", btc);
MerchantLnd = new LndMockTester(this, "TEST_MERCHANTLND", "https://lnd:lnd@127.0.0.1:53280/", "merchant_lnd", btc);
PayTester = new BTCPayServerTester(Path.Combine(_Directory, "pay"))
{
@ -78,41 +82,54 @@ namespace BTCPayServer.Tests
/// <summary>
/// Connect a customer LN node to the merchant LN node
/// </summary>
public void PrepareLightning()
public void PrepareLightning(LightningConnectionType lndBackend)
{
PrepareLightningAsync().GetAwaiter().GetResult();
ILightningInvoiceClient client = MerchantCharge.Client;
if (lndBackend == LightningConnectionType.LndREST)
client = MerchantLnd.Client;
PrepareLightningAsync(client).GetAwaiter().GetResult();
}
private static readonly string[] SKIPPED_STATES =
{ "ONCHAIN", "CHANNELD_SHUTTING_DOWN", "CLOSINGD_SIGEXCHANGE", "CLOSINGD_COMPLETE", "FUNDING_SPEND_SEEN" };
/// <summary>
/// Connect a customer LN node to the merchant LN node
/// </summary>
/// <returns></returns>
public async Task PrepareLightningAsync()
private async Task PrepareLightningAsync(ILightningInvoiceClient client)
{
bool awaitingLocking = false;
while (true)
{
var skippedStates = new[] { "ONCHAIN", "CHANNELD_SHUTTING_DOWN", "CLOSINGD_SIGEXCHANGE", "CLOSINGD_COMPLETE", "FUNDING_SPEND_SEEN" };
var channel = (await CustomerLightningD.ListPeersAsync())
var merchantInfo = await WaitLNSynched(client, CustomerLightningD, MerchantLightningD);
var peers = await CustomerLightningD.ListPeersAsync();
var filteringToTargetedPeers = peers.Where(a => a.Id == merchantInfo.NodeId);
var channel = filteringToTargetedPeers
.SelectMany(p => p.Channels)
.Where(c => !skippedStates.Contains(c.State ?? ""))
.Where(c => !SKIPPED_STATES.Contains(c.State ?? ""))
.FirstOrDefault();
switch (channel?.State)
{
case null:
var merchantInfo = await WaitLNSynched();
var clightning = new NodeInfo(merchantInfo.Id, MerchantCharge.P2PHost, merchantInfo.Port);
await CustomerLightningD.ConnectAsync(clightning);
var address = await CustomerLightningD.NewAddressAsync();
await ExplorerNode.SendToAddressAsync(address, Money.Coins(0.2m));
await ExplorerNode.SendToAddressAsync(address, Money.Coins(0.5m));
ExplorerNode.Generate(1);
await WaitLNSynched();
await WaitLNSynched(client, CustomerLightningD, MerchantLightningD);
await Task.Delay(1000);
await CustomerLightningD.FundChannelAsync(clightning, Money.Satoshis(16777215));
var merchantNodeInfo = new NodeInfo(merchantInfo.NodeId, merchantInfo.Address, merchantInfo.P2PPort);
await CustomerLightningD.ConnectAsync(merchantNodeInfo);
await CustomerLightningD.FundChannelAsync(merchantNodeInfo, Money.Satoshis(16777215));
break;
case "CHANNELD_AWAITING_LOCKIN":
ExplorerNode.Generate(1);
await WaitLNSynched();
ExplorerNode.Generate(awaitingLocking ? 1 : 10);
await WaitLNSynched(client, CustomerLightningD, MerchantLightningD);
awaitingLocking = true;
break;
case "CHANNELD_NORMAL":
return;
@ -122,23 +139,29 @@ namespace BTCPayServer.Tests
}
}
private async Task<GetInfoResponse> WaitLNSynched()
private async Task<LightningNodeInformation> WaitLNSynched(params ILightningInvoiceClient[] clients)
{
while (true)
{
var merchantInfo = await MerchantCharge.Client.GetInfoAsync();
var blockCount = await ExplorerNode.GetBlockCountAsync();
if (merchantInfo.BlockHeight != blockCount)
{
await Task.Delay(1000);
}
else
{
return merchantInfo;
}
var synching = clients.Select(c => WaitLNSynchedCore(blockCount, c)).ToArray();
await Task.WhenAll(synching);
if (synching.All(c => c.Result != null))
return synching[0].Result;
await Task.Delay(1000);
}
}
private async Task<LightningNodeInformation> WaitLNSynchedCore(int blockCount, ILightningInvoiceClient client)
{
var merchantInfo = await client.GetInfo();
if (merchantInfo.BlockHeight == blockCount)
{
return merchantInfo;
}
return null;
}
public void SendLightningPayment(Invoice invoice)
{
SendLightningPaymentAsync(invoice).GetAwaiter().GetResult();
@ -152,8 +175,10 @@ namespace BTCPayServer.Tests
}
public CLightningRPCClient CustomerLightningD { get; set; }
public CLightningRPCClient MerchantLightningD { get; private set; }
public ChargeTester MerchantCharge { get; private set; }
public LndMockTester MerchantLnd { get; set; }
internal string GetEnvironment(string variable, string defaultValue)
{
@ -185,106 +210,18 @@ namespace BTCPayServer.Tests
HttpClient _Http = new HttpClient();
class MockHttpRequest : HttpRequest
{
Uri serverUri;
public MockHttpRequest(Uri serverUri)
{
this.serverUri = serverUri;
}
public override HttpContext HttpContext => throw new NotImplementedException();
public override string Method
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override string Scheme
{
get => serverUri.Scheme;
set => throw new NotImplementedException();
}
public override bool IsHttps
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override HostString Host
{
get => new HostString(serverUri.Host, serverUri.Port);
set => throw new NotImplementedException();
}
public override PathString PathBase
{
get => "";
set => throw new NotImplementedException();
}
public override PathString Path
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override QueryString QueryString
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override IQueryCollection Query
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override string Protocol
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override IHeaderDictionary Headers => throw new NotImplementedException();
public override IRequestCookieCollection Cookies
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override long? ContentLength
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override string ContentType
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override Stream Body
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override bool HasFormContentType => throw new NotImplementedException();
public override IFormCollection Form
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override Task<IFormCollection> ReadFormAsync(CancellationToken cancellationToken = default(CancellationToken))
{
throw new NotImplementedException();
}
}
public BTCPayServerTester PayTester
{
get; set;
}
public List<string> Stores { get; internal set; } = new List<string>();
public void Dispose()
{
foreach(var store in Stores)
{
Xunit.Assert.True(PayTester.StoreRepository.DeleteStore(store).GetAwaiter().GetResult());
}
if (PayTester != null)
PayTester.Dispose();
}

@ -65,6 +65,7 @@ namespace BTCPayServer.Tests
var store = this.GetController<UserStoresController>();
await store.CreateStore(new CreateStoreViewModel() { Name = "Test Store" });
StoreId = store.CreatedStoreId;
parent.Stores.Add(StoreId);
}
public BTCPayNetwork SupportedNetwork { get; set; }
@ -117,7 +118,7 @@ namespace BTCPayServer.Tests
{
get; set;
}
public void RegisterLightningNode(string cryptoCode, LightningConnectionType connectionType)
{
RegisterLightningNodeAsync(cryptoCode, connectionType).GetAwaiter().GetResult();
@ -126,15 +127,24 @@ namespace BTCPayServer.Tests
public async Task RegisterLightningNodeAsync(string cryptoCode, LightningConnectionType connectionType)
{
var storeController = this.GetController<StoresController>();
string connectionString = null;
if (connectionType == LightningConnectionType.Charge)
connectionString = "type=charge;server=" + parent.MerchantCharge.Client.Uri.AbsoluteUri;
else if (connectionType == LightningConnectionType.CLightning)
connectionString = "type=clightning;server=" + parent.MerchantLightningD.Address.AbsoluteUri;
else if (connectionType == LightningConnectionType.LndREST)
connectionString = $"type=lnd-rest;server={parent.MerchantLnd.Swagger.BaseUrl};allowinsecure=true";
else
throw new NotSupportedException(connectionType.ToString());
await storeController.AddLightningNode(StoreId, new LightningNodeViewModel()
{
Url = connectionType == LightningConnectionType.Charge ? parent.MerchantCharge.Client.Uri.AbsoluteUri :
connectionType == LightningConnectionType.CLightning ? parent.MerchantLightningD.Address.AbsoluteUri
: throw new NotSupportedException(connectionType.ToString()),
ConnectionString = connectionString,
SkipPortTest = true
}, "save", "BTC");
if (storeController.ModelState.ErrorCount != 0)
Assert.False(true, storeController.ModelState.FirstOrDefault().Value.Errors[0].ErrorMessage);
}
}
}
}

@ -36,7 +36,9 @@ using BTCPayServer.Services.Stores;
using System.Net.Http;
using System.Text;
using BTCPayServer.Rating;
using BTCPayServer.Validation;
using ExchangeSharp;
using System.Security.Cryptography.X509Certificates;
namespace BTCPayServer.Tests
{
@ -48,6 +50,27 @@ namespace BTCPayServer.Tests
Logs.LogProvider = new XUnitLogProvider(helper);
}
[Fact]
public void CanHandleUriValidation()
{
var attribute = new UriAttribute();
Assert.True(attribute.IsValid("http://localhost"));
Assert.True(attribute.IsValid("http://localhost:1234"));
Assert.True(attribute.IsValid("https://localhost"));
Assert.True(attribute.IsValid("https://127.0.0.1"));
Assert.True(attribute.IsValid("http://127.0.0.1"));
Assert.True(attribute.IsValid("http://127.0.0.1:1234"));
Assert.True(attribute.IsValid("http://gozo.com"));
Assert.True(attribute.IsValid("https://gozo.com"));
Assert.True(attribute.IsValid("https://gozo.com:1234"));
Assert.True(attribute.IsValid("https://gozo.com:1234/test.css"));
Assert.True(attribute.IsValid("https://gozo.com:1234/test.png"));
Assert.False(attribute.IsValid("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud e"));
Assert.False(attribute.IsValid(2));
Assert.False(attribute.IsValid("http://"));
Assert.False(attribute.IsValid("httpdsadsa.com"));
}
[Fact]
public void CanCalculateCryptoDue2()
{
@ -252,7 +275,7 @@ namespace BTCPayServer.Tests
entity.PaymentTolerance = 100;
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(0), accounting.MinimumTotalDue);
Assert.Equal(Money.Satoshis(1), accounting.MinimumTotalDue);
}
@ -265,7 +288,7 @@ namespace BTCPayServer.Tests
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
// Set tolerance to 50%
var stores = user.GetController<StoresController>();
var vm = Assert.IsType<StoreViewModel>(Assert.IsType<ViewResult>(stores.UpdateStore()).Model);
@ -276,7 +299,7 @@ namespace BTCPayServer.Tests
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Buyer = new Buyer() { email = "test@fwf.com" },
Price = 5000.0,
Price = 5000.0m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
@ -296,6 +319,22 @@ namespace BTCPayServer.Tests
}
}
[Fact]
public void RoundupCurrenciesCorrectly()
{
foreach (var test in new[]
{
(0.0005m, "$0.0005 (USD)"),
(0.001m, "$0.001 (USD)"),
(0.01m, "$0.01 (USD)"),
(0.1m, "$0.10 (USD)"),
})
{
var actual = InvoiceController.FormatCurrency(test.Item1, "USD", new CurrencyNameTable());
Assert.Equal(test.Item2, actual);
}
}
[Fact]
public void CanPayUsingBIP70()
{
@ -305,10 +344,11 @@ namespace BTCPayServer.Tests
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
Assert.True(user.BitPay.TestAccess(Facade.Merchant));
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Buyer = new Buyer() { email = "test@fwf.com" },
Price = 5000.0,
Price = 5000.0m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
@ -370,14 +410,21 @@ namespace BTCPayServer.Tests
var testResult = storeController.AddLightningNode(user.StoreId, new LightningNodeViewModel()
{
Url = tester.MerchantCharge.Client.Uri.AbsoluteUri
ConnectionString = "type=charge;server=" + tester.MerchantCharge.Client.Uri.AbsoluteUri,
SkipPortTest = true // We can't test this as the IP can't be resolved by the test host :(
}, "test", "BTC").GetAwaiter().GetResult();
Assert.DoesNotContain("Error", ((LightningNodeViewModel)Assert.IsType<ViewResult>(testResult).Model).StatusMessage, StringComparison.OrdinalIgnoreCase);
Assert.True(storeController.ModelState.IsValid);
Assert.IsType<RedirectToActionResult>(storeController.AddLightningNode(user.StoreId, new LightningNodeViewModel()
{
Url = tester.MerchantCharge.Client.Uri.AbsoluteUri
ConnectionString = "type=charge;server=" + tester.MerchantCharge.Client.Uri.AbsoluteUri
}, "save", "BTC").GetAwaiter().GetResult());
// Make sure old connection string format does not work
Assert.IsType<ViewResult>(storeController.AddLightningNode(user.StoreId, new LightningNodeViewModel()
{
ConnectionString = tester.MerchantCharge.Client.Uri.AbsoluteUri
}, "save", "BTC").GetAwaiter().GetResult());
var storeVm = Assert.IsType<Models.StoreViewModels.StoreViewModel>(Assert.IsType<ViewResult>(storeController.UpdateStore()).Model);
@ -389,106 +436,148 @@ namespace BTCPayServer.Tests
public void CanParseLightningURL()
{
LightningConnectionString conn = null;
Assert.True(LightningConnectionString.TryParse("/test/a", out conn));
Assert.Equal("unix://test/a", conn.ToString());
Assert.Equal("unix://test/a", conn.ToUri(true).AbsoluteUri);
Assert.Equal("unix://test/a", conn.ToUri(false).AbsoluteUri);
Assert.Equal(LightningConnectionType.CLightning, conn.ConnectionType);
Assert.True(LightningConnectionString.TryParse("unix://test/a", out conn));
Assert.Equal("unix://test/a", conn.ToString());
Assert.Equal("unix://test/a", conn.ToUri(true).AbsoluteUri);
Assert.Equal("unix://test/a", conn.ToUri(false).AbsoluteUri);
Assert.Equal(LightningConnectionType.CLightning, conn.ConnectionType);
Assert.True(LightningConnectionString.TryParse("unix://test/a", out conn));
Assert.Equal("unix://test/a", conn.ToString());
Assert.Equal("unix://test/a", conn.ToUri(true).AbsoluteUri);
Assert.Equal("unix://test/a", conn.ToUri(false).AbsoluteUri);
Assert.Equal(LightningConnectionType.CLightning, conn.ConnectionType);
Assert.True(LightningConnectionString.TryParse("tcp://test/a", out conn));
Assert.Equal("tcp://test/a", conn.ToString());
Assert.Equal("tcp://test/a", conn.ToUri(true).AbsoluteUri);
Assert.Equal("tcp://test/a", conn.ToUri(false).AbsoluteUri);
Assert.Equal(LightningConnectionType.CLightning, conn.ConnectionType);
Assert.True(LightningConnectionString.TryParse("http://aaa:bbb@test/a", out conn));
Assert.Equal("http://aaa:bbb@test/a", conn.ToString());
Assert.Equal("http://aaa:bbb@test/a", conn.ToUri(true).AbsoluteUri);
Assert.Equal("http://test/a", conn.ToUri(false).AbsoluteUri);
Assert.Equal(LightningConnectionType.Charge, conn.ConnectionType);
Assert.Equal("aaa", conn.Username);
Assert.Equal("bbb", conn.Password);
Assert.False(LightningConnectionString.TryParse("lol://aaa:bbb@test/a", out conn));
Assert.False(LightningConnectionString.TryParse("https://test/a", out conn));
Assert.False(LightningConnectionString.TryParse("unix://dwewoi:dwdwqd@test/a", out conn));
}
[Fact]
public void CanSendLightningPayment2()
{
using (var tester = ServerTester.Create())
Assert.True(LightningConnectionString.TryParse("/test/a", true, out conn));
for (int i = 0; i < 2; i++)
{
tester.Start();
tester.PrepareLightning();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterLightningNode("BTC", LightningConnectionType.CLightning);
user.RegisterDerivationScheme("BTC");
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 0.01,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description"
});
tester.SendLightningPayment(invoice);
Eventually(() =>
{
var localInvoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal("complete", localInvoice.Status);
Assert.Equal("False", localInvoice.ExceptionStatus.ToString());
});
if (i == 1)
Assert.True(LightningConnectionString.TryParse(conn.ToString(), false, out conn));
Assert.Equal(i == 0, conn.IsLegacy);
Assert.Equal("type=clightning;server=unix://test/a", conn.ToString());
Assert.Equal("unix://test/a", conn.ToUri(true).AbsoluteUri);
Assert.Equal("unix://test/a", conn.ToUri(false).AbsoluteUri);
Assert.Equal(LightningConnectionType.CLightning, conn.ConnectionType);
}
Assert.True(LightningConnectionString.TryParse("unix://test/a", true, out conn));
for (int i = 0; i < 2; i++)
{
if (i == 1)
Assert.True(LightningConnectionString.TryParse(conn.ToString(), false, out conn));
Assert.Equal("type=clightning;server=unix://test/a", conn.ToString());
Assert.Equal("unix://test/a", conn.ToUri(true).AbsoluteUri);
Assert.Equal("unix://test/a", conn.ToUri(false).AbsoluteUri);
Assert.Equal(LightningConnectionType.CLightning, conn.ConnectionType);
}
Assert.True(LightningConnectionString.TryParse("unix://test/a", true, out conn));
for (int i = 0; i < 2; i++)
{
if (i == 1)
Assert.True(LightningConnectionString.TryParse(conn.ToString(), false, out conn));
Assert.Equal("type=clightning;server=unix://test/a", conn.ToString());
Assert.Equal("unix://test/a", conn.ToUri(true).AbsoluteUri);
Assert.Equal("unix://test/a", conn.ToUri(false).AbsoluteUri);
Assert.Equal(LightningConnectionType.CLightning, conn.ConnectionType);
}
Assert.True(LightningConnectionString.TryParse("tcp://test/a", true, out conn));
for (int i = 0; i < 2; i++)
{
if (i == 1)
Assert.True(LightningConnectionString.TryParse(conn.ToString(), false, out conn));
Assert.Equal("type=clightning;server=tcp://test/a", conn.ToString());
Assert.Equal("tcp://test/a", conn.ToUri(true).AbsoluteUri);
Assert.Equal("tcp://test/a", conn.ToUri(false).AbsoluteUri);
Assert.Equal(LightningConnectionType.CLightning, conn.ConnectionType);
}
Assert.True(LightningConnectionString.TryParse("http://aaa:bbb@test/a", true, out conn));
for (int i = 0; i < 2; i++)
{
if (i == 1)
Assert.True(LightningConnectionString.TryParse(conn.ToString(), false, out conn));
Assert.Equal("type=charge;server=http://aaa:bbb@test/a", conn.ToString());
Assert.Equal("http://aaa:bbb@test/a", conn.ToUri(true).AbsoluteUri);
Assert.Equal("http://test/a", conn.ToUri(false).AbsoluteUri);
Assert.Equal(LightningConnectionType.Charge, conn.ConnectionType);
Assert.Equal("aaa", conn.Username);
Assert.Equal("bbb", conn.Password);
}
Assert.True(LightningConnectionString.TryParse("http://api-token:bbb@test/a", true, out conn));
for (int i = 0; i < 2; i++)
{
if (i == 1)
Assert.True(LightningConnectionString.TryParse(conn.ToString(), false, out conn));
Assert.Equal("type=charge;server=http://test/a;api-token=bbb", conn.ToString());
}
Assert.False(LightningConnectionString.TryParse("lol://aaa:bbb@test/a", true, out conn));
Assert.False(LightningConnectionString.TryParse("https://test/a", true, out conn));
Assert.False(LightningConnectionString.TryParse("unix://dwewoi:dwdwqd@test/a", true, out conn));
Assert.False(LightningConnectionString.TryParse("tcp://test/a", false, out conn));
Assert.False(LightningConnectionString.TryParse("type=charge;server=http://aaa:bbb@test/a;unk=lol", false, out conn));
Assert.False(LightningConnectionString.TryParse("type=charge;server=tcp://aaa:bbb@test/a", false, out conn));
Assert.False(LightningConnectionString.TryParse("type=charge", false, out conn));
Assert.False(LightningConnectionString.TryParse("type=clightning", false, out conn));
Assert.True(LightningConnectionString.TryParse("type=clightning;server=tcp://aaa:bbb@test/a", false, out conn));
Assert.True(LightningConnectionString.TryParse("type=clightning;server=/aaa:bbb@test/a", false, out conn));
Assert.True(LightningConnectionString.TryParse("type=clightning;server=unix://aaa:bbb@test/a", false, out conn));
Assert.False(LightningConnectionString.TryParse("type=clightning;server=wtf://aaa:bbb@test/a", false, out conn));
var macaroon = "0201036c6e640247030a10b0dbbde28f009f83d330bde05075ca251201301a160a0761646472657373120472656164120577726974651a170a08696e766f6963657312047265616412057772697465000006200ae088692e67cf14e767c3d2a4a67ce489150bf810654ff980e1b7a7e263d5e8";
var certthumbprint = "c51bb1d402306d0da00e85581b32aa56166bcbab7eb888ff925d7167eb436d06";
// We get this format from "openssl x509 -noout -fingerprint -sha256 -inform pem -in <certificate>"
var certthumbprint2 = "C5:1B:B1:D4:02:30:6D:0D:A0:0E:85:58:1B:32:AA:56:16:6B:CB:AB:7E:B8:88:FF:92:5D:71:67:EB:43:6D:06";
var lndUri = $"type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/;macaroon={macaroon};certthumbprint={certthumbprint}";
var lndUri2 = $"type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/;macaroon={macaroon};certthumbprint={certthumbprint2}";
var certificateHash = new X509Certificate2(Encoders.Hex.DecodeData("2d2d2d2d2d424547494e2043455254494649434154452d2d2d2d2d0a4d494942396a4343415a7967417749424167495156397a62474252724e54716b4e4b55676d72524d377a414b42676771686b6a4f50515144416a41784d5238770a485159445651514b45785a73626d5167595856306232646c626d56795958526c5a43426a5a584a304d51347744415944565151444577564754304e56557a41650a467730784f4441304d6a55794d7a517a4d6a4261467730784f5441324d6a41794d7a517a4d6a42614d444578487a416442674e5642416f54466d78755a4342680a645852765a3256755a584a686447566b49474e6c636e5178446a414d42674e5642414d5442555a50513156544d466b77457759484b6f5a497a6a3043415159490a4b6f5a497a6a304441516344516741454b7557424568564f75707965434157476130766e713262712f59396b41755a78616865646d454553482b753936436d450a397577486b4b2b4a7667547a66385141783550513741357254637155374b57595170303175364f426c5443426b6a414f42674e56485138424166384542414d430a4171517744775944565230544151482f42415577417745422f7a427642674e56485245456144426d6767564754304e565534494a6247396a5957786f62334e300a6877522f4141414268784141414141414141414141414141414141414141414268775373474f69786877514b41457342687753702f717473687754417141724c0a687753702f6d4a72687753702f754f77687753702f714e59687753702f6874436877514b70514157687753702f6c42514d416f4743437147534d343942414d430a413067414d45554349464866716d595a5043647a4a5178386b47586859473834394c31766541364c784d6f7a4f5774356d726835416945413662756e51556c710a6558553070474168776c3041654d726a4d4974394c7652736179756162565a593278343d0a2d2d2d2d2d454e442043455254494649434154452d2d2d2d2d0a"))
.GetCertHash(System.Security.Cryptography.HashAlgorithmName.SHA256);
Assert.True(LightningConnectionString.TryParse(lndUri, false, out conn));
Assert.True(LightningConnectionString.TryParse(lndUri2, false, out var conn2));
Assert.Equal(conn2.ToString(), conn.ToString());
Assert.Equal(lndUri, conn.ToString());
Assert.Equal(LightningConnectionType.LndREST, conn.ConnectionType);
Assert.Equal(macaroon, Encoders.Hex.EncodeData(conn.Macaroon));
Assert.Equal(certthumbprint.Replace(":", "", StringComparison.OrdinalIgnoreCase).ToLowerInvariant(), Encoders.Hex.EncodeData(conn.CertificateThumbprint));
Assert.True(certificateHash.SequenceEqual(conn.CertificateThumbprint));
// AllowInsecure can be set to allow http
Assert.False(LightningConnectionString.TryParse($"type=lnd-rest;server=http://127.0.0.1:53280/;macaroon={macaroon};allowinsecure=false", false, out conn2));
Assert.True(LightningConnectionString.TryParse($"type=lnd-rest;server=http://127.0.0.1:53280/;macaroon={macaroon};allowinsecure=true", false, out conn2));
Assert.True(LightningConnectionString.TryParse($"type=lnd-rest;server=http://127.0.0.1:53280/;macaroon={macaroon};allowinsecure=true", false, out conn2));
}
[Fact]
public void CanSendLightningPayment()
public void CanSendLightningPaymentCLightning()
{
ProcessLightningPayment(LightningConnectionType.CLightning);
}
[Fact]
public void CanSendLightningPaymentCharge()
{
ProcessLightningPayment(LightningConnectionType.Charge);
}
[Fact]
public void CanSendLightningPaymentLnd()
{
ProcessLightningPayment(LightningConnectionType.LndREST);
}
void ProcessLightningPayment(LightningConnectionType type)
{
// For easier debugging and testing
// LightningLikePaymentHandler.LIGHTNING_TIMEOUT = int.MaxValue;
using (var tester = ServerTester.Create())
{
tester.Start();
tester.PrepareLightning();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterLightningNode("BTC", LightningConnectionType.Charge);
user.RegisterLightningNode("BTC", type);
user.RegisterDerivationScheme("BTC");
tester.PrepareLightning(type);
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(CanSendLightningPaymentCore(tester, user));
Task.WaitAll(Enumerable.Range(0, 5)
.Select(_ => CanSendLightningPaymentCore(tester, user))
@ -498,10 +587,13 @@ namespace BTCPayServer.Tests
async Task CanSendLightningPaymentCore(ServerTester tester, TestAccount user)
{
await Task.Delay(TimeSpan.FromSeconds(RandomUtils.GetUInt32() % 5));
// TODO: If this parameter is less than 1 second we start having concurrency problems
await Task.Delay(TimeSpan.FromMilliseconds(1000));
//
var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice()
{
Price = 0.01,
Price = 0.01m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
@ -554,7 +646,7 @@ namespace BTCPayServer.Tests
acc.RegisterDerivationScheme("BTC");
var invoice = acc.BitPay.CreateInvoice(new Invoice()
{
Price = 5.0,
Price = 5.0m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
@ -598,6 +690,53 @@ namespace BTCPayServer.Tests
}
}
[Fact]
public void CanListInvoices()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var acc = tester.NewAccount();
acc.GrantAccess();
acc.RegisterDerivationScheme("BTC");
// First we try payment with a merchant having only BTC
var invoice = acc.BitPay.CreateInvoice(new Invoice()
{
Price = 500,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
var cashCow = tester.ExplorerNode;
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
var firstPayment = invoice.CryptoInfo[0].TotalDue - Money.Satoshis(10);
cashCow.SendToAddress(invoiceAddress, firstPayment);
Eventually(() =>
{
invoice = acc.BitPay.GetInvoice(invoice.Id);
Assert.Equal(firstPayment, invoice.CryptoInfo[0].Paid);
});
AssertSearchInvoice(acc, true, invoice.Id, $"storeid:{acc.StoreId}");
AssertSearchInvoice(acc, false, invoice.Id, $"storeid:blah");
AssertSearchInvoice(acc, true, invoice.Id, $"{invoice.Id}");
AssertSearchInvoice(acc, true, invoice.Id, $"exceptionstatus:paidPartial");
AssertSearchInvoice(acc, false, invoice.Id, $"exceptionstatus:paidOver");
AssertSearchInvoice(acc, true, invoice.Id, $"unusual:true");
AssertSearchInvoice(acc, false, invoice.Id, $"unusual:false");
}
}
private void AssertSearchInvoice(TestAccount acc, bool expected, string invoiceId, string filter)
{
var result = (Models.InvoicingModels.InvoicesModel)((ViewResult)acc.GetController<InvoiceController>().ListInvoices(filter).Result).Model;
Assert.Equal(expected, result.Invoices.Any(i => i.InvoiceId == invoiceId));
}
[Fact]
public void CanRBFPayment()
{
@ -609,7 +748,7 @@ namespace BTCPayServer.Tests
user.RegisterDerivationScheme("BTC");
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5000.0,
Price = 5000.0m,
Currency = "USD"
}, Facade.Merchant);
var payment1 = invoice.BtcDue + Money.Coins(0.0001m);
@ -685,8 +824,30 @@ namespace BTCPayServer.Tests
Assert.False(user.BitPay.TestAccess(Facade.Merchant));
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
Assert.True(user.BitPay.TestAccess(Facade.Merchant));
// Test request pairing code client side
var storeController = user.GetController<StoresController>();
storeController.CreateToken(new CreateTokenViewModel()
{
Facade = Facade.Merchant.ToString(),
Label = "test2",
StoreId = user.StoreId
}).GetAwaiter().GetResult();
Assert.NotNull(storeController.GeneratedPairingCode);
var k = new Key();
var bitpay = new Bitpay(k, tester.PayTester.ServerUri);
bitpay.AuthorizeClient(new PairingCode(storeController.GeneratedPairingCode)).Wait();
Assert.True(bitpay.TestAccess(Facade.Merchant));
Assert.True(bitpay.TestAccess(Facade.PointOfSale));
// Same with new instance
bitpay = new Bitpay(k, tester.PayTester.ServerUri);
Assert.True(bitpay.TestAccess(Facade.Merchant));
Assert.True(bitpay.TestAccess(Facade.PointOfSale));
// Can generate API Key
var repo = tester.PayTester.GetService<TokenRepository>();
Assert.Empty(repo.GetLegacyAPIKeys(user.StoreId).GetAwaiter().GetResult());
@ -709,7 +870,7 @@ namespace BTCPayServer.Tests
message.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Encoders.Base64.EncodeData(Encoders.ASCII.DecodeData(apiKey)));
var invoice = new Invoice()
{
Price = 5000.0,
Price = 5000.0m,
Currency = "USD"
};
message.Content = new StringContent(JsonConvert.SerializeObject(invoice), Encoding.UTF8, "application/json");
@ -751,7 +912,7 @@ namespace BTCPayServer.Tests
storeController.Rates(vm).Wait();
var invoice2 = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5000.0,
Price = 5000.0m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
@ -775,7 +936,7 @@ namespace BTCPayServer.Tests
// First we try payment with a merchant having only BTC
var invoice1 = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5000.0,
Price = 5000.0m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
@ -793,7 +954,7 @@ namespace BTCPayServer.Tests
var invoice2 = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5000.0,
Price = 5000.0m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
@ -828,6 +989,11 @@ namespace BTCPayServer.Tests
Assert.Single(invoice.CryptoInfo);
Assert.Equal("LTC", invoice.CryptoInfo[0].CryptoCode);
Assert.True(invoice.PaymentCodes.ContainsKey("LTC"));
Assert.True(invoice.SupportedTransactionCurrencies.ContainsKey("LTC"));
Assert.True(invoice.SupportedTransactionCurrencies["LTC"].Enabled);
Assert.True(invoice.PaymentSubtotals.ContainsKey("LTC"));
Assert.True(invoice.PaymentTotals.ContainsKey("LTC"));
var cashCow = tester.LTCExplorerNode;
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
var firstPayment = Money.Coins(0.1m);
@ -849,7 +1015,7 @@ namespace BTCPayServer.Tests
// 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(1.0m, invoice.Rate);
Assert.NotEqual(invoice.BtcDue, invoice.CryptoInfo[0].Due); // Should be BTC rate
cashCow.SendToAddress(invoiceAddress, invoice.CryptoInfo[0].Due);
@ -936,7 +1102,7 @@ namespace BTCPayServer.Tests
// First we try payment with a merchant having only BTC
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5000.0,
Price = 5000.0m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
@ -962,13 +1128,23 @@ namespace BTCPayServer.Tests
Assert.Single(checkout.AvailableCryptos);
Assert.Equal("BTC", checkout.CryptoCode);
Assert.Single(invoice.PaymentCodes);
Assert.Single(invoice.SupportedTransactionCurrencies);
Assert.Single(invoice.SupportedTransactionCurrencies);
Assert.Single(invoice.PaymentSubtotals);
Assert.Single(invoice.PaymentTotals);
Assert.True(invoice.PaymentCodes.ContainsKey("BTC"));
Assert.True(invoice.SupportedTransactionCurrencies.ContainsKey("BTC"));
Assert.True(invoice.SupportedTransactionCurrencies["BTC"].Enabled);
Assert.True(invoice.PaymentSubtotals.ContainsKey("BTC"));
Assert.True(invoice.PaymentTotals.ContainsKey("BTC"));
//////////////////////
// Retry now with LTC enabled
user.RegisterDerivationScheme("LTC");
invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5000.0,
Price = 5000.0m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
@ -1010,6 +1186,18 @@ namespace BTCPayServer.Tests
checkout = (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id, "LTC").GetAwaiter().GetResult()).Value;
Assert.Equal(2, checkout.AvailableCryptos.Count);
Assert.Equal("LTC", checkout.CryptoCode);
Assert.Equal(2, invoice.PaymentCodes.Count());
Assert.Equal(2, invoice.SupportedTransactionCurrencies.Count());
Assert.Equal(2, invoice.SupportedTransactionCurrencies.Count());
Assert.Equal(2, invoice.PaymentSubtotals.Count());
Assert.Equal(2, invoice.PaymentTotals.Count());
Assert.True(invoice.PaymentCodes.ContainsKey("LTC"));
Assert.True(invoice.SupportedTransactionCurrencies.ContainsKey("LTC"));
Assert.True(invoice.SupportedTransactionCurrencies["LTC"].Enabled);
Assert.True(invoice.PaymentSubtotals.ContainsKey("LTC"));
Assert.True(invoice.PaymentTotals.ContainsKey("LTC"));
}
}
@ -1090,7 +1278,7 @@ namespace BTCPayServer.Tests
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 1.5,
Price = 1.5m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
@ -1103,7 +1291,7 @@ namespace BTCPayServer.Tests
invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5.5,
Price = 5.5m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
@ -1150,9 +1338,9 @@ namespace BTCPayServer.Tests
Assert.Equal("orange", vmview.Items[1].Title);
Assert.Equal(10.0m, vmview.Items[1].Price.Value);
Assert.Equal("$5.00", vmview.Items[0].Price.Formatted);
Assert.IsType<RedirectResult>(apps.ViewPointOfSale(appId, 0, "orange").Result);
Assert.IsType<RedirectResult>(apps.ViewPointOfSale(appId, 0, null, null, null, null, "orange").Result);
var invoice = user.BitPay.GetInvoices().First();
Assert.Equal(10.00, invoice.Price);
Assert.Equal(10.00m, invoice.Price);
Assert.Equal("CAD", invoice.Currency);
Assert.Equal("orange", invoice.ItemDesc);
}
@ -1174,8 +1362,9 @@ namespace BTCPayServer.Tests
Assert.NotNull(vm.SelectedAppType);
Assert.Null(vm.Name);
vm.Name = "test";
vm.SelectedAppType = AppType.PointOfSale.ToString();
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.CreateApp(vm).Result);
Assert.Equal(nameof(apps.ListApps), redirectToAction.ActionName);
Assert.Equal(nameof(apps.UpdatePointOfSale), redirectToAction.ActionName);
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model);
var appList2 = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps2.ListApps().Result).Model);
Assert.Single(appList.Apps);
@ -1203,7 +1392,7 @@ namespace BTCPayServer.Tests
user.RegisterDerivationScheme("BTC");
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5000.0,
Price = 5000.0m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
@ -1213,6 +1402,8 @@ namespace BTCPayServer.Tests
var repo = tester.PayTester.GetService<InvoiceRepository>();
var ctx = tester.PayTester.GetService<ApplicationDbContextFactory>().CreateContext();
Assert.Equal(0, invoice.CryptoInfo[0].TxCount);
Assert.True(invoice.MinerFees.ContainsKey("BTC"));
Assert.Equal(100m, invoice.MinerFees["BTC"].SatoshiPerBytes);
Eventually(() =>
{
var textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery()
@ -1308,12 +1499,12 @@ namespace BTCPayServer.Tests
{
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("complete", localInvoice.Status);
Assert.NotEqual(0.0, localInvoice.Rate);
Assert.NotEqual(0.0m, localInvoice.Rate);
});
invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5000.0,
Price = 5000.0m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
@ -1352,10 +1543,10 @@ namespace BTCPayServer.Tests
var quadri = new QuadrigacxRateProvider();
var rates = quadri.GetRatesAsync().GetAwaiter().GetResult();
Assert.NotEmpty(rates);
Assert.NotEqual(0.0m, rates.First().Value);
Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("BTC_CAD")).Value);
Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("BTC_USD")).Value);
Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("LTC_CAD")).Value);
Assert.NotEqual(0.0m, rates.First().BidAsk.Bid);
Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("BTC_CAD")).Bid);
Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("BTC_USD")).Bid);
Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("LTC_CAD")).Bid);
Assert.Null(rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("LTC_USD")));
}
@ -1364,7 +1555,7 @@ namespace BTCPayServer.Tests
{
var provider = new BTCPayNetworkProvider(NetworkType.Mainnet);
var factory = CreateBTCPayRateFactory(provider);
foreach (var result in factory
.DirectProviders
.Select(p => (ExpectedName: p.Key, ResultAsync: p.Value.GetRatesAsync()))
@ -1376,11 +1567,11 @@ namespace BTCPayServer.Tests
Assert.NotEmpty(exchangeRates.ByExchange[result.ExpectedName]);
// This check if the currency pair is using right currency pair
Assert.Contains(exchangeRates.ByExchange[result.ExpectedName],
e => ( e.CurrencyPair == new CurrencyPair("BTC", "USD") ||
Assert.Contains(exchangeRates.ByExchange[result.ExpectedName],
e => (e.CurrencyPair == new CurrencyPair("BTC", "USD") ||
e.CurrencyPair == new CurrencyPair("BTC", "EUR") ||
e.CurrencyPair == new CurrencyPair("BTC", "USDT"))
&& e.Value > 1.0m // 1BTC will always be more than 1USD
&& e.BidAsk.Bid > 1.0m // 1BTC will always be more than 1USD
);
}
}
@ -1423,7 +1614,6 @@ namespace BTCPayServer.Tests
RateRules.TryParse("X_X = coinaverage(X_X);", out var rateRules);
var factory = CreateBTCPayRateFactory(provider);
factory.DirectProviders.Clear();
factory.CacheSpan = TimeSpan.FromSeconds(10);
var fetchedRate = factory.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();

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

@ -17,9 +17,10 @@ services:
TESTS_POSTGRES: User ID=postgres;Host=postgres;Port=5432;Database=btcpayserver
TESTS_PORT: 80
TESTS_HOSTNAME: tests
TEST_MERCHANTLIGHTNINGD: "/etc/merchant_lightningd_datadir/lightning-rpc"
TEST_CUSTOMERLIGHTNINGD: "/etc/customer_lightningd_datadir/lightning-rpc"
TEST_MERCHANTCHARGE: http://api-token:foiewnccewuify@lightning-charged:9112/
TEST_MERCHANTLIGHTNINGD: "type=clightning;server=/etc/merchant_lightningd_datadir/lightning-rpc"
TEST_CUSTOMERLIGHTNINGD: "type=clightning;server=/etc/customer_lightningd_datadir/lightning-rpc"
TEST_MERCHANTCHARGE: "type=charge;server=https://lightning-charged:9112/;api-token=foiewnccewuify;allowinsecure=true"
TEST_MERCHANTLND: "type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/;allowinsecure=true"
TESTS_INCONTAINER: "true"
expose:
- "80"
@ -44,9 +45,25 @@ services:
- customer_lightningd
- merchant_lightningd
- lightning-charged
- customer_lnd
- merchant_lnd
devlnd:
image: nicolasdorier/docker-bitcoin:0.16.0
environment:
BITCOIN_EXTRA_ARGS: |
regtest=1
connect=bitcoind:39388
links:
- nbxplorer
- postgres
- customer_lnd
- merchant_lnd
nbxplorer:
image: nicolasdorier/nbxplorer:1.0.2.3
image: nicolasdorier/nbxplorer:1.0.2.14
ports:
- "32838:32838"
expose:
@ -80,8 +97,13 @@ services:
rpcport=43782
port=39388
whitelist=0.0.0.0/0
zmqpubrawtx=tcp://0.0.0.0:28332
zmqpubrawblock=tcp://0.0.0.0:28332
zmqpubrawtxlock=tcp://0.0.0.0:28332
zmqpubhashblock=tcp://0.0.0.0:28332
ports:
- "43782:43782"
- "28332:28332"
expose:
- "43782" # RPC
- "39388" # P2P
@ -89,14 +111,15 @@ services:
- "bitcoin_datadir:/data"
customer_lightningd:
image: nicolasdorier/clightning:0.0.0.12-dev
image: nicolasdorier/clightning:v0.6-dev
environment:
EXPOSE_TCP: "true"
LIGHTNINGD_OPT: |
bitcoin-datadir=/etc/bitcoin
bitcoin-rpcconnect=bitcoind
network=regtest
ipaddr=customer_lightningd
bind-addr=0.0.0.0
announce-addr=customer_lightningd
log-level=debug
dev-broadcast-interval=1000
ports:
@ -111,7 +134,7 @@ services:
- bitcoind
lightning-charged:
image: shesek/lightning-charge:0.3.9
image: shesek/lightning-charge:0.3.15
environment:
NETWORK: regtest
API_TOKEN: foiewnccewuify
@ -130,13 +153,14 @@ services:
- merchant_lightningd
merchant_lightningd:
image: nicolasdorier/clightning:0.0.0.11-dev
image: nicolasdorier/clightning:v0.6-dev
environment:
EXPOSE_TCP: "true"
LIGHTNINGD_OPT: |
bitcoin-datadir=/etc/bitcoin
bitcoin-rpcconnect=bitcoind
ipaddr=merchant_lightningd
bind-addr=0.0.0.0
announce-addr=merchant_lightningd
network=regtest
log-level=debug
dev-broadcast-interval=1000
@ -175,8 +199,59 @@ services:
expose:
- "5432"
merchant_lnd:
image: btcpayserver/lnd:0.4.2.0
environment:
LND_CHAIN: "btc"
LND_ENVIRONMENT: "regtest"
LND_EXTRA_ARGS: |
restlisten=0.0.0.0:8080
bitcoin.node=bitcoind
bitcoind.rpchost=bitcoind:43782
bitcoind.zmqpath=tcp://bitcoind:28332
externalip=merchant_lnd:9735
no-macaroons=1
debuglevel=debug
noencryptwallet=1
ports:
- "53280:8080"
expose:
- "9735"
volumes:
- "merchant_lnd_datadir:/data"
- "bitcoin_datadir:/deps/.bitcoin"
links:
- bitcoind
customer_lnd:
image: btcpayserver/lnd:0.4.2.0
environment:
LND_CHAIN: "btc"
LND_ENVIRONMENT: "regtest"
LND_EXTRA_ARGS: |
restlisten=0.0.0.0:8080
bitcoin.node=bitcoind
bitcoind.rpchost=bitcoind:43782
bitcoind.zmqpath=tcp://bitcoind:28332
externalip=customer_lnd:10009
no-macaroons=1
debuglevel=debug
noencryptwallet=1
ports:
- "53281:8080"
expose:
- "8080"
- "10009"
volumes:
- "customer_lnd_datadir:/root/.lnd"
- "bitcoin_datadir:/deps/.bitcoin"
links:
- bitcoind
volumes:
bitcoin_datadir:
customer_lightningd_datadir:
merchant_lightningd_datadir:
lightning_charge_datadir:
customer_lnd_datadir:
merchant_lnd_datadir:

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

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

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

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

@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
using NBitcoin;
using NBXplorer;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
public void InitFeathercoin()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("FTC");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Feathercoin",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://explorer.feathercoin.com/tx/{0}" : "https://explorer.feathercoin.com/tx/{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "feathercoin",
DefaultRateRules = new[]
{
"FTC_X = FTC_BTC * BTC_X",
"FTC_BTC = bittrex(FTC_BTC)"
},
CryptoImagePath = "imlegacy/feathercoin.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("8'") : new KeyPath("1'")
});
}
}
}

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

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

@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
using NBitcoin;
using NBXplorer;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
public void InitMonacoin()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("MONA");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Monacoin",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://mona.insight.monaco-ex.org/insight/tx/{0}" : "https://testnet-mona.insight.monaco-ex.org/insight/tx/{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "monacoin",
DefaultRateRules = new[]
{
"MONA_X = MONA_BTC * BTC_X",
"MONA_BTC = zaif(MONA_BTC)"
},
CryptoImagePath = "imlegacy/monacoin.png",
LightningImagePath = "imlegacy/mona-lightning.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("22'") : new KeyPath("1'")
});
}
}
}

@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
using NBitcoin;
using NBXplorer;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
public void InitPolis()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("POLIS");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Polis",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://insight.polispay.org/tx/{0}" : "https://insight.polispay.org/tx/{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "polis",
DefaultRateRules = new[]
{
"POLIS_X = POLIS_BTC * BTC_X",
"POLIS_BTC = cryptopia(POLIS_BTC)"
},
CryptoImagePath = "imlegacy/polis.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("1997'") : new KeyPath("1'")
});
}
}
}

@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
using NBitcoin;
using NBXplorer;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
public void InitUfo()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("UFO");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Ufo",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://chainz.cryptoid.info/ufo/tx.dws?{0}" : "https://chainz.cryptoid.info/ufo/tx.dws?{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "ufo",
DefaultRateRules = new[]
{
"UFO_X = UFO_BTC * BTC_X",
"UFO_BTC = coinexchange(UFO_BTC)"
},
CryptoImagePath = "imlegacy/ufo.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("202'") : new KeyPath("1'")
});
}
}
}

@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
using NBitcoin;
using NBXplorer;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
public void InitViacoin()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("VIA");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Viacoin",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://explorer.viacoin.org/tx/{0}" : "https://explorer.viacoin.org/tx/{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "viacoin",
DefaultRateRules = new[]
{
"VIA_X = VIA_BTC * BTC_X",
"VIA_BTC = bittrex(VIA_BTC)"
},
CryptoImagePath = "imlegacy/viacoin.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("14'") : new KeyPath("1'")
});
}
}
}

@ -48,6 +48,13 @@ namespace BTCPayServer
InitBitcoin();
InitLitecoin();
InitDogecoin();
InitBitcoinGold();
InitMonacoin();
InitPolis();
InitFeathercoin();
InitGroestlcoin();
InitViacoin();
//InitUfo();
}
/// <summary>

@ -1,8 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<Version>1.0.2.5</Version>
<TargetFramework>netcoreapp2.1</TargetFramework>
<Version>1.0.2.60</Version>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup>
<ItemGroup>
@ -30,38 +30,35 @@
<EmbeddedResource Include="Currencies.txt" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BuildBundlerMinifier" Version="2.6.375" />
<PackageReference Include="BuildBundlerMinifier" Version="2.7.385" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.4.1" />
<PackageReference Include="Hangfire" Version="1.6.19" />
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.2" />
<PackageReference Include="Hangfire.PostgreSql" Version="1.4.8.1" />
<PackageReference Include="LedgerWallet" Version="1.0.1.36" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="2.6.1" />
<PackageReference Include="Meziantou.AspNetCore.BundleTagHelpers" Version="1.0.1" />
<PackageReference Include="Hangfire.PostgreSql" Version="1.4.8.2" />
<PackageReference Include="LedgerWallet" Version="2.0.0" />
<PackageReference Include="Meziantou.AspNetCore.BundleTagHelpers" Version="2.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="2.6.0" />
<PackageReference Include="NBitcoin" Version="4.1.1.4" />
<PackageReference Include="NBitpayClient" Version="1.0.0.18" />
<PackageReference Include="NBitcoin" Version="4.1.1.31" />
<PackageReference Include="NBitpayClient" Version="1.0.0.29" />
<PackageReference Include="DBreeze" Version="1.87.0" />
<PackageReference Include="NBXplorer.Client" Version="1.0.2.4" />
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.1" />
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.2" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.13" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.0.1" />
<PackageReference Include="System.ValueTuple" Version="4.4.0" />
<PackageReference Include="System.Xml.XmlSerializer" Version="4.0.11" />
<PackageReference Include="NBXplorer.Client" Version="1.0.2.14" />
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.2" />
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.3" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.16" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.1.0" />
<PackageReference Include="System.Xml.XmlSerializer" Version="4.3.0" />
<PackageReference Include="Text.Analyzers" Version="2.6.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.2" PrivateAssets="All" />
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version=" 2.1.0" PrivateAssets="All" />
<PackageReference Include="YamlDotNet" Version="4.3.1" />
</ItemGroup>
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0" />
<DotNetCliToolReference Include="Microsoft.Extensions.SecretManager.Tools" Version="2.0.0" />
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.0" />
</ItemGroup>
@ -114,6 +111,16 @@
<ItemGroup>
<Folder Include="Build\" />
<Folder Include="wwwroot\vendor\clipboard.js\" />
<Folder Include="wwwroot\vendor\highlightjs\" />
</ItemGroup>
<ItemGroup>
<Content Update="Views\Server\LNDGRPCServices.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\Services.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
</ItemGroup>
<ItemGroup>

@ -74,16 +74,40 @@ namespace BTCPayServer.Configuration
setting.ExplorerUri = conf.GetOrDefault<Uri>($"{net.CryptoCode}.explorer.url", net.NBXplorerNetwork.DefaultSettings.DefaultUrl);
setting.CookieFile = conf.GetOrDefault<string>($"{net.CryptoCode}.explorer.cookiefile", net.NBXplorerNetwork.DefaultSettings.DefaultCookieFile);
NBXplorerConnectionSettings.Add(setting);
var lightning = conf.GetOrDefault<string>($"{net.CryptoCode}.lightning", string.Empty);
if(lightning.Length != 0)
{
if(!LightningConnectionString.TryParse(lightning, out var connectionString, out var error))
var lightning = conf.GetOrDefault<string>($"{net.CryptoCode}.lightning", string.Empty);
if (lightning.Length != 0)
{
throw new ConfigException($"Invalid setting {net.CryptoCode}.lightning, you need to pass either " +
$"the absolute path to the unix socket of a running CLightning instance (eg. /root/.lightning/lightning-rpc), " +
$"or the url to a charge server with crendetials (eg. https://apitoken@API_TOKEN_SECRET:charge.example.com/)");
if (!LightningConnectionString.TryParse(lightning, true, out var connectionString, out var error))
{
throw new ConfigException($"Invalid setting {net.CryptoCode}.lightning, " + Environment.NewLine +
$"If you have a lightning server use: 'type=clightning;server=/root/.lightning/lightning-rpc', " + Environment.NewLine +
$"If you have a lightning charge server: 'type=charge;server=https://charge.example.com;api-token=yourapitoken'" + Environment.NewLine +
$"If you have a lnd server: 'type=lnd-rest;server=https://lnd:lnd@lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
$" lnd server: 'type=lnd-rest;server=https://lnd:lnd@lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
error);
}
if (connectionString.IsLegacy)
{
Logs.Configuration.LogWarning($"Setting {net.CryptoCode}.lightning will work but use an deprecated format, please replace it by '{connectionString.ToString()}'");
}
InternalLightningByCryptoCode.Add(net.CryptoCode, connectionString);
}
}
{
var lightning = conf.GetOrDefault<string>($"{net.CryptoCode}.external.lnd.grpc", string.Empty);
if (lightning.Length != 0)
{
if (!LightningConnectionString.TryParse(lightning, false, out var connectionString, out var error))
{
throw new ConfigException($"Invalid setting {net.CryptoCode}.external.lnd.grpc, " + Environment.NewLine +
$"lnd server: 'type=lnd-grpc;server=https://lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
$"lnd server: 'type=lnd-grpc;server=https://lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
error);
}
ExternalServicesByCryptoCode.Add(net.CryptoCode, new ExternalLNDGRPC(connectionString));
}
InternalLightningByCryptoCode.Add(net.CryptoCode, connectionString);
}
}
@ -97,11 +121,12 @@ namespace BTCPayServer.Configuration
if (!RootPath.StartsWith("/", StringComparison.InvariantCultureIgnoreCase))
RootPath = "/" + RootPath;
var old = conf.GetOrDefault<Uri>("internallightningnode", null);
if(old != null)
if (old != null)
throw new ConfigException($"internallightningnode should not be used anymore, use btclightning instead");
}
public string RootPath { get; set; }
public Dictionary<string, LightningConnectionString> InternalLightningByCryptoCode { get; set; } = new Dictionary<string, LightningConnectionString>();
public ExternalServices ExternalServicesByCryptoCode { get; set; } = new ExternalServices();
public BTCPayNetworkProvider NetworkProvider { get; set; }
public string PostgresConnectionString
@ -129,4 +154,29 @@ namespace BTCPayServer.Configuration
return builder.ToString();
}
}
public class ExternalServices : MultiValueDictionary<string, ExternalService>
{
public IEnumerable<T> GetServices<T>(string cryptoCode) where T : ExternalService
{
if (!this.TryGetValue(cryptoCode.ToUpperInvariant(), out var services))
return Array.Empty<T>();
return services.OfType<T>();
}
}
public class ExternalService
{
}
public class ExternalLNDGRPC : ExternalService
{
public ExternalLNDGRPC(LightningConnectionString connectionString)
{
ConnectionString = connectionString;
}
public LightningConnectionString ConnectionString { get; set; }
}
}

@ -27,19 +27,20 @@ namespace BTCPayServer.Configuration
};
app.HelpOption("-? | -h | --help");
app.Option("-n | --network", $"Set the network among (mainnet,testnet,regtest) (default: mainnet)", CommandOptionType.SingleValue);
app.Option("--testnet | -testnet", $"Use testnet (Deprecated, use --network instead)", CommandOptionType.BoolValue);
app.Option("--regtest | -regtest", $"Use regtest (Deprecated, use --network instead)", CommandOptionType.BoolValue);
app.Option("--chains | -c", $"Chains to support comma separated (default: btc, available: {chains})", CommandOptionType.SingleValue);
app.Option("--postgres", $"Connection string to postgres database (default: sqlite is used)", CommandOptionType.SingleValue);
app.Option("--externalurl", $"The expected external url of this service, to use if BTCPay is behind a reverse proxy (default: empty, use the incoming HTTP request to figure out)", CommandOptionType.SingleValue);
app.Option("--bundlejscss", $"Bundle javascript and css files for better performance (default: true)", CommandOptionType.SingleValue);
app.Option("--testnet | -testnet", $"Use testnet (deprecated, use --network instead)", CommandOptionType.BoolValue);
app.Option("--regtest | -regtest", $"Use regtest (deprecated, use --network instead)", CommandOptionType.BoolValue);
app.Option("--chains | -c", $"Chains to support as a comma separated (default: btc; available: {chains})", CommandOptionType.SingleValue);
app.Option("--postgres", $"Connection string to a PostgreSQL database (default: SQLite)", CommandOptionType.SingleValue);
app.Option("--externalurl", $"The expected external URL of this service, to use if BTCPay is behind a reverse proxy (default: empty, use the incoming HTTP request to figure out)", CommandOptionType.SingleValue);
app.Option("--bundlejscss", $"Bundle JavaScript and CSS files for better performance (default: true)", CommandOptionType.SingleValue);
app.Option("--rootpath", "The root path in the URL to access BTCPay (default: /)", CommandOptionType.SingleValue);
foreach (var network in provider.GetAll())
{
var crypto = network.CryptoCode.ToLowerInvariant();
app.Option($"--{crypto}explorerurl", $"Url of the NBxplorer for {network.CryptoCode} (default: {network.NBXplorerNetwork.DefaultSettings.DefaultUrl})", CommandOptionType.SingleValue);
app.Option($"--{crypto}explorerurl", $"URL of the NBXplorer for {network.CryptoCode} (default: {network.NBXplorerNetwork.DefaultSettings.DefaultUrl})", CommandOptionType.SingleValue);
app.Option($"--{crypto}explorercookiefile", $"Path to the cookie file (default: {network.NBXplorerNetwork.DefaultSettings.DefaultCookieFile})", CommandOptionType.SingleValue);
app.Option($"--{crypto}lightning", $"Easy configuration of lightning for the server adnistrator: Must be a unix socket of CLightning (lightning-rpc) or URL to a charge server (default: empty)", CommandOptionType.SingleValue);
app.Option($"--{crypto}lightning", $"Easy configuration of lightning for the server administrator: Must be a UNIX socket of c-lightning (lightning-rpc) or URL to a charge server (default: empty)", CommandOptionType.SingleValue);
app.Option($"--{crypto}externallndgrpc", $"The LND gRPC configuration BTCPay will expose to easily connect to the internal lnd wallet from Zap wallet (default: empty)", CommandOptionType.SingleValue);
}
return app;
}

@ -12,7 +12,8 @@ using System.Threading.Tasks;
namespace BTCPayServer.Controllers
{
[BitpayAPIConstraint]
[Authorize(AuthenticationSchemes = Security.Policies.BitpayAuthentication)]
[BitpayAPIConstraint(true)]
public class AccessTokenController : Controller
{
TokenRepository _TokenRepository;
@ -30,6 +31,7 @@ namespace BTCPayServer.Controllers
[HttpPost]
[Route("tokens")]
[AllowAnonymous]
public async Task<DataWrapper<List<PairingCodeResponse>>> Tokens([FromBody] TokenRequest request)
{
PairingCodeEntity pairingEntity = null;
@ -53,7 +55,7 @@ namespace BTCPayServer.Controllers
else
{
var sin = this.User.GetSIN() ?? request.Id;
if (string.IsNullOrEmpty(request.Id) || !NBitpayClient.Extensions.BitIdExtensions.ValidateSIN(request.Id))
if (string.IsNullOrEmpty(sin) || !NBitpayClient.Extensions.BitIdExtensions.ValidateSIN(sin))
throw new BitpayHttpException(400, "'id' property is required, alternatively, use BitId");
pairingEntity = await _TokenRepository.GetPairingAsync(request.PairingCode);
@ -77,6 +79,7 @@ namespace BTCPayServer.Controllers
{
new PairingCodeResponse()
{
Policies = new Newtonsoft.Json.Linq.JArray(),
PairingCode = pairingEntity.Id,
PairingExpiration = pairingEntity.Expiration,
DateCreated = pairingEntity.CreatedTime,

@ -17,6 +17,8 @@ using YamlDotNet.RepresentationModel;
using System.IO;
using BTCPayServer.Services.Rates;
using System.Globalization;
using System.Text;
using System.Text.Encodings.Web;
namespace BTCPayServer.Controllers
{
@ -57,15 +59,56 @@ namespace BTCPayServer.Controllers
var app = await GetOwnedApp(appId, AppType.PointOfSale);
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
return View(new UpdatePointOfSaleViewModel() { Title = settings.Title, ShowCustomAmount = settings.ShowCustomAmount, Currency = settings.Currency, Template = settings.Template });
var vm = new UpdatePointOfSaleViewModel()
{
Title = settings.Title,
ShowCustomAmount = settings.ShowCustomAmount,
Currency = settings.Currency,
Template = settings.Template
};
if (HttpContext?.Request != null)
{
var appUrl = HttpContext.Request.GetAbsoluteRoot().WithTrailingSlash() + $"apps/{appId}/pos";
var encoder = HtmlEncoder.Default;
if (settings.ShowCustomAmount)
{
StringBuilder builder = new StringBuilder();
builder.AppendLine($"<form method=\"POST\" action=\"{encoder.Encode(appUrl)}\">");
builder.AppendLine($" <input type=\"hidden\" name=\"amount\" value=\"100\" />");
builder.AppendLine($" <input type=\"hidden\" name=\"email\" value=\"customer@example.com\" />");
builder.AppendLine($" <input type=\"hidden\" name=\"orderId\" value=\"CustomOrderId\" />");
builder.AppendLine($" <input type=\"hidden\" name=\"notificationUrl\" value=\"https://example.com/callbacks\" />");
builder.AppendLine($" <input type=\"hidden\" name=\"redirectUrl\" value=\"https://example.com/thanksyou\" />");
builder.AppendLine($" <button type=\"submit\">Buy now</button>");
builder.AppendLine($"</form>");
vm.Example1 = builder.ToString();
}
try
{
var items = Parse(settings.Template, settings.Currency);
var builder = new StringBuilder();
builder.AppendLine($"<form method=\"POST\" action=\"{encoder.Encode(appUrl)}\">");
builder.AppendLine($" <input type=\"hidden\" name=\"email\" value=\"customer@example.com\" />");
builder.AppendLine($" <input type=\"hidden\" name=\"orderId\" value=\"CustomOrderId\" />");
builder.AppendLine($" <input type=\"hidden\" name=\"notificationUrl\" value=\"https://example.com/callbacks\" />");
builder.AppendLine($" <input type=\"hidden\" name=\"redirectUrl\" value=\"https://example.com/thanksyou\" />");
builder.AppendLine($" <button type=\"submit\" name=\"choiceKey\" value=\"{items[0].Id}\">Buy now</button>");
builder.AppendLine($"</form>");
vm.Example2 = builder.ToString();
}
catch { }
vm.InvoiceUrl = appUrl + "invoices/SkdsDghkdP3D3qkj7bLq3";
}
vm.ExampleCallback = "{\n \"id\":\"SkdsDghkdP3D3qkj7bLq3\",\n \"url\":\"https://btcpay.example.com/invoice?id=SkdsDghkdP3D3qkj7bLq3\",\n \"status\":\"paid\",\n \"price\":10,\n \"currency\":\"EUR\",\n \"invoiceTime\":1520373130312,\n \"expirationTime\":1520374030312,\n \"currentTime\":1520373179327,\n \"exceptionStatus\":false,\n \"buyerFields\":{\n \"buyerEmail\":\"customer@example.com\",\n \"buyerNotify\":false\n },\n \"paymentSubtotals\": {\n \"BTC\":114700\n },\n \"paymentTotals\": {\n \"BTC\":118400\n },\n \"transactionCurrency\": \"BTC\",\n \"amountPaid\": \"1025900\",\n \"exchangeRates\": {\n \"BTC\": {\n \"EUR\": 8721.690715789999,\n \"USD\": 10817.99\n }\n }\n}";
return View(vm);
}
[HttpPost]
[Route("{appId}/settings/pos")]
public async Task<IActionResult> UpdatePointOfSale(string appId, UpdatePointOfSaleViewModel vm)
{
if (_Currencies.GetCurrencyData(vm.Currency) == null)
if (_Currencies.GetCurrencyData(vm.Currency, false) == null)
ModelState.AddModelError(nameof(vm.Currency), "Invalid currency");
try
{
@ -91,7 +134,7 @@ namespace BTCPayServer.Controllers
});
await UpdateAppSettings(app);
StatusMessage = "App updated";
return RedirectToAction(nameof(UpdatePointOfSale));
return RedirectToAction(nameof(ListApps));
}
[HttpGet]
@ -102,8 +145,9 @@ namespace BTCPayServer.Controllers
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
var currency = _Currencies.GetCurrencyData(settings.Currency);
var currency = _Currencies.GetCurrencyData(settings.Currency, false);
double step = currency == null ? 1 : Math.Pow(10, -(currency.Divisibility));
return View(new ViewPointOfSaleViewModel()
{
Title = settings.Title,
@ -118,7 +162,8 @@ namespace BTCPayServer.Controllers
using (var ctx = _ContextFactory.CreateContext())
{
return await ctx.Apps
.Where(us => us.Id == appId && us.AppType == appType.ToString())
.Where(us => us.Id == appId &&
us.AppType == appType.ToString())
.FirstOrDefaultAsync();
}
}
@ -162,7 +207,14 @@ namespace BTCPayServer.Controllers
[HttpPost]
[Route("{appId}/pos")]
public async Task<IActionResult> ViewPointOfSale(string appId, double amount, string choiceKey)
[IgnoreAntiforgeryToken]
public async Task<IActionResult> ViewPointOfSale(string appId,
decimal amount,
string email,
string orderId,
string notificationUrl,
string redirectUrl,
string choiceKey)
{
var app = await GetApp(appId, AppType.PointOfSale);
if (string.IsNullOrEmpty(choiceKey) && amount <= 0)
@ -172,12 +224,12 @@ namespace BTCPayServer.Controllers
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
if(string.IsNullOrEmpty(choiceKey) && !settings.ShowCustomAmount)
if (string.IsNullOrEmpty(choiceKey) && !settings.ShowCustomAmount)
{
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
}
string title = null;
double price = 0.0;
var price = 0.0m;
if (!string.IsNullOrEmpty(choiceKey))
{
var choices = Parse(settings.Template, settings.Currency);
@ -185,20 +237,26 @@ namespace BTCPayServer.Controllers
if (choice == null)
return NotFound();
title = choice.Title;
price = (double)choice.Price.Value;
price = choice.Price.Value;
}
else
{
if (!settings.ShowCustomAmount)
return NotFound();
price = amount;
title = settings.Title;
}
var store = await GetStore(app);
var invoice = await _InvoiceController.CreateInvoiceCore(new NBitpayClient.Invoice()
{
ItemDesc = title,
Currency = settings.Currency,
Price = price,
BuyerEmail = email,
OrderId = orderId,
NotificationURL = notificationUrl,
RedirectURL = redirectUrl,
FullNotifications = true
}, store, HttpContext.Request.GetAbsoluteRoot());
return Redirect(invoice.Data.Url);
}

@ -102,9 +102,9 @@ namespace BTCPayServer.Controllers
StatusMessage = "Error: You are not owner of this store";
return RedirectToAction(nameof(ListApps));
}
var id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(32));
using (var ctx = _ContextFactory.CreateContext())
{
var id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(32));
var appData = new AppData() { Id = id };
appData.StoreDataId = selectedStore;
appData.Name = vm.Name;
@ -113,6 +113,9 @@ namespace BTCPayServer.Controllers
await ctx.SaveChangesAsync();
}
StatusMessage = "App successfully created";
if (appType == AppType.PointOfSale)
return RedirectToAction(nameof(UpdatePointOfSale), new { appId = id });
return RedirectToAction(nameof(ListApps));
}
@ -176,24 +179,19 @@ namespace BTCPayServer.Controllers
using (var ctx = _ContextFactory.CreateContext())
{
return await ctx.UserStore
.Where(us => us.ApplicationUserId == userId)
.Select(us => new
{
IsOwner = us.Role == StoreRoles.Owner,
StoreId = us.StoreDataId,
StoreName = us.StoreData.StoreName,
Apps = us.StoreData.Apps
})
.SelectMany(us => us.Apps.Select(app => new ListAppsViewModel.ListAppViewModel()
{
IsOwner = us.IsOwner,
AppName = app.Name,
AppType = app.AppType,
Id = app.Id,
StoreId = us.StoreId,
StoreName = us.StoreName
}))
.ToArrayAsync();
.Where(us => us.ApplicationUserId == userId)
.Join(ctx.Apps, us => us.StoreDataId, app => app.StoreDataId,
(us, app) =>
new ListAppsViewModel.ListAppViewModel()
{
IsOwner = us.Role == StoreRoles.Owner,
StoreId = us.StoreDataId,
StoreName = us.StoreData.StoreName,
AppName = app.Name,
AppType = app.AppType,
Id = app.Id
})
.ToArrayAsync();
}
}

@ -13,11 +13,14 @@ using BTCPayServer.Data;
using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Cors;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using BTCPayServer.Security;
namespace BTCPayServer.Controllers
{
[EnableCors("BitpayAPI")]
[BitpayAPIConstraint]
[Authorize(Policies.CanUseStore.Key, AuthenticationSchemes = Policies.BitpayAuthentication)]
public class InvoiceControllerAPI : Controller
{
private InvoiceController _InvoiceController;
@ -43,9 +46,10 @@ namespace BTCPayServer.Controllers
[HttpGet]
[Route("invoices/{id}")]
[AllowAnonymous]
public async Task<DataWrapper<InvoiceResponse>> GetInvoice(string id, string token)
{
var invoice = await _InvoiceRepository.GetInvoice(HttpContext.GetStoreData().Id, id);
var invoice = await _InvoiceRepository.GetInvoice(null, id);
if (invoice == null)
throw new BitpayHttpException(404, "Object not found");
var resp = invoice.EntityToDTO(_NetworkProvider);

@ -78,7 +78,7 @@ namespace BTCPayServer.Controllers
var wallet = _WalletProvider.GetWallet(network);
if (wallet == null)
return NotFound();
var payment = PaymentMessage.Load(Request.Body);
var payment = PaymentMessage.Load(Request.Body, network.NBitcoinNetwork);
var unused = wallet.BroadcastTransactionsAsync(payment.Transactions);
await _InvoiceRepository.AddRefundsAsync(invoiceId, payment.RefundTo.Select(p => new TxOut(p.Amount, p.Script)).ToArray(), network.NBitcoinNetwork);
return new PaymentAckActionResult(payment.CreateACK(invoiceId + " is currently processing, thanks for your purchase..."));

@ -51,14 +51,17 @@ 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",
TransactionSpeed = invoice.SpeedPolicy == SpeedPolicy.HighSpeed ? "high" :
invoice.SpeedPolicy == SpeedPolicy.MediumSpeed ? "medium" :
invoice.SpeedPolicy == SpeedPolicy.LowMediumSpeed ? "low-medium" :
"low",
RefundEmail = invoice.RefundMail,
CreatedDate = invoice.InvoiceTime,
ExpirationDate = invoice.ExpirationTime,
MonitoringDate = invoice.MonitoringExpiration,
OrderId = invoice.OrderId,
BuyerInformation = invoice.BuyerInformation,
Fiat = FormatCurrency((decimal)dto.Price, dto.Currency),
Fiat = FormatCurrency((decimal)dto.Price, dto.Currency, _CurrencyNameTable),
NotificationUrl = invoice.NotificationURL,
RedirectUrl = invoice.RedirectURL,
ProductInformation = invoice.ProductInformation,
@ -82,7 +85,7 @@ namespace BTCPayServer.Controllers
{
cryptoPayment.Address = onchainMethod.DepositAddress;
}
cryptoPayment.Rate = FormatCurrency(data);
cryptoPayment.Rate = ExchangeRate(data);
cryptoPayment.PaymentUrl = cryptoInfo.PaymentUrls.BIP21;
model.CryptoPayments.Add(cryptoPayment);
}
@ -172,6 +175,7 @@ namespace BTCPayServer.Controllers
[Route("invoice")]
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest", false)]
[XFrameOptionsAttribute(null)]
[ReferrerPolicyAttribute("origin")]
public async Task<IActionResult> Checkout(string invoiceId, string id = null, string paymentMethodId = null)
{
//Keep compatibility with Bitpay
@ -183,6 +187,20 @@ namespace BTCPayServer.Controllers
if (model == null)
return NotFound();
_CSP.Add(new ConsentSecurityPolicy("script-src", "'unsafe-eval'")); // Needed by Vue
if (!string.IsNullOrEmpty(model.CustomCSSLink) &&
Uri.TryCreate(model.CustomCSSLink, UriKind.Absolute, out var uri))
{
_CSP.Clear();
}
if (!string.IsNullOrEmpty(model.CustomLogoLink) &&
Uri.TryCreate(model.CustomLogoLink, UriKind.Absolute, out uri))
{
_CSP.Clear();
}
return View(nameof(Checkout), model);
}
@ -201,6 +219,12 @@ namespace BTCPayServer.Controllers
var paymentMethodId = PaymentMethodId.Parse(paymentMethodIdStr);
var network = _NetworkProvider.GetNetwork(paymentMethodId.CryptoCode);
if (network == null && isDefaultCrypto)
{
network = _NetworkProvider.GetAll().FirstOrDefault();
paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
paymentMethodIdStr = paymentMethodId.ToString();
}
if (invoice == null || network == null)
return null;
if (!invoice.Support(paymentMethodId))
@ -210,6 +234,7 @@ namespace BTCPayServer.Controllers
var paymentMethodTemp = invoice.GetPaymentMethods(_NetworkProvider).First();
network = paymentMethodTemp.Network;
paymentMethodId = paymentMethodTemp.GetId();
paymentMethodIdStr = paymentMethodId.ToString();
}
var paymentMethod = invoice.GetPaymentMethod(paymentMethodId, _NetworkProvider);
@ -223,6 +248,8 @@ namespace BTCPayServer.Controllers
{
CryptoCode = network.CryptoCode,
PaymentMethodId = paymentMethodId.ToString(),
PaymentMethodName = GetDisplayName(paymentMethodId, network),
CryptoImage = GetImage(paymentMethodId, network),
IsLightning = paymentMethodId.PaymentType == PaymentTypes.LightningLike,
ServerUrl = HttpContext.Request.GetAbsoluteRoot(),
OrderId = invoice.OrderId,
@ -232,15 +259,16 @@ namespace BTCPayServer.Controllers
CustomCSSLink = storeBlob.CustomCSS?.AbsoluteUri,
CustomLogoLink = storeBlob.CustomLogo?.AbsoluteUri,
BtcAddress = paymentMethodDetails.GetPaymentDestination(),
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ToString(),
BtcDue = accounting.Due.ToString(),
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ToString(),
OrderAmountFiat = OrderAmountFromInvoice(network.CryptoCode, invoice.ProductInformation),
CustomerEmail = invoice.RefundMail,
RequiresRefundEmail = storeBlob.RequiresRefundEmail,
ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds),
MaxTimeSeconds = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalSeconds,
MaxTimeMinutes = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalMinutes,
ItemDesc = invoice.ProductInformation.ItemDesc,
Rate = FormatCurrency(paymentMethod),
Rate = ExchangeRate(paymentMethod),
MerchantRefLink = invoice.RedirectURL ?? "/",
StoreName = store.StoreName,
InvoiceBitcoinUrl = paymentMethodId.PaymentType == PaymentTypes.BTCLike ? cryptoInfo.PaymentUrls.BIP21 :
@ -253,7 +281,6 @@ namespace BTCPayServer.Controllers
TxCount = accounting.TxRequired,
BtcPaid = accounting.Paid.ToString(),
Status = invoice.Status,
CryptoImage = "/" + GetImage(paymentMethodId, network),
NetworkFee = paymentMethodDetails.GetTxFee(),
IsMultiCurrency = invoice.GetPayments().Select(p => p.GetPaymentMethodId()).Concat(new[] { paymentMethod.GetId() }).Distinct().Count() > 1,
AllowCoinConversion = storeBlob.AllowCoinConversion,
@ -262,10 +289,14 @@ namespace BTCPayServer.Controllers
.Select(kv => new PaymentModel.AvailableCrypto()
{
PaymentMethodId = kv.GetId().ToString(),
CryptoImage = "/" + GetImage(kv.GetId(), kv.Network),
CryptoCode = kv.GetId().CryptoCode,
PaymentMethodName = GetDisplayName(kv.GetId(), kv.Network),
IsLightning = kv.GetId().PaymentType == PaymentTypes.LightningLike,
CryptoImage = GetImage(kv.GetId(), kv.Network),
Link = Url.Action(nameof(Checkout), new { invoiceId = invoiceId, paymentMethodId = kv.GetId().ToString() })
}).Where(c => c.CryptoImage != "/")
.ToList()
.OrderByDescending(a => a.CryptoCode == "BTC").ThenBy(a => a.PaymentMethodName).ThenBy(a => a.IsLightning ? 1 : 0)
.ToList()
};
var expiration = TimeSpan.FromSeconds(model.ExpirationSeconds);
@ -273,19 +304,58 @@ namespace BTCPayServer.Controllers
return model;
}
private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network)
private string GetDisplayName(PaymentMethodId paymentMethodId, BTCPayNetwork network)
{
return (paymentMethodId.PaymentType == PaymentTypes.BTCLike ? Url.Content(network.CryptoImagePath) : Url.Content(network.LightningImagePath));
return paymentMethodId.PaymentType == PaymentTypes.BTCLike ?
network.DisplayName : network.DisplayName + " (via Lightning)";
}
private string FormatCurrency(PaymentMethod paymentMethod)
private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network)
{
var res = paymentMethodId.PaymentType == PaymentTypes.BTCLike ?
Url.Content(network.CryptoImagePath) : Url.Content(network.LightningImagePath);
return "/" + res;
}
private string OrderAmountFromInvoice(string cryptoCode, ProductInformation productInformation)
{
// if invoice source currency is the same as currently display currency, no need for "order amount from invoice"
if (cryptoCode == productInformation.Currency)
return null;
return FormatCurrency(productInformation.Price, productInformation.Currency, _CurrencyNameTable);
}
private string ExchangeRate(PaymentMethod paymentMethod)
{
string currency = paymentMethod.ParentEntity.ProductInformation.Currency;
return FormatCurrency(paymentMethod.Rate, currency);
return FormatCurrency(paymentMethod.Rate, currency, _CurrencyNameTable);
}
public string FormatCurrency(decimal price, string currency)
public static string FormatCurrency(decimal price, string currency, CurrencyNameTable currencies)
{
return price.ToString("C", _CurrencyNameTable.GetCurrencyProvider(currency)) + $" ({currency})";
var provider = currencies.GetNumberFormatInfo(currency, true);
var currencyData = currencies.GetCurrencyData(currency, true);
var divisibility = currencyData.Divisibility;
while (true)
{
var rounded = decimal.Round(price, divisibility, MidpointRounding.AwayFromZero);
if ((Math.Abs(rounded - price) / price) < 0.001m)
{
price = rounded;
break;
}
divisibility++;
}
if (divisibility != provider.CurrencyDecimalDigits)
{
provider = (NumberFormatInfo)provider.Clone();
provider.CurrencyDecimalDigits = divisibility;
}
if (currencyData.Crypto)
return price.ToString("C", provider);
else
return price.ToString("C", provider) + $" ({currency})";
}
[HttpGet]
@ -314,7 +384,7 @@ namespace BTCPayServer.Controllers
{
leases.Add(_EventAggregator.Subscribe<Events.InvoiceDataChangedEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
leases.Add(_EventAggregator.Subscribe<Events.InvoiceNewAddressEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
leases.Add(_EventAggregator.Subscribe<Events.InvoiceEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
leases.Add(_EventAggregator.Subscribe<Events.InvoiceEvent>(async o => await NotifySocket(webSocket, o.Invoice.Id, invoiceId)));
while (true)
{
var message = await webSocket.ReceiveAsync(DummyBuffer, default(CancellationToken));
@ -370,6 +440,9 @@ namespace BTCPayServer.Controllers
Count = count,
Skip = skip,
UserId = GetUserId(),
Unusual = !filterString.Filters.ContainsKey("unusual") ? null
: !bool.TryParse(filterString.Filters["unusual"].First(), out var r) ? (bool?)null
: r,
Status = filterString.Filters.ContainsKey("status") ? filterString.Filters["status"].ToArray() : null,
ExceptionStatus = filterString.Filters.ContainsKey("exceptionstatus") ? filterString.Filters["exceptionstatus"].ToArray() : null,
StoreId = filterString.Filters.ContainsKey("storeid") ? filterString.Filters["storeid"].ToArray() : null
@ -380,7 +453,7 @@ namespace BTCPayServer.Controllers
{
Status = invoice.Status + (invoice.ExceptionStatus == null ? string.Empty : $" ({invoice.ExceptionStatus})"),
ShowCheckout = invoice.Status == "new",
Date = (DateTimeOffset.UtcNow - invoice.InvoiceTime).Prettify() + " ago",
Date = invoice.InvoiceTime,
InvoiceId = invoice.Id,
OrderId = invoice.OrderId ?? string.Empty,
RedirectUrl = invoice.RedirectURL ?? string.Empty,
@ -417,7 +490,7 @@ namespace BTCPayServer.Controllers
var stores = await _StoreRepository.GetStoresByUserId(GetUserId());
model.Stores = new SelectList(stores, nameof(StoreData.Id), nameof(StoreData.StoreName), model.StoreId);
var store = stores.FirstOrDefault(s => s.Id == model.StoreId);
if(store == null)
if (store == null)
{
ModelState.AddModelError(nameof(model.StoreId), "Store not found");
}
@ -490,8 +563,11 @@ namespace BTCPayServer.Controllers
[BitpayAPIConstraint(false)]
public async Task<IActionResult> InvalidatePaidInvoice(string invoiceId)
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
if (invoice == null)
return NotFound();
await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoiceId);
_EventAggregator.Publish(new InvoiceEvent(invoiceId, 1008, "invoice_markedInvalid"));
_EventAggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1008, "invoice_markedInvalid"));
return RedirectToAction(nameof(ListInvoices));
}

@ -41,12 +41,14 @@ using NBXplorer;
using BTCPayServer.HostedServices;
using BTCPayServer.Payments;
using BTCPayServer.Rating;
using BTCPayServer.Security;
namespace BTCPayServer.Controllers
{
public partial class InvoiceController : Controller
{
InvoiceRepository _InvoiceRepository;
ContentSecurityPolicies _CSP;
BTCPayRateProviderFactory _RateProvider;
StoreRepository _StoreRepository;
UserManager<ApplicationUser> _UserManager;
@ -64,6 +66,7 @@ namespace BTCPayServer.Controllers
StoreRepository storeRepository,
EventAggregator eventAggregator,
BTCPayWalletProvider walletProvider,
ContentSecurityPolicies csp,
BTCPayNetworkProvider networkProvider)
{
_ServiceProvider = serviceProvider;
@ -75,6 +78,7 @@ namespace BTCPayServer.Controllers
_EventAggregator = eventAggregator;
_NetworkProvider = networkProvider;
_WalletProvider = walletProvider;
_CSP = csp;
}
@ -109,10 +113,12 @@ namespace BTCPayServer.Controllers
}
entity.ProductInformation = Map<Invoice, ProductInformation>(invoice);
entity.RedirectURL = invoice.RedirectURL ?? store.StoreWebsite;
if (!Uri.IsWellFormedUriString(entity.RedirectURL, UriKind.Absolute))
entity.RedirectURL = null;
entity.Status = "new";
entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy);
HashSet<CurrencyPair> currencyPairsToFetch = new HashSet<CurrencyPair>();
var rules = storeBlob.GetRateRules(_NetworkProvider);
@ -141,32 +147,26 @@ namespace BTCPayServer.Controllers
PaymentMethod: CreatePaymentMethodAsync(fetchingByCurrencyPair, o.Handler, o.SupportedPaymentMethod, o.Network, entity, store)))
.ToList();
List<string> paymentMethodErrors = new List<string>();
List<string> invoiceLogs = new List<string>();
List<ISupportedPaymentMethod> supported = new List<ISupportedPaymentMethod>();
var paymentMethods = new PaymentMethodDictionary();
foreach(var pair in fetchingByCurrencyPair)
foreach (var pair in fetchingByCurrencyPair)
{
var rateResult = await pair.Value;
bool hasError = false;
if(rateResult.Errors.Count != 0)
invoiceLogs.Add($"{pair.Key}: The rating rule is {rateResult.Rule}");
invoiceLogs.Add($"{pair.Key}: The evaluated rating rule is {rateResult.EvaluatedRule}");
if (rateResult.Errors.Count != 0)
{
var allRateRuleErrors = string.Join(", ", rateResult.Errors.ToArray());
paymentMethodErrors.Add($"{pair.Key}: Rate rule error ({allRateRuleErrors})");
hasError = true;
invoiceLogs.Add($"{pair.Key}: Rate rule error ({allRateRuleErrors})");
}
if(rateResult.ExchangeExceptions.Count != 0)
if (rateResult.ExchangeExceptions.Count != 0)
{
foreach(var ex in rateResult.ExchangeExceptions)
foreach (var ex in rateResult.ExchangeExceptions)
{
paymentMethodErrors.Add($"{pair.Key}: Exception reaching exchange {ex.ExchangeName} ({ex.Exception.Message})");
invoiceLogs.Add($"{pair.Key}: Exception reaching exchange {ex.ExchangeName} ({ex.Exception.Message})");
}
hasError = true;
}
if(hasError)
{
paymentMethodErrors.Add($"{pair.Key}: The rule is {rateResult.Rule}");
paymentMethodErrors.Add($"{pair.Key}: Evaluated rule is {rateResult.EvaluatedRule}");
}
}
@ -182,11 +182,11 @@ namespace BTCPayServer.Controllers
}
catch (PaymentMethodUnavailableException ex)
{
paymentMethodErrors.Add($"{o.SupportedPaymentMethod.PaymentId.CryptoCode} ({o.SupportedPaymentMethod.PaymentId.PaymentType}): Payment method unavailable ({ex.Message})");
invoiceLogs.Add($"{o.SupportedPaymentMethod.PaymentId.CryptoCode} ({o.SupportedPaymentMethod.PaymentId.PaymentType}): Payment method unavailable ({ex.Message})");
}
catch (Exception ex)
{
paymentMethodErrors.Add($"{o.SupportedPaymentMethod.PaymentId.CryptoCode} ({o.SupportedPaymentMethod.PaymentId.PaymentType}): Unexpected exception ({ex.ToString()})");
invoiceLogs.Add($"{o.SupportedPaymentMethod.PaymentId.CryptoCode} ({o.SupportedPaymentMethod.PaymentId.PaymentType}): Unexpected exception ({ex.ToString()})");
}
}
@ -194,7 +194,7 @@ namespace BTCPayServer.Controllers
{
StringBuilder errors = new StringBuilder();
errors.AppendLine("No payment method available for this store");
foreach (var error in paymentMethodErrors)
foreach (var error in invoiceLogs)
{
errors.AppendLine(error);
}
@ -204,9 +204,9 @@ namespace BTCPayServer.Controllers
entity.SetSupportedPaymentMethods(supported);
entity.SetPaymentMethods(paymentMethods);
entity.PosData = invoice.PosData;
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, paymentMethodErrors, _NetworkProvider);
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, invoiceLogs, _NetworkProvider);
_EventAggregator.Publish(new Events.InvoiceEvent(entity, 1001, "invoice_created"));
_EventAggregator.Publish(new Events.InvoiceEvent(entity.EntityToDTO(_NetworkProvider), 1001, "invoice_created"));
var resp = entity.EntityToDTO(_NetworkProvider);
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
}
@ -277,6 +277,7 @@ namespace BTCPayServer.Controllers
return defaultPolicy;
var mappings = new Dictionary<string, SpeedPolicy>();
mappings.Add("low", SpeedPolicy.LowSpeed);
mappings.Add("low-medium", SpeedPolicy.LowMediumSpeed);
mappings.Add("medium", SpeedPolicy.MediumSpeed);
mappings.Add("high", SpeedPolicy.HighSpeed);
if (!mappings.TryGetValue(transactionSpeed, out SpeedPolicy policy))

@ -36,11 +36,12 @@ namespace BTCPayServer.Controllers
[BitpayAPIConstraint]
public async Task<IActionResult> GetRates(string currencyPairs, string storeId)
{
storeId = storeId ?? this.HttpContext.GetStoreData()?.Id;
var result = await GetRates2(currencyPairs, storeId);
var rates = (result as JsonResult)?.Value as NBitpayClient.Rate[];
var rates = (result as JsonResult)?.Value as Rate[];
if (rates == null)
return result;
return Json(new DataWrapper<NBitpayClient.Rate[]>(rates));
return Json(new DataWrapper<Rate[]>(rates));
}
[Route("api/rates")]
@ -54,8 +55,9 @@ namespace BTCPayServer.Controllers
return result;
}
var store = await _StoreRepo.FindStore(storeId);
var store = this.HttpContext.GetStoreData();
if(store == null || store.Id != storeId)
store = await _StoreRepo.FindStore(storeId);
if (store == null)
{
var result = Json(new BitpayErrorsModel() { Error = "Store not found" });
@ -86,7 +88,8 @@ namespace BTCPayServer.Controllers
{
CryptoCode = r.Pair.Left,
Code = r.Pair.Right,
Name = _CurrencyNameTable.GetCurrencyData(r.Pair.Right)?.Name,
CurrencyPair = r.Pair.ToString(),
Name = _CurrencyNameTable.GetCurrencyData(r.Pair.Right, true).Name,
Value = r.Value.Value
}).Where(n => n.Name != null).ToArray());
}
@ -106,6 +109,14 @@ namespace BTCPayServer.Controllers
get;
set;
}
[JsonProperty(PropertyName = "currencyPair")]
public string CurrencyPair
{
get;
set;
}
[JsonProperty(PropertyName = "code")]
public string Code
{

@ -1,13 +1,19 @@
using BTCPayServer.HostedServices;
using BTCPayServer.Configuration;
using Microsoft.Extensions.Logging;
using BTCPayServer.HostedServices;
using BTCPayServer.Models;
using BTCPayServer.Models.ServerViewModels;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services;
using BTCPayServer.Services.Mails;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Validations;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBitcoin.DataEncoders;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
@ -25,14 +31,23 @@ namespace BTCPayServer.Controllers
private UserManager<ApplicationUser> _UserManager;
SettingsRepository _SettingsRepository;
private BTCPayRateProviderFactory _RateProviderFactory;
private StoreRepository _StoreRepository;
LightningConfigurationProvider _LnConfigProvider;
BTCPayServerOptions _Options;
public ServerController(UserManager<ApplicationUser> userManager,
Configuration.BTCPayServerOptions options,
BTCPayRateProviderFactory rateProviderFactory,
SettingsRepository settingsRepository)
SettingsRepository settingsRepository,
LightningConfigurationProvider lnConfigProvider,
Services.Stores.StoreRepository storeRepository)
{
_Options = options;
_UserManager = userManager;
_SettingsRepository = settingsRepository;
_RateProviderFactory = rateProviderFactory;
_StoreRepository = storeRepository;
_LnConfigProvider = lnConfigProvider;
}
[Route("server/rates")]
@ -74,7 +89,7 @@ namespace BTCPayServer.Controllers
try
{
var service = GetCoinaverageService(vm, true);
if(service != null)
if (service != null)
await service.TestAuthAsync();
}
catch
@ -188,6 +203,7 @@ namespace BTCPayServer.Controllers
if (user == null)
return NotFound();
await _UserManager.DeleteAsync(user);
await _StoreRepository.CleanUnreachableStores();
StatusMessage = "User deleted";
return RedirectToAction(nameof(ListUsers));
}
@ -220,6 +236,122 @@ namespace BTCPayServer.Controllers
return View(settings);
}
[Route("server/services")]
public IActionResult Services()
{
var result = new ServicesViewModel();
foreach (var cryptoCode in _Options.ExternalServicesByCryptoCode.Keys)
{
{
int i = 0;
foreach (var grpcService in _Options.ExternalServicesByCryptoCode.GetServices<ExternalLNDGRPC>(cryptoCode))
{
result.LNDServices.Add(new ServicesViewModel.LNDServiceViewModel()
{
Crypto = cryptoCode,
Type = "gRPC",
Index = i++,
});
}
}
}
return View(result);
}
[Route("server/services/lnd-grpc/{cryptoCode}/{index}")]
public IActionResult LNDGRPCServices(string cryptoCode, int index, uint? nonce)
{
var external = GetExternalLNDConnectionString(cryptoCode, index);
if (external == null)
return NotFound();
var model = new LNDGRPCServicesViewModel();
model.Host = $"{external.BaseUri.DnsSafeHost}:{external.BaseUri.Port}";
model.SSL = external.BaseUri.Scheme == "https";
if (external.CertificateThumbprint != null)
{
model.CertificateThumbprint = Encoders.Hex.EncodeData(external.CertificateThumbprint);
}
if (external.Macaroon != null)
{
model.Macaroon = Encoders.Hex.EncodeData(external.Macaroon);
}
if (nonce != null)
{
var configKey = GetConfigKey("lnd-grpc", cryptoCode, index, nonce.Value);
var lnConfig = _LnConfigProvider.GetConfig(configKey);
if (lnConfig != null)
{
model.QRCodeLink = $"{this.Request.GetAbsoluteRoot().WithTrailingSlash()}lnd-config/{configKey}/lnd.config";
model.QRCode = $"config={model.QRCodeLink}";
}
}
return View(model);
}
private static uint GetConfigKey(string type, string cryptoCode, int index, uint nonce)
{
return (uint)HashCode.Combine(type, cryptoCode, index, nonce);
}
[Route("lnd-config/{configKey}/lnd.config")]
[AllowAnonymous]
public IActionResult GetLNDConfig(uint configKey)
{
var conf = _LnConfigProvider.GetConfig(configKey);
if (conf == null)
return NotFound();
return Json(conf);
}
[Route("server/services/lnd-grpc/{cryptoCode}/{index}")]
[HttpPost]
public IActionResult LNDGRPCServicesPOST(string cryptoCode, int index)
{
var external = GetExternalLNDConnectionString(cryptoCode, index);
if (external == null)
return NotFound();
LightningConfigurations confs = new LightningConfigurations();
LightningConfiguration conf = new LightningConfiguration();
conf.Type = "grpc";
conf.CryptoCode = cryptoCode;
conf.Host = external.BaseUri.DnsSafeHost;
conf.Port = external.BaseUri.Port;
conf.SSL = external.BaseUri.Scheme == "https";
conf.Macaroon = external.Macaroon == null ? null : Encoders.Hex.EncodeData(external.Macaroon);
conf.CertificateThumbprint = external.CertificateThumbprint == null ? null : Encoders.Hex.EncodeData(external.CertificateThumbprint);
confs.Configurations.Add(conf);
var nonce = RandomUtils.GetUInt32();
var configKey = GetConfigKey("lnd-grpc", cryptoCode, index, nonce);
_LnConfigProvider.KeepConfig(configKey, confs);
return RedirectToAction(nameof(LNDGRPCServices), new { cryptoCode = cryptoCode, nonce = nonce });
}
private LightningConnectionString GetExternalLNDConnectionString(string cryptoCode, int index)
{
var connectionString = _Options.ExternalServicesByCryptoCode.GetServices<ExternalLNDGRPC>(cryptoCode).Skip(index).Select(c => c.ConnectionString).FirstOrDefault();
if (connectionString == null)
return null;
connectionString = connectionString.Clone();
if(connectionString.MacaroonFilePath != null)
{
try
{
connectionString.Macaroon = System.IO.File.ReadAllBytes(connectionString.MacaroonFilePath);
connectionString.MacaroonFilePath = null;
}
catch
{
Logging.Logs.Configuration.LogWarning($"{cryptoCode}: The macaroon file path of the external LND grpc config was not found ({connectionString.MacaroonFilePath})");
return null;
}
}
return connectionString;
}
[Route("server/theme")]
public async Task<IActionResult> Theme()
{
@ -243,7 +375,7 @@ namespace BTCPayServer.Controllers
{
try
{
if(!model.Settings.IsComplete())
if (!model.Settings.IsComplete())
{
model.StatusMessage = "Error: Required fields missing";
return View(model);

@ -10,6 +10,7 @@ using BTCPayServer.Data;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Services;
using LedgerWallet;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBXplorer.DerivationStrategy;
@ -194,139 +195,221 @@ namespace BTCPayServer.Controllers
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
var hw = new HardwareWalletService(webSocket);
object result = null;
try
using (var normalOperationTimeout = new CancellationTokenSource())
using (var signTimeout = new CancellationTokenSource())
{
BTCPayNetwork network = null;
if (cryptoCode != null)
normalOperationTimeout.CancelAfter(TimeSpan.FromMinutes(30));
var hw = new HardwareWalletService(webSocket);
object result = null;
try
{
network = _NetworkProvider.GetNetwork(cryptoCode);
if (network == null)
throw new FormatException("Invalid value for crypto code");
}
BitcoinAddress destinationAddress = null;
if (destination != null)
{
try
BTCPayNetwork network = null;
if (cryptoCode != null)
{
destinationAddress = BitcoinAddress.Create(destination, network.NBitcoinNetwork);
}
catch { }
if (destinationAddress == null)
throw new FormatException("Invalid value for destination");
}
FeeRate feeRateValue = null;
if (feeRate != null)
{
try
{
feeRateValue = new FeeRate(Money.Satoshis(int.Parse(feeRate, CultureInfo.InvariantCulture)), 1);
}
catch { }
if (feeRateValue == null || feeRateValue.FeePerK <= Money.Zero)
throw new FormatException("Invalid value for fee rate");
}
Money amountBTC = null;
if (amount != null)
{
try
{
amountBTC = Money.Parse(amount);
}
catch { }
if (amountBTC == null || amountBTC <= Money.Zero)
throw new FormatException("Invalid value for amount");
}
bool subsctractFeesValue = false;
if (substractFees != null)
{
try
{
subsctractFeesValue = bool.Parse(substractFees);
}
catch { throw new FormatException("Invalid value for subtract fees"); }
}
if (command == "test")
{
result = await hw.Test();
}
if (command == "getxpub")
{
var getxpubResult = await hw.GetExtPubKey(network, account);
result = getxpubResult;
}
if (command == "getinfo")
{
var strategy = GetDirectDerivationStrategy(store, network);
var strategyBase = GetDerivationStrategy(store, network);
if (strategy == null || !await hw.SupportDerivation(network, strategy))
{
throw new Exception($"This store is not configured to use this ledger");
network = _NetworkProvider.GetNetwork(cryptoCode);
if (network == null)
throw new FormatException("Invalid value for crypto code");
}
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
BitcoinAddress destinationAddress = null;
if (destination != null)
{
var broadcastResult = await wallet.BroadcastTransactionsAsync(new List<Transaction>() { transaction });
if (!broadcastResult[0].Success)
try
{
throw new Exception($"RPC Error while broadcasting: {broadcastResult[0].RPCCode} {broadcastResult[0].RPCCodeMessage} {broadcastResult[0].RPCMessage}");
destinationAddress = BitcoinAddress.Create(destination, network.NBitcoinNetwork);
}
catch { }
if (destinationAddress == null)
throw new FormatException("Invalid value for destination");
}
catch (Exception ex)
FeeRate feeRateValue = null;
if (feeRate != null)
{
throw new Exception("Error while broadcasting: " + ex.Message);
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");
}
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)
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(normalOperationTimeout.Token);
}
if (command == "getxpub")
{
var getxpubResult = await hw.GetExtPubKey(network, account, normalOperationTimeout.Token);
result = getxpubResult;
}
if (command == "getinfo")
{
var strategy = GetDirectDerivationStrategy(store, network);
var strategyBase = GetDerivationStrategy(store, network);
if (strategy == null || await hw.GetKeyPath(network, strategy, normalOperationTimeout.Token) == null)
{
throw new Exception($"This store is not configured to use this ledger");
}
var feeProvider = _FeeRateProvider.CreateFeeProvider(network);
var recommendedFees = feeProvider.GetFeeRateAsync();
var balance = _WalletProvider.GetWallet(network).GetBalance(strategyBase);
result = new GetInfoResult() { Balance = (double)(await balance).ToDecimal(MoneyUnit.BTC), RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi };
}
if (command == "sendtoaddress")
{
if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
throw new Exception($"{network.CryptoCode}: not started or fully synched");
var strategy = GetDirectDerivationStrategy(store, network);
var strategyBase = GetDerivationStrategy(store, network);
var wallet = _WalletProvider.GetWallet(network);
var change = wallet.GetChangeAddressAsync(strategyBase);
var unspentCoins = await wallet.GetUnspentCoins(strategyBase);
var changeAddress = await change;
var send = new[] { (
destination: destinationAddress as IDestination,
amount: amountBTC,
substractFees: subsctractFeesValue) };
foreach (var element in send)
{
if (element.destination == null)
throw new ArgumentNullException(nameof(element.destination));
if (element.amount == null)
throw new ArgumentNullException(nameof(element.amount));
if (element.amount <= Money.Zero)
throw new ArgumentOutOfRangeException(nameof(element.amount), "The amount should be above zero");
}
var foundKeyPath = await hw.GetKeyPath(network, strategy, normalOperationTimeout.Token);
if (foundKeyPath == null)
{
throw new HardwareWalletException($"This store is not configured to use this ledger");
}
TransactionBuilder builder = new TransactionBuilder();
builder.StandardTransactionPolicy.MinRelayTxFee = summary.Status.BitcoinStatus.MinRelayTxFee;
builder.SetConsensusFactory(network.NBitcoinNetwork);
builder.AddCoins(unspentCoins.Select(c => c.Coin).ToArray());
foreach (var element in send)
{
builder.Send(element.destination, element.amount);
if (element.substractFees)
builder.SubtractFees();
}
builder.SetChange(changeAddress.Item1);
if (network.MinFee == null)
{
builder.SendEstimatedFees(feeRateValue);
}
else
{
var estimatedFee = builder.EstimateFees(feeRateValue);
if (network.MinFee > estimatedFee)
builder.SendFees(network.MinFee);
else
builder.SendEstimatedFees(feeRateValue);
}
builder.Shuffle();
var unsigned = builder.BuildTransaction(false);
var keypaths = new Dictionary<Script, KeyPath>();
foreach (var c in unspentCoins)
{
keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath);
}
var hasChange = unsigned.Outputs.Count == 2;
var usedCoins = builder.FindSpentCoins(unsigned);
Dictionary<uint256, Transaction> parentTransactions = new Dictionary<uint256, Transaction>();
if (!strategy.Segwit)
{
var parentHashes = usedCoins.Select(c => c.Outpoint.Hash).ToHashSet();
var explorer = _ExplorerProvider.GetExplorerClient(network);
var getTransactionAsyncs = parentHashes.Select(h => (Op: explorer.GetTransactionAsync(h), Hash: h)).ToList();
foreach (var getTransactionAsync in getTransactionAsyncs)
{
var tx = (await getTransactionAsync.Op);
if (tx == null)
throw new Exception($"Parent transaction {getTransactionAsync.Hash} not found");
parentTransactions.Add(tx.Transaction.GetHash(), tx.Transaction);
}
}
signTimeout.CancelAfter(TimeSpan.FromMinutes(5));
var transaction = await hw.SignTransactionAsync(usedCoins.Select(c => new SignatureRequest
{
InputTransaction = parentTransactions.TryGet(c.Outpoint.Hash),
InputCoin = c,
KeyPath = foundKeyPath.Derive(keypaths[c.TxOut.ScriptPubKey]),
PubKey = strategy.Root.Derive(keypaths[c.TxOut.ScriptPubKey]).PubKey
}).ToArray(), unsigned, hasChange ? foundKeyPath.Derive(changeAddress.Item2) : null, signTimeout.Token);
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 }; }
finally { hw.Dispose(); }
try
{
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);
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();
}
}
catch { }
finally
{
await webSocket.CloseSocket();
}
return new EmptyResult();
}
@ -336,8 +419,6 @@ namespace BTCPayServer.Controllers
var directStrategy = strategy as DirectDerivationStrategy;
if (directStrategy == null)
directStrategy = (strategy as P2SHDerivationStrategy).Inner as DirectDerivationStrategy;
if (!directStrategy.Segwit)
return null;
return directStrategy;
}

@ -26,16 +26,15 @@ namespace BTCPayServer.Controllers
return NotFound();
LightningNodeViewModel vm = new LightningNodeViewModel();
vm.CryptoCode = cryptoCode;
vm.InternalLightningNode = GetInternalLighningNode(cryptoCode)?.ToUri(true)?.AbsoluteUri;
vm.InternalLightningNode = GetInternalLighningNode(cryptoCode)?.ToString();
SetExistingValues(store, vm);
return View(vm);
}
private void SetExistingValues(StoreData store, LightningNodeViewModel vm)
{
vm.Url = GetExistingLightningSupportedPaymentMethod(vm.CryptoCode, store)?.GetLightningUrl()?.ToString();
vm.ConnectionString = GetExistingLightningSupportedPaymentMethod(vm.CryptoCode, store)?.GetLightningUrl()?.ToString();
}
private LightningSupportedPaymentMethod GetExistingLightningSupportedPaymentMethod(string cryptoCode, StoreData store)
{
var id = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
@ -65,7 +64,7 @@ namespace BTCPayServer.Controllers
var network = vm.CryptoCode == null ? null : _ExplorerProvider.GetNetwork(vm.CryptoCode);
var internalLightning = GetInternalLighningNode(network.CryptoCode);
vm.InternalLightningNode = internalLightning?.ToUri(true)?.AbsoluteUri;
vm.InternalLightningNode = internalLightning?.ToString();
if (network == null)
{
ModelState.AddModelError(nameof(vm.CryptoCode), "Invalid network");
@ -74,33 +73,57 @@ namespace BTCPayServer.Controllers
PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.LightningLike);
Payments.Lightning.LightningSupportedPaymentMethod paymentMethod = null;
if (!string.IsNullOrEmpty(vm.Url))
if (!string.IsNullOrEmpty(vm.ConnectionString))
{
if (!LightningConnectionString.TryParse(vm.Url, out var connectionString, out var error))
if (!LightningConnectionString.TryParse(vm.ConnectionString, false, out var connectionString, out var error))
{
ModelState.AddModelError(nameof(vm.Url), $"Invalid URL ({error})");
ModelState.AddModelError(nameof(vm.ConnectionString), $"Invalid URL ({error})");
return View(vm);
}
var internalDomain = internalLightning?.ToUri(false)?.DnsSafeHost;
bool isLocal = (internalDomain == "127.0.0.1" || internalDomain == "localhost");
if(connectionString.ConnectionType == LightningConnectionType.LndGRPC)
{
ModelState.AddModelError(nameof(vm.ConnectionString), $"BTCPay does not support gRPC connections");
return View(vm);
}
var internalDomain = internalLightning?.BaseUri?.DnsSafeHost;
bool isInternalNode = connectionString.ConnectionType == LightningConnectionType.CLightning ||
connectionString.BaseUri.DnsSafeHost == internalDomain ||
isLocal;
(internalDomain == "127.0.0.1" || internalDomain == "localhost");
if (connectionString.BaseUri.Scheme == "http" && !isLocal)
if (connectionString.BaseUri.Scheme == "http")
{
if (!isInternalNode || (isInternalNode && !CanUseInternalLightning()))
if (!isInternalNode)
{
ModelState.AddModelError(nameof(vm.Url), "The url must be HTTPS");
ModelState.AddModelError(nameof(vm.ConnectionString), "The url must be HTTPS");
return View(vm);
}
}
if(connectionString.MacaroonFilePath != null)
{
if(!CanUseInternalLightning())
{
ModelState.AddModelError(nameof(vm.ConnectionString), "You are not authorized to use macaroonfilepath");
return View(vm);
}
if(!System.IO.File.Exists(connectionString.MacaroonFilePath))
{
ModelState.AddModelError(nameof(vm.ConnectionString), "The macaroonfilepath file does exist");
return View(vm);
}
if(!System.IO.Path.IsPathRooted(connectionString.MacaroonFilePath))
{
ModelState.AddModelError(nameof(vm.ConnectionString), "The macaroonfilepath should be fully rooted");
return View(vm);
}
}
if (isInternalNode && !CanUseInternalLightning())
{
ModelState.AddModelError(nameof(vm.Url), "Unauthorized url");
ModelState.AddModelError(nameof(vm.ConnectionString), "Unauthorized url");
return View(vm);
}
@ -110,6 +133,7 @@ namespace BTCPayServer.Controllers
};
paymentMethod.SetLightningUrl(connectionString);
}
if (command == "save")
{
store.SetSupportedPaymentMethod(paymentMethodId, paymentMethod);
@ -121,7 +145,7 @@ namespace BTCPayServer.Controllers
{
if (paymentMethod == null)
{
ModelState.AddModelError(nameof(vm.Url), "Missing url parameter");
ModelState.AddModelError(nameof(vm.ConnectionString), "Missing url parameter");
return View(vm);
}
var handler = (LightningLikePaymentHandler)_ServiceProvider.GetRequiredService<IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>>();
@ -135,7 +159,7 @@ namespace BTCPayServer.Controllers
await handler.TestConnection(info, cts.Token);
}
}
vm.StatusMessage = $"Connection to the lightning node succeed ({info})";
vm.StatusMessage = $"Connection to the lightning node succeeded ({info})";
}
catch (Exception ex)
{

@ -53,8 +53,7 @@ namespace BTCPayServer.Controllers
ExplorerClientProvider explorerProvider,
IFeeProviderFactory feeRateProvider,
LanguageService langService,
IHostingEnvironment env,
CoinAverageSettings coinAverage)
IHostingEnvironment env)
{
_RateFactory = rateFactory;
_Dashboard = dashboard;
@ -72,9 +71,7 @@ namespace BTCPayServer.Controllers
_ServiceProvider = serviceProvider;
_BtcpayServerOptions = btcpayServerOptions;
_BTCPayEnv = btcpayEnv;
_CoinAverage = coinAverage;
}
CoinAverageSettings _CoinAverage;
NBXplorerDashboard _Dashboard;
BTCPayServerOptions _BtcpayServerOptions;
BTCPayServerEnvironment _BTCPayEnv;
@ -286,7 +283,7 @@ namespace BTCPayServer.Controllers
{
CurrencyPair = fetch.Key.ToString(),
Error = testResult.Errors.Count != 0,
Rule = testResult.Errors.Count == 0 ? testResult.Rule + " = " + testResult.Value.Value.ToString(CultureInfo.InvariantCulture)
Rule = testResult.Errors.Count == 0 ? testResult.Rule + " = " + testResult.Value.Value.ToString(CultureInfo.InvariantCulture)
: testResult.EvaluatedRule
});
}
@ -316,7 +313,7 @@ namespace BTCPayServer.Controllers
Action = "Continue",
Title = "Rate rule scripting",
Description = scripting ?
"This action will mofify your current rate sources. Are you sure to turn on rate rules scripting? (Advanced users)"
"This action will modify your current rate sources. Are you sure to turn on rate rules scripting? (Advanced users)"
: "This action will delete your rate script. Are you sure to turn off rate rules scripting?",
ButtonClass = "btn-primary"
});
@ -427,6 +424,7 @@ namespace BTCPayServer.Controllers
vm.StoreWebsite = store.StoreWebsite;
vm.NetworkFee = !storeBlob.NetworkFeeDisabled;
vm.SpeedPolicy = store.SpeedPolicy;
vm.CanDelete = _Repo.CanDeleteStores();
AddPaymentMethods(store, vm);
vm.MonitoringExpiration = storeBlob.MonitoringExpiration;
vm.InvoiceExpiration = storeBlob.InvoiceExpiration;
@ -471,10 +469,8 @@ namespace BTCPayServer.Controllers
[HttpPost]
[Route("{storeId}")]
public async Task<IActionResult> UpdateStore(StoreViewModel model)
public async Task<IActionResult> UpdateStore(StoreViewModel model, string command = null)
{
AddPaymentMethods(StoreData, model);
bool needUpdate = false;
if (StoreData.SpeedPolicy != model.SpeedPolicy)
{
@ -514,11 +510,34 @@ namespace BTCPayServer.Controllers
{
storeId = StoreData.Id
});
}
[HttpGet]
[Route("{storeId}/delete")]
public IActionResult DeleteStore(string storeId)
{
return View("Confirm", new ConfirmModel()
{
Action = "Delete this store",
Title = "Delete this store",
Description = "This action is irreversible and will remove all information related to this store. (Invoices, Apps etc...)",
ButtonClass = "btn-danger"
});
}
[HttpPost]
[Route("{storeId}/delete")]
public async Task<IActionResult> DeleteStorePost(string storeId)
{
await _Repo.DeleteStore(StoreData.Id);
StatusMessage = "Success: Store successfully deleted";
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
}
private CoinAverageExchange[] GetSupportedExchanges()
{
return _CoinAverage.AvailableExchanges
return _RateFactory.GetSupportedExchanges()
.Select(c => c.Value)
.OrderBy(s => s.Name, StringComparer.OrdinalIgnoreCase)
.ToArray();
@ -646,11 +665,11 @@ namespace BTCPayServer.Controllers
{
var stores = await _Repo.GetStoresByUserId(userId);
model.Stores = new SelectList(stores.Where(s => s.HasClaim(Policies.CanModifyStoreSettings.Key)), nameof(StoreData.Id), nameof(StoreData.StoreName), storeId);
}
if (model.Stores.Count() == 0)
{
StatusMessage = "Error: You need to be owner of at least one store before pairing";
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
if (model.Stores.Count() == 0)
{
StatusMessage = "Error: You need to be owner of at least one store before pairing";
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
}
}
return View(model);
}

@ -35,21 +35,7 @@ namespace BTCPayServer.Controllers
_NetworkProvider = networkProvider;
_UserManager = userManager;
_WalletProvider = walletProvider;
}
[HttpGet]
[Route("{storeId}/delete")]
public IActionResult DeleteStore(string storeId)
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
return View("Confirm", new ConfirmModel()
{
Title = "Delete store " + store.StoreName,
Description = "This store will still be accessible to users sharing it",
Action = "Delete"
});
}
}
[HttpGet]
[Route("create")]
@ -63,8 +49,23 @@ namespace BTCPayServer.Controllers
get; set;
}
[HttpGet]
[Route("{storeId}/me/delete")]
public IActionResult DeleteStore(string storeId)
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
return View("Confirm", new ConfirmModel()
{
Title = "Delete store " + store.StoreName,
Description = "This store will still be accessible to users sharing it",
Action = "Delete"
});
}
[HttpPost]
[Route("{storeId}/delete")]
[Route("{storeId}/me/delete")]
public async Task<IActionResult> DeleteStorePost(string storeId)
{
var userId = GetUserId();
@ -121,7 +122,10 @@ namespace BTCPayServer.Controllers
var store = await _Repo.CreateStore(GetUserId(), vm.Name);
CreatedStoreId = store.Id;
StatusMessage = "Store successfully created";
return RedirectToAction(nameof(ListStores));
return RedirectToAction(nameof(StoresController.UpdateStore), "Stores", new
{
storeId = store.Id
});
}
private static async Task<string> GetBalanceString((BTCPayWallet Wallet, DerivationStrategyBase DerivationStrategy) _)

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

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

@ -102,14 +102,27 @@ namespace BTCPayServer.Data
{
base.OnModelCreating(builder);
builder.Entity<InvoiceData>()
.HasIndex(o => o.StoreDataId);
.HasOne(o => o.StoreData)
.WithMany(a => a.Invoices).OnDelete(DeleteBehavior.Cascade);
builder.Entity<InvoiceData>().HasIndex(o => o.StoreDataId);
builder.Entity<PaymentData>()
.HasIndex(o => o.InvoiceDataId);
.HasOne(o => o.InvoiceData)
.WithMany(i => i.Payments).OnDelete(DeleteBehavior.Cascade);
builder.Entity<PaymentData>()
.HasIndex(o => o.InvoiceDataId);
builder.Entity<RefundAddressesData>()
.HasOne(o => o.InvoiceData)
.WithMany(i => i.RefundAddresses).OnDelete(DeleteBehavior.Cascade);
builder.Entity<RefundAddressesData>()
.HasIndex(o => o.InvoiceDataId);
builder.Entity<UserStore>()
.HasOne(o => o.StoreData)
.WithMany(i => i.UserStores).OnDelete(DeleteBehavior.Cascade);
builder.Entity<UserStore>()
.HasKey(t => new
{
@ -117,9 +130,16 @@ namespace BTCPayServer.Data
t.StoreDataId
});
builder.Entity<APIKeyData>()
.HasOne(o => o.StoreData)
.WithMany(i => i.APIKeys)
.HasForeignKey(i => i.StoreId).OnDelete(DeleteBehavior.Cascade);
builder.Entity<APIKeyData>()
.HasIndex(o => o.StoreId);
builder.Entity<AppData>()
.HasOne(o => o.StoreData)
.WithMany(i => i.Apps).OnDelete(DeleteBehavior.Cascade);
builder.Entity<AppData>()
.HasOne(a => a.StoreData);
@ -133,6 +153,10 @@ namespace BTCPayServer.Data
.WithMany(t => t.UserStores)
.HasForeignKey(pt => pt.StoreDataId);
builder.Entity<AddressInvoiceData>()
.HasOne(o => o.InvoiceData)
.WithMany(i => i.AddressInvoices).OnDelete(DeleteBehavior.Cascade);
builder.Entity<AddressInvoiceData>()
#pragma warning disable CS0618
.HasKey(o => o.Address);
@ -141,12 +165,24 @@ namespace BTCPayServer.Data
builder.Entity<PairingCodeData>()
.HasKey(o => o.Id);
builder.Entity<PendingInvoiceData>()
.HasOne(o => o.InvoiceData)
.WithMany(o => o.PendingInvoices)
.HasForeignKey(o => o.Id).OnDelete(DeleteBehavior.Cascade);
builder.Entity<PairedSINData>()
.HasOne(o => o.StoreData)
.WithMany(i => i.PairedSINs).OnDelete(DeleteBehavior.Cascade);
builder.Entity<PairedSINData>(b =>
{
b.HasIndex(o => o.SIN);
b.HasIndex(o => o.StoreDataId);
});
builder.Entity<HistoricalAddressInvoiceData>()
.HasOne(o => o.InvoiceData)
.WithMany(i => i.HistoricalAddressInvoices).OnDelete(DeleteBehavior.Cascade);
builder.Entity<HistoricalAddressInvoiceData>()
.HasKey(o => new
{
@ -156,6 +192,10 @@ namespace BTCPayServer.Data
#pragma warning restore CS0618
});
builder.Entity<InvoiceEventData>()
.HasOne(o => o.InvoiceData)
.WithMany(i => i.Events).OnDelete(DeleteBehavior.Cascade);
builder.Entity<InvoiceEventData>()
.HasKey(o => new
{

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

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

@ -80,5 +80,6 @@ namespace BTCPayServer.Data
{
get; set;
}
public List<PendingInvoiceData> PendingInvoices { get; set; }
}
}

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

@ -21,6 +21,9 @@ namespace BTCPayServer.Data
{
get; set;
}
public StoreData StoreData { get; set; }
public string Label
{
get;

@ -11,5 +11,6 @@ namespace BTCPayServer.Data
{
get; set;
}
public InvoiceData InvoiceData { get; set; }
}
}

@ -34,12 +34,13 @@ namespace BTCPayServer.Data
{
get; set;
}
public List<AppData> Apps
{
get; set;
}
public List<InvoiceData> Invoices { get; set; }
[Obsolete("Use GetDerivationStrategies instead")]
public string DerivationStrategy
{
@ -192,6 +193,8 @@ namespace BTCPayServer.Data
}
[Obsolete("Use GetDefaultCrypto instead")]
public string DefaultCrypto { get; set; }
public List<PairedSINData> PairedSINs { get; set; }
public IEnumerable<APIKeyData> APIKeys { get; set; }
#pragma warning disable CS0618
public string GetDefaultCrypto()

@ -72,7 +72,7 @@ namespace BTCPayServer
}
try
{
var data = Encoders.Base58Check.DecodeData(parts[i]);
var data = Network.GetBase58CheckEncoder().DecodeData(parts[i]);
if (data.Length < 4)
continue;
var prefix = Utils.ToUInt32(data, false);
@ -80,7 +80,7 @@ namespace BTCPayServer
for (int ii = 0; ii < 4; ii++)
data[ii] = standardPrefix[ii];
var derivationScheme = new BitcoinExtPubKey(Encoders.Base58Check.EncodeData(data), Network).ToString();
var derivationScheme = new BitcoinExtPubKey(Network.GetBase58CheckEncoder().EncodeData(data), Network).ToString();
electrumMapping.TryGetValue(prefix, out string[] labels);
if (labels != null)
{

@ -8,24 +8,20 @@ namespace BTCPayServer.Events
{
public class InvoiceEvent
{
public InvoiceEvent(InvoiceEntity invoice, int code, string name) : this(invoice.Id, code, name)
public InvoiceEvent(Models.InvoiceResponse invoice, int code, string name)
{
}
public InvoiceEvent(string invoiceId, int code, string name)
{
InvoiceId = invoiceId;
Invoice = invoice;
EventCode = code;
Name = name;
}
public string InvoiceId { get; set; }
public Models.InvoiceResponse Invoice { get; set; }
public int EventCode { get; set; }
public string Name { get; set; }
public override string ToString()
{
return $"Invoice {InvoiceId} new event: {Name} ({EventCode})";
return $"Invoice {Invoice.Id} new event: {Name} ({EventCode})";
}
}
}

@ -6,21 +6,6 @@ using BTCPayServer.HostedServices;
namespace BTCPayServer.Events
{
public class NBXplorerErrorEvent
{
public NBXplorerErrorEvent(BTCPayNetwork network, string errorMessage)
{
Message = errorMessage;
Network = network;
}
public string Message { get; set; }
public BTCPayNetwork Network { get; set; }
public override string ToString()
{
return $"{Network.CryptoCode}: NBXplorer error `{Message}`";
}
}
public class NBXplorerStateChangedEvent
{
public NBXplorerStateChangedEvent(BTCPayNetwork network, NBXplorerState old, NBXplorerState newState)

@ -31,33 +31,12 @@ using System.Security.Claims;
using System.Globalization;
using BTCPayServer.Services;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
namespace BTCPayServer
{
public static class Extensions
{
public static string Prettify(this TimeSpan timeSpan)
{
if (timeSpan.TotalMinutes < 1)
{
return $"{(int)timeSpan.TotalSeconds} second{Plural((int)timeSpan.TotalSeconds)}";
}
if (timeSpan.TotalHours < 1)
{
return $"{(int)timeSpan.TotalMinutes} minute{Plural((int)timeSpan.TotalMinutes)}";
}
if (timeSpan.Days < 1)
{
return $"{(int)timeSpan.TotalHours} hour{Plural((int)timeSpan.TotalHours)}";
}
return $"{(int)timeSpan.TotalDays} day{Plural((int)timeSpan.TotalDays)}";
}
private static string Plural(int totalDays)
{
return totalDays > 1 ? "s" : string.Empty;
}
public static string PrettyPrint(this TimeSpan expiration)
{
StringBuilder builder = new StringBuilder();
@ -104,6 +83,15 @@ namespace BTCPayServer
return activeProvider != "Microsoft.EntityFrameworkCore.Sqlite";
}
public static bool SupportDropForeignKey(this Microsoft.EntityFrameworkCore.Migrations.Migration migration, string activeProvider)
{
return activeProvider != "Microsoft.EntityFrameworkCore.Sqlite";
}
public static bool SupportDropForeignKey(this DatabaseFacade facade)
{
return facade.ProviderName != "Microsoft.EntityFrameworkCore.Sqlite";
}
public static async Task<Dictionary<uint256, TransactionResult>> GetTransactions(this BTCPayWallet client, uint256[] hashes, CancellationToken cts = default(CancellationToken))
{
hashes = hashes.Distinct().ToArray();
@ -120,6 +108,26 @@ namespace BTCPayServer
return str + "/";
}
public static void SetHeaderOnStarting(this HttpResponse resp, string name, string value)
{
if (resp.HasStarted)
return;
resp.OnStarting(() =>
{
SetHeader(resp, name, value);
return Task.CompletedTask;
});
}
public static void SetHeader(this HttpResponse resp, string name, string value)
{
var existing = resp.Headers[name].FirstOrDefault();
if (existing != null && value == null)
resp.Headers.Remove(name);
else
resp.Headers[name] = value;
}
public static string GetAbsoluteRoot(this HttpRequest request)
{
return string.Concat(
@ -131,7 +139,7 @@ namespace BTCPayServer
public static string GetAbsoluteUri(this HttpRequest request, string redirectUrl)
{
bool isRelative =
bool isRelative =
(redirectUrl.Length > 0 && redirectUrl[0] == '/')
|| !new Uri(redirectUrl, UriKind.RelativeOrAbsolute).IsAbsoluteUri;
return isRelative ? request.GetAbsoluteRoot() + redirectUrl : redirectUrl;
@ -163,7 +171,7 @@ namespace BTCPayServer
public static void AddRange<T>(this HashSet<T> hashSet, IEnumerable<T> items)
{
foreach(var item in items)
foreach (var item in items)
{
hashSet.Add(item);
}
@ -179,6 +187,30 @@ namespace BTCPayServer
NBitcoin.Extensions.TryAdd(ctx.Items, "BitpayAuth", value);
}
public static async Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
{
using (var delayCTS = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
{
var waiting = Task.Delay(-1, delayCTS.Token);
var doing = task;
await Task.WhenAny(waiting, doing);
delayCTS.Cancel();
cancellationToken.ThrowIfCancellationRequested();
return await doing;
}
}
public static async Task WithCancellation(this Task task, CancellationToken cancellationToken)
{
using (var delayCTS = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
{
var waiting = Task.Delay(-1, delayCTS.Token);
var doing = task;
await Task.WhenAny(waiting, doing);
delayCTS.Cancel();
cancellationToken.ThrowIfCancellationRequested();
}
}
public static (string Signature, String Id, String Authorization) GetBitpayAuth(this HttpContext ctx)
{
ctx.Items.TryGetValue("BitpayAuth", out object obj);

@ -0,0 +1,106 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Security;
using Microsoft.AspNetCore.Mvc.Filters;
namespace BTCPayServer.Filters
{
public interface IContentSecurityPolicy : IFilterMetadata { }
public class ContentSecurityPolicyAttribute : Attribute, IActionFilter, IContentSecurityPolicy
{
public void OnActionExecuted(ActionExecutedContext context)
{
}
public bool AutoSelf { get; set; } = true;
public bool UnsafeInline { get; set; } = true;
public bool FixWebsocket { get; set; } = true;
public string FontSrc { get; set; } = null;
public string ImgSrc { get; set; } = null;
public string DefaultSrc { get; set; }
public string StyleSrc { get; set; }
public string ScriptSrc { get; set; }
public void OnActionExecuting(ActionExecutingContext context)
{
if (context.IsEffectivePolicy<IContentSecurityPolicy>(this))
{
var policies = context.HttpContext.RequestServices.GetService(typeof(ContentSecurityPolicies)) as ContentSecurityPolicies;
if (policies == null)
return;
if (DefaultSrc != null)
{
policies.Add(new ConsentSecurityPolicy("default-src", DefaultSrc));
}
if (UnsafeInline)
{
policies.Add(new ConsentSecurityPolicy("script-src", "'unsafe-inline'"));
}
if (!string.IsNullOrEmpty(FontSrc))
{
policies.Add(new ConsentSecurityPolicy("font-src", FontSrc));
}
if (!string.IsNullOrEmpty(ImgSrc))
{
policies.Add(new ConsentSecurityPolicy("img-src", ImgSrc));
}
if (!string.IsNullOrEmpty(StyleSrc))
{
policies.Add(new ConsentSecurityPolicy("style-src", StyleSrc));
}
if (!string.IsNullOrEmpty(ScriptSrc))
{
policies.Add(new ConsentSecurityPolicy("script-src", ScriptSrc));
}
if (FixWebsocket && AutoSelf) // Self does not match wss:// and ws:// :(
{
var request = context.HttpContext.Request;
var url = string.Concat(
request.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase) ? "ws" : "wss",
"://",
request.Host.ToUriComponent(),
request.PathBase.ToUriComponent());
policies.Add(new ConsentSecurityPolicy("connect-src", url));
}
context.HttpContext.Response.OnStarting(() =>
{
if (!policies.HasRules)
return Task.CompletedTask;
if (AutoSelf)
{
bool hasSelf = false;
foreach (var group in policies.Rules.GroupBy(p => p.Name))
{
hasSelf = group.Any(g => g.Value.Contains("'self'", StringComparison.OrdinalIgnoreCase));
if (!hasSelf && !group.Any(g => g.Value.Contains("'none'", StringComparison.OrdinalIgnoreCase) ||
g.Value.Contains("*", StringComparison.OrdinalIgnoreCase)))
{
policies.Add(new ConsentSecurityPolicy(group.Key, "'self'"));
hasSelf = true;
}
if (hasSelf)
{
foreach (var authorized in policies.Authorized)
{
policies.Add(new ConsentSecurityPolicy(group.Key, authorized));
}
}
}
}
context.HttpContext.Response.SetHeader("Content-Security-Policy", policies.ToString());
return Task.CompletedTask;
});
}
}
}
}

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Filters;
namespace BTCPayServer.Filters
{
public interface IReferrerPolicy : IFilterMetadata { }
public class ReferrerPolicyAttribute : Attribute, IActionFilter
{
public ReferrerPolicyAttribute(string value)
{
Value = value;
}
public string Value { get; set; }
public void OnActionExecuted(ActionExecutedContext context)
{
}
public void OnActionExecuting(ActionExecutingContext context)
{
if (context.IsEffectivePolicy<ReferrerPolicyAttribute>(this))
{
context.HttpContext.Response.SetHeaderOnStarting("Referrer-Policy", Value);
}
}
}
}

@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Filters;
namespace BTCPayServer.Filters
{
public class XContentTypeOptionsAttribute : Attribute, IActionFilter
{
public XContentTypeOptionsAttribute(string value)
{
Value = value;
}
public void OnActionExecuted(ActionExecutedContext context)
{
}
public string Value { get; set; }
public void OnActionExecuting(ActionExecutingContext context)
{
context.HttpContext.Response.SetHeaderOnStarting("X-Content-Type-Options", Value);
}
}
}

@ -23,11 +23,7 @@ namespace BTCPayServer.Filters
public void OnActionExecuting(ActionExecutingContext context)
{
var existing = context.HttpContext.Response.Headers["x-frame-options"].FirstOrDefault();
if (existing != null && Value == null)
context.HttpContext.Response.Headers.Remove("x-frame-options");
else
context.HttpContext.Response.Headers["x-frame-options"] = Value;
context.HttpContext.Response.SetHeaderOnStarting("X-Frame-Options", Value);
}
}
}

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
namespace BTCPayServer.Filters
{
public class XXSSProtectionAttribute : Attribute, IActionFilter
{
public void OnActionExecuted(ActionExecutedContext context)
{
}
public void OnActionExecuting(ActionExecutingContext context)
{
context.HttpContext.Response.SetHeaderOnStarting("X-XSS-Protection", "1; mode=block");
}
}
}

@ -11,6 +11,8 @@ using NBXplorer.Models;
using System.Collections.Concurrent;
using BTCPayServer.Events;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Mvc.Filters;
using BTCPayServer.Security;
namespace BTCPayServer.HostedServices
{
@ -41,6 +43,40 @@ namespace BTCPayServer.HostedServices
{
get { return _creativeStartUri; }
}
public bool ShowRegister { get; set; }
internal void Update(PoliciesSettings data)
{
ShowRegister = !data.LockSubscription;
}
}
public class ContentSecurityPolicyCssThemeManager : Attribute, IActionFilter, IOrderedFilter
{
public int Order => 1001;
public void OnActionExecuted(ActionExecutedContext context)
{
}
public void OnActionExecuting(ActionExecutingContext context)
{
var manager = context.HttpContext.RequestServices.GetService(typeof(CssThemeManager)) as CssThemeManager;
var policies = context.HttpContext.RequestServices.GetService(typeof(ContentSecurityPolicies)) as ContentSecurityPolicies;
if (manager != null && policies != null)
{
if(manager.CreativeStartUri != null && Uri.TryCreate(manager.CreativeStartUri, UriKind.Absolute, out var uri))
{
policies.Clear();
}
if (manager.BootstrapUri != null && Uri.TryCreate(manager.BootstrapUri, UriKind.Absolute, out uri))
{
policies.Clear();
}
}
}
}
public class CssThemeManagerHostedService : BaseAsyncService
@ -58,10 +94,19 @@ namespace BTCPayServer.HostedServices
{
return new[]
{
CreateLoopTask(ListenForThemeChanges)
CreateLoopTask(ListenForThemeChanges),
CreateLoopTask(ListenForPoliciesChanges),
};
}
async Task ListenForPoliciesChanges()
{
await new SynchronizationContextRemover();
var data = (await _SettingsRepository.GetSettingAsync<PoliciesSettings>()) ?? new PoliciesSettings();
_CssThemeManager.Update(data);
await _SettingsRepository.WaitSettingsChanged<PoliciesSettings>(Cancellation);
}
async Task ListenForThemeChanges()
{
await new SynchronizationContextRemover();

@ -198,7 +198,11 @@ namespace BTCPayServer.HostedServices
PosData = dto.PosData,
Price = dto.Price,
Status = dto.Status,
BuyerFields = invoice.RefundMail == null ? null : new Newtonsoft.Json.Linq.JObject() { new JProperty("buyerEmail", invoice.RefundMail) }
BuyerFields = invoice.RefundMail == null ? null : new Newtonsoft.Json.Linq.JObject() { new JProperty("buyerEmail", invoice.RefundMail) },
PaymentSubtotals = dto.PaymentSubtotals,
PaymentTotals = dto.PaymentTotals,
AmountPaid = dto.AmountPaid,
ExchangeRates = dto.ExchangeRates
};
// We keep backward compatibility with bitpay by passing BTC info to the notification
@ -207,7 +211,7 @@ namespace BTCPayServer.HostedServices
if (btcCryptoInfo != null)
{
#pragma warning disable CS0618
notification.Rate = (double)dto.Rate;
notification.Rate = dto.Rate;
notification.Url = dto.Url;
notification.BTCDue = dto.BTCDue;
notification.BTCPaid = dto.BTCPaid;
@ -304,8 +308,11 @@ namespace BTCPayServer.HostedServices
{
leases.Add(_EventAggregator.Subscribe<InvoiceEvent>(async e =>
{
var invoice = await _InvoiceRepository.GetInvoice(null, e.InvoiceId);
await SaveEvent(invoice.Id, e);
var invoice = await _InvoiceRepository.GetInvoice(null, e.Invoice.Id);
List<Task> tasks = new List<Task>();
// Awaiting this later help make sure invoices should arrive in order
tasks.Add(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)
@ -315,20 +322,22 @@ namespace BTCPayServer.HostedServices
e.Name == "invoice_failedToConfirm" ||
e.Name == "invoice_markedInvalid" ||
e.Name == "invoice_failedToConfirm" ||
e.Name == "invoice_completed"
e.Name == "invoice_completed" ||
e.Name == "invoice_expiredPaidPartial"
)
await Notify(invoice);
tasks.Add(Notify(invoice));
}
if (e.Name == "invoice_confirmed")
{
await Notify(invoice);
tasks.Add(Notify(invoice));
}
if (invoice.ExtendedNotifications)
{
await Notify(invoice, e.EventCode, e.Name);
tasks.Add(Notify(invoice, e.EventCode, e.Name));
}
await Task.WhenAll(tasks.ToArray());
}));

@ -66,10 +66,10 @@ namespace BTCPayServer.HostedServices
context.MarkDirty();
await _InvoiceRepository.UnaffectAddress(invoice.Id);
context.Events.Add(new InvoiceEvent(invoice, 1004, "invoice_expired"));
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1004, "invoice_expired"));
invoice.Status = "expired";
if(invoice.ExceptionStatus == "paidPartial")
context.Events.Add(new InvoiceEvent(invoice, 2000, "invoice_expiredPaidPartial"));
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 2000, "invoice_expiredPaidPartial"));
}
var payments = invoice.GetPayments().Where(p => p.Accounted).ToArray();
@ -84,7 +84,7 @@ namespace BTCPayServer.HostedServices
{
if (invoice.Status == "new")
{
context.Events.Add(new InvoiceEvent(invoice, 1003, "invoice_paidInFull"));
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1003, "invoice_paidInFull"));
invoice.Status = "paid";
invoice.ExceptionStatus = accounting.Paid > accounting.TotalDue ? "paidOver" : null;
await _InvoiceRepository.UnaffectAddress(invoice.Id);
@ -93,7 +93,7 @@ namespace BTCPayServer.HostedServices
else if (invoice.Status == "expired" && invoice.ExceptionStatus != "paidLate")
{
invoice.ExceptionStatus = "paidLate";
context.Events.Add(new InvoiceEvent(invoice, 1009, "invoice_paidAfterExpiration"));
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1009, "invoice_paidAfterExpiration"));
context.MarkDirty();
}
}
@ -139,14 +139,14 @@ namespace BTCPayServer.HostedServices
(confirmedAccounting.Paid < accounting.MinimumTotalDue))
{
await _InvoiceRepository.UnaffectAddress(invoice.Id);
context.Events.Add(new InvoiceEvent(invoice, 1013, "invoice_failedToConfirm"));
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1013, "invoice_failedToConfirm"));
invoice.Status = "invalid";
context.MarkDirty();
}
else if (confirmedAccounting.Paid >= accounting.MinimumTotalDue)
{
await _InvoiceRepository.UnaffectAddress(invoice.Id);
context.Events.Add(new InvoiceEvent(invoice, 1005, "invoice_confirmed"));
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1005, "invoice_confirmed"));
invoice.Status = "confirmed";
context.MarkDirty();
}
@ -157,7 +157,7 @@ namespace BTCPayServer.HostedServices
var completedAccounting = paymentMethod.Calculate(p => p.GetCryptoPaymentData().PaymentCompleted(p, network));
if (completedAccounting.Paid >= accounting.MinimumTotalDue)
{
context.Events.Add(new InvoiceEvent(invoice, 1006, "invoice_completed"));
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1006, "invoice_completed"));
invoice.Status = "complete";
context.MarkDirty();
}
@ -249,13 +249,13 @@ namespace BTCPayServer.HostedServices
{
if (b.Name == "invoice_created")
{
Watch(b.InvoiceId);
await Wait(b.InvoiceId);
Watch(b.Invoice.Id);
await Wait(b.Invoice.Id);
}
if (b.Name == "invoice_receivedPayment")
{
Watch(b.InvoiceId);
Watch(b.Invoice.Id);
}
}));
return Task.CompletedTask;
@ -291,7 +291,7 @@ namespace BTCPayServer.HostedServices
if (updateContext.Dirty)
{
await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.Status, invoice.ExceptionStatus);
updateContext.Events.Add(new InvoiceDataChangedEvent(invoice));
updateContext.Events.Insert(0, new InvoiceDataChangedEvent(invoice));
}
foreach (var evt in updateContext.Events)

@ -0,0 +1,89 @@
using System;
using Microsoft.Extensions.Logging;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Services;
using BTCPayServer.Services.Stores;
using BTCPayServer.Logging;
namespace BTCPayServer.HostedServices
{
public class MigratorHostedService : BaseAsyncService
{
private ApplicationDbContextFactory _DBContextFactory;
private StoreRepository _StoreRepository;
private BTCPayNetworkProvider _NetworkProvider;
private SettingsRepository _Settings;
public MigratorHostedService(
BTCPayNetworkProvider networkProvider,
StoreRepository storeRepository,
ApplicationDbContextFactory dbContextFactory,
SettingsRepository settingsRepository)
{
_DBContextFactory = dbContextFactory;
_StoreRepository = storeRepository;
_NetworkProvider = networkProvider;
_Settings = settingsRepository;
}
internal override Task[] InitializeTasks()
{
return new[]
{
Update()
};
}
private async Task Update()
{
try
{
var settings = (await _Settings.GetSettingAsync<MigrationSettings>()) ?? new MigrationSettings();
if (!settings.DeprecatedLightningConnectionStringCheck)
{
await DeprecatedLightningConnectionStringCheck();
settings.DeprecatedLightningConnectionStringCheck = true;
await _Settings.UpdateSetting(settings);
}
if (!settings.UnreachableStoreCheck)
{
await UnreachableStoreCheck();
settings.UnreachableStoreCheck = true;
await _Settings.UpdateSetting(settings);
}
}
catch (Exception ex)
{
Logs.PayServer.LogError(ex, "Error on the MigratorHostedService");
throw;
}
}
private Task UnreachableStoreCheck()
{
return _StoreRepository.CleanUnreachableStores();
}
private async Task DeprecatedLightningConnectionStringCheck()
{
using (var ctx = _DBContextFactory.CreateContext())
{
foreach (var store in await ctx.Stores.ToArrayAsync())
{
foreach (var method in store.GetSupportedPaymentMethods(_NetworkProvider).OfType<Payments.Lightning.LightningSupportedPaymentMethod>())
{
var lightning = method.GetLightningUrl();
if (lightning.IsLegacy)
{
method.SetLightningUrl(lightning);
store.SetSupportedPaymentMethod(method.PaymentId, method);
}
}
}
await ctx.SaveChangesAsync();
}
}
}
}

@ -192,7 +192,7 @@ namespace BTCPayServer.HostedServices
{
State = NBXplorerState.NotConnected;
status = null;
_Aggregator.Publish(new NBXplorerErrorEvent(_Network, error));
Logs.PayServer.LogError($"{_Network.CryptoCode}: NBXplorer error `{error}`");
}
_Dashboard.Publish(_Network, State, status, error);

@ -12,7 +12,6 @@ using NBitcoin;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using System.IO;
using Microsoft.Data.Sqlite;
using NBXplorer;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Hosting;
@ -93,6 +92,7 @@ namespace BTCPayServer.Hosting
return opts.NetworkProvider;
});
services.TryAddSingleton<LightningConfigurationProvider>();
services.TryAddSingleton<LanguageService>();
services.TryAddSingleton<NBXplorerDashboard>();
services.TryAddSingleton<StoreRepository>();
@ -105,7 +105,9 @@ namespace BTCPayServer.Hosting
});
services.AddSingleton<CssThemeManager>();
services.Configure<MvcOptions>((o) => { o.Filters.Add(new ContentSecurityPolicyCssThemeManager()); });
services.AddSingleton<IHostedService, CssThemeManagerHostedService>();
services.AddSingleton<IHostedService, MigratorHostedService>();
services.AddSingleton<Payments.IPaymentMethodHandler<DerivationStrategy>, Payments.Bitcoin.BitcoinLikePaymentHandler>();
services.AddSingleton<IHostedService, Payments.Bitcoin.NBXplorerListener>();
@ -118,7 +120,6 @@ namespace BTCPayServer.Hosting
services.AddSingleton<IHostedService, InvoiceWatcher>();
services.AddSingleton<IHostedService, RatesHostedService>();
services.AddTransient<IConfigureOptions<MvcOptions>, BTCPayClaimsFilter>();
services.AddTransient<IConfigureOptions<MvcOptions>, BitpayClaimsFilter>();
services.TryAddSingleton<ExplorerClientProvider>();
services.TryAddSingleton<Bitpay>(o =>
@ -138,6 +139,7 @@ namespace BTCPayServer.Hosting
// bundling
services.AddAuthorization(o => Policies.AddBTCPayPolicies(o));
BitpayAuthentication.AddAuthentication(services);
services.AddBundles();
services.AddTransient<BundleOptions>(provider =>

@ -79,12 +79,13 @@ namespace BTCPayServer.Hosting
if (!httpContext.Request.Path.HasValue)
return false;
var isJson = (httpContext.Request.ContentType ?? string.Empty).StartsWith("application/json", StringComparison.OrdinalIgnoreCase);
var path = httpContext.Request.Path.Value;
if (
bitpayAuth &&
path == "/invoices" &&
httpContext.Request.Method == "POST" &&
(httpContext.Request.ContentType ?? string.Empty).StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
isJson)
return true;
if (
@ -94,9 +95,9 @@ namespace BTCPayServer.Hosting
return true;
if (
bitpayAuth &&
path.StartsWith("/invoices/", StringComparison.OrdinalIgnoreCase) &&
httpContext.Request.Method == "GET")
httpContext.Request.Method == "GET" &&
(isJson || httpContext.Request.Query.ContainsKey("token")))
return true;
if (path.Equals("/rates", StringComparison.OrdinalIgnoreCase) &&

@ -35,11 +35,11 @@ using Hangfire.Annotations;
using Microsoft.Extensions.DependencyInjection.Extensions;
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;
using BTCPayServer.Security;
namespace BTCPayServer.Hosting
{
@ -80,8 +80,19 @@ namespace BTCPayServer.Hosting
services.AddMvc(o =>
{
o.Filters.Add(new XFrameOptionsAttribute("DENY"));
o.Filters.Add(new XContentTypeOptionsAttribute("nosniff"));
o.Filters.Add(new XXSSProtectionAttribute());
o.Filters.Add(new ReferrerPolicyAttribute("same-origin"));
//o.Filters.Add(new ContentSecurityPolicyAttribute()
//{
// FontSrc = "'self' https://fonts.gstatic.com/",
// ImgSrc = "'self' data:",
// DefaultSrc = "'none'",
// StyleSrc = "'self' 'unsafe-inline'",
// ScriptSrc = "'self' 'unsafe-inline'"
//});
});
services.TryAddScoped<ContentSecurityPolicies>();
services.Configure<IdentityOptions>(options =>
{
options.Password.RequireDigit = false;
@ -104,10 +115,6 @@ namespace BTCPayServer.Hosting
b.AllowAnyMethod().AllowAnyHeader().AllowAnyOrigin();
});
});
services.Configure<IOptions<ApplicationInsightsServiceOptions>>(o =>
{
o.Value.DeveloperMode = _Env.IsDevelopment();
});
// Needed to debug U2F for ledger support
//services.Configure<KestrelServerOptions>(kestrel =>
@ -146,12 +153,8 @@ namespace BTCPayServer.Hosting
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
//App insight do not that by itself...
loggerFactory.AddApplicationInsights(prov, LogLevel.Information);
app.UsePayServer();
app.UseStaticFiles();
app.UseAuthentication();

@ -1,13 +1,14 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Console;
using Microsoft.Extensions.Logging.Console.Internal;
using System;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions.Internal;
using Microsoft.Extensions.Logging.Console;
using Microsoft.Extensions.Logging.Console.Internal;
namespace BTCPayServer.Logging
{
@ -20,19 +21,18 @@ namespace BTCPayServer.Logging
}
public ILogger CreateLogger(string categoryName)
{
return new CustomConsoleLogger(categoryName, (a, b) => true, false, _Processor);
return new CustomerConsoleLogger(categoryName, (a, b) => true, null, _Processor);
}
public void Dispose()
{
}
}
/// <summary>
/// A variant of ASP.NET Core ConsoleLogger which does not make new line for the category
/// </summary>
public class CustomConsoleLogger : ILogger
public class CustomerConsoleLogger : ILogger
{
private static readonly string _loglevelPadding = ": ";
private static readonly string _messagePadding;
@ -47,19 +47,33 @@ namespace BTCPayServer.Logging
[ThreadStatic]
private static StringBuilder _logBuilder;
static CustomConsoleLogger()
static CustomerConsoleLogger()
{
var logLevelString = GetLogLevelString(LogLevel.Information);
_messagePadding = new string(' ', logLevelString.Length + _loglevelPadding.Length);
_newLineWithMessagePadding = Environment.NewLine + _messagePadding;
}
public CustomConsoleLogger(string name, Func<string, LogLevel, bool> filter, bool includeScopes, ConsoleLoggerProcessor loggerProcessor)
public CustomerConsoleLogger(string name, Func<string, LogLevel, bool> filter, bool includeScopes)
: this(name, filter, includeScopes ? new LoggerExternalScopeProvider() : null, new ConsoleLoggerProcessor())
{
Name = name ?? throw new ArgumentNullException(nameof(name));
Filter = filter ?? ((category, logLevel) => true);
IncludeScopes = includeScopes;
}
internal CustomerConsoleLogger(string name, Func<string, LogLevel, bool> filter, IExternalScopeProvider scopeProvider)
: this(name, filter, scopeProvider, new ConsoleLoggerProcessor())
{
}
internal CustomerConsoleLogger(string name, Func<string, LogLevel, bool> filter, IExternalScopeProvider scopeProvider, ConsoleLoggerProcessor loggerProcessor)
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
Name = name;
Filter = filter ?? ((category, logLevel) => true);
ScopeProvider = scopeProvider;
_queueProcessor = loggerProcessor;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
@ -80,7 +94,12 @@ namespace BTCPayServer.Logging
}
set
{
_queueProcessor.Console = value ?? throw new ArgumentNullException(nameof(value));
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
_queueProcessor.Console = value;
}
}
@ -92,13 +111,13 @@ namespace BTCPayServer.Logging
}
set
{
_filter = value ?? throw new ArgumentNullException(nameof(value));
}
}
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
public bool IncludeScopes
{
get; set;
_filter = value;
}
}
public string Name
@ -106,6 +125,16 @@ namespace BTCPayServer.Logging
get;
}
internal IExternalScopeProvider ScopeProvider
{
get; set;
}
public bool DisableColors
{
get; set;
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
if (!IsEnabled(logLevel))
@ -154,10 +183,7 @@ namespace BTCPayServer.Logging
while (lenAfter++ < 18)
logBuilder.Append(" ");
// scope information
if (IncludeScopes)
{
GetScopeInformation(logBuilder);
}
GetScopeInformation(logBuilder);
if (!string.IsNullOrEmpty(message))
{
@ -202,18 +228,15 @@ namespace BTCPayServer.Logging
public bool IsEnabled(LogLevel logLevel)
{
if (logLevel == LogLevel.None)
{
return false;
}
return Filter(Name, logLevel);
}
public IDisposable BeginScope<TState>(TState state)
{
if (state == null)
{
throw new ArgumentNullException(nameof(state));
}
return ConsoleLogScope.Push(Name, state);
}
public IDisposable BeginScope<TState>(TState state) => ScopeProvider?.Push(state) ?? NullScope.Instance;
private static string GetLogLevelString(LogLevel logLevel)
{
@ -238,6 +261,11 @@ namespace BTCPayServer.Logging
private ConsoleColors GetLogLevelConsoleColors(LogLevel logLevel)
{
if (DisableColors)
{
return new ConsoleColors(null, null);
}
// We must explicitly set the background color if we are setting the foreground color,
// since just setting one can look bad on the users console.
switch (logLevel)
@ -259,30 +287,25 @@ namespace BTCPayServer.Logging
}
}
private void GetScopeInformation(StringBuilder builder)
private void GetScopeInformation(StringBuilder stringBuilder)
{
var current = ConsoleLogScope.Current;
string scopeLog = string.Empty;
var length = builder.Length;
while (current != null)
var scopeProvider = ScopeProvider;
if (scopeProvider != null)
{
if (length == builder.Length)
{
scopeLog = $"=> {current}";
}
else
{
scopeLog = $"=> {current} ";
}
var initialLength = stringBuilder.Length;
builder.Insert(length, scopeLog);
current = current.Parent;
}
if (builder.Length > length)
{
builder.Insert(length, _messagePadding);
builder.AppendLine();
scopeProvider.ForEachScope((scope, state) =>
{
var (builder, length) = state;
var first = length == builder.Length;
builder.Append(first ? "=> " : " => ").Append(scope);
}, (stringBuilder, initialLength));
if (stringBuilder.Length > initialLength)
{
stringBuilder.Insert(initialLength, _messagePadding);
stringBuilder.AppendLine();
}
}
}
@ -333,9 +356,9 @@ namespace BTCPayServer.Logging
// Start Console message queue processor
_outputTask = Task.Factory.StartNew(
ProcessLogQueue,
this,
default(CancellationToken),
TaskCreationOptions.LongRunning, TaskScheduler.Default);
state: this,
cancellationToken: default(CancellationToken),
creationOptions: TaskCreationOptions.LongRunning, scheduler: TaskScheduler.Default);
}
public virtual void EnqueueMessage(LogMessageEntry message)

@ -15,9 +15,14 @@ namespace BTCPayServer.Logging
}
public static void Configure(ILoggerFactory factory)
{
Configuration = factory.CreateLogger("Configuration");
PayServer = factory.CreateLogger("PayServer");
Events = factory.CreateLogger("Events");
if (factory == null)
Configure(new FuncLoggerFactory(n => NullLogger.Instance));
else
{
Configuration = factory.CreateLogger("Configuration");
PayServer = factory.CreateLogger("PayServer");
Events = factory.CreateLogger("Events");
}
}
public static ILogger Configuration
{

@ -12,10 +12,10 @@ namespace BTCPayServer.Migrations
name: "AspNetRoles",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
ConcurrencyStamp = table.Column<string>(type: "TEXT", nullable: true),
Name = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
NormalizedName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true)
Id = table.Column<string>(nullable: false),
ConcurrencyStamp = table.Column<string>(nullable: true),
Name = table.Column<string>(maxLength: 256, nullable: true),
NormalizedName = table.Column<string>(maxLength: 256, nullable: true)
},
constraints: table =>
{
@ -26,21 +26,21 @@ namespace BTCPayServer.Migrations
name: "AspNetUsers",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
AccessFailedCount = table.Column<int>(type: "INTEGER", nullable: false),
ConcurrencyStamp = table.Column<string>(type: "TEXT", nullable: true),
Email = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
Id = table.Column<string>(nullable: false),
AccessFailedCount = table.Column<int>(nullable: false),
ConcurrencyStamp = table.Column<string>(nullable: true),
Email = table.Column<string>(maxLength: 256, nullable: true),
EmailConfirmed = table.Column<bool>(nullable: false),
LockoutEnabled = table.Column<bool>(nullable: false),
LockoutEnd = table.Column<DateTimeOffset>(nullable: true),
NormalizedEmail = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
NormalizedUserName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
PasswordHash = table.Column<string>(type: "TEXT", nullable: true),
PhoneNumber = table.Column<string>(type: "TEXT", nullable: true),
NormalizedEmail = table.Column<string>(maxLength: 256, nullable: true),
NormalizedUserName = table.Column<string>(maxLength: 256, nullable: true),
PasswordHash = table.Column<string>(nullable: true),
PhoneNumber = table.Column<string>(nullable: true),
PhoneNumberConfirmed = table.Column<bool>(nullable: false),
SecurityStamp = table.Column<string>(type: "TEXT", nullable: true),
SecurityStamp = table.Column<string>(nullable: true),
TwoFactorEnabled = table.Column<bool>(nullable: false),
UserName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true)
UserName = table.Column<string>(maxLength: 256, nullable: true)
},
constraints: table =>
{
@ -51,12 +51,12 @@ namespace BTCPayServer.Migrations
name: "Stores",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
DerivationStrategy = table.Column<string>(type: "TEXT", nullable: true),
SpeedPolicy = table.Column<int>(type: "INTEGER", nullable: false),
Id = table.Column<string>(nullable: false),
DerivationStrategy = table.Column<string>(nullable: true),
SpeedPolicy = table.Column<int>(nullable: false),
StoreCertificate = table.Column<byte[]>(nullable: true),
StoreName = table.Column<string>(type: "TEXT", nullable: true),
StoreWebsite = table.Column<string>(type: "TEXT", nullable: true)
StoreName = table.Column<string>(nullable: true),
StoreWebsite = table.Column<string>(nullable: true)
},
constraints: table =>
{
@ -67,11 +67,11 @@ namespace BTCPayServer.Migrations
name: "AspNetRoleClaims",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
Id = table.Column<int>(nullable: false)
.Annotation("Sqlite:Autoincrement", true),
ClaimType = table.Column<string>(type: "TEXT", nullable: true),
ClaimValue = table.Column<string>(type: "TEXT", nullable: true),
RoleId = table.Column<string>(type: "TEXT", nullable: false)
ClaimType = table.Column<string>(nullable: true),
ClaimValue = table.Column<string>(nullable: true),
RoleId = table.Column<string>(nullable: false)
},
constraints: table =>
{
@ -88,11 +88,11 @@ namespace BTCPayServer.Migrations
name: "AspNetUserClaims",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
Id = table.Column<int>(nullable: false)
.Annotation("Sqlite:Autoincrement", true),
ClaimType = table.Column<string>(type: "TEXT", nullable: true),
ClaimValue = table.Column<string>(type: "TEXT", nullable: true),
UserId = table.Column<string>(type: "TEXT", nullable: false)
ClaimType = table.Column<string>(nullable: true),
ClaimValue = table.Column<string>(nullable: true),
UserId = table.Column<string>(nullable: false)
},
constraints: table =>
{
@ -109,10 +109,10 @@ namespace BTCPayServer.Migrations
name: "AspNetUserLogins",
columns: table => new
{
LoginProvider = table.Column<string>(type: "TEXT", nullable: false),
ProviderKey = table.Column<string>(type: "TEXT", nullable: false),
ProviderDisplayName = table.Column<string>(type: "TEXT", nullable: true),
UserId = table.Column<string>(type: "TEXT", nullable: false)
LoginProvider = table.Column<string>(nullable: false),
ProviderKey = table.Column<string>(nullable: false),
ProviderDisplayName = table.Column<string>(nullable: true),
UserId = table.Column<string>(nullable: false)
},
constraints: table =>
{
@ -129,8 +129,8 @@ namespace BTCPayServer.Migrations
name: "AspNetUserRoles",
columns: table => new
{
UserId = table.Column<string>(type: "TEXT", nullable: false),
RoleId = table.Column<string>(type: "TEXT", nullable: false)
UserId = table.Column<string>(nullable: false),
RoleId = table.Column<string>(nullable: false)
},
constraints: table =>
{
@ -153,10 +153,10 @@ namespace BTCPayServer.Migrations
name: "AspNetUserTokens",
columns: table => new
{
UserId = table.Column<string>(type: "TEXT", nullable: false),
LoginProvider = table.Column<string>(type: "TEXT", nullable: false),
Name = table.Column<string>(type: "TEXT", nullable: false),
Value = table.Column<string>(type: "TEXT", nullable: true)
UserId = table.Column<string>(nullable: false),
LoginProvider = table.Column<string>(nullable: false),
Name = table.Column<string>(nullable: false),
Value = table.Column<string>(nullable: true)
},
constraints: table =>
{
@ -173,15 +173,15 @@ namespace BTCPayServer.Migrations
name: "Invoices",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
Id = table.Column<string>(nullable: false),
Blob = table.Column<byte[]>(nullable: true),
Created = table.Column<DateTimeOffset>(nullable: false),
CustomerEmail = table.Column<string>(type: "TEXT", nullable: true),
ExceptionStatus = table.Column<string>(type: "TEXT", nullable: true),
ItemCode = table.Column<string>(type: "TEXT", nullable: true),
OrderId = table.Column<string>(type: "TEXT", nullable: true),
Status = table.Column<string>(type: "TEXT", nullable: true),
StoreDataId = table.Column<string>(type: "TEXT", nullable: true)
CustomerEmail = table.Column<string>(nullable: true),
ExceptionStatus = table.Column<string>(nullable: true),
ItemCode = table.Column<string>(nullable: true),
OrderId = table.Column<string>(nullable: true),
Status = table.Column<string>(nullable: true),
StoreDataId = table.Column<string>(nullable: true)
},
constraints: table =>
{
@ -198,9 +198,9 @@ namespace BTCPayServer.Migrations
name: "UserStore",
columns: table => new
{
ApplicationUserId = table.Column<string>(type: "TEXT", nullable: false),
StoreDataId = table.Column<string>(type: "TEXT", nullable: false),
Role = table.Column<string>(type: "TEXT", nullable: true)
ApplicationUserId = table.Column<string>(nullable: false),
StoreDataId = table.Column<string>(nullable: false),
Role = table.Column<string>(nullable: true)
},
constraints: table =>
{
@ -223,9 +223,9 @@ namespace BTCPayServer.Migrations
name: "Payments",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
Id = table.Column<string>(nullable: false),
Blob = table.Column<byte[]>(nullable: true),
InvoiceDataId = table.Column<string>(type: "TEXT", nullable: true)
InvoiceDataId = table.Column<string>(nullable: true)
},
constraints: table =>
{
@ -242,9 +242,9 @@ namespace BTCPayServer.Migrations
name: "RefundAddresses",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
Id = table.Column<string>(nullable: false),
Blob = table.Column<byte[]>(nullable: true),
InvoiceDataId = table.Column<string>(type: "TEXT", nullable: true)
InvoiceDataId = table.Column<string>(nullable: true)
},
constraints: table =>
{

@ -12,8 +12,8 @@ namespace BTCPayServer.Migrations
name: "Settings",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
Value = table.Column<string>(type: "TEXT", nullable: true)
Id = table.Column<string>(nullable: false),
Value = table.Column<string>(nullable: true)
},
constraints: table =>
{

@ -12,8 +12,8 @@ namespace BTCPayServer.Migrations
name: "AddressInvoices",
columns: table => new
{
Address = table.Column<string>(type: "TEXT", nullable: false),
InvoiceDataId = table.Column<string>(type: "TEXT", nullable: true)
Address = table.Column<string>(nullable: false),
InvoiceDataId = table.Column<string>(nullable: true)
},
constraints: table =>
{

@ -12,13 +12,13 @@ namespace BTCPayServer.Migrations
name: "PairedSINData",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
Facade = table.Column<string>(type: "TEXT", nullable: true),
Label = table.Column<string>(type: "TEXT", nullable: true),
Name = table.Column<string>(type: "TEXT", nullable: true),
Id = table.Column<string>(nullable: false),
Facade = table.Column<string>(nullable: true),
Label = table.Column<string>(nullable: true),
Name = table.Column<string>(nullable: true),
PairingTime = table.Column<DateTimeOffset>(nullable: false),
SIN = table.Column<string>(type: "TEXT", nullable: true),
StoreDataId = table.Column<string>(type: "TEXT", nullable: true)
SIN = table.Column<string>(nullable: true),
StoreDataId = table.Column<string>(nullable: true)
},
constraints: table =>
{
@ -29,15 +29,15 @@ namespace BTCPayServer.Migrations
name: "PairingCodes",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
Id = table.Column<string>(nullable: false),
DateCreated = table.Column<DateTime>(nullable: false),
Expiration = table.Column<DateTimeOffset>(nullable: false),
Facade = table.Column<string>(type: "TEXT", nullable: true),
Label = table.Column<string>(type: "TEXT", nullable: true),
Name = table.Column<string>(type: "TEXT", nullable: true),
SIN = table.Column<string>(type: "TEXT", nullable: true),
StoreDataId = table.Column<string>(type: "TEXT", nullable: true),
TokenValue = table.Column<string>(type: "TEXT", nullable: true)
Facade = table.Column<string>(nullable: true),
Label = table.Column<string>(nullable: true),
Name = table.Column<string>(nullable: true),
SIN = table.Column<string>(nullable: true),
StoreDataId = table.Column<string>(nullable: true),
TokenValue = table.Column<string>(nullable: true)
},
constraints: table =>
{

@ -22,7 +22,7 @@ namespace BTCPayServer.Migrations
name: "PendingInvoices",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false)
Id = table.Column<string>(nullable: false)
},
constraints: table =>
{

@ -17,8 +17,8 @@ namespace BTCPayServer.Migrations
name: "HistoricalAddressInvoices",
columns: table => new
{
InvoiceDataId = table.Column<string>(type: "TEXT", nullable: false),
Address = table.Column<string>(type: "TEXT", nullable: false),
InvoiceDataId = table.Column<string>(nullable: false),
Address = table.Column<string>(nullable: false),
Assigned = table.Column<DateTimeOffset>(nullable: false),
UnAssigned = table.Column<DateTimeOffset>(nullable: true)
},

@ -0,0 +1,578 @@
// <auto-generated />
using System;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20180719095626_CanDeleteStores")]
partial class CanDeleteStores
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.1.0-rtm-30799");
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
b.Property<string>("Address")
.ValueGeneratedOnAdd();
b.Property<DateTimeOffset?>("CreatedTime");
b.Property<string>("InvoiceDataId");
b.HasKey("Address");
b.HasIndex("InvoiceDataId");
b.ToTable("AddressInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasMaxLength(50);
b.Property<string>("StoreId")
.HasMaxLength(50);
b.HasKey("Id");
b.HasIndex("StoreId");
b.ToTable("ApiKeys");
});
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("AppType");
b.Property<DateTimeOffset>("Created");
b.Property<string>("Name");
b.Property<string>("Settings");
b.Property<string>("StoreDataId");
b.HasKey("Id");
b.HasIndex("StoreDataId");
b.ToTable("Apps");
});
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
{
b.Property<string>("InvoiceDataId");
b.Property<string>("Address");
b.Property<DateTimeOffset>("Assigned");
b.Property<string>("CryptoCode");
b.Property<DateTimeOffset?>("UnAssigned");
b.HasKey("InvoiceDataId", "Address");
b.ToTable("HistoricalAddressInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<byte[]>("Blob");
b.Property<DateTimeOffset>("Created");
b.Property<string>("CustomerEmail");
b.Property<string>("ExceptionStatus");
b.Property<string>("ItemCode");
b.Property<string>("OrderId");
b.Property<string>("Status");
b.Property<string>("StoreDataId");
b.HasKey("Id");
b.HasIndex("StoreDataId");
b.ToTable("Invoices");
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
{
b.Property<string>("InvoiceDataId");
b.Property<string>("UniqueId");
b.Property<string>("Message");
b.Property<DateTimeOffset>("Timestamp");
b.HasKey("InvoiceDataId", "UniqueId");
b.ToTable("InvoiceEvents");
});
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Facade");
b.Property<string>("Label");
b.Property<DateTimeOffset>("PairingTime");
b.Property<string>("SIN");
b.Property<string>("StoreDataId");
b.HasKey("Id");
b.HasIndex("SIN");
b.HasIndex("StoreDataId");
b.ToTable("PairedSINData");
});
modelBuilder.Entity("BTCPayServer.Data.PairingCodeData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("DateCreated");
b.Property<DateTimeOffset>("Expiration");
b.Property<string>("Facade");
b.Property<string>("Label");
b.Property<string>("SIN");
b.Property<string>("StoreDataId");
b.Property<string>("TokenValue");
b.HasKey("Id");
b.ToTable("PairingCodes");
});
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<bool>("Accounted");
b.Property<byte[]>("Blob");
b.Property<string>("InvoiceDataId");
b.HasKey("Id");
b.HasIndex("InvoiceDataId");
b.ToTable("Payments");
});
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
{
b.Property<string>("Id");
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")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("APIKeys")
.HasForeignKey("StoreId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("Apps")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("HistoricalAddressInvoices")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("Invoices")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("Events")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("PairedSINs")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("Payments")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("PendingInvoices")
.HasForeignKey("Id")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("RefundAddresses")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
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,172 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace BTCPayServer.Migrations
{
public partial class CanDeleteStores : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
if (this.SupportDropForeignKey(migrationBuilder.ActiveProvider))
{
migrationBuilder.DropForeignKey(
name: "FK_AddressInvoices_Invoices_InvoiceDataId",
table: "AddressInvoices");
migrationBuilder.DropForeignKey(
name: "FK_Apps_Stores_StoreDataId",
table: "Apps");
migrationBuilder.DropForeignKey(
name: "FK_Invoices_Stores_StoreDataId",
table: "Invoices");
migrationBuilder.DropForeignKey(
name: "FK_Payments_Invoices_InvoiceDataId",
table: "Payments");
migrationBuilder.DropForeignKey(
name: "FK_RefundAddresses_Invoices_InvoiceDataId",
table: "RefundAddresses");
migrationBuilder.AddForeignKey(
name: "FK_AddressInvoices_Invoices_InvoiceDataId",
table: "AddressInvoices",
column: "InvoiceDataId",
principalTable: "Invoices",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_ApiKeys_Stores_StoreId",
table: "ApiKeys",
column: "StoreId",
principalTable: "Stores",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_Apps_Stores_StoreDataId",
table: "Apps",
column: "StoreDataId",
principalTable: "Stores",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_Invoices_Stores_StoreDataId",
table: "Invoices",
column: "StoreDataId",
principalTable: "Stores",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_PairedSINData_Stores_StoreDataId",
table: "PairedSINData",
column: "StoreDataId",
principalTable: "Stores",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_Payments_Invoices_InvoiceDataId",
table: "Payments",
column: "InvoiceDataId",
principalTable: "Invoices",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_PendingInvoices_Invoices_Id",
table: "PendingInvoices",
column: "Id",
principalTable: "Invoices",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_RefundAddresses_Invoices_InvoiceDataId",
table: "RefundAddresses",
column: "InvoiceDataId",
principalTable: "Invoices",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_AddressInvoices_Invoices_InvoiceDataId",
table: "AddressInvoices");
migrationBuilder.DropForeignKey(
name: "FK_ApiKeys_Stores_StoreId",
table: "ApiKeys");
migrationBuilder.DropForeignKey(
name: "FK_Apps_Stores_StoreDataId",
table: "Apps");
migrationBuilder.DropForeignKey(
name: "FK_Invoices_Stores_StoreDataId",
table: "Invoices");
migrationBuilder.DropForeignKey(
name: "FK_PairedSINData_Stores_StoreDataId",
table: "PairedSINData");
migrationBuilder.DropForeignKey(
name: "FK_Payments_Invoices_InvoiceDataId",
table: "Payments");
migrationBuilder.DropForeignKey(
name: "FK_PendingInvoices_Invoices_Id",
table: "PendingInvoices");
migrationBuilder.DropForeignKey(
name: "FK_RefundAddresses_Invoices_InvoiceDataId",
table: "RefundAddresses");
migrationBuilder.AddForeignKey(
name: "FK_AddressInvoices_Invoices_InvoiceDataId",
table: "AddressInvoices",
column: "InvoiceDataId",
principalTable: "Invoices",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
migrationBuilder.AddForeignKey(
name: "FK_Apps_Stores_StoreDataId",
table: "Apps",
column: "StoreDataId",
principalTable: "Stores",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
migrationBuilder.AddForeignKey(
name: "FK_Invoices_Stores_StoreDataId",
table: "Invoices",
column: "StoreDataId",
principalTable: "Stores",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
migrationBuilder.AddForeignKey(
name: "FK_Payments_Invoices_InvoiceDataId",
table: "Payments",
column: "InvoiceDataId",
principalTable: "Invoices",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
migrationBuilder.AddForeignKey(
name: "FK_RefundAddresses_Invoices_InvoiceDataId",
table: "RefundAddresses",
column: "InvoiceDataId",
principalTable: "Invoices",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
}
}
}

@ -1,13 +1,9 @@
// <auto-generated />
using System;
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;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace BTCPayServer.Migrations
{
@ -18,7 +14,7 @@ namespace BTCPayServer.Migrations
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.0.2-rtm-10011");
.HasAnnotation("ProductVersion", "2.1.0-rtm-30799");
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
@ -202,8 +198,7 @@ namespace BTCPayServer.Migrations
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Id");
b.HasKey("Id");
@ -442,19 +437,29 @@ namespace BTCPayServer.Migrations
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("AddressInvoices")
.HasForeignKey("InvoiceDataId");
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("APIKeys")
.HasForeignKey("StoreId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("Apps")
.HasForeignKey("StoreDataId");
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData")
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("HistoricalAddressInvoices")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
@ -463,30 +468,49 @@ namespace BTCPayServer.Migrations
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany()
.HasForeignKey("StoreDataId");
.WithMany("Invoices")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData")
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("Events")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("PairedSINs")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("Payments")
.HasForeignKey("InvoiceDataId");
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("PendingInvoices")
.HasForeignKey("Id")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("RefundAddresses")
.HasForeignKey("InvoiceDataId");
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>

@ -20,5 +20,9 @@ namespace BTCPayServer.Models.AppViewModels
[Display(Name = "User can input custom amount")]
public bool ShowCustomAmount { get; set; }
public string Example1 { get; internal set; }
public string Example2 { get; internal set; }
public string ExampleCallback { get; internal set; }
public string InvoiceUrl { get; internal set; }
}
}

@ -79,7 +79,7 @@ namespace BTCPayServer.Models
//"price":5
[JsonProperty("price")]
public double Price
public decimal Price
{
get; set;
}
@ -94,7 +94,7 @@ namespace BTCPayServer.Models
//"exRates":{"USD":4320.02}
[JsonProperty("exRates")]
[Obsolete("Use CryptoInfo.ExRates instead")]
public Dictionary<string, double> ExRates
public Dictionary<string, decimal> ExRates
{
get; set;
}
@ -224,6 +224,29 @@ namespace BTCPayServer.Models
{
get; set;
}
[JsonProperty("paymentSubtotals")]
public Dictionary<string, long> PaymentSubtotals { get; set; }
[JsonProperty("paymentTotals")]
public Dictionary<string, long> PaymentTotals { get; set; }
[JsonProperty("amountPaid", DefaultValueHandling = DefaultValueHandling.Include)]
public long AmountPaid { get; set; }
[JsonProperty("minerFees")]
public Dictionary<string, NBitpayClient.MinerFeeInfo> MinerFees { get; set; }
[JsonProperty("exchangeRates")]
public Dictionary<string, Dictionary<string, decimal>> ExchangeRates { get; set; }
[JsonProperty("supportedTransactionCurrencies")]
public Dictionary<string, NBitpayClient.InvoiceSupportedTransactionCurrency> SupportedTransactionCurrencies { get; set; }
[JsonProperty("addresses")]
public Dictionary<string, string> Addresses { get; set; }
[JsonProperty("paymentCodes")]
public Dictionary<string, NBitpayClient.InvoicePaymentUrls> PaymentCodes { get; set; }
}
public class Flags
{
@ -233,4 +256,5 @@ namespace BTCPayServer.Models
get; set;
}
}
}

@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Validation;
namespace BTCPayServer.Models.InvoicingModels
{
@ -14,7 +15,7 @@ namespace BTCPayServer.Models.InvoicingModels
Currency = "USD";
}
[Required]
public double? Amount
public decimal? Amount
{
get; set;
}
@ -52,8 +53,7 @@ namespace BTCPayServer.Models.InvoicingModels
get; set;
}
[Url]
[Uri]
public string NotificationUrl
{
get; set;

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

@ -12,6 +12,9 @@ namespace BTCPayServer.Models.InvoicingModels
public string PaymentMethodId { get; set; }
public string CryptoImage { get; set; }
public string Link { get; set; }
public string PaymentMethodName { get; set; }
public bool IsLightning { get; set; }
public string CryptoCode { get; set; }
}
public string HtmlTitle { get; set; }
public string CustomCSSLink { get; set; }
@ -30,13 +33,13 @@ namespace BTCPayServer.Models.InvoicingModels
public string Status { get; set; }
public string MerchantRefLink { get; set; }
public int MaxTimeSeconds { get; set; }
// These properties are not used in client side code
public string StoreName { get; set; }
public string ItemDesc { get; set; }
public string TimeLeft { get; set; }
public string Rate { get; set; }
public string OrderAmount { get; set; }
public string OrderAmountFiat { get; set; }
public string InvoiceBitcoinUrl { get; set; }
public string InvoiceBitcoinUrlQR { get; set; }
public int TxCount { get; set; }
@ -44,12 +47,13 @@ namespace BTCPayServer.Models.InvoicingModels
public string StoreEmail { get; set; }
public string OrderId { get; set; }
public string CryptoImage { get; set; }
public decimal NetworkFee { get; set; }
public bool IsMultiCurrency { get; set; }
public int MaxTimeMinutes { get; internal set; }
public string PaymentType { get; internal set; }
public string PaymentMethodId { get; internal set; }
public int MaxTimeMinutes { get; set; }
public string PaymentType { get; set; }
public string PaymentMethodId { get; set; }
public string PaymentMethodName { get; set; }
public string CryptoImage { get; set; }
public bool AllowCoinConversion { get; set; }
public string PeerInfo { get; set; }

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models.ServerViewModels
{
public class LNDGRPCServicesViewModel
{
public string Host { get; set; }
public bool SSL { get; set; }
public string Macaroon { get; set; }
public string CertificateThumbprint { get; set; }
public string QRCode { get; set; }
public string QRCodeLink { get; set; }
}
}

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models.ServerViewModels
{
public class ServicesViewModel
{
public class LNDServiceViewModel
{
public string Crypto { get; set; }
public string Type { get; set; }
public int Index { get; set; }
}
public List<LNDServiceViewModel> LNDServices { get; set; } = new List<LNDServiceViewModel>();
}
}

@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services;
using BTCPayServer.Validation;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace BTCPayServer.Models.StoreViewModels
@ -42,10 +43,10 @@ namespace BTCPayServer.Models.StoreViewModels
public string OnChainMinValue { get; set; }
[Display(Name = "Link to a custom CSS stylesheet")]
[Url]
[Uri]
public string CustomCSS { get; set; }
[Display(Name = "Link to a custom logo")]
[Url]
[Uri]
public string CustomLogo { get; set; }
[Display(Name = "Custom HTML title to display on Checkout page")]

@ -9,8 +9,8 @@ namespace BTCPayServer.Models.StoreViewModels
{
public class LightningNodeViewModel
{
[Display(Name = "Lightning charge url")]
public string Url
[Display(Name = "Connection string")]
public string ConnectionString
{
get;
set;

@ -44,7 +44,7 @@ namespace BTCPayServer.Models.StoreViewModels
public string ScriptTest { get; set; }
public CoinAverageExchange[] AvailableExchanges { get; set; }
[Display(Name = "Multiply the rate by ...")]
[Display(Name = "Multiply the rate by... (Setting to 1.01 would apply a discount of 1% to the purchase)")]
[Range(0.01, 10.0)]
public double RateMultiplier
{

@ -1,6 +1,7 @@
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Validation;
using BTCPayServer.Validations;
using Microsoft.AspNetCore.Mvc.Rendering;
using System;
@ -24,6 +25,7 @@ namespace BTCPayServer.Models.StoreViewModels
}
public bool CanDelete { get; set; }
public string Id { get; set; }
[Display(Name = "Store Name")]
[Required]
@ -34,7 +36,7 @@ namespace BTCPayServer.Models.StoreViewModels
get; set;
}
[Url]
[Uri]
[Display(Name = "Store Website")]
[MaxLength(500)]
public string StoreWebsite

@ -3,6 +3,7 @@ using System;
using System.Collections.Generic;
using System.Text;
using NBitcoin;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Models
{
@ -44,6 +45,9 @@ namespace BTCPayServer.Models
public class PairingCodeResponse
{
[JsonProperty(PropertyName = "policies")]
public JArray Policies { get; set; }
[JsonProperty(PropertyName = "pairingCode")]
public string PairingCode
{

@ -68,6 +68,10 @@ namespace BTCPayServer.Payments.Bitcoin
{
return ConfirmationCount >= 1;
}
else if (speedPolicy == SpeedPolicy.LowMediumSpeed)
{
return ConfirmationCount >= 2;
}
else if (speedPolicy == SpeedPolicy.LowSpeed)
{
return ConfirmationCount >= 6;

@ -28,7 +28,7 @@ namespace BTCPayServer.Payments.Bitcoin
{
EventAggregator _Aggregator;
ExplorerClientProvider _ExplorerClients;
IApplicationLifetime _Lifetime;
Microsoft.Extensions.Hosting.IApplicationLifetime _Lifetime;
InvoiceRepository _InvoiceRepository;
private TaskCompletionSource<bool> _RunningTask;
private CancellationTokenSource _Cts;
@ -39,7 +39,7 @@ namespace BTCPayServer.Payments.Bitcoin
BTCPayWalletProvider wallets,
InvoiceRepository invoiceRepository,
BTCPayNetworkProvider networkProvider,
EventAggregator aggregator, IApplicationLifetime lifetime)
EventAggregator aggregator, Microsoft.Extensions.Hosting.IApplicationLifetime lifetime)
{
PollInterval = TimeSpan.FromMinutes(1.0);
_Wallets = wallets;
@ -161,7 +161,7 @@ namespace BTCPayServer.Payments.Bitcoin
{
var payment = await _InvoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, paymentData, network.CryptoCode);
if(payment != null)
await ReceivedPayment(wallet, invoice.Id, payment, evt.DerivationStrategy);
await ReceivedPayment(wallet, invoice, payment, evt.DerivationStrategy);
}
else
{
@ -207,6 +207,8 @@ namespace BTCPayServer.Payments.Bitcoin
async Task<InvoiceEntity> UpdatePaymentStates(BTCPayWallet wallet, string invoiceId)
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, false);
if (invoice == null)
return null;
List<PaymentEntity> updatedPaymentEntities = new List<PaymentEntity>();
var transactions = await wallet.GetTransactions(GetAllBitcoinPaymentData(invoice)
.Select(p => p.Outpoint.Hash)
@ -315,6 +317,8 @@ namespace BTCPayServer.Payments.Bitcoin
foreach (var invoiceId in invoices)
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, true);
if (invoice == null)
continue;
var alreadyAccounted = GetAllBitcoinPaymentData(invoice).Select(p => p.Outpoint).ToHashSet();
var strategy = GetDerivationStrategy(invoice, network);
if (strategy == null)
@ -332,8 +336,12 @@ namespace BTCPayServer.Payments.Bitcoin
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++;
{
invoice = await ReceivedPayment(wallet, invoice, payment, strategy);
if(invoice == null)
continue;
totalPayment++;
}
}
}
return totalPayment;
@ -346,10 +354,12 @@ namespace BTCPayServer.Payments.Bitcoin
.FirstOrDefault();
}
private async Task<InvoiceEntity> ReceivedPayment(BTCPayWallet wallet, string invoiceId, PaymentEntity payment, DerivationStrategyBase strategy)
private async Task<InvoiceEntity> ReceivedPayment(BTCPayWallet wallet, InvoiceEntity invoice, PaymentEntity payment, DerivationStrategyBase strategy)
{
var paymentData = (BitcoinLikePaymentData)payment.GetCryptoPaymentData();
var invoice = (await UpdatePaymentStates(wallet, invoiceId));
invoice = (await UpdatePaymentStates(wallet, invoice.Id));
if (invoice == null)
return null;
var paymentMethod = invoice.GetPaymentMethod(wallet.Network, PaymentTypes.BTCLike, _ExplorerClients.NetworkProviders);
if (paymentMethod != null &&
paymentMethod.GetPaymentMethodDetails() is BitcoinLikeOnChainPaymentMethod btc &&
@ -358,13 +368,13 @@ namespace BTCPayServer.Payments.Bitcoin
{
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));
await _InvoiceRepository.NewAddress(invoice.Id, btc, wallet.Network);
_Aggregator.Publish(new InvoiceNewAddressEvent(invoice.Id, address.ToString(), wallet.Network));
paymentMethod.SetPaymentMethodDetails(btc);
invoice.SetPaymentMethod(paymentMethod);
}
wallet.InvalidateCache(strategy);
_Aggregator.Publish(new InvoiceEvent(invoiceId, 1002, "invoice_receivedPayment"));
_Aggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1002, "invoice_receivedPayment"));
return invoice;
}
public Task StopAsync(CancellationToken cancellationToken)

@ -156,7 +156,7 @@ namespace BTCPayServer.Payments.Lightning.Charge
async Task<LightningInvoice> ILightningInvoiceClient.CreateInvoice(LightMoney amount, string description, TimeSpan expiry, CancellationToken cancellation)
{
var invoice = await CreateInvoiceAsync(new CreateInvoiceRequest() { Amount = amount, Expiry = expiry, Description = description ?? "" });
var invoice = await CreateInvoiceAsync(new CreateInvoiceRequest() { Amount = amount, Expiry = expiry, Description = description ?? "" }, cancellation);
return new LightningInvoice() { Id = invoice.Id, Amount = amount, BOLT11 = invoice.PayReq, Status = "unpaid" };
}

@ -4,6 +4,8 @@ using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Payments.Lightning.Charge;
using BTCPayServer.Payments.Lightning.CLightning;
using NBitcoin;
using BTCPayServer.Payments.Lightning.Lnd;
namespace BTCPayServer.Payments.Lightning
{
@ -12,16 +14,38 @@ namespace BTCPayServer.Payments.Lightning
public ILightningInvoiceClient CreateClient(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
{
var uri = supportedPaymentMethod.GetLightningUrl();
if (uri.ConnectionType == LightningConnectionType.Charge)
return CreateClient(uri, network.NBitcoinNetwork);
}
public static ILightningInvoiceClient CreateClient(LightningConnectionString connString, Network network)
{
if (connString.ConnectionType == LightningConnectionType.Charge)
{
return new ChargeClient(uri.ToUri(true), network.NBitcoinNetwork);
return new ChargeClient(connString.ToUri(true), network);
}
else if (uri.ConnectionType == LightningConnectionType.CLightning)
else if (connString.ConnectionType == LightningConnectionType.CLightning)
{
return new CLightningRPCClient(uri.ToUri(false), network.NBitcoinNetwork);
return new CLightningRPCClient(connString.ToUri(false), network);
}
else if (connString.ConnectionType == LightningConnectionType.LndREST)
{
return new LndInvoiceClient(new LndSwaggerClient(new LndRestSettings(connString.BaseUri)
{
Macaroon = connString.Macaroon,
MacaroonFilePath = connString.MacaroonFilePath,
CertificateThumbprint = connString.CertificateThumbprint,
AllowInsecure = connString.AllowInsecure,
}));
}
else
throw new NotSupportedException($"Unsupported connection string for lightning server ({uri.ConnectionType})");
throw new NotSupportedException($"Unsupported connection string for lightning server ({connString.ConnectionType})");
}
public static ILightningInvoiceClient CreateClient(string connectionString, Network network)
{
if (!Payments.Lightning.LightningConnectionString.TryParse(connectionString, false, out var conn, out string error))
throw new FormatException($"Invalid format ({error})");
return Payments.Lightning.LightningClientFactory.CreateClient(conn, network);
}
}
}

@ -1,25 +1,298 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Primitives;
using NBitcoin.DataEncoders;
namespace BTCPayServer.Payments.Lightning
{
public enum LightningConnectionType
{
Charge,
CLightning
CLightning,
LndREST,
LndGRPC
}
public class LightningConnectionString
{
public static bool TryParse(string str, out LightningConnectionString connectionString)
static Dictionary<string, LightningConnectionType> typeMapping;
static Dictionary<LightningConnectionType, string> typeMappingReverse;
static LightningConnectionString()
{
return TryParse(str, out connectionString, out var error);
typeMapping = new Dictionary<string, LightningConnectionType>();
typeMapping.Add("clightning", LightningConnectionType.CLightning);
typeMapping.Add("charge", LightningConnectionType.Charge);
typeMapping.Add("lnd-rest", LightningConnectionType.LndREST);
typeMapping.Add("lnd-grpc", LightningConnectionType.LndGRPC);
typeMappingReverse = new Dictionary<LightningConnectionType, string>();
foreach (var kv in typeMapping)
{
typeMappingReverse.Add(kv.Value, kv.Key);
}
}
public static bool TryParse(string str, out LightningConnectionString connectionString, out string error)
public static bool TryParse(string str, bool supportLegacy, out LightningConnectionString connectionString)
{
return TryParse(str, supportLegacy, out connectionString, out var error);
}
public static bool TryParse(string str, bool supportLegacy, out LightningConnectionString connectionString, out string error)
{
if (str == null)
throw new ArgumentNullException(nameof(str));
if (supportLegacy)
{
var parsed = TryParseLegacy(str, out connectionString, out error);
if (!parsed)
{
parsed = TryParseNewFormat(str, out connectionString, out error);
}
return parsed;
}
else
{
return TryParseNewFormat(str, out connectionString, out error);
}
}
private static bool TryParseNewFormat(string str, out LightningConnectionString connectionString, out string error)
{
connectionString = null;
error = null;
var parts = str.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
Dictionary<string, string> keyValues = new Dictionary<string, string>();
foreach (var part in parts.Select(p => p.Trim()))
{
var idx = part.IndexOf('=', StringComparison.OrdinalIgnoreCase);
if (idx == -1)
{
error = "The format of the connectionString should a list of key=value delimited by semicolon";
return false;
}
var key = part.Substring(0, idx).Trim().ToLowerInvariant();
var value = part.Substring(idx + 1).Trim();
if (keyValues.ContainsKey(key))
{
error = $"Duplicate key {key}";
return false;
}
keyValues.Add(key, value);
}
var possibleTypes = String.Join(", ", typeMapping.Select(k => k.Key).ToArray());
LightningConnectionString result = new LightningConnectionString();
var type = Take(keyValues, "type");
if (type == null)
{
error = $"The key 'type' is mandatory, possible values are {possibleTypes}";
return false;
}
if (!typeMapping.TryGetValue(type.ToLowerInvariant(), out var connectionType))
{
error = $"The key 'type' is invalid, possible values are {possibleTypes}";
return false;
}
result.ConnectionType = connectionType;
switch (connectionType)
{
case LightningConnectionType.Charge:
{
var server = Take(keyValues, "server");
if (server == null)
{
error = $"The key 'server' is mandatory for charge connection strings";
return false;
}
if (!Uri.TryCreate(server, UriKind.Absolute, out var uri)
|| (uri.Scheme != "http" && uri.Scheme != "https"))
{
error = $"The key 'server' should be an URI starting by http:// or https://";
return false;
}
parts = uri.UserInfo.Split(':');
if (!string.IsNullOrEmpty(uri.UserInfo) && parts.Length == 2)
{
result.Username = parts[0];
result.Password = parts[1];
}
else
{
var apiToken = Take(keyValues, "api-token");
if (apiToken == null)
{
error = "The key 'api-token' is not found";
return false;
}
result.Username = "api-token";
result.Password = apiToken;
}
result.BaseUri = new UriBuilder(uri) { UserName = "", Password = "" }.Uri;
}
break;
case LightningConnectionType.CLightning:
{
var server = Take(keyValues, "server");
if (server == null)
{
error = $"The key 'server' is mandatory for charge connection strings";
return false;
}
if (server.StartsWith("//", StringComparison.OrdinalIgnoreCase))
server = "unix:" + str;
else if (server.StartsWith("/", StringComparison.OrdinalIgnoreCase))
server = "unix:/" + str;
if (!Uri.TryCreate(server, UriKind.Absolute, out var uri)
|| (uri.Scheme != "tcp" && uri.Scheme != "unix"))
{
error = $"The key 'server' should be an URI starting by tcp:// or unix:// or a path to the 'lightning-rpc' unix socket";
return false;
}
result.BaseUri = uri;
}
break;
case LightningConnectionType.LndREST:
case LightningConnectionType.LndGRPC:
{
var server = Take(keyValues, "server");
if (server == null)
{
error = $"The key 'server' is mandatory for lnd connection strings";
return false;
}
if (!Uri.TryCreate(server, UriKind.Absolute, out var uri)
|| (uri.Scheme != "http" && uri.Scheme != "https"))
{
error = $"The key 'server' should be an URI starting by http:// or https://";
return false;
}
parts = uri.UserInfo.Split(':');
if (!string.IsNullOrEmpty(uri.UserInfo) && parts.Length == 2)
{
result.Username = parts[0];
result.Password = parts[1];
}
result.BaseUri = new UriBuilder(uri) { UserName = "", Password = "" }.Uri;
var macaroon = Take(keyValues, "macaroon");
if (macaroon != null)
{
try
{
result.Macaroon = Encoder.DecodeData(macaroon);
}
catch
{
error = $"The key 'macaroon' format should be in hex";
return false;
}
}
var macaroonFilePath = Take(keyValues, "macaroonfilepath");
if (macaroonFilePath != null)
{
if (macaroon != null)
{
error = $"The key 'macaroon' is already specified";
return false;
}
if (!macaroonFilePath.EndsWith(".macaroon", StringComparison.OrdinalIgnoreCase))
{
error = $"The key 'macaroonfilepath' should point to a .macaroon file";
return false;
}
result.MacaroonFilePath = macaroonFilePath;
}
string securitySet = null;
var certthumbprint = Take(keyValues, "certthumbprint");
if (certthumbprint != null)
{
try
{
var bytes = Encoders.Hex.DecodeData(certthumbprint.Replace(":", string.Empty, StringComparison.OrdinalIgnoreCase));
if (bytes.Length != 32)
{
error = $"The key 'certthumbprint' has invalid length: it should be the SHA256 of the PEM format of the certificate (32 bytes)";
return false;
}
result.CertificateThumbprint = bytes;
}
catch
{
error = $"The key 'certthumbprint' has invalid format: it should be the SHA256 of the PEM format of the certificate";
return false;
}
securitySet = "certthumbprint";
}
var allowinsecureStr = Take(keyValues, "allowinsecure");
if (allowinsecureStr != null)
{
var allowedValues = new[] { "true", "false" };
if (!allowedValues.Any(v => v.Equals(allowinsecureStr, StringComparison.OrdinalIgnoreCase)))
{
error = $"The key 'allowinsecure' should be true or false";
return false;
}
bool allowInsecure = allowinsecureStr.Equals("true", StringComparison.OrdinalIgnoreCase);
if (securitySet != null && allowInsecure)
{
error = $"The key 'allowinsecure' conflict with '{securitySet}'";
return false;
}
result.AllowInsecure = allowInsecure;
}
if (!result.AllowInsecure && result.BaseUri.Scheme == "http")
{
error = $"The key 'allowinsecure' is false, but server's Uri is not using https";
return false;
}
}
break;
default:
throw new NotSupportedException(connectionType.ToString());
}
if (keyValues.Count != 0)
{
error = $"Unknown keys ({String.Join(", ", keyValues.Select(k => k.Key).ToArray())})";
return false;
}
connectionString = result;
return true;
}
public LightningConnectionString Clone()
{
LightningConnectionString.TryParse(this.ToString(), false, out var result);
return result;
}
private static string Take(Dictionary<string, string> keyValues, string key)
{
if (keyValues.TryGetValue(key, out var v))
keyValues.Remove(key);
return v;
}
private static bool TryParseLegacy(string str, out LightningConnectionString connectionString, out string error)
{
if (str.StartsWith('/'))
str = "unix:" + str;
var result = new LightningConnectionString();
@ -27,7 +300,7 @@ namespace BTCPayServer.Payments.Lightning
error = null;
Uri uri;
if (!System.Uri.TryCreate(str, UriKind.Absolute, out uri))
if (!Uri.TryCreate(str, UriKind.Absolute, out uri))
{
error = "Invalid URL";
return false;
@ -40,7 +313,6 @@ namespace BTCPayServer.Payments.Lightning
error = $"The url support the following protocols {protocols}";
return false;
}
if (uri.Scheme == "unix")
{
str = uri.AbsoluteUri.Substring("unix:".Length);
@ -49,8 +321,12 @@ namespace BTCPayServer.Payments.Lightning
str = str.Substring(1);
}
uri = new Uri("unix://" + str, UriKind.Absolute);
result.ConnectionType = LightningConnectionType.CLightning;
}
if (uri.Scheme == "tcp")
result.ConnectionType = LightningConnectionType.CLightning;
if (uri.Scheme == "http" || uri.Scheme == "https")
{
var parts = uri.UserInfo.Split(':');
@ -61,6 +337,7 @@ namespace BTCPayServer.Payments.Lightning
}
result.Username = parts[0];
result.Password = parts[1];
result.ConnectionType = LightningConnectionType.Charge;
}
else if (!string.IsNullOrEmpty(uri.UserInfo))
{
@ -68,6 +345,7 @@ namespace BTCPayServer.Payments.Lightning
return false;
}
result.BaseUri = new UriBuilder(uri) { UserName = "", Password = "" }.Uri;
result.IsLegacy = true;
connectionString = result;
return true;
}
@ -80,15 +358,17 @@ namespace BTCPayServer.Payments.Lightning
public string Username { get; set; }
public string Password { get; set; }
public Uri BaseUri { get; set; }
public bool IsLegacy { get; private set; }
public LightningConnectionType ConnectionType
{
get
{
return BaseUri.Scheme == "http" || BaseUri.Scheme == "https" ? LightningConnectionType.Charge
: LightningConnectionType.CLightning;
}
get;
set;
}
public byte[] Macaroon { get; set; }
public string MacaroonFilePath { get; set; }
public byte[] CertificateThumbprint { get; set; }
public bool AllowInsecure { get; set; }
public Uri ToUri(bool withCredentials)
{
@ -101,10 +381,58 @@ namespace BTCPayServer.Payments.Lightning
return BaseUri;
}
}
static NBitcoin.DataEncoders.DataEncoder Encoder = NBitcoin.DataEncoders.Encoders.Hex;
public override string ToString()
{
return ToUri(true).AbsoluteUri;
var type = typeMappingReverse[ConnectionType];
StringBuilder builder = new StringBuilder();
builder.Append($"type={type}");
switch (ConnectionType)
{
case LightningConnectionType.Charge:
if (Username == null || Username == "api-token")
{
builder.Append($";server={BaseUri};api-token={Password}");
}
else
{
builder.Append($";server={ToUri(true)}");
}
break;
case LightningConnectionType.CLightning:
builder.Append($";server={BaseUri}");
break;
case LightningConnectionType.LndREST:
case LightningConnectionType.LndGRPC:
if (Username == null)
{
builder.Append($";server={BaseUri}");
}
else
{
builder.Append($";server={ToUri(true)}");
}
if (Macaroon != null)
{
builder.Append($";macaroon={Encoder.EncodeData(Macaroon)}");
}
if (MacaroonFilePath != null)
{
builder.Append($";macaroonfilepath={MacaroonFilePath}");
}
if (CertificateThumbprint != null)
{
builder.Append($";certthumbprint={Encoders.Hex.EncodeData(CertificateThumbprint)}");
}
if (AllowInsecure)
{
builder.Append($";allowinsecure=true");
}
break;
default:
throw new NotSupportedException(type);
}
return builder.ToString();
}
}
}

@ -15,6 +15,8 @@ namespace BTCPayServer.Payments.Lightning
{
public class LightningLikePaymentHandler : PaymentMethodHandlerBase<LightningSupportedPaymentMethod>
{
public static int LIGHTNING_TIMEOUT = 5000;
NBXplorerDashboard _Dashboard;
LightningClientFactory _LightningClientFactory;
public LightningLikePaymentHandler(
@ -36,17 +38,25 @@ namespace BTCPayServer.Payments.Lightning
expiry = TimeSpan.FromSeconds(1);
LightningInvoice lightningInvoice = null;
try
string description = storeBlob.LightningDescriptionTemplate;
description = description.Replace("{StoreName}", store.StoreName ?? "", StringComparison.OrdinalIgnoreCase)
.Replace("{ItemDescription}", invoice.ProductInformation.ItemDesc ?? "", StringComparison.OrdinalIgnoreCase)
.Replace("{OrderId}", invoice.OrderId ?? "", StringComparison.OrdinalIgnoreCase);
using (var cts = new CancellationTokenSource(LIGHTNING_TIMEOUT))
{
string description = storeBlob.LightningDescriptionTemplate;
description = description.Replace("{StoreName}", store.StoreName ?? "", StringComparison.OrdinalIgnoreCase)
.Replace("{ItemDescription}", invoice.ProductInformation.ItemDesc ?? "", StringComparison.OrdinalIgnoreCase)
.Replace("{OrderId}", invoice.OrderId ?? "", StringComparison.OrdinalIgnoreCase);
lightningInvoice = await client.CreateInvoice(new LightMoney(due, LightMoneyUnit.BTC), description, expiry);
}
catch (Exception ex)
{
throw new PaymentMethodUnavailableException($"Impossible to create lightning invoice ({ex.Message})", ex);
try
{
lightningInvoice = await client.CreateInvoice(new LightMoney(due, LightMoneyUnit.BTC), description, expiry, cts.Token);
}
catch (OperationCanceledException) when (cts.IsCancellationRequested)
{
throw new PaymentMethodUnavailableException($"The lightning node did not replied in a timely maner");
}
catch (Exception ex)
{
throw new PaymentMethodUnavailableException($"Impossible to create lightning invoice ({ex.Message})", ex);
}
}
var nodeInfo = await test;
return new LightningLikePaymentMethodDetails()
@ -62,34 +72,36 @@ namespace BTCPayServer.Payments.Lightning
if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
throw new PaymentMethodUnavailableException($"Full node not available");
var cts = new CancellationTokenSource(5000);
var client = _LightningClientFactory.CreateClient(supportedPaymentMethod, network);
LightningNodeInformation info = null;
try
using (var cts = new CancellationTokenSource(LIGHTNING_TIMEOUT))
{
info = await client.GetInfo(cts.Token);
}
catch (OperationCanceledException) when (cts.IsCancellationRequested)
{
throw new PaymentMethodUnavailableException($"The lightning node did not replied in a timely maner");
}
catch (Exception ex)
{
throw new PaymentMethodUnavailableException($"Error while connecting to the API ({ex.Message})");
}
var client = _LightningClientFactory.CreateClient(supportedPaymentMethod, network);
LightningNodeInformation info = null;
try
{
info = await client.GetInfo(cts.Token);
}
catch (OperationCanceledException) when (cts.IsCancellationRequested)
{
throw new PaymentMethodUnavailableException($"The lightning node did not replied in a timely maner");
}
catch (Exception ex)
{
throw new PaymentMethodUnavailableException($"Error while connecting to the API ({ex.Message})");
}
if (info.Address == null)
{
throw new PaymentMethodUnavailableException($"No lightning node public address has been configured");
}
if (info.Address == null)
{
throw new PaymentMethodUnavailableException($"No lightning node public address has been configured");
}
var blocksGap = Math.Abs(info.BlockHeight - summary.Status.ChainHeight);
if (blocksGap > 10)
{
throw new PaymentMethodUnavailableException($"The lightning is not synched ({blocksGap} blocks)");
}
var blocksGap = Math.Abs(info.BlockHeight - summary.Status.ChainHeight);
if (blocksGap > 10)
{
throw new PaymentMethodUnavailableException($"The lightning is not synched ({blocksGap} blocks)");
}
return new NodeInfo(info.NodeId, info.Address, info.P2PPort);
return new NodeInfo(info.NodeId, info.Address, info.P2PPort);
}
}
public async Task TestConnection(NodeInfo nodeInfo, CancellationToken cancellation)
@ -111,7 +123,7 @@ namespace BTCPayServer.Payments.Lightning
using (var tcp = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp))
{
await WithTimeout(tcp.ConnectAsync(new IPEndPoint(address, nodeInfo.Port)), cancellation);
await tcp.ConnectAsync(new IPEndPoint(address, nodeInfo.Port)).WithCancellation(cancellation);
}
}
catch (Exception ex)
@ -119,15 +131,5 @@ namespace BTCPayServer.Payments.Lightning
throw new PaymentMethodUnavailableException($"Error while connecting to the lightning node via {nodeInfo.Host}:{nodeInfo.Port} ({ex.Message})");
}
}
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);
}
}
}

@ -46,7 +46,7 @@ namespace BTCPayServer.Payments.Lightning
{
if (inv.Name == "invoice_created")
{
await EnsureListening(inv.InvoiceId, false);
await EnsureListening(inv.Invoice.Id, false);
}
}));
@ -143,11 +143,12 @@ namespace BTCPayServer.Payments.Lightning
CancellationTokenSource _Cts = new CancellationTokenSource();
private async Task Listen(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
{
ILightningListenInvoiceSession session = null;
try
{
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Start listening {supportedPaymentMethod.GetLightningUrl().BaseUri}");
var charge = _LightningClientFactory.CreateClient(supportedPaymentMethod, network);
var session = await charge.Listen(_Cts.Token);
var lightningClient = _LightningClientFactory.CreateClient(supportedPaymentMethod, network);
session = await lightningClient.Listen(_Cts.Token);
while (true)
{
var notification = await session.WaitInvoice(_Cts.Token);
@ -179,6 +180,10 @@ namespace BTCPayServer.Payments.Lightning
Logs.PayServer.LogError(ex, $"{supportedPaymentMethod.CryptoCode} (Lightning): Error while contacting {supportedPaymentMethod.GetLightningUrl().BaseUri}");
DoneListening(supportedPaymentMethod.GetLightningUrl());
}
finally
{
session?.Dispose();
}
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Stop listening {supportedPaymentMethod.GetLightningUrl().BaseUri}");
}
@ -189,8 +194,12 @@ namespace BTCPayServer.Payments.Lightning
BOLT11 = notification.BOLT11,
Amount = notification.Amount
}, network.CryptoCode, accounted: true);
if(payment != null)
_Aggregator.Publish(new InvoiceEvent(listenedInvoice.InvoiceId, 1002, "invoice_receivedPayment"));
if (payment != null)
{
var invoice = await _InvoiceRepository.GetInvoice(null, listenedInvoice.InvoiceId);
if(invoice != null)
_Aggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1002, "invoice_receivedPayment"));
}
}
List<Task> _ListeningLightning = new List<Task>();

@ -8,18 +8,41 @@ namespace BTCPayServer.Payments.Lightning
public class LightningSupportedPaymentMethod : ISupportedPaymentMethod
{
public string CryptoCode { get; set; }
[Obsolete("Use Get/SetLightningUrl")]
public string Username { get; set; }
[Obsolete("Use Get/SetLightningUrl")]
public string Password { get; set; }
// This property MUST be after CryptoCode or else JSON serialization fails
public PaymentMethodId PaymentId => new PaymentMethodId(CryptoCode, PaymentTypes.LightningLike);
[Obsolete("Use Get/SetLightningUrl")]
public string LightningChargeUrl { get; set; }
[Obsolete("Use Get/SetLightningUrl")]
public string LightningConnectionString { get; set; }
public LightningConnectionString GetLightningUrl()
{
#pragma warning disable CS0618 // Type or member is obsolete
var fullUri = new UriBuilder(LightningChargeUrl) { UserName = Username, Password = Password }.Uri.AbsoluteUri;
if(!LightningConnectionString.TryParse(fullUri, out var connectionString, out var error))
if (!string.IsNullOrEmpty(LightningConnectionString))
{
throw new FormatException(error);
if (!BTCPayServer.Payments.Lightning.LightningConnectionString.TryParse(LightningConnectionString, false, out var connectionString, out var error))
{
throw new FormatException(error);
}
return connectionString;
}
else
{
var fullUri = new UriBuilder(LightningChargeUrl) { UserName = Username, Password = Password }.Uri.AbsoluteUri;
if (!BTCPayServer.Payments.Lightning.LightningConnectionString.TryParse(fullUri, true, out var connectionString, out var error))
{
throw new FormatException(error);
}
return connectionString;
}
return connectionString;
#pragma warning restore CS0618 // Type or member is obsolete
}
@ -29,16 +52,11 @@ namespace BTCPayServer.Payments.Lightning
throw new ArgumentNullException(nameof(connectionString));
#pragma warning disable CS0618 // Type or member is obsolete
Username = connectionString.Username;
Password = connectionString.Password;
LightningChargeUrl = connectionString.BaseUri.AbsoluteUri;
LightningConnectionString = connectionString.ToString();
Username = null;
Password = null;
LightningChargeUrl = null;
#pragma warning restore CS0618 // Type or member is obsolete
}
[Obsolete("Use Get/SetLightningUrl")]
public string Username { get; set; }
[Obsolete("Use Get/SetLightningUrl")]
public string Password { get; set; }
public PaymentMethodId PaymentId => new PaymentMethodId(CryptoCode, PaymentTypes.LightningLike);
}
}

@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using NBitcoin.DataEncoders;
namespace BTCPayServer.Payments.Lightning.Lnd
{
public abstract class LndAuthentication
{
public class FixedMacaroonAuthentication : LndAuthentication
{
public FixedMacaroonAuthentication(byte[] macaroon)
{
if (macaroon == null)
throw new ArgumentNullException(nameof(macaroon));
Macaroon = macaroon;
}
public byte[] Macaroon { get; set; }
public override void AddAuthentication(HttpRequestMessage httpRequest)
{
httpRequest.Headers.Add("Grpc-Metadata-macaroon", Encoders.Hex.EncodeData(Macaroon));
}
}
public class NullAuthentication : LndAuthentication
{
public static NullAuthentication Instance { get; } = new NullAuthentication();
private NullAuthentication()
{
}
public override void AddAuthentication(HttpRequestMessage httpRequest)
{
}
}
public class MacaroonFileAuthentication : LndAuthentication
{
public MacaroonFileAuthentication(string filePath)
{
if (filePath == null)
throw new ArgumentNullException(nameof(filePath));
// Because this dump the whole file, let's make sure it is indeed the macaroon
if (!filePath.EndsWith(".macaroon", StringComparison.OrdinalIgnoreCase))
throw new ArgumentException(message: "filePath is not a macaroon file", paramName: nameof(filePath));
FilePath = filePath;
}
public string FilePath { get; set; }
public override void AddAuthentication(HttpRequestMessage httpRequest)
{
try
{
var bytes = File.ReadAllBytes(FilePath);
httpRequest.Headers.Add("Grpc-Metadata-macaroon", Encoders.Hex.EncodeData(bytes));
}
catch
{
}
}
}
public abstract void AddAuthentication(HttpRequestMessage httpRequest);
}
}

@ -0,0 +1,273 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Security;
using System.Runtime.ExceptionServices;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using NBitcoin;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Payments.Lightning.Lnd
{
public class LndInvoiceClient : ILightningInvoiceClient
{
class LndInvoiceClientSession : ILightningListenInvoiceSession
{
private LndSwaggerClient _Parent;
Channel<LightningInvoice> _Invoices = Channel.CreateBounded<LightningInvoice>(50);
CancellationTokenSource _Cts = new CancellationTokenSource();
HttpClient _Client;
HttpResponseMessage _Response;
Stream _Body;
StreamReader _Reader;
Task _ListenLoop;
public LndInvoiceClientSession(LndSwaggerClient parent)
{
_Parent = parent;
}
public Task StartListening()
{
try
{
_Client = _Parent.CreateHttpClient();
_Client.Timeout = TimeSpan.FromMilliseconds(Timeout.Infinite);
var request = new HttpRequestMessage(HttpMethod.Get, _Parent.BaseUrl.WithTrailingSlash() + "v1/invoices/subscribe");
_Parent._Authentication.AddAuthentication(request);
_ListenLoop = ListenLoop(request);
}
catch
{
Dispose();
}
return Task.CompletedTask;
}
private async Task ListenLoop(HttpRequestMessage request)
{
try
{
_Response = await _Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, _Cts.Token);
_Body = await _Response.Content.ReadAsStreamAsync();
_Reader = new StreamReader(_Body);
while (!_Cts.IsCancellationRequested)
{
string line = await _Reader.ReadLineAsync().WithCancellation(_Cts.Token);
if (line != null)
{
if (line.StartsWith("{\"result\":", StringComparison.OrdinalIgnoreCase))
{
var invoiceString = JObject.Parse(line)["result"].ToString();
LnrpcInvoice parsedInvoice = _Parent.Deserialize<LnrpcInvoice>(invoiceString);
await _Invoices.Writer.WriteAsync(ConvertLndInvoice(parsedInvoice), _Cts.Token);
}
else if (line.StartsWith("{\"error\":", StringComparison.OrdinalIgnoreCase))
{
var errorString = JObject.Parse(line)["error"].ToString();
var error = _Parent.Deserialize<LndError>(errorString);
throw new LndException(error);
}
else
{
throw new LndException("Unknown result from LND: " + line);
}
}
}
}
catch when (_Cts.IsCancellationRequested)
{
}
catch (Exception ex)
{
_Invoices.Writer.TryComplete(ex);
}
finally
{
Dispose(false);
}
}
public async Task<LightningInvoice> WaitInvoice(CancellationToken cancellation)
{
try
{
return await _Invoices.Reader.ReadAsync(cancellation);
}
catch (ChannelClosedException ex) when (ex.InnerException == null)
{
throw new TaskCanceledException();
}
catch (ChannelClosedException ex)
{
ExceptionDispatchInfo.Capture(ex.InnerException).Throw();
throw;
}
}
public void Dispose()
{
Dispose(true);
}
void Dispose(bool waitLoop)
{
if (_Cts.IsCancellationRequested)
return;
_Cts.Cancel();
_Reader?.Dispose();
_Reader = null;
_Body?.Dispose();
_Body = null;
_Response?.Dispose();
_Response = null;
_Client?.Dispose();
_Client = null;
if (waitLoop)
_ListenLoop?.Wait();
_Invoices.Writer.TryComplete();
}
}
public LndSwaggerClient _rpcClient;
public LndInvoiceClient(LndSwaggerClient swaggerClient)
{
if (swaggerClient == null)
throw new ArgumentNullException(nameof(swaggerClient));
_rpcClient = swaggerClient;
}
public async Task<LightningInvoice> CreateInvoice(LightMoney amount, string description, TimeSpan expiry,
CancellationToken cancellation = default(CancellationToken))
{
var strAmount = ConvertInv.ToString(amount.ToUnit(LightMoneyUnit.Satoshi));
var strExpiry = ConvertInv.ToString(Math.Round(expiry.TotalSeconds, 0));
// lnd requires numbers sent as strings. don't ask
var resp = await _rpcClient.AddInvoiceAsync(new LnrpcInvoice
{
Value = strAmount,
Memo = description,
Expiry = strExpiry
});
var invoice = new LightningInvoice
{
Id = BitString(resp.R_hash),
Amount = amount,
BOLT11 = resp.Payment_request,
Status = "unpaid"
};
return invoice;
}
public async Task<LightningNodeInformation> GetInfo(CancellationToken cancellation = default(CancellationToken))
{
var resp = await _rpcClient.GetInfoAsync(cancellation);
var nodeInfo = new LightningNodeInformation
{
BlockHeight = (int?)resp.Block_height ?? 0,
NodeId = resp.Identity_pubkey
};
var node = await _rpcClient.GetNodeInfoAsync(resp.Identity_pubkey, cancellation);
if (node.Node.Addresses == null || node.Node.Addresses.Count == 0)
throw new Exception("Lnd External IP not set, make sure you use --externalip=$EXTERNALIP parameter on lnd");
var firstNodeInfo = node.Node.Addresses.First();
var externalHostPort = firstNodeInfo.Addr.Split(':');
nodeInfo.Address = externalHostPort[0];
nodeInfo.P2PPort = ConvertInv.ToInt32(externalHostPort[1]);
return nodeInfo;
}
public async Task<LightningInvoice> GetInvoice(string invoiceId, CancellationToken cancellation = default(CancellationToken))
{
var resp = await _rpcClient.LookupInvoiceAsync(invoiceId, null, cancellation);
return ConvertLndInvoice(resp);
}
public async Task<ILightningListenInvoiceSession> Listen(CancellationToken cancellation = default(CancellationToken))
{
var session = new LndInvoiceClientSession(this._rpcClient);
await session.StartListening();
return session;
}
internal static LightningInvoice ConvertLndInvoice(LnrpcInvoice resp)
{
var invoice = new LightningInvoice
{
// TODO: Verify id corresponds to R_hash
Id = BitString(resp.R_hash),
Amount = new LightMoney(ConvertInv.ToInt64(resp.Value), LightMoneyUnit.Satoshi),
BOLT11 = resp.Payment_request,
Status = "unpaid"
};
if (resp.Settle_date != null)
{
invoice.PaidAt = DateTimeOffset.FromUnixTimeSeconds(ConvertInv.ToInt64(resp.Settle_date));
invoice.Status = "paid";
}
else
{
var invoiceExpiry = ConvertInv.ToInt64(resp.Creation_date) + ConvertInv.ToInt64(resp.Expiry);
if (DateTimeOffset.FromUnixTimeSeconds(invoiceExpiry) > DateTimeOffset.UtcNow)
{
invoice.Status = "expired";
}
}
return invoice;
}
// utility static methods... maybe move to separate class
private static string BitString(byte[] bytes)
{
return BitConverter.ToString(bytes)
.Replace("-", "", StringComparison.InvariantCulture)
.ToLower(CultureInfo.InvariantCulture);
}
// Invariant culture conversion
public static class ConvertInv
{
public static int ToInt32(string str)
{
return Convert.ToInt32(str, CultureInfo.InvariantCulture.NumberFormat);
}
public static long ToInt64(string str)
{
return Convert.ToInt64(str, CultureInfo.InvariantCulture.NumberFormat);
}
public static string ToString(decimal d)
{
return d.ToString(CultureInfo.InvariantCulture);
}
public static string ToString(double d)
{
return d.ToString(CultureInfo.InvariantCulture);
}
}
}
}

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