diff --git a/BTCPayServer.Tests/Lnd/UnitTests.cs b/BTCPayServer.Tests/Lnd/UnitTests.cs index ecc0747c4..fd893dace 100644 --- a/BTCPayServer.Tests/Lnd/UnitTests.cs +++ b/BTCPayServer.Tests/Lnd/UnitTests.cs @@ -58,8 +58,26 @@ namespace BTCPayServer.Tests.Lnd Assert.Equal(createInvoice.BOLT11, getInvoice.BOLT11); } + // integration tests + [Fact] + public async Task TestWaitListenInvoice() + { + var merchantInvoice = await InvoiceClient.CreateInvoice(10000, "Hello world", TimeSpan.FromSeconds(3600)); - //integration tests + 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); + } [Fact] public async Task CreateLndInvoiceAndPay() diff --git a/BTCPayServer/Payments/Lightning/Lnd/LndInvoiceClient.cs b/BTCPayServer/Payments/Lightning/Lnd/LndInvoiceClient.cs index e182b0240..8484a32a2 100644 --- a/BTCPayServer/Payments/Lightning/Lnd/LndInvoiceClient.cs +++ b/BTCPayServer/Payments/Lightning/Lnd/LndInvoiceClient.cs @@ -70,21 +70,18 @@ namespace BTCPayServer.Payments.Lightning.Lnd var resp = await _rpcClient.LookupInvoiceAsync(invoiceId, null, cancellation); return ConvertLndInvoice(resp); } - - // TODO: These two methods where you wait on invoice are still work in progress + public Task Listen(CancellationToken cancellation = default(CancellationToken)) { - throw new NotImplementedException(); - //return Task.FromResult(this); + Task.Run(_rpcClient.StartSubscribeInvoiceThread); + return Task.FromResult(this); } async Task ILightningListenInvoiceSession.WaitInvoice(CancellationToken cancellation) { - throw new NotImplementedException(); - //var resp = await _rpcClient.SubscribeInvoicesAsync(cancellation); - //return ConvertLndInvoice(resp); + var resp = await _rpcClient.InvoiceResponse.Task; + return ConvertLndInvoice(resp); } - // Eof work in progress // utility static methods... maybe move to separate class diff --git a/BTCPayServer/Payments/Lightning/Lnd/LndSwaggerClientCustomHttp.cs b/BTCPayServer/Payments/Lightning/Lnd/LndSwaggerClientCustomHttp.cs index 8d0d02ca7..c4b6c0699 100644 --- a/BTCPayServer/Payments/Lightning/Lnd/LndSwaggerClientCustomHttp.cs +++ b/BTCPayServer/Payments/Lightning/Lnd/LndSwaggerClientCustomHttp.cs @@ -1,10 +1,14 @@ 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; @@ -12,7 +16,7 @@ namespace BTCPayServer.Payments.Lightning.Lnd { public class LndSwaggerClientCustomHttp : LndSwaggerClient, IDisposable { - public LndSwaggerClientCustomHttp(string baseUrl, HttpClient httpClient) + protected LndSwaggerClientCustomHttp(string baseUrl, HttpClient httpClient) : base(baseUrl, httpClient) { _HttpClient = httpClient; @@ -28,23 +32,36 @@ namespace BTCPayServer.Payments.Lightning.Lnd // public static LndSwaggerClientCustomHttp Create(Uri uri, Network network, byte[] tlsCertificate = null, byte[] grpcMacaroon = null) { - // for development we are working with custom build of lnd that allows no macaroons and http - var clientWithNoMacaroonsTls = tlsCertificate == null || grpcMacaroon == null; + var factory = new HttpClientFactoryForLnd(tlsCertificate, grpcMacaroon); + var httpClient = factory.Generate(); - var httpClient = clientWithNoMacaroonsTls ? new HttpClient() : - HttpClientFactoryForLnd.Generate(tlsCertificate, grpcMacaroon); + var swagger = new LndSwaggerClientCustomHttp(uri.ToString().TrimEnd('/'), httpClient); + swagger.HttpClientFactory = factory; - return new LndSwaggerClientCustomHttp(uri.ToString().TrimEnd('/'), httpClient); + return swagger; } } internal class HttpClientFactoryForLnd { - internal static HttpClient Generate(byte[] tlsCertificate, byte[] grpcMacaroon) + public HttpClientFactoryForLnd(byte[] tlsCertificate = null, byte[] grpcMacaroon = null) { - var httpClient = new HttpClient(GetCertificate(tlsCertificate)); - var macaroonHex = BitConverter.ToString(grpcMacaroon).Replace("-", "", StringComparison.InvariantCulture); - httpClient.DefaultRequestHeaders.Add("Grpc-Metadata-macaroon", macaroonHex); + TlsCertificate = tlsCertificate; + GrpcMacaroon = grpcMacaroon; + } + + public byte[] TlsCertificate { get; set; } + public byte[] GrpcMacaroon { get; set; } + + public HttpClient Generate() + { + var httpClient = new HttpClient(GetCertificate(TlsCertificate)); + + if (GrpcMacaroon != null) + { + var macaroonHex = BitConverter.ToString(GrpcMacaroon).Replace("-", "", StringComparison.InvariantCulture); + httpClient.DefaultRequestHeaders.Add("Grpc-Metadata-macaroon", macaroonHex); + } return httpClient; } @@ -92,5 +109,49 @@ namespace BTCPayServer.Payments.Lightning.Lnd public partial class LndSwaggerClient { + internal HttpClientFactoryForLnd HttpClientFactory { get; set; } + + public TaskCompletionSource InvoiceResponse = new TaskCompletionSource(); + public TaskCompletionSource SubscribeLost = new TaskCompletionSource(); + + // TODO: Refactor swagger generated wrapper to include this method directly + public async Task StartSubscribeInvoiceThread() + { + var urlBuilder = new StringBuilder(); + urlBuilder.Append(BaseUrl).Append("/v1/invoices/subscribe"); + + using (var client = HttpClientFactory.Generate()) + { + client.Timeout = TimeSpan.FromMilliseconds(Timeout.Infinite); + + var request = new HttpRequestMessage(HttpMethod.Get, urlBuilder.ToString()); + + using (var response = await client.SendAsync( + request, + HttpCompletionOption.ResponseHeadersRead)) + { + using (var body = await response.Content.ReadAsStreamAsync()) + using (var reader = new StreamReader(body)) + { + try + { + while (!reader.EndOfStream) + { + string line = reader.ReadLine(); + LnrpcInvoice parsedInvoice = Newtonsoft.Json.JsonConvert.DeserializeObject(line, _settings.Value); + + InvoiceResponse?.SetResult(parsedInvoice); + } + } + catch (Exception e) + { + // TODO: check that the exception type is actually from a closed stream. + Debug.WriteLine(e.Message); + SubscribeLost?.SetResult(this); + } + } + } + } + } } }