using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Net.WebSockets; using System.Text; using System.Threading; using System.Threading.Tasks; using NBitcoin; using NBXplorer; using Newtonsoft.Json; namespace BTCPayServer.Payments.Lightning.Charge { public class ChargeClient : ILightningInvoiceClient { private Uri _Uri; public Uri Uri { get { return _Uri; } } private Network _Network; static HttpClient _Client = new HttpClient(); public ChargeClient(Uri uri, Network network) { if (uri == null) throw new ArgumentNullException(nameof(uri)); if (network == null) throw new ArgumentNullException(nameof(network)); this._Uri = uri; this._Network = network; if (uri.UserInfo == null) throw new ArgumentException(paramName: nameof(uri), message: "User information not present in uri"); var userInfo = uri.UserInfo.Split(':'); if (userInfo.Length != 2) throw new ArgumentException(paramName: nameof(uri), message: "User information not present in uri"); Credentials = new NetworkCredential(userInfo[0], userInfo[1]); } public async Task CreateInvoiceAsync(CreateInvoiceRequest request, CancellationToken cancellation = default(CancellationToken)) { var message = CreateMessage(HttpMethod.Post, "invoice"); Dictionary parameters = new Dictionary(); parameters.Add("msatoshi", request.Amont.MilliSatoshi.ToString(CultureInfo.InvariantCulture)); parameters.Add("expiry", ((int)request.Expiry.TotalSeconds).ToString(CultureInfo.InvariantCulture)); message.Content = new FormUrlEncodedContent(parameters); var result = await _Client.SendAsync(message, cancellation); result.EnsureSuccessStatusCode(); var content = await result.Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject(content); } public async Task Listen(CancellationToken cancellation = default(CancellationToken)) { var socket = new ClientWebSocket(); socket.Options.SetRequestHeader("Authorization", $"Basic {GetBase64Creds()}"); var uri = new UriBuilder(Uri) { UserName = null, Password = null }.Uri.AbsoluteUri; if (!uri.EndsWith('/')) uri += "/"; uri += "ws"; uri = ToWebsocketUri(uri); await socket.ConnectAsync(new Uri(uri), cancellation); return new ChargeSession(socket); } private static string ToWebsocketUri(string uri) { if (uri.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) uri = uri.Replace("https://", "wss://", StringComparison.OrdinalIgnoreCase); if (uri.StartsWith("http://", StringComparison.OrdinalIgnoreCase)) uri = uri.Replace("http://", "ws://", StringComparison.OrdinalIgnoreCase); return uri; } public NetworkCredential Credentials { get; set; } public GetInfoResponse GetInfo() { return GetInfoAsync().GetAwaiter().GetResult(); } public async Task GetInvoice(string invoiceId, CancellationToken cancellation = default(CancellationToken)) { var request = CreateMessage(HttpMethod.Get, $"invoice/{invoiceId}"); var message = await _Client.SendAsync(request, cancellation); if (message.StatusCode == HttpStatusCode.NotFound) return null; message.EnsureSuccessStatusCode(); var content = await message.Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject(content); } public async Task GetInfoAsync(CancellationToken cancellation = default(CancellationToken)) { var request = CreateMessage(HttpMethod.Get, "info"); var message = await _Client.SendAsync(request, cancellation); message.EnsureSuccessStatusCode(); var content = await message.Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject(content); } private HttpRequestMessage CreateMessage(HttpMethod method, string path) { var uri = GetFullUri(path); var request = new HttpRequestMessage(method, uri); request.Headers.Authorization = new AuthenticationHeaderValue("Basic", GetBase64Creds()); return request; } private string GetBase64Creds() { return Convert.ToBase64String(Encoding.ASCII.GetBytes($"{Credentials.UserName}:{Credentials.Password}")); } private Uri GetFullUri(string partialUrl) { var uri = _Uri.AbsoluteUri; if (!uri.EndsWith("/", StringComparison.InvariantCultureIgnoreCase)) uri += "/"; return new Uri(uri + partialUrl); } async Task ILightningInvoiceClient.GetInvoice(string invoiceId, CancellationToken cancellation) { var invoice = await GetInvoice(invoiceId, cancellation); if (invoice == null) return null; return ChargeClient.ToLightningInvoice(invoice); } async Task ILightningInvoiceClient.Listen(CancellationToken cancellation) { return await Listen(cancellation); } internal static LightningInvoice ToLightningInvoice(ChargeInvoice invoice) { return new LightningInvoice() { Id = invoice.Id ?? invoice.Label, Amount = invoice.MilliSatoshi, BOLT11 = invoice.PaymentRequest, PaidAt = invoice.PaidAt, Status = invoice.Status }; } async Task ILightningInvoiceClient.CreateInvoice(LightMoney amount, TimeSpan expiry, CancellationToken cancellation) { var invoice = await CreateInvoiceAsync(new CreateInvoiceRequest() { Amont = amount, Expiry = expiry }); return new LightningInvoice() { Id = invoice.Id, Amount = amount, BOLT11 = invoice.PayReq, Status = "unpaid" }; } async Task ILightningInvoiceClient.GetInfo(CancellationToken cancellation) { var info = await GetInfoAsync(cancellation); var address = info.Address.Select(a => a.Address).FirstOrDefault(); var port = info.Port; return new LightningNodeInformation() { NodeId = info.Id, P2PPort = port, Address = address, BlockHeight = info.BlockHeight }; } } }