Compare commits

...

170 Commits

Author SHA1 Message Date
4b743bb998 bump 2018-07-24 22:26:48 +09:00
0af1adcfb8 Can update server 2018-07-24 22:10:37 +09:00
c048e4eeaf bump 2018-07-24 19:05:11 +09:00
64194896ba Add curl dependency because of https://github.com/dotnet/corefx/issues/30003 2018-07-24 19:04:48 +09:00
1d4472cc08 Show inner exception if SSL connection fail when changing domain 2018-07-24 18:47:55 +09:00
d8bc7ef4b8 Better description for changing domain 2018-07-24 18:23:16 +09:00
27cea81cb2 Add ability to change domain from server settings 2018-07-24 17:04:57 +09:00
b0d6216ffc Better timestamp for invoice logs, fix bugs which can happen if invoice get deleted 2018-07-24 12:19:43 +09:00
060876d07f Do not round satoshis to 8 decimal 2018-07-23 18:15:40 +09:00
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
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
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
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
6cefd9c3e7 Merge remote-tracking branch 'source/master' into dev-lndrpc 2018-05-16 04:50:46 -05: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
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
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
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
146 changed files with 17241 additions and 724 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
Data
DerivationSchemeParser.cs
Events
Extensions.cs
Filters
HostedServices
Hosting
Logging
Migrations
Models
Payments
Program.cs
Properties
Security
Services
Views
bundleconfig.json
wwwroot
DockerfileREADME.md

@ -5,6 +5,7 @@
<IsPackable>false</IsPackable>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
<LangVersion>7.2</LangVersion>
</PropertyGroup>
<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();
@ -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; }

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

@ -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)
{
@ -189,9 +214,14 @@ namespace BTCPayServer.Tests
{
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);
}
}
}
}

@ -38,6 +38,7 @@ using System.Text;
using BTCPayServer.Rating;
using BTCPayServer.Validation;
using ExchangeSharp;
using System.Security.Cryptography.X509Certificates;
namespace BTCPayServer.Tests
{
@ -321,7 +322,7 @@ namespace BTCPayServer.Tests
[Fact]
public void RoundupCurrenciesCorrectly()
{
foreach(var test in new[]
foreach (var test in new[]
{
(0.0005m, "$0.0005 (USD)"),
(0.001m, "$0.001 (USD)"),
@ -409,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);
@ -428,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.01m,
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.01m,
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))
@ -537,7 +587,10 @@ 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.01m,
@ -1309,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);

@ -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.8
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,7 +111,7 @@ services:
- "bitcoin_datadir:/data"
customer_lightningd:
image: nicolasdorier/clightning:0.0.0.20-dev
image: nicolasdorier/clightning:v0.6-dev
environment:
EXPOSE_TCP: "true"
LIGHTNINGD_OPT: |
@ -112,7 +134,7 @@ services:
- bitcoind
lightning-charged:
image: shesek/lightning-charge:0.3.12
image: shesek/lightning-charge:0.3.15
environment:
NETWORK: regtest
API_TOKEN: foiewnccewuify
@ -131,7 +153,7 @@ services:
- merchant_lightningd
merchant_lightningd:
image: nicolasdorier/clightning:0.0.0.20-dev
image: nicolasdorier/clightning:v0.6-dev
environment:
EXPOSE_TCP: "true"
LIGHTNINGD_OPT: |
@ -177,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'")
});

@ -10,6 +10,7 @@ namespace BTCPayServer
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,
@ -19,8 +20,8 @@ namespace BTCPayServer
"BTG_X = BTG_BTC * BTC_X",
"BTG_BTC = bitfinex(BTG_BTC)",
},
CryptoImagePath = "imlegacy/btg-symbol.svg",
LightningImagePath = "imlegacy/btg-symbol.svg",
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)
});
}
}

@ -16,6 +16,7 @@ namespace BTCPayServer
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,

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

@ -16,6 +16,7 @@ namespace BTCPayServer
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,

@ -16,6 +16,7 @@ namespace BTCPayServer
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,

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

@ -52,6 +52,9 @@ namespace BTCPayServer
InitMonacoin();
InitPolis();
InitFeathercoin();
InitGroestlcoin();
InitViacoin();
//InitUfo();
}
/// <summary>

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<Version>1.0.2.34</Version>
<Version>1.0.2.68</Version>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup>
<ItemGroup>
@ -35,19 +35,20 @@
<PackageReference Include="Hangfire" Version="1.6.19" />
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.2" />
<PackageReference Include="Hangfire.PostgreSql" Version="1.4.8.2" />
<PackageReference Include="LedgerWallet" Version="1.0.1.36" />
<PackageReference Include="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.10" />
<PackageReference Include="NBitpayClient" Version="1.0.0.28" />
<PackageReference Include="NBitcoin" Version="4.1.1.32" />
<PackageReference Include="NBitpayClient" Version="1.0.0.29" />
<PackageReference Include="DBreeze" Version="1.87.0" />
<PackageReference Include="NBXplorer.Client" Version="1.0.2.10" />
<PackageReference Include="NBXplorer.Client" Version="1.0.2.15" />
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.2" />
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.3" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.14" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.16" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.1.0" />
<PackageReference Include="SSH.NET" Version="2016.1.0" />
<PackageReference Include="System.Xml.XmlSerializer" Version="4.3.0" />
<PackageReference Include="Text.Analyzers" Version="2.6.0" />
</ItemGroup>
@ -111,6 +112,19 @@
<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\Maintenance.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;
}

@ -134,7 +134,7 @@ namespace BTCPayServer.Controllers
});
await UpdateAppSettings(app);
StatusMessage = "App updated";
return RedirectToAction(nameof(UpdatePointOfSale));
return RedirectToAction(nameof(ListApps));
}
[HttpGet]
@ -162,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();
}
}

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

@ -5,6 +5,7 @@ using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using BTCPayServer.Models;
using NBitcoin.DataEncoders;
namespace BTCPayServer.Controllers
{

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

@ -175,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
@ -186,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);
}
@ -233,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,
@ -264,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,
@ -273,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);
@ -284,9 +304,17 @@ namespace BTCPayServer.Controllers
return model;
}
private string GetDisplayName(PaymentMethodId paymentMethodId, BTCPayNetwork network)
{
return paymentMethodId.PaymentType == PaymentTypes.BTCLike ?
network.DisplayName : network.DisplayName + " (via Lightning)";
}
private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network)
{
return (paymentMethodId.PaymentType == PaymentTypes.BTCLike ? Url.Content(network.CryptoImagePath) : Url.Content(network.LightningImagePath));
var res = paymentMethodId.PaymentType == PaymentTypes.BTCLike ?
Url.Content(network.CryptoImagePath) : Url.Content(network.LightningImagePath);
return "/" + res;
}
private string OrderAmountFromInvoice(string cryptoCode, ProductInformation productInformation)
@ -323,7 +351,7 @@ namespace BTCPayServer.Controllers
provider = (NumberFormatInfo)provider.Clone();
provider.CurrencyDecimalDigits = divisibility;
}
if (currencyData.Crypto)
return price.ToString("C", provider);
else
@ -356,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));
@ -535,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,11 +78,14 @@ namespace BTCPayServer.Controllers
_EventAggregator = eventAggregator;
_NetworkProvider = networkProvider;
_WalletProvider = walletProvider;
_CSP = csp;
}
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl)
{
InvoiceLogs logs = new InvoiceLogs();
logs.Write("Creation of invoice starting");
var entity = new InvoiceEntity
{
InvoiceTime = DateTimeOffset.UtcNow
@ -115,7 +121,6 @@ namespace BTCPayServer.Controllers
entity.Status = "new";
entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy);
HashSet<CurrencyPair> currencyPairsToFetch = new HashSet<CurrencyPair>();
var rules = storeBlob.GetRateRules(_NetworkProvider);
@ -133,6 +138,7 @@ namespace BTCPayServer.Controllers
var rateRules = storeBlob.GetRateRules(_NetworkProvider);
var fetchingByCurrencyPair = _RateProvider.FetchRates(currencyPairsToFetch, rateRules);
var fetchingAll = WhenAllFetched(logs, fetchingByCurrencyPair);
var supportedPaymentMethods = store.GetSupportedPaymentMethods(_NetworkProvider)
.Select(c =>
(Handler: (IPaymentMethodHandler)_ServiceProvider.GetService(typeof(IPaymentMethodHandler<>).MakeGenericType(c.GetType())),
@ -141,65 +147,26 @@ namespace BTCPayServer.Controllers
.Where(c => c.Network != null)
.Select(o =>
(SupportedPaymentMethod: o.SupportedPaymentMethod,
PaymentMethod: CreatePaymentMethodAsync(fetchingByCurrencyPair, o.Handler, o.SupportedPaymentMethod, o.Network, entity, store)))
PaymentMethod: CreatePaymentMethodAsync(fetchingByCurrencyPair, o.Handler, o.SupportedPaymentMethod, o.Network, entity, store, logs)))
.ToList();
List<string> paymentMethodErrors = new List<string>();
List<ISupportedPaymentMethod> supported = new List<ISupportedPaymentMethod>();
var paymentMethods = new PaymentMethodDictionary();
foreach(var pair in fetchingByCurrencyPair)
{
var rateResult = await pair.Value;
bool hasError = false;
if(rateResult.Errors.Count != 0)
{
var allRateRuleErrors = string.Join(", ", rateResult.Errors.ToArray());
paymentMethodErrors.Add($"{pair.Key}: Rate rule error ({allRateRuleErrors})");
hasError = true;
}
if(rateResult.ExchangeExceptions.Count != 0)
{
foreach(var ex in rateResult.ExchangeExceptions)
{
paymentMethodErrors.Add($"{pair.Key}: Exception reaching exchange {ex.ExchangeName} ({ex.Exception.Message})");
}
hasError = true;
}
if(hasError)
{
paymentMethodErrors.Add($"{pair.Key}: The rule is {rateResult.Rule}");
paymentMethodErrors.Add($"{pair.Key}: Evaluated rule is {rateResult.EvaluatedRule}");
}
}
foreach (var o in supportedPaymentMethods)
{
try
{
var paymentMethod = await o.PaymentMethod;
if (paymentMethod == null)
throw new PaymentMethodUnavailableException("Payment method unavailable");
supported.Add(o.SupportedPaymentMethod);
paymentMethods.Add(paymentMethod);
}
catch (PaymentMethodUnavailableException ex)
{
paymentMethodErrors.Add($"{o.SupportedPaymentMethod.PaymentId.CryptoCode} ({o.SupportedPaymentMethod.PaymentId.PaymentType}): Payment method unavailable ({ex.Message})");
}
catch (Exception ex)
{
paymentMethodErrors.Add($"{o.SupportedPaymentMethod.PaymentId.CryptoCode} ({o.SupportedPaymentMethod.PaymentId.PaymentType}): Unexpected exception ({ex.ToString()})");
}
var paymentMethod = await o.PaymentMethod;
if (paymentMethod == null)
continue;
supported.Add(o.SupportedPaymentMethod);
paymentMethods.Add(paymentMethod);
}
if (supported.Count == 0)
{
StringBuilder errors = new StringBuilder();
errors.AppendLine("No payment method available for this store");
foreach (var error in paymentMethodErrors)
foreach (var error in logs.ToList())
{
errors.AppendLine(error);
errors.AppendLine(error.ToString());
}
throw new BitpayHttpException(400, errors.ToString());
}
@ -207,71 +174,108 @@ namespace BTCPayServer.Controllers
entity.SetSupportedPaymentMethods(supported);
entity.SetPaymentMethods(paymentMethods);
entity.PosData = invoice.PosData;
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, paymentMethodErrors, _NetworkProvider);
_EventAggregator.Publish(new Events.InvoiceEvent(entity, 1001, "invoice_created"));
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, logs, _NetworkProvider);
await fetchingAll;
_EventAggregator.Publish(new Events.InvoiceEvent(entity.EntityToDTO(_NetworkProvider), 1001, "invoice_created"));
var resp = entity.EntityToDTO(_NetworkProvider);
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
}
private async Task<PaymentMethod> CreatePaymentMethodAsync(Dictionary<CurrencyPair, Task<RateResult>> fetchingByCurrencyPair, IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network, InvoiceEntity entity, StoreData store)
private Task WhenAllFetched(InvoiceLogs logs, Dictionary<CurrencyPair, Task<RateResult>> fetchingByCurrencyPair)
{
var storeBlob = store.GetStoreBlob();
var rate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, entity.ProductInformation.Currency)];
if (rate.Value == null)
return null;
PaymentMethod paymentMethod = new PaymentMethod();
paymentMethod.ParentEntity = entity;
paymentMethod.Network = network;
paymentMethod.SetId(supportedPaymentMethod.PaymentId);
paymentMethod.Rate = rate.Value.Value;
var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, store, network);
if (storeBlob.NetworkFeeDisabled)
paymentDetails.SetNoTxFee();
paymentMethod.SetPaymentMethodDetails(paymentDetails);
Func<Money, Money, bool> compare = null;
CurrencyValue limitValue = null;
string errorMessage = null;
if (supportedPaymentMethod.PaymentId.PaymentType == PaymentTypes.LightningLike &&
storeBlob.LightningMaxValue != null)
return Task.WhenAll(fetchingByCurrencyPair.Select(async pair =>
{
compare = (a, b) => a > b;
limitValue = storeBlob.LightningMaxValue;
errorMessage = "The amount of the invoice is too high to be paid with lightning";
}
else if (supportedPaymentMethod.PaymentId.PaymentType == PaymentTypes.BTCLike &&
storeBlob.OnChainMinValue != null)
{
compare = (a, b) => a < b;
limitValue = storeBlob.OnChainMinValue;
errorMessage = "The amount of the invoice is too low to be paid on chain";
}
if (compare != null)
{
var limitValueRate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, limitValue.Currency)];
if (limitValueRate.Value.HasValue)
var rateResult = await pair.Value;
logs.Write($"{pair.Key}: The rating rule is {rateResult.Rule}");
logs.Write($"{pair.Key}: The evaluated rating rule is {rateResult.EvaluatedRule}");
if (rateResult.Errors.Count != 0)
{
var limitValueCrypto = Money.Coins(limitValue.Value / limitValueRate.Value.Value);
if (compare(paymentMethod.Calculate().Due, limitValueCrypto))
var allRateRuleErrors = string.Join(", ", rateResult.Errors.ToArray());
logs.Write($"{pair.Key}: Rate rule error ({allRateRuleErrors})");
}
if (rateResult.ExchangeExceptions.Count != 0)
{
foreach (var ex in rateResult.ExchangeExceptions)
{
throw new PaymentMethodUnavailableException(errorMessage);
logs.Write($"{pair.Key}: Exception reaching exchange {ex.ExchangeName} ({ex.Exception.Message})");
}
}
}
///////////////
}).ToArray());
}
private async Task<PaymentMethod> CreatePaymentMethodAsync(Dictionary<CurrencyPair, Task<RateResult>> fetchingByCurrencyPair, IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network, InvoiceEntity entity, StoreData store, InvoiceLogs logs)
{
try
{
var storeBlob = store.GetStoreBlob();
var rate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, entity.ProductInformation.Currency)];
if (rate.Value == null)
{
return null;
}
PaymentMethod paymentMethod = new PaymentMethod();
paymentMethod.ParentEntity = entity;
paymentMethod.Network = network;
paymentMethod.SetId(supportedPaymentMethod.PaymentId);
paymentMethod.Rate = rate.Value.Value;
var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, store, network);
if (storeBlob.NetworkFeeDisabled)
paymentDetails.SetNoTxFee();
paymentMethod.SetPaymentMethodDetails(paymentDetails);
Func<Money, Money, bool> compare = null;
CurrencyValue limitValue = null;
string errorMessage = null;
if (supportedPaymentMethod.PaymentId.PaymentType == PaymentTypes.LightningLike &&
storeBlob.LightningMaxValue != null)
{
compare = (a, b) => a > b;
limitValue = storeBlob.LightningMaxValue;
errorMessage = "The amount of the invoice is too high to be paid with lightning";
}
else if (supportedPaymentMethod.PaymentId.PaymentType == PaymentTypes.BTCLike &&
storeBlob.OnChainMinValue != null)
{
compare = (a, b) => a < b;
limitValue = storeBlob.OnChainMinValue;
errorMessage = "The amount of the invoice is too low to be paid on chain";
}
if (compare != null)
{
var limitValueRate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, limitValue.Currency)];
if (limitValueRate.Value.HasValue)
{
var limitValueCrypto = Money.Coins(limitValue.Value / limitValueRate.Value.Value);
if (compare(paymentMethod.Calculate().Due, limitValueCrypto))
{
logs.Write($"{supportedPaymentMethod.PaymentId.CryptoCode}: {errorMessage}");
return null;
}
}
}
///////////////
#pragma warning disable CS0618
if (paymentMethod.GetId().IsBTCOnChain)
{
entity.TxFee = paymentMethod.TxFee;
entity.Rate = paymentMethod.Rate;
entity.DepositAddress = paymentMethod.DepositAddress;
}
if (paymentMethod.GetId().IsBTCOnChain)
{
entity.TxFee = paymentMethod.TxFee;
entity.Rate = paymentMethod.Rate;
entity.DepositAddress = paymentMethod.DepositAddress;
}
#pragma warning restore CS0618
return paymentMethod;
return paymentMethod;
}
catch (PaymentMethodUnavailableException ex)
{
logs.Write($"{supportedPaymentMethod.PaymentId.CryptoCode}: Payment method unavailable ({ex.Message})");
}
catch (Exception ex)
{
logs.Write($"{supportedPaymentMethod.PaymentId.CryptoCode}: Unexpected exception ({ex.ToString()})");
}
return null;
}
private SpeedPolicy ParseSpeedPolicy(string transactionSpeed, SpeedPolicy defaultPolicy)

@ -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;
@ -16,6 +22,8 @@ using System.Net;
using System.Net.Http;
using System.Net.Mail;
using System.Threading.Tasks;
using Renci.SshNet;
using BTCPayServer.Logging;
namespace BTCPayServer.Controllers
{
@ -25,14 +33,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 +91,7 @@ namespace BTCPayServer.Controllers
try
{
var service = GetCoinaverageService(vm, true);
if(service != null)
if (service != null)
await service.TestAuthAsync();
}
catch
@ -134,6 +151,149 @@ namespace BTCPayServer.Controllers
return View(userVM);
}
[Route("server/maintenance")]
public IActionResult Maintenance()
{
MaintenanceViewModel vm = new MaintenanceViewModel();
vm.UserName = "btcpayserver";
vm.DNSDomain = this.Request.Host.Host;
if (IPAddress.TryParse(vm.DNSDomain, out var unused))
vm.DNSDomain = null;
return View(vm);
}
[Route("server/maintenance")]
[HttpPost]
public async Task<IActionResult> Maintenance(MaintenanceViewModel vm, string command)
{
if (!ModelState.IsValid)
return View(vm);
if (command == "changedomain")
{
if (string.IsNullOrWhiteSpace(vm.DNSDomain))
{
ModelState.AddModelError(nameof(vm.DNSDomain), $"Required field");
return View(vm);
}
vm.DNSDomain = vm.DNSDomain.Trim().ToLowerInvariant();
if (IPAddress.TryParse(vm.DNSDomain, out var unused))
{
ModelState.AddModelError(nameof(vm.DNSDomain), $"This should be a domain name");
return View(vm);
}
if (vm.DNSDomain.Equals(this.Request.Host.Host, StringComparison.InvariantCultureIgnoreCase))
{
ModelState.AddModelError(nameof(vm.DNSDomain), $"The server is already set to use this domain");
return View(vm);
}
var builder = new UriBuilder();
using (var client = new HttpClient(new HttpClientHandler()
{
ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
}))
{
try
{
builder.Scheme = this.Request.Scheme;
builder.Host = vm.DNSDomain;
if (this.Request.Host.Port != null)
builder.Port = this.Request.Host.Port.Value;
builder.Path = "runid";
builder.Query = $"expected={RunId}";
var response = await client.GetAsync(builder.Uri);
if (!response.IsSuccessStatusCode)
{
ModelState.AddModelError(nameof(vm.DNSDomain), $"Invalid host ({vm.DNSDomain} is not pointing to this BTCPay instance)");
return View(vm);
}
}
catch (Exception ex)
{
var messages = new List<object>();
messages.Add(ex.Message);
if (ex.InnerException != null)
messages.Add(ex.InnerException.Message);
ModelState.AddModelError(nameof(vm.DNSDomain), $"Invalid domain ({string.Join(", ", messages.ToArray())})");
return View(vm);
}
}
var error = RunSSH(vm, command, $"sudo bash -c '. /etc/profile.d/btcpay-env.sh && . changedomain.sh {vm.DNSDomain}'");
if (error != null)
return error;
builder.Path = null;
builder.Query = null;
StatusMessage = $"Domain name changing... the server will restart, please use \"{builder.Uri.AbsoluteUri}\"";
}
else if (command == "update")
{
var error = RunSSH(vm, command, $"sudo bash -c '. /etc/profile.d/btcpay-env.sh && btcpay-update.sh'");
if (error != null)
return error;
StatusMessage = $"The server might restart soon if an update is available...";
}
else
{
return NotFound();
}
return RedirectToAction(nameof(Maintenance));
}
public static string RunId = Encoders.Hex.EncodeData(NBitcoin.RandomUtils.GetBytes(32));
[HttpGet]
[Route("runid")]
[AllowAnonymous]
public IActionResult SeeRunId(string expected = null)
{
if (expected == RunId)
return Ok();
return BadRequest();
}
private IActionResult RunSSH(MaintenanceViewModel vm, string command, string ssh)
{
var sshClient = vm.CreateSSHClient(this.Request.Host.Host);
try
{
sshClient.Connect();
}
catch (Renci.SshNet.Common.SshAuthenticationException)
{
ModelState.AddModelError(nameof(vm.Password), "Invalid credentials");
sshClient.Dispose();
return View(vm);
}
catch (Exception ex)
{
var message = ex.Message;
if (ex is AggregateException aggrEx && aggrEx.InnerException?.Message != null)
{
message = aggrEx.InnerException.Message;
}
ModelState.AddModelError(nameof(vm.UserName), $"Connection problem ({message})");
sshClient.Dispose();
return View(vm);
}
var sshCommand = sshClient.CreateCommand(ssh);
sshCommand.CommandTimeout = TimeSpan.FromMinutes(1.0);
sshCommand.BeginExecute(ar =>
{
try
{
Logs.PayServer.LogInformation("Running SSH command: " + command);
var result = sshCommand.EndExecute(ar);
Logs.PayServer.LogInformation("SSH command executed: " + result);
}
catch (Exception ex)
{
Logs.PayServer.LogWarning("Error while executing SSH command: " + ex.Message);
}
sshClient.Dispose();
});
return null;
}
private static bool IsAdmin(IList<string> roles)
{
return roles.Contains(Roles.ServerAdmin, StringComparer.Ordinal);
@ -188,6 +348,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 +381,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 +520,7 @@ namespace BTCPayServer.Controllers
{
try
{
if(!model.Settings.IsComplete())
if (!model.Settings.IsComplete())
{
model.StatusMessage = "Error: Required fields missing";
return View(model);

@ -195,204 +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.GetKeyPath(network, strategy) == null)
{
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 };
}
BitcoinAddress destinationAddress = null;
if (destination != null)
{
try
{
destinationAddress = BitcoinAddress.Create(destination, network.NBitcoinNetwork);
}
catch { }
if (destinationAddress == null)
throw new FormatException("Invalid value for destination");
}
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);
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");
}
var unspentCoins = await wallet.GetUnspentCoins(strategyBase);
var changeAddress = await change;
var send = new[] { (
destination: destinationAddress as IDestination,
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);
if (foundKeyPath == null)
{
throw new HardwareWalletException($"This store is not configured to use this ledger");
}
TransactionBuilder builder = new TransactionBuilder();
builder.StandardTransactionPolicy.MinRelayTxFee = summary.Status.BitcoinStatus.MinRelayTxFee;
builder.SetConsensusFactory(network.NBitcoinNetwork);
builder.AddCoins(unspentCoins.Select(c => c.Coin).ToArray());
foreach (var element in send)
{
builder.Send(element.destination, element.amount);
if (element.substractFees)
builder.SubtractFees();
}
builder.SetChange(changeAddress.Item1);
builder.SendEstimatedFees(feeRateValue);
builder.Shuffle();
var unsigned = builder.BuildTransaction(false);
var keypaths = new Dictionary<Script, KeyPath>();
foreach (var c in unspentCoins)
{
keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath);
}
var hasChange = unsigned.Outputs.Count == 2;
var usedCoins = builder.FindSpentCoins(unsigned);
Dictionary<uint256, Transaction> parentTransactions = new Dictionary<uint256, Transaction>();
if(!strategy.Segwit)
{
var parentHashes = usedCoins.Select(c => c.Outpoint.Hash).ToHashSet();
var explorer = _ExplorerProvider.GetExplorerClient(network);
var getTransactionAsyncs = parentHashes.Select(h => (Op: explorer.GetTransactionAsync(h), Hash: h)).ToList();
foreach(var getTransactionAsync in getTransactionAsyncs)
foreach (var element in send)
{
var tx = (await getTransactionAsync.Op);
if(tx == null)
throw new Exception($"Parent transaction {getTransactionAsync.Hash} not found");
parentTransactions.Add(tx.Transaction.GetHash(), tx.Transaction);
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 transaction = await hw.SignTransactionAsync(usedCoins.Select(c => new SignatureRequest
{
InputTransaction = parentTransactions.TryGet(c.Outpoint.Hash),
InputCoin = c,
KeyPath = foundKeyPath.Derive(keypaths[c.TxOut.ScriptPubKey]),
PubKey = strategy.Root.Derive(keypaths[c.TxOut.ScriptPubKey]).PubKey
}).ToArray(), unsigned, hasChange ? foundKeyPath.Derive(changeAddress.Item2) : null);
try
{
var broadcastResult = await wallet.BroadcastTransactionsAsync(new List<Transaction>() { transaction });
if (!broadcastResult[0].Success)
var foundKeyPath = await hw.GetKeyPath(network, strategy, normalOperationTimeout.Token);
if (foundKeyPath == null)
{
throw new Exception($"RPC Error while broadcasting: {broadcastResult[0].RPCCode} {broadcastResult[0].RPCCodeMessage} {broadcastResult[0].RPCMessage}");
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 (Exception ex)
{
throw new Exception("Error while broadcasting: " + ex.Message);
}
wallet.InvalidateCache(strategyBase);
result = new SendToAddressResult() { TransactionId = transaction.GetHash().ToString() };
}
}
catch (OperationCanceledException)
{ result = new LedgerTestResult() { Success = false, Error = "Timeout" }; }
catch (Exception ex)
{ result = new LedgerTestResult() { Success = false, Error = ex.Message }; }
try
{
if (result != null)
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();
}

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

@ -283,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
});
}
@ -313,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"
});
@ -424,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;
@ -468,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)
{
@ -511,6 +510,29 @@ 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()

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

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

@ -29,6 +29,15 @@ namespace BTCPayServer.Data
_Type = type;
}
public DatabaseType Type
{
get
{
return _Type;
}
}
public ApplicationDbContext CreateContext()
{
var builder = new DbContextOptionsBuilder<ApplicationDbContext>();

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

@ -31,6 +31,7 @@ using System.Security.Claims;
using System.Globalization;
using BTCPayServer.Services;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
namespace BTCPayServer
{
@ -82,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();
@ -98,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(
@ -109,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;
@ -141,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);
}
@ -157,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
{
@ -50,6 +52,33 @@ namespace BTCPayServer.HostedServices
}
}
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
{
private SettingsRepository _SettingsRepository;

@ -308,7 +308,9 @@ namespace BTCPayServer.HostedServices
{
leases.Add(_EventAggregator.Subscribe<InvoiceEvent>(async e =>
{
var invoice = await _InvoiceRepository.GetInvoice(null, e.InvoiceId);
var invoice = await _InvoiceRepository.GetInvoice(null, e.Invoice.Id);
if (invoice == null)
return;
List<Task> tasks = new List<Task>();
// Awaiting this later help make sure invoices should arrive in order

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

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

@ -92,6 +92,7 @@ namespace BTCPayServer.Hosting
return opts.NetworkProvider;
});
services.TryAddSingleton<LightningConfigurationProvider>();
services.TryAddSingleton<LanguageService>();
services.TryAddSingleton<NBXplorerDashboard>();
services.TryAddSingleton<StoreRepository>();
@ -104,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>();

@ -39,6 +39,7 @@ 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
{
@ -79,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;

@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Logging
{
public class InvoiceLog
{
public DateTimeOffset Timestamp { get; set; }
public string Log { get; set; }
public override string ToString()
{
return $"{Timestamp.UtcDateTime}: {Log}";
}
}
public class InvoiceLogs
{
List<InvoiceLog> _InvoiceLogs = new List<InvoiceLog>();
public void Write(string data)
{
lock (_InvoiceLogs)
{
_InvoiceLogs.Add(new InvoiceLog() { Timestamp = DateTimeOffset.UtcNow, Log = data });
}
}
public List<InvoiceLog> ToList()
{
lock (_InvoiceLogs)
{
return _InvoiceLogs.ToList();
}
}
}
}

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

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

@ -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,8 +33,7 @@ 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; }
@ -45,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,24 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Renci.SshNet;
namespace BTCPayServer.Models.ServerViewModels
{
public class MaintenanceViewModel
{
[Required]
public string UserName { get; set; }
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
[Display(Name = "Change domain")]
public string DNSDomain { get; set; }
public SshClient CreateSSHClient(string host)
{
return new SshClient(host, UserName, Password);
}
}
}

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

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

@ -25,6 +25,7 @@ namespace BTCPayServer.Models.StoreViewModels
}
public bool CanDelete { get; set; }
public string Id { get; set; }
[Display(Name = "Store Name")]
[Required]

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

@ -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(
@ -41,7 +43,7 @@ namespace BTCPayServer.Payments.Lightning
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(5000))
using (var cts = new CancellationTokenSource(LIGHTNING_TIMEOUT))
{
try
{
@ -70,7 +72,7 @@ namespace BTCPayServer.Payments.Lightning
if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
throw new PaymentMethodUnavailableException($"Full node not available");
using (var cts = new CancellationTokenSource(5000))
using (var cts = new CancellationTokenSource(LIGHTNING_TIMEOUT))
{
var client = _LightningClientFactory.CreateClient(supportedPaymentMethod, network);
LightningNodeInformation info = null;
@ -121,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)
@ -129,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);
}
}
}
}

@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Payments.Lightning.Lnd
{
public class LndRestSettings
{
public LndRestSettings()
{
}
public LndRestSettings(Uri uri)
{
Uri = uri;
}
public Uri Uri { get; set; }
/// <summary>
/// The SHA256 of the PEM certificate
/// </summary>
public byte[] CertificateThumbprint { get; set; }
public byte[] Macaroon { get; set; }
public bool AllowInsecure { get; set; }
public string MacaroonFilePath { get; set; }
public LndAuthentication CreateLndAuthentication()
{
if (Macaroon != null)
return new LndAuthentication.FixedMacaroonAuthentication(Macaroon);
if (!string.IsNullOrEmpty(MacaroonFilePath))
return new LndAuthentication.MacaroonFileAuthentication(MacaroonFilePath);
return LndAuthentication.NullAuthentication.Instance;
}
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,119 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Security;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using NBitcoin.DataEncoders;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Payments.Lightning.Lnd
{
public class LndException : Exception
{
public LndException(string message) : base(message)
{
}
public LndException(LndError error) : base(error.Message)
{
if (error == null)
throw new ArgumentNullException(nameof(error));
_Error = error;
}
private readonly LndError _Error;
public LndError Error
{
get
{
return _Error;
}
}
}
// {"grpc_code":2,"http_code":500,"message":"rpc error: code = Unknown desc = expected 1 macaroon, got 0","http_status":"Internal Server Error"}
public class LndError
{
[JsonProperty("grpc_code")]
public int GRPCCode { get; set; }
[JsonProperty("http_code")]
public int HttpCode { get; set; }
[JsonProperty("message")]
public string Message { get; set; }
[JsonProperty("http_status")]
public string HttpStatus { get; set; }
}
public partial class LndSwaggerClient
{
public LndSwaggerClient(LndRestSettings settings)
{
if (settings == null)
throw new ArgumentNullException(nameof(settings));
_LndSettings = settings;
_Authentication = settings.CreateLndAuthentication();
BaseUrl = settings.Uri.AbsoluteUri.TrimEnd('/');
_httpClient = CreateHttpClient(settings);
_settings = new System.Lazy<Newtonsoft.Json.JsonSerializerSettings>(() =>
{
var json = new Newtonsoft.Json.JsonSerializerSettings();
UpdateJsonSerializerSettings(json);
return json;
});
}
LndRestSettings _LndSettings;
internal LndAuthentication _Authentication;
partial void PrepareRequest(HttpClient client, HttpRequestMessage request, string url)
{
_Authentication.AddAuthentication(request);
}
internal static HttpClient CreateHttpClient(LndRestSettings settings)
{
var handler = new HttpClientHandler
{
SslProtocols = SslProtocols.Tls12
};
var expectedThumbprint = settings.CertificateThumbprint?.ToArray();
if (expectedThumbprint != null)
{
handler.ServerCertificateCustomValidationCallback = (request, cert, chain, errors) =>
{
var actualCert = chain.ChainElements[chain.ChainElements.Count - 1].Certificate;
var hash = actualCert.GetCertHash(System.Security.Cryptography.HashAlgorithmName.SHA256);
return hash.SequenceEqual(expectedThumbprint);
};
}
if (settings.AllowInsecure)
{
handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
}
else
{
if (settings.Uri.Scheme == "http")
throw new InvalidOperationException("AllowInsecure is set to false, but the URI is not using https");
}
return new HttpClient(handler);
}
internal HttpClient CreateHttpClient()
{
return LndSwaggerClient.CreateHttpClient(_LndSettings);
}
internal T Deserialize<T>(string str)
{
return JsonConvert.DeserializeObject<T>(str, _settings.Value);
}
}
}

@ -31,9 +31,14 @@ namespace BTCPayServer
var logger = loggerFactory.CreateLogger("Configuration");
try
{
// This is the only way toat LoadArgs can print to console. Because LoadArgs is called by the HostBuilder before Logs.Configure is called
var conf = new DefaultConfiguration() { Logger = logger }.CreateConfiguration(args);
if (conf == null)
return;
Logs.Configure(loggerFactory);
new BTCPayServerOptions().LoadArgs(conf);
Logs.Configure(null);
/////
host = new WebHostBuilder()
.UseKestrel()
@ -64,6 +69,8 @@ namespace BTCPayServer
finally
{
processor.Dispose();
if(host == null)
Logs.Configuration.LogError("Configuration error");
if (host != null)
host.Dispose();
loggerProvider.Dispose();

@ -3,16 +3,17 @@
"Docker-Regtest": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"BTCPAY_NETWORK": "regtest",
"BTCPAY_LTCEXPLORERURL": "http://127.0.0.1:32838/",
"BTCPAY_BTCEXPLORERURL": "http://127.0.0.1:32838/",
"ASPNETCORE_ENVIRONMENT": "Development",
"BTCPAY_CHAINS": "btc,ltc",
"BTCPAY_BTCLIGHTNING": "http://api-token:foiewnccewuify@127.0.0.1:54938/",
"BTCPAY_POSTGRES": "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver",
"BTCPAY_BUNDLEJSCSS": "false"
},
"environmentVariables": {
"BTCPAY_NETWORK": "regtest",
"BTCPAY_LTCEXPLORERURL": "http://127.0.0.1:32838/",
"BTCPAY_BTCEXPLORERURL": "http://127.0.0.1:32838/",
"ASPNETCORE_ENVIRONMENT": "Development",
"BTCPAY_CHAINS": "btc,ltc",
"BTCPAY_BTCLIGHTNING": "type=charge;server=http://127.0.0.1:54938/;api-token=foiewnccewuify",
"BTCPAY_BTCEXTERNALLNDGRPC": "type=lnd-grpc;server=https://lnd:lnd@127.0.0.1:53280/;allowinsecure=true",
"BTCPAY_POSTGRES": "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver",
"BTCPAY_BUNDLEJSCSS": "false"
},
"applicationUrl": "http://127.0.0.1:14142/"
}
}

@ -0,0 +1,120 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BTCPayServer.Security
{
public class ConsentSecurityPolicy
{
public ConsentSecurityPolicy(string name, string value)
{
_Value = value;
_Name = name;
}
private readonly string _Name;
public string Name
{
get
{
return _Name;
}
}
private readonly string _Value;
public string Value
{
get
{
return _Value;
}
}
public override bool Equals(object obj)
{
ConsentSecurityPolicy item = obj as ConsentSecurityPolicy;
if (item == null)
return false;
return GetHashCode().Equals(item.GetHashCode());
}
public static bool operator ==(ConsentSecurityPolicy a, ConsentSecurityPolicy b)
{
if (System.Object.ReferenceEquals(a, b))
return true;
if (((object)a == null) || ((object)b == null))
return false;
return a.GetHashCode() == b.GetHashCode();
}
public static bool operator !=(ConsentSecurityPolicy a, ConsentSecurityPolicy b)
{
return !(a == b);
}
public override int GetHashCode()
{
return HashCode.Combine(Name, Value);
}
}
public class ContentSecurityPolicies
{
public ContentSecurityPolicies()
{
}
HashSet<ConsentSecurityPolicy> _Policies = new HashSet<ConsentSecurityPolicy>();
public void Add(ConsentSecurityPolicy policy)
{
if (_Policies.Any(p => p.Name == policy.Name && p.Value == policy.Name))
return;
_Policies.Add(policy);
}
public IEnumerable<ConsentSecurityPolicy> Rules => _Policies;
public bool HasRules => _Policies.Count != 0;
public override string ToString()
{
StringBuilder value = new StringBuilder();
bool firstGroup = true;
foreach(var group in Rules.GroupBy(r => r.Name))
{
if (!firstGroup)
{
value.Append(';');
}
List<string> values = new List<string>();
values.Add(group.Key);
foreach (var v in group)
{
values.Add(v.Value);
}
foreach(var i in authorized)
{
values.Add(i);
}
value.Append(String.Join(" ", values.OfType<object>().ToArray()));
firstGroup = false;
}
return value.ToString();
}
internal void Clear()
{
authorized.Clear();
_Policies.Clear();
}
HashSet<string> authorized = new HashSet<string>();
internal void AddAllAuthorized(string v)
{
authorized.Add(v);
}
public IEnumerable<string> Authorized => authorized;
}
}

@ -7,13 +7,17 @@ using System.Threading.Tasks;
using System.Text;
using NBXplorer;
using NBitcoin;
using Microsoft.AspNetCore.Http;
namespace BTCPayServer.Services
{
public class BTCPayServerEnvironment
{
public BTCPayServerEnvironment(IHostingEnvironment env, BTCPayNetworkProvider provider)
public BTCPayServerEnvironment(IHostingEnvironment env, BTCPayNetworkProvider provider, IHttpContextAccessor httpContext)
{
ExpectedHost = httpContext.HttpContext.Request.Host.Value;
ExpectedDomain = httpContext.HttpContext.Request.Host.Host;
ExpectedProtocol = httpContext.HttpContext.Request.Scheme;
Version = typeof(BTCPayServerEnvironment).GetTypeInfo().Assembly.GetCustomAttribute<AssemblyFileVersionAttribute>().Version;
#if DEBUG
Build = "Debug";
@ -27,6 +31,11 @@ namespace BTCPayServer.Services
{
get; set;
}
public string ExpectedDomain { get; set; }
public string ExpectedHost { get; set; }
public string ExpectedProtocol { get; set; }
public NetworkType NetworkType { get; set; }
public string Version
{

@ -20,9 +20,9 @@ namespace BTCPayServer.Services
public HardwareWalletException(string message) : base(message) { }
public HardwareWalletException(string message, Exception inner) : base(message, inner) { }
}
public class HardwareWalletService
public class HardwareWalletService : IDisposable
{
class WebSocketTransport : LedgerWallet.Transports.ILedgerTransport
class WebSocketTransport : LedgerWallet.Transports.ILedgerTransport, IDisposable
{
private readonly WebSocket webSocket;
@ -33,26 +33,36 @@ namespace BTCPayServer.Services
this.webSocket = webSocket;
}
public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(10);
public async Task<byte[][]> Exchange(byte[][] apdus)
SemaphoreSlim _Semaphore = new SemaphoreSlim(1, 1);
public async Task<byte[][]> Exchange(byte[][] apdus, CancellationToken cancellationToken)
{
await _Semaphore.WaitAsync();
List<byte[]> responses = new List<byte[]>();
using (CancellationTokenSource cts = new CancellationTokenSource(Timeout))
try
{
foreach (var apdu in apdus)
{
await this.webSocket.SendAsync(new ArraySegment<byte>(apdu), WebSocketMessageType.Binary, true, cts.Token);
await this.webSocket.SendAsync(new ArraySegment<byte>(apdu), WebSocketMessageType.Binary, true, cancellationToken);
}
foreach (var apdu in apdus)
{
byte[] response = new byte[300];
var result = await this.webSocket.ReceiveAsync(new ArraySegment<byte>(response), cts.Token);
var result = await this.webSocket.ReceiveAsync(new ArraySegment<byte>(response), cancellationToken);
Array.Resize(ref response, result.Count);
responses.Add(response);
}
}
finally
{
_Semaphore.Release();
}
return responses.ToArray();
}
public void Dispose()
{
_Semaphore.Dispose();
}
}
private readonly LedgerClient _Ledger;
@ -72,20 +82,20 @@ namespace BTCPayServer.Services
_Ledger = new LedgerClient(_Transport);
}
public async Task<LedgerTestResult> Test()
public async Task<LedgerTestResult> Test(CancellationToken cancellation)
{
var version = await _Ledger.GetFirmwareVersionAsync();
var version = await Ledger.GetFirmwareVersionAsync(cancellation);
return new LedgerTestResult() { Success = true };
}
public async Task<GetXPubResult> GetExtPubKey(BTCPayNetwork network, int account)
public async Task<GetXPubResult> GetExtPubKey(BTCPayNetwork network, int account, CancellationToken cancellation)
{
if (network == null)
throw new ArgumentNullException(nameof(network));
var segwit = network.NBitcoinNetwork.Consensus.SupportSegwit;
var path = network.GetRootKeyPath().Derive(account, true);
var pubkey = await GetExtPubKey(_Ledger, network, path, false);
var pubkey = await GetExtPubKey(Ledger, network, path, false, cancellation);
var derivation = new DerivationStrategyFactory(network.NBitcoinNetwork).CreateDirectDerivationStrategy(pubkey, new DerivationStrategyOptions()
{
P2SH = segwit,
@ -94,11 +104,11 @@ namespace BTCPayServer.Services
return new GetXPubResult() { ExtPubKey = derivation.ToString(), KeyPath = path };
}
private static async Task<BitcoinExtPubKey> GetExtPubKey(LedgerClient ledger, BTCPayNetwork network, KeyPath account, bool onlyChaincode)
private static async Task<BitcoinExtPubKey> GetExtPubKey(LedgerClient ledger, BTCPayNetwork network, KeyPath account, bool onlyChaincode, CancellationToken cancellation)
{
try
{
var pubKey = await ledger.GetWalletPubKeyAsync(account);
var pubKey = await ledger.GetWalletPubKeyAsync(account, cancellation: cancellation);
try
{
pubKey.GetAddress(network.NBitcoinNetwork);
@ -108,7 +118,7 @@ namespace BTCPayServer.Services
if (network.NBitcoinNetwork.NetworkType == NetworkType.Mainnet)
throw new Exception($"The opened ledger app does not seems to support {network.NBitcoinNetwork.Name}.");
}
var fingerprint = onlyChaincode ? new byte[4] : (await ledger.GetWalletPubKeyAsync(account.Parent)).UncompressedPublicKey.Compress().Hash.ToBytes().Take(4).ToArray();
var fingerprint = onlyChaincode ? new byte[4] : (await ledger.GetWalletPubKeyAsync(account.Parent, cancellation: cancellation)).UncompressedPublicKey.Compress().Hash.ToBytes().Take(4).ToArray();
var extpubkey = new ExtPubKey(pubKey.UncompressedPublicKey.Compress(), pubKey.ChainCode, (byte)account.Indexes.Length, fingerprint, account.Indexes.Last()).GetWif(network.NBitcoinNetwork);
return extpubkey;
}
@ -118,10 +128,10 @@ namespace BTCPayServer.Services
}
}
public async Task<KeyPath> GetKeyPath(BTCPayNetwork network, DirectDerivationStrategy directStrategy)
public async Task<KeyPath> GetKeyPath(BTCPayNetwork network, DirectDerivationStrategy directStrategy, CancellationToken cancellation)
{
List<KeyPath> derivations = new List<KeyPath>();
if(network.NBitcoinNetwork.Consensus.SupportSegwit)
if (network.NBitcoinNetwork.Consensus.SupportSegwit)
derivations.Add(new KeyPath("49'"));
derivations.Add(new KeyPath("44'"));
KeyPath foundKeyPath = null;
@ -132,7 +142,7 @@ namespace BTCPayServer.Services
{
try
{
var extpubkey = await GetExtPubKey(_Ledger, network, account, true);
var extpubkey = await GetExtPubKey(Ledger, network, account, true, cancellation);
if (directStrategy.Root.PubKey == extpubkey.ExtPubKey.PubKey)
{
foundKeyPath = account;
@ -150,10 +160,16 @@ namespace BTCPayServer.Services
public async Task<Transaction> SignTransactionAsync(SignatureRequest[] signatureRequests,
Transaction unsigned,
KeyPath changeKeyPath)
KeyPath changeKeyPath,
CancellationToken cancellationToken)
{
_Transport.Timeout = TimeSpan.FromMinutes(5);
return await Ledger.SignTransactionAsync(signatureRequests, unsigned, changeKeyPath);
return await Ledger.SignTransactionAsync(signatureRequests, unsigned, changeKeyPath, cancellationToken);
}
public void Dispose()
{
if (_Transport != null)
_Transport.Dispose();
}
}

@ -709,7 +709,8 @@ namespace BTCPayServer.Services.Invoices
accounting.Due = Money.Max(accounting.TotalDue - accounting.Paid, Money.Zero);
accounting.DueUncapped = accounting.TotalDue - accounting.Paid;
accounting.NetworkFee = accounting.TotalDue - totalDueNoNetworkCost;
accounting.MinimumTotalDue = Money.Max(Money.Satoshis(1), Money.Satoshis(accounting.TotalDue.Satoshi * (1.0m - ((decimal)ParentEntity.PaymentTolerance / 100.0m))));
var minimumTotalDueSatoshi = Math.Max(1.0m, accounting.TotalDue.Satoshi * (1.0m - ((decimal)ParentEntity.PaymentTolerance / 100.0m)));
accounting.MinimumTotalDue = Money.Satoshis(minimumTotalDueSatoshi);
return accounting;
}

@ -19,6 +19,7 @@ using System.Globalization;
using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Logging;
using BTCPayServer.Payments;
using System.Data.Common;
namespace BTCPayServer.Services.Invoices
{
@ -101,7 +102,7 @@ namespace BTCPayServer.Services.Invoices
}
}
public async Task<InvoiceEntity> CreateInvoiceAsync(string storeId, InvoiceEntity invoice, IEnumerable<string> creationLogs, BTCPayNetworkProvider networkProvider)
public async Task<InvoiceEntity> CreateInvoiceAsync(string storeId, InvoiceEntity invoice, InvoiceLogs creationLogs, BTCPayNetworkProvider networkProvider)
{
List<string> textSearch = new List<string>();
invoice = Clone(invoice, null);
@ -147,13 +148,13 @@ namespace BTCPayServer.Services.Invoices
}
context.PendingInvoices.Add(new PendingInvoiceData() { Id = invoice.Id });
foreach(var log in creationLogs)
foreach(var log in creationLogs.ToList())
{
context.InvoiceEvents.Add(new InvoiceEventData()
{
InvoiceDataId = invoice.Id,
Message = log,
Timestamp = invoice.InvoiceTime,
Message = log.Log,
Timestamp = log.Timestamp,
UniqueId = Encoders.Hex.EncodeData(RandomUtils.GetBytes(10))
});
}
@ -244,7 +245,11 @@ namespace BTCPayServer.Services.Invoices
Timestamp = DateTimeOffset.UtcNow,
UniqueId = Encoders.Hex.EncodeData(RandomUtils.GetBytes(10))
});
await context.SaveChangesAsync();
try
{
await context.SaveChangesAsync();
}
catch(DbUpdateException) { } // Probably the invoice does not exists anymore
}
}

@ -0,0 +1,54 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NBitcoin;
namespace BTCPayServer.Services
{
public class LightningConfigurationProvider
{
ConcurrentDictionary<ulong, (DateTimeOffset expiration, LightningConfigurations config)> _Map = new ConcurrentDictionary<ulong, (DateTimeOffset expiration, LightningConfigurations config)>();
public ulong KeepConfig(ulong secret, LightningConfigurations configuration)
{
CleanExpired();
_Map.AddOrReplace(secret, (DateTimeOffset.UtcNow + TimeSpan.FromMinutes(10), configuration));
return secret;
}
public LightningConfigurations GetConfig(ulong secret)
{
CleanExpired();
if (!_Map.TryGetValue(secret, out var value))
return null;
return value.config;
}
private void CleanExpired()
{
foreach(var item in _Map)
{
if(item.Value.expiration < DateTimeOffset.UtcNow)
{
_Map.TryRemove(item.Key, out var unused);
}
}
}
}
public class LightningConfigurations
{
public List<LightningConfiguration> Configurations { get; set; } = new List<LightningConfiguration>();
}
public class LightningConfiguration
{
public string Type { get; set; }
public string CryptoCode { get; set; }
public string Host { get; set; }
public int Port { get; set; }
public bool SSL { get; set; }
public string CertificateThumbprint { get; set; }
public string Macaroon { get; set; }
}
}

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Services
{
public class MigrationSettings
{
public bool UnreachableStoreCheck { get; set; }
public bool DeprecatedLightningConnectionStringCheck { get; set; }
}
}

@ -112,6 +112,20 @@ namespace BTCPayServer.Services.Stores
}
}
public async Task CleanUnreachableStores()
{
using (var ctx = _ContextFactory.CreateContext())
{
if (!ctx.Database.SupportDropForeignKey())
return;
foreach (var store in await ctx.Stores.Where(s => s.UserStores.Where(u => u.Role == StoreRoles.Owner).Count() == 0).ToArrayAsync())
{
ctx.Stores.Remove(store);
}
await ctx.SaveChangesAsync();
}
}
public async Task RemoveStoreUser(string storeId, string userId)
{
using (var ctx = _ContextFactory.CreateContext())
@ -120,6 +134,27 @@ namespace BTCPayServer.Services.Stores
ctx.UserStore.Add(userStore);
ctx.Entry<UserStore>(userStore).State = EntityState.Deleted;
await ctx.SaveChangesAsync();
}
await DeleteStoreIfOrphan(storeId);
}
private async Task DeleteStoreIfOrphan(string storeId)
{
using (var ctx = _ContextFactory.CreateContext())
{
if (ctx.Database.SupportDropForeignKey())
{
if (await ctx.UserStore.Where(u => u.StoreDataId == storeId && u.Role == StoreRoles.Owner).CountAsync() == 0)
{
var store = await ctx.Stores.FindAsync(storeId);
if (store != null)
{
ctx.Stores.Remove(store);
await ctx.SaveChangesAsync();
}
}
}
}
}
@ -158,6 +193,7 @@ namespace BTCPayServer.Services.Stores
ctx.UserStore.Remove(storeUser);
await ctx.SaveChangesAsync();
}
await DeleteStoreIfOrphan(storeId);
}
public async Task UpdateStore(StoreData store)
@ -169,5 +205,28 @@ namespace BTCPayServer.Services.Stores
await ctx.SaveChangesAsync().ConfigureAwait(false);
}
}
public async Task<bool> DeleteStore(string storeId)
{
using (var ctx = _ContextFactory.CreateContext())
{
if (!ctx.Database.SupportDropForeignKey())
return false;
var store = await ctx.Stores.FindAsync(storeId);
if (store == null)
return false;
ctx.Stores.Remove(store);
await ctx.SaveChangesAsync();
return true;
}
}
public bool CanDeleteStores()
{
using (var ctx = _ContextFactory.CreateContext())
{
return ctx.Database.SupportDropForeignKey();
}
}
}
}

@ -74,8 +74,8 @@
</section>
@section Scripts {
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/default.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js"></script>
<link rel="stylesheet" href="~/vendor/highlightjs/default.min.css">
<script src="~/vendor/highlightjs/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
}

@ -57,11 +57,14 @@
<div class="container text-center">
<h2>Video tutorials</h2>
<div class="row">
<div class="col-md-6 text-center">
<iframe width="400" height="225" src="https://www.youtube.com/embed/xh3Eac66qc4" frameborder="0" allowfullscreen></iframe>
<div class="col-md-4 text-center">
</div>
<div class="col-md-4 text-center">
<a href="https://www.youtube.com/channel/UCpG9WL6TJuoNfFVkaDMp9ug" target="_blank">
<img src="~/img/youtube.png" height="225" width="400" />
</a>
</div>
<div class="col-md-6 text-center">
<iframe width="400" height="225" src="https://www.youtube.com/embed/Xo_vApXTZBU" frameborder="0" allowfullscreen></iframe>
</div>
</div>
</div>

@ -42,14 +42,30 @@
</div>
</div>
<div class="single-item-order__right">
<div class="payment__currencies">
@foreach (var crypto in Model.AvailableCryptos)
{
<a href="@crypto.Link" onclick="return changeCurrency('@crypto.PaymentMethodId');">
<img style="height:32px; margin-left:5px;" alt="@crypto.PaymentMethodId" src="@crypto.CryptoImage" />
</a>
}
</div>
@if (Model.AvailableCryptos.Count > 1)
{
<div class="payment__currencies" onclick="openPaymentMethodDialog()">
<img v-bind:src="srvModel.cryptoImage" />
<span class="clickable_underline">{{srvModel.paymentMethodName}} ({{srvModel.cryptoCode}})</span>
<span v-show="srvModel.isLightning">&#9889;</span>
<span class="clickable_indicator fa fa-angle-right"></span>
</div>
<div id="vexPopupDialog">
<ul class="vexmenu">
@foreach (var crypto in Model.AvailableCryptos)
{
<li class="vexmenuitem">
<a href="@crypto.Link" onclick="return closePaymentMethodDialog('@crypto.PaymentMethodId');">
<img alt="@crypto.PaymentMethodName" src="@crypto.CryptoImage" />
@crypto.PaymentMethodName
@(crypto.IsLightning ? Html.Raw("&#9889;") : null)
<span>@crypto.CryptoCode</span>
</a>
</li>
}
</ul>
</div>
}
<div class="payment__spinner">
<partial name="Checkout-Spinner" />
</div>

@ -20,12 +20,15 @@
</script>
<bundle name="wwwroot/bundles/checkout-bundle.min.js" />
<script>vex.defaultOptions.className = 'vex-theme-btcpay'</script>
@if(Model.CustomCSSLink != null)
@if (Model.CustomCSSLink != null)
{
<link href="@Model.CustomCSSLink" rel="stylesheet" />
}
<style type="text/css">
</style>
</head>
<body style="background: #E4E4E4">
<noscript>
@ -82,7 +85,7 @@
$('select').prettyDropdown({
classic: false,
height: 30,
height: 32,
reverse: true,
hoverIntent: 5000
});

@ -0,0 +1,106 @@
@model BTCPayServer.Models.ServerViewModels.LNDGRPCServicesViewModel
@{
ViewData.SetActivePageAndTitle(ServerNavPages.Services);
}
<h4>GRPC settings</h4>
<partial name="_StatusMessage" for="@TempData["StatusMessage"]" />
<div class="row">
<div class="col-md-6">
<div asp-validation-summary="All" class="text-danger"></div>
</div>
</div>
<div class="row">
<div class="col-md-8">
<div class="form-group">
<p>
<span>BTCPay exposes gRPC services for outside consumption, you will find connection informaiton here.<br /></span>
</p>
</div>
<div class="form-group">
<h5>QR Code connection</h5>
<p>
<span>You can use this QR Code to connect your Zap wallet to your LND instance.<br /></span>
<span>This QR Code is only valid for 10 minutes</span>
</p>
</div>
<div class="form-group">
@if(Model.QRCode == null)
{
<div class="form-group">
<form method="post">
<button type="submit" class="btn btn-primary">Show QR Code</button>
</form>
</div>
}
else
{
<div class="form-group">
<div id="qrCode"></div>
<div id="qrCodeData" data-url="@Html.Raw(Model.QRCode)"></div>
</div>
<p>See QR Code information by clicking <a href="#detailsQR" data-toggle="collapse">here</a></p>
<div id="detailsQR" class="collapse">
<div class="form-group">
<label>QR Code data</label>
<input asp-for="QRCode" readonly class="form-control" />
</div>
<div class="form-group">
Click <a href="@Model.QRCodeLink" target="_blank">here</a> to open the configuration file.
</div>
</div>
}
<div class="form-group">
<h5>More details...</h5>
<p>Alternatively, you can see the settings by clicking <a href="#details" data-toggle="collapse">here</a></p>
</div>
<div id="details" class="collapse">
<div class="form-group">
<label asp-for="Host"></label>
<input asp-for="Host" readonly class="form-control" />
</div>
<div class="form-group">
<label asp-for="SSL"></label>
<input asp-for="SSL" disabled type="checkbox" class="form-check-inline" />
</div>
@if(Model.Macaroon != null)
{
<div class="form-group">
<label asp-for="Macaroon"></label>
<input asp-for="Macaroon" readonly class="form-control" />
</div>
}
@if(Model.CertificateThumbprint != null)
{
<div class="form-group">
<label asp-for="CertificateThumbprint"></label>
<input asp-for="CertificateThumbprint" readonly class="form-control" />
</div>
}
</div>
</div>
</div>
</div>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
@if(Model.QRCode != null)
{
<script type="text/javascript" src="~/js/qrcode.min.js"></script>
<script type="text/javascript">
new QRCode(document.getElementById("qrCode"),
{
text: "@Html.Raw(Model.QRCode)",
width: 150,
height: 150
});
</script>
}
}

@ -0,0 +1,62 @@
@model BTCPayServer.Models.ServerViewModels.MaintenanceViewModel
@{
ViewData.SetActivePageAndTitle(ServerNavPages.Maintainance);
}
<h4>@ViewData["Title"]</h4>
<partial name="_StatusMessage" for="@TempData["TempDataProperty-StatusMessage"]" />
<div class="row">
<div class="col-md-8">
<form method="post">
<div class="form-group">
<h5>SSH Settings</h5>
<span>For changing any settings, you need to enter your SSH credentials:</span>
</div>
<div class="form-group">
<label asp-for="UserName"></label>
<input asp-for="UserName" class="form-control" />
<span asp-validation-for="UserName" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Password"></label>
<input asp-for="Password" class="form-control" />
<span asp-validation-for="Password" class="text-danger"></span>
</div>
<div class="form-group">
<h5>Change domain name</h5>
<span>You can change the domain name of your server by following <a href="https://github.com/btcpayserver/btcpayserver-doc/blob/master/ChangeDomain.md">this guide</a></span>
</div>
<div class="form-group">
<div class="input-group">
<input asp-for="DNSDomain" class="form-control" />
<span class="input-group-btn">
<button name="command" type="submit" class="btn btn-primary" value="changedomain" title="Change domain">
<span class="fa fa-check"></span> Confirm
</button>
</span>
</div>
<span asp-validation-for="DNSDomain" class="text-danger"></span>
</div>
<div class="form-group">
<h5>Update</h5>
<span>Click here to update your server</span>
</div>
<div class="form-group">
<div class="input-group">
<button name="command" type="submit" class="btn btn-primary" value="update">Update</button>
</div>
</div>
</form>
</div>
</div>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
}

@ -7,6 +7,6 @@ namespace BTCPayServer.Views.Server
{
public enum ServerNavPages
{
Index, Users, Rates, Emails, Policies, Theme, Hangfire
Index, Users, Rates, Emails, Policies, Theme, Hangfire, Services, Maintainance
}
}

@ -0,0 +1,53 @@
@model BTCPayServer.Models.ServerViewModels.ServicesViewModel
@{
ViewData.SetActivePageAndTitle(ServerNavPages.Services);
}
<h4>@ViewData["Title"]</h4>
<partial name="_StatusMessage" for="@TempData["StatusMessage"]" />
<div class="row">
<div class="col-md-6">
<div asp-validation-summary="All" class="text-danger"></div>
</div>
</div>
<div class="row">
@if(Model.LNDServices.Count != 0)
{
<div class="col-md-8">
<div class="form-group">
<h5>LND nodes</h5>
<span>You can get access to internal LND services here. For gRPC, only BTC is supported.</span>
</div>
<div class="form-group">
<table class="table table-sm table-responsive-md">
<thead>
<tr>
<th>Crypto</th>
<th>Access Type</th>
<th style="text-align:right">Actions</th>
</tr>
</thead>
<tbody>
@foreach(var lnd in Model.LNDServices)
{
<tr>
<td>@lnd.Crypto</td>
<td>@lnd.Type</td>
<td style="text-align:right">
<a asp-action="LNDGRPCServices" asp-route-cryptoCode="@lnd.Crypto" asp-route-index="@lnd.Index">See information</a>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}
</div>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
}

@ -3,7 +3,9 @@
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Rates)" asp-action="Rates">Rates</a>
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Emails)" asp-action="Emails">Email server</a>
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Policies)" asp-action="Policies">Policies</a>
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Services)" asp-action="Services">Services</a>
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Theme)" asp-action="Theme">Theme</a>
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Maintainance)" asp-action="Maintenance">Maintenance</a>
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Hangfire)" href="~/hangfire" target="_blank">Hangfire</a>
</div>

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