mirror of
https://github.com/aljazceru/BTCPayServerPlugins.git
synced 2025-12-17 07:34:24 +01:00
Merge pull request #97 from NicolasDorier/improv-blink
Blink Lightning Client improvements
This commit is contained in:
@@ -1,19 +1,19 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
using System.Reactive.Disposables;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Channels;
|
using System.Threading.Channels;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.HostedServices;
|
|
||||||
using BTCPayServer.Lightning;
|
using BTCPayServer.Lightning;
|
||||||
|
using BTCPayServer.Payments.Lightning;
|
||||||
using GraphQL;
|
using GraphQL;
|
||||||
using GraphQL.Client.Abstractions.Websocket;
|
using GraphQL.Client.Abstractions.Websocket;
|
||||||
using GraphQL.Client.Http;
|
using GraphQL.Client.Http;
|
||||||
using GraphQL.Client.Http.Websocket;
|
using GraphQL.Client.Http.Websocket;
|
||||||
using GraphQL.Client.Serializer.Newtonsoft;
|
using GraphQL.Client.Serializer.Newtonsoft;
|
||||||
using Microsoft.AspNetCore.OutputCaching;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
@@ -23,13 +23,88 @@ using Network = NBitcoin.Network;
|
|||||||
|
|
||||||
namespace BTCPayServer.Plugins.Blink;
|
namespace BTCPayServer.Plugins.Blink;
|
||||||
|
|
||||||
public class BlinkLightningClient : ILightningClient
|
public enum BlinkCurrency
|
||||||
{
|
{
|
||||||
|
BTC,
|
||||||
|
USD
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BlinkLightningClient : IExtendedLightningClient
|
||||||
|
{
|
||||||
|
async Task<BlinkCurrency?> FetchWalletCurrency()
|
||||||
|
{
|
||||||
|
var walletId = await GetWalletId();
|
||||||
|
var reques = new GraphQLRequest
|
||||||
|
{
|
||||||
|
Query = @"
|
||||||
|
query InvoiceByPaymentHash($walletId: WalletId!) {
|
||||||
|
me {
|
||||||
|
defaultAccount {
|
||||||
|
walletById(walletId: $walletId) {
|
||||||
|
walletCurrency
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}",
|
||||||
|
OperationName = "InvoiceByPaymentHash",
|
||||||
|
Variables = new
|
||||||
|
{
|
||||||
|
walletId = walletId
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var response = await _client.SendQueryAsync<JObject>(reques);
|
||||||
|
return response.Data["me"]?["defaultAccount"]?["walletById"]?["walletCurrency"]?.ToString() switch
|
||||||
|
{
|
||||||
|
string str => ParseBlinkCurrency(str),
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static BlinkCurrency ParseBlinkCurrency(string str)
|
||||||
|
=> str switch
|
||||||
|
{
|
||||||
|
"BTC" => BlinkCurrency.BTC,
|
||||||
|
"USD" => BlinkCurrency.USD,
|
||||||
|
_ => throw new FormatException("Invalid Blink Wallet Currency (Only BTC and USD are supported)")
|
||||||
|
};
|
||||||
|
|
||||||
|
record WalletInfo(BlinkCurrency WalletCurrency, string WalletId);
|
||||||
|
|
||||||
|
private WalletInfo? _WalletInfo;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The connection string may or may not have the walletId and the currency
|
||||||
|
/// This method fetches the default wallet-id and the currency of the wallet if we
|
||||||
|
/// do not already have this information and cache it.
|
||||||
|
/// Now, the connection string generated by Blink should include both, so this is only to
|
||||||
|
/// accomodate legacy connection string format
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="InvalidOperationException"></exception>
|
||||||
|
async Task<WalletInfo> GetWalletInfo()
|
||||||
|
{
|
||||||
|
if (_WalletInfo is not null)
|
||||||
|
return _WalletInfo;
|
||||||
|
if (ExplicitWalletId is null)
|
||||||
|
{
|
||||||
|
var defaultWallet = await FetchNetworkAndDefaultWallet();
|
||||||
|
_WalletInfo = new(ExplicitCurrency ?? defaultWallet.DefaultWalletCurrency, defaultWallet.DefaultWalletId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var currency = ExplicitCurrency ?? await FetchWalletCurrency() ??
|
||||||
|
throw new InvalidOperationException("Invalid Blink Wallet Id");
|
||||||
|
_WalletInfo = new(currency, ExplicitWalletId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _WalletInfo;
|
||||||
|
}
|
||||||
|
|
||||||
private readonly string _apiKey;
|
private readonly string _apiKey;
|
||||||
private readonly Uri _apiEndpoint;
|
private readonly Uri _apiEndpoint;
|
||||||
public string? WalletId { get; set; }
|
|
||||||
|
|
||||||
public string? WalletCurrency { get; set; }
|
async Task<string> GetWalletId() => ExplicitWalletId ?? (await GetWalletInfo()).WalletId;
|
||||||
|
public string? ExplicitWalletId { get; set; }
|
||||||
|
|
||||||
private readonly Network _network;
|
private readonly Network _network;
|
||||||
public ILogger Logger;
|
public ILogger Logger;
|
||||||
@@ -39,11 +114,12 @@ public class BlinkLightningClient : ILightningClient
|
|||||||
{
|
{
|
||||||
[JsonProperty("X-API-KEY")] public string ApiKey { get; set; }
|
[JsonProperty("X-API-KEY")] public string ApiKey { get; set; }
|
||||||
}
|
}
|
||||||
public BlinkLightningClient(string apiKey, Uri apiEndpoint, string walletId, Network network, HttpClient httpClient, ILogger logger)
|
public BlinkLightningClient(string apiKey, Uri apiEndpoint, string? walletId, BlinkCurrency? currency, Network network, HttpClient httpClient, ILogger logger)
|
||||||
{
|
{
|
||||||
_apiKey = apiKey;
|
_apiKey = apiKey;
|
||||||
_apiEndpoint = apiEndpoint;
|
_apiEndpoint = apiEndpoint;
|
||||||
WalletId = walletId;
|
ExplicitWalletId = walletId;
|
||||||
|
ExplicitCurrency = currency;
|
||||||
_network = network;
|
_network = network;
|
||||||
Logger = logger;
|
Logger = logger;
|
||||||
_client = new GraphQLHttpClient(new GraphQLHttpClientOptions() {EndPoint = _apiEndpoint,
|
_client = new GraphQLHttpClient(new GraphQLHttpClientOptions() {EndPoint = _apiEndpoint,
|
||||||
@@ -66,13 +142,15 @@ public class BlinkLightningClient : ILightningClient
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public BlinkCurrency? ExplicitCurrency { get; set; }
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return $"type=blink;server={_apiEndpoint};api-key={_apiKey}{(WalletId is null? "":$";wallet-id={WalletId}")}";
|
return $"type=blink;server={_apiEndpoint};api-key={_apiKey}{(ExplicitWalletId is null? "":$";wallet-id={ExplicitWalletId}")}";
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<LightningInvoice?> GetInvoice(string invoiceId,
|
public async Task<LightningInvoice?> GetInvoice(string invoiceId,
|
||||||
CancellationToken cancellation = new CancellationToken())
|
CancellationToken cancellation = default)
|
||||||
{
|
{
|
||||||
|
|
||||||
var reques = new GraphQLRequest
|
var reques = new GraphQLRequest
|
||||||
@@ -96,7 +174,7 @@ query InvoiceByPaymentHash($paymentHash: PaymentHash!, $walletId: WalletId!) {
|
|||||||
OperationName = "InvoiceByPaymentHash",
|
OperationName = "InvoiceByPaymentHash",
|
||||||
Variables = new
|
Variables = new
|
||||||
{
|
{
|
||||||
walletId = WalletId,
|
walletId = await GetWalletId(),
|
||||||
paymentHash = invoiceId
|
paymentHash = invoiceId
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -169,7 +247,7 @@ query Invoices($walletId: WalletId!) {
|
|||||||
OperationName = "Invoices",
|
OperationName = "Invoices",
|
||||||
Variables = new
|
Variables = new
|
||||||
{
|
{
|
||||||
walletId = WalletId
|
walletId = await GetWalletId()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var response = await _client.SendQueryAsync<dynamic>(reques, cancellation);
|
var response = await _client.SendQueryAsync<dynamic>(reques, cancellation);
|
||||||
@@ -218,7 +296,7 @@ query TransactionsByPaymentHash($paymentHash: PaymentHash!, $walletId: WalletId!
|
|||||||
OperationName = "TransactionsByPaymentHash",
|
OperationName = "TransactionsByPaymentHash",
|
||||||
Variables = new
|
Variables = new
|
||||||
{
|
{
|
||||||
walletId = WalletId,
|
walletId = await GetWalletId(),
|
||||||
paymentHash = paymentHash
|
paymentHash = paymentHash
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -308,7 +386,7 @@ query Transactions($walletId: WalletId!) {
|
|||||||
OperationName = "Transactions",
|
OperationName = "Transactions",
|
||||||
Variables = new
|
Variables = new
|
||||||
{
|
{
|
||||||
walletId = WalletId
|
walletId = await GetWalletId(),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var response = await _client.SendQueryAsync<dynamic>(reques, cancellation);
|
var response = await _client.SendQueryAsync<dynamic>(reques, cancellation);
|
||||||
@@ -328,9 +406,8 @@ query Transactions($walletId: WalletId!) {
|
|||||||
public async Task<LightningInvoice> CreateInvoice(CreateInvoiceParams createInvoiceRequest,
|
public async Task<LightningInvoice> CreateInvoice(CreateInvoiceParams createInvoiceRequest,
|
||||||
CancellationToken cancellation = new())
|
CancellationToken cancellation = new())
|
||||||
{
|
{
|
||||||
string query;
|
var isUSD = (await GetWalletInfo()).WalletCurrency == BlinkCurrency.USD;
|
||||||
|
var query = isUSD ? @"
|
||||||
query = WalletCurrency?.Equals("btc", StringComparison.InvariantCultureIgnoreCase) is not true ? @"
|
|
||||||
mutation lnInvoiceCreate($input: LnUsdInvoiceBtcDenominatedCreateOnBehalfOfRecipientInput!) {
|
mutation lnInvoiceCreate($input: LnUsdInvoiceBtcDenominatedCreateOnBehalfOfRecipientInput!) {
|
||||||
lnUsdInvoiceBtcDenominatedCreateOnBehalfOfRecipient(input: $input) {
|
lnUsdInvoiceBtcDenominatedCreateOnBehalfOfRecipient(input: $input) {
|
||||||
invoice {
|
invoice {
|
||||||
@@ -370,7 +447,7 @@ mutation lnInvoiceCreate($input: LnInvoiceCreateOnBehalfOfRecipientInput!) {
|
|||||||
{
|
{
|
||||||
input = new
|
input = new
|
||||||
{
|
{
|
||||||
recipientWalletId = WalletId,
|
recipientWalletId = await GetWalletId(),
|
||||||
memo = createInvoiceRequest.Description,
|
memo = createInvoiceRequest.Description,
|
||||||
descriptionHash = createInvoiceRequest.DescriptionHash?.ToString(),
|
descriptionHash = createInvoiceRequest.DescriptionHash?.ToString(),
|
||||||
amount = (long)createInvoiceRequest.Amount.ToUnit(LightMoneyUnit.Satoshi),
|
amount = (long)createInvoiceRequest.Amount.ToUnit(LightMoneyUnit.Satoshi),
|
||||||
@@ -380,13 +457,13 @@ expiresIn = (int)createInvoiceRequest.Expiry.TotalMinutes
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
var response = await _client.SendQueryAsync<dynamic>(reques, cancellation);
|
var response = await _client.SendQueryAsync<dynamic>(reques, cancellation);
|
||||||
var inv = (WalletCurrency?.Equals("btc", StringComparison.InvariantCultureIgnoreCase) is not true
|
var inv = (isUSD
|
||||||
? response.Data.lnUsdInvoiceBtcDenominatedCreateOnBehalfOfRecipient.invoice
|
? response.Data.lnUsdInvoiceBtcDenominatedCreateOnBehalfOfRecipient.invoice
|
||||||
: response.Data.lnInvoiceCreateOnBehalfOfRecipient.invoice)as JObject;
|
: response.Data.lnInvoiceCreateOnBehalfOfRecipient.invoice) as JObject;
|
||||||
|
|
||||||
if (inv is null)
|
if (inv is null)
|
||||||
{
|
{
|
||||||
var errors = (WalletCurrency?.Equals("btc", StringComparison.InvariantCultureIgnoreCase) is not true
|
var errors = (isUSD
|
||||||
? response.Data.lnUsdInvoiceBtcDenominatedCreateOnBehalfOfRecipient.errors
|
? response.Data.lnUsdInvoiceBtcDenominatedCreateOnBehalfOfRecipient.errors
|
||||||
: response.Data.lnInvoiceCreateOnBehalfOfRecipient.errors) as JArray;
|
: response.Data.lnInvoiceCreateOnBehalfOfRecipient.errors) as JArray;
|
||||||
|
|
||||||
@@ -405,120 +482,88 @@ expiresIn = (int)createInvoiceRequest.Expiry.TotalMinutes
|
|||||||
|
|
||||||
public class BlinkListener : ILightningInvoiceListener
|
public class BlinkListener : ILightningInvoiceListener
|
||||||
{
|
{
|
||||||
|
private volatile int _WebSocketErrorCount = 0;
|
||||||
private readonly BlinkLightningClient _lightningClient;
|
private readonly BlinkLightningClient _lightningClient;
|
||||||
private readonly Channel<LightningInvoice> _invoices = Channel.CreateUnbounded<LightningInvoice>();
|
private readonly Channel<string> _invoices = Channel.CreateUnbounded<string>();
|
||||||
private readonly IDisposable _subscription;
|
private readonly IDisposable _subscription;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
public BlinkListener(GraphQLHttpClient httpClient, BlinkLightningClient lightningClient, ILogger logger)
|
public BlinkListener(GraphQLHttpClient httpClient, BlinkLightningClient lightningClient, ILogger logger)
|
||||||
{
|
{
|
||||||
try
|
_logger = logger;
|
||||||
|
_lightningClient = lightningClient;
|
||||||
|
var stream = httpClient.CreateSubscriptionStream<JObject>(new GraphQLRequest()
|
||||||
{
|
{
|
||||||
_logger = logger;
|
Query = @"subscription myUpdates {
|
||||||
_lightningClient = lightningClient;
|
myUpdates {
|
||||||
var stream = httpClient.CreateSubscriptionStream<JObject>(new GraphQLRequest()
|
update {
|
||||||
{
|
... on LnUpdate {
|
||||||
Query = @"subscription myUpdates {
|
transaction {
|
||||||
myUpdates {
|
initiationVia {
|
||||||
update {
|
... on InitiationViaLn {
|
||||||
... on LnUpdate {
|
paymentHash
|
||||||
transaction {
|
|
||||||
initiationVia {
|
|
||||||
... on InitiationViaLn {
|
|
||||||
paymentHash
|
|
||||||
}
|
|
||||||
}
|
|
||||||
direction
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
direction
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
", OperationName = "myUpdates"
|
", OperationName = "myUpdates"
|
||||||
});
|
}, webSocketExceptionHandler: (wse) =>
|
||||||
|
|
||||||
_subscription = stream.Subscribe(async response =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if(response.Data is null)
|
|
||||||
return;
|
|
||||||
if (response.Data.SelectToken("myUpdates.update.transaction.direction")?.Value<string>() != "RECEIVE")
|
|
||||||
return;
|
|
||||||
var invoiceId = response.Data
|
|
||||||
.SelectToken("myUpdates.update.transaction.initiationVia.paymentHash")?.Value<string>();
|
|
||||||
if (invoiceId is null)
|
|
||||||
return;
|
|
||||||
if (await _lightningClient.GetInvoice(invoiceId) is LightningInvoice inv)
|
|
||||||
{
|
|
||||||
_invoices.Writer.TryWrite(inv);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
_logger.LogError(e, "Error while processing detecting lightning invoice payment");
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
_wsSubscriptionDisposable = httpClient.WebsocketConnectionState.Subscribe(state =>
|
|
||||||
{
|
|
||||||
if (state == GraphQLWebsocketConnectionState.Disconnected)
|
|
||||||
{
|
|
||||||
streamEnded.TrySetResult();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
{
|
||||||
logger.LogError(e, "Error while creating lightning invoice listener");
|
_logger.LogWarning(wse, $"Websocket error to Blink... ({_WebSocketErrorCount})");
|
||||||
}
|
if (Interlocked.Increment(ref _WebSocketErrorCount) == 10)
|
||||||
|
{
|
||||||
|
_invoices.Writer.TryComplete(wse);
|
||||||
|
_logger.LogError(wse, "Connection to Blink WebSocket closed.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_subscription = stream.Subscribe(response =>
|
||||||
|
{
|
||||||
|
_WebSocketErrorCount = 0;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if(response?.Data is null)
|
||||||
|
return;
|
||||||
|
if (response.Data.SelectToken("myUpdates.update.transaction.direction")?.Value<string>() != "RECEIVE")
|
||||||
|
return;
|
||||||
|
var invoiceId = response.Data
|
||||||
|
.SelectToken("myUpdates.update.transaction.initiationVia.paymentHash")?.Value<string>();
|
||||||
|
if (invoiceId is null)
|
||||||
|
return;
|
||||||
|
_invoices.Writer.TryWrite(invoiceId);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogError(e, "Error while processing detecting lightning invoice payment");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (e) =>
|
||||||
|
{
|
||||||
|
_invoices.Writer.TryComplete(e);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_subscription.Dispose();
|
_subscription.Dispose();
|
||||||
_invoices.Writer.TryComplete();
|
_invoices.Writer.TryComplete(new ObjectDisposedException(nameof(BlinkListener)));
|
||||||
_wsSubscriptionDisposable.Dispose();
|
|
||||||
streamEnded.TrySetResult();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private TaskCompletionSource streamEnded = new();
|
|
||||||
private readonly IDisposable _wsSubscriptionDisposable;
|
|
||||||
|
|
||||||
public async Task<LightningInvoice> WaitInvoice(CancellationToken cancellation)
|
public async Task<LightningInvoice> WaitInvoice(CancellationToken cancellation)
|
||||||
{
|
{
|
||||||
var resultz = await Task.WhenAny(streamEnded.Task, _invoices.Reader.ReadAsync(cancellation).AsTask());
|
await foreach (var id in _invoices.Reader.ReadAllAsync(cancellation))
|
||||||
if (resultz is Task<LightningInvoice> res)
|
|
||||||
{
|
{
|
||||||
return await res;
|
var invoice = await _lightningClient.GetInvoice(id, cancellation);
|
||||||
|
if (invoice is not null)
|
||||||
|
return invoice;
|
||||||
}
|
}
|
||||||
|
throw new ChannelClosedException();
|
||||||
// Enhanced logging to identify which task completed and its details
|
|
||||||
var taskType = resultz.GetType();
|
|
||||||
var typeName = taskType.Name.Contains('`') ? taskType.Name.Split('`')[0] : taskType.Name;
|
|
||||||
var genericArgs = taskType.GenericTypeArguments;
|
|
||||||
var genericTypeInfo = genericArgs.Length > 0
|
|
||||||
? $"<{string.Join(", ", genericArgs.Select(t => t.Name))}>"
|
|
||||||
: "";
|
|
||||||
|
|
||||||
_logger.LogInformation("WaitInvoice completed - Task Type: {TaskType}{GenericArgs}, " +
|
|
||||||
"Status: {Status}, IsCompletedSuccessfully: {IsCompletedSuccessfully}, " +
|
|
||||||
"IsFaulted: {IsFaulted}",
|
|
||||||
typeName,
|
|
||||||
genericTypeInfo,
|
|
||||||
resultz.Status,
|
|
||||||
resultz.IsCompletedSuccessfully,
|
|
||||||
resultz.IsFaulted);
|
|
||||||
|
|
||||||
if (resultz.IsFaulted && resultz.Exception != null)
|
|
||||||
{
|
|
||||||
_logger.LogError("Task completed with fault: {Exception}", resultz.Exception);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new LightningInvoice { Id = Guid.NewGuid().ToString() }; // Return a dummy invoice so calling listening logic exits
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public async Task<(Network Network, string DefaultWalletId, string DefaultWalletCurrency)> GetNetworkAndDefaultWallet(CancellationToken cancellation =default)
|
public async Task<(Network Network, string DefaultWalletId, BlinkCurrency DefaultWalletCurrency)> FetchNetworkAndDefaultWallet(CancellationToken cancellation =default)
|
||||||
{
|
{
|
||||||
|
|
||||||
var reques = new GraphQLRequest
|
var reques = new GraphQLRequest
|
||||||
@@ -552,7 +597,7 @@ query GetNetworkAndDefaultWallet {
|
|||||||
"regtest" => Network.RegTest,
|
"regtest" => Network.RegTest,
|
||||||
_ => throw new ArgumentOutOfRangeException()
|
_ => throw new ArgumentOutOfRangeException()
|
||||||
};
|
};
|
||||||
return (network, defaultWalletId, defaultWalletCurrency);
|
return (network, defaultWalletId, ParseBlinkCurrency(defaultWalletCurrency));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<LightningNodeInformation> GetInfo(CancellationToken cancellation = new CancellationToken())
|
public Task<LightningNodeInformation> GetInfo(CancellationToken cancellation = new CancellationToken())
|
||||||
@@ -563,6 +608,9 @@ query GetNetworkAndDefaultWallet {
|
|||||||
|
|
||||||
public async Task<LightningNodeBalance> GetBalance(CancellationToken cancellation = new())
|
public async Task<LightningNodeBalance> GetBalance(CancellationToken cancellation = new())
|
||||||
{
|
{
|
||||||
|
// Avoid call if we already know that fetching the balance is useless
|
||||||
|
if ((ExplicitCurrency ?? _WalletInfo?.WalletCurrency) is not (null or BlinkCurrency.BTC))
|
||||||
|
throw new NotSupportedException();
|
||||||
var request = new GraphQLRequest
|
var request = new GraphQLRequest
|
||||||
{
|
{
|
||||||
Query = @"
|
Query = @"
|
||||||
@@ -579,25 +627,25 @@ query GetWallet($walletId: WalletId!) {
|
|||||||
}",
|
}",
|
||||||
OperationName = "GetWallet",
|
OperationName = "GetWallet",
|
||||||
Variables = new {
|
Variables = new {
|
||||||
walletId = WalletId
|
walletId = await GetWalletId()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
var response = await _client.SendQueryAsync<dynamic>(request, cancellation);
|
var response = await _client.SendQueryAsync<JObject>(request, cancellation);
|
||||||
|
var walletById = response.Data["me"]?["defaultAccount"]?["walletById"];
|
||||||
WalletCurrency = response.Data.me.defaultAccount.walletById.walletCurrency;
|
if (walletById?["walletCurrency"]?.Value<string>() is string currency &&
|
||||||
if (response.Data.me.defaultAccount.walletById.walletCurrency == "BTC")
|
walletById?["balance"]?.Value<long?>() is long balance &&
|
||||||
|
ParseBlinkCurrency(currency) is BlinkCurrency.BTC)
|
||||||
{
|
{
|
||||||
return new LightningNodeBalance()
|
return new LightningNodeBalance()
|
||||||
{
|
{
|
||||||
OffchainBalance = new OffchainBalance()
|
OffchainBalance = new OffchainBalance()
|
||||||
{
|
{
|
||||||
Local = LightMoney.Satoshis((long)response.Data.me.defaultAccount.walletById.balance)
|
Local = LightMoney.Satoshis(balance)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
throw new NotSupportedException();
|
||||||
return new LightningNodeBalance();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<PayResponse> Pay(PayInvoiceParams payParams,
|
public async Task<PayResponse> Pay(PayInvoiceParams payParams,
|
||||||
@@ -607,7 +655,7 @@ query GetWallet($walletId: WalletId!) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async Task<PayResponse> Pay(string bolt11, PayInvoiceParams payParams,
|
public async Task<PayResponse> Pay(string bolt11, PayInvoiceParams payParams,
|
||||||
CancellationToken cancellation = new CancellationToken())
|
CancellationToken cancellation = default)
|
||||||
{
|
{
|
||||||
|
|
||||||
var request = new GraphQLRequest
|
var request = new GraphQLRequest
|
||||||
@@ -647,7 +695,7 @@ mutation LnInvoicePaymentSend($input: LnInvoicePaymentInput!) {
|
|||||||
OperationName = "LnInvoicePaymentSend",
|
OperationName = "LnInvoicePaymentSend",
|
||||||
Variables = new {
|
Variables = new {
|
||||||
input = new {
|
input = new {
|
||||||
walletId = WalletId,
|
walletId = await GetWalletId(),
|
||||||
paymentRequest = bolt11,
|
paymentRequest = bolt11,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -689,8 +737,8 @@ mutation LnInvoicePaymentSend($input: LnInvoicePaymentInput!) {
|
|||||||
"FAILURE" => LightningPaymentStatus.Failed,
|
"FAILURE" => LightningPaymentStatus.Failed,
|
||||||
"PENDING" => LightningPaymentStatus.Pending,
|
"PENDING" => LightningPaymentStatus.Pending,
|
||||||
"SUCCESS" => LightningPaymentStatus.Complete,
|
"SUCCESS" => LightningPaymentStatus.Complete,
|
||||||
null => LightningPaymentStatus.Unknown,
|
string status => throw new ArgumentOutOfRangeException($"Unknown status received by blink ({status})"),
|
||||||
_ => throw new ArgumentOutOfRangeException()
|
_ => LightningPaymentStatus.Unknown,
|
||||||
},
|
},
|
||||||
Preimage = response["transaction"]["settlementVia"]?["preImage"].Value<string>() is null? null: new uint256(response["transaction"]["settlementVia"]["preImage"].Value<string>()),
|
Preimage = response["transaction"]["settlementVia"]?["preImage"].Value<string>() is null? null: new uint256(response["transaction"]["settlementVia"]["preImage"].Value<string>()),
|
||||||
};
|
};
|
||||||
@@ -731,4 +779,36 @@ mutation LnInvoicePaymentSend($input: LnInvoicePaymentInput!) {
|
|||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<ValidationResult?> Validate()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var res = await FetchNetworkAndDefaultWallet();
|
||||||
|
if (res.Network != this._network)
|
||||||
|
{
|
||||||
|
return new ValidationResult(
|
||||||
|
$"The wallet is not on the right network ({res.Network.Name} instead of {_network.Name})");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ExplicitWalletId is null && string.IsNullOrEmpty(res.DefaultWalletId))
|
||||||
|
{
|
||||||
|
return new ValidationResult($"The wallet-id is not set and no default wallet is set");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return new ValidationResult($"Invalid server or api key");
|
||||||
|
}
|
||||||
|
|
||||||
|
var walletCurrency = await FetchWalletCurrency();
|
||||||
|
if (walletCurrency is null)
|
||||||
|
return new ValidationResult($"Invalid wallet id");
|
||||||
|
if (ExplicitCurrency != null && walletCurrency != ExplicitCurrency)
|
||||||
|
return new ValidationResult($"Invalid currency (it should be {walletCurrency})");
|
||||||
|
return ValidationResult.Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? DisplayName => "Blink";
|
||||||
|
public Uri? ServerUri => _client.HttpClient.BaseAddress;
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ using System;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using BTCPayServer.Lightning;
|
using BTCPayServer.Lightning;
|
||||||
|
using BTCPayServer.Payments.Lightning;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Network = NBitcoin.Network;
|
using Network = NBitcoin.Network;
|
||||||
|
|
||||||
@@ -82,48 +83,21 @@ public class BlinkLightningConnectionStringHandler : ILightningConnectionStringH
|
|||||||
client.BaseAddress = uri;
|
client.BaseAddress = uri;
|
||||||
|
|
||||||
kv.TryGetValue("wallet-id", out var walletId);
|
kv.TryGetValue("wallet-id", out var walletId);
|
||||||
var bclient = new BlinkLightningClient(apiKey, uri, walletId, network, client, _loggerFactory.CreateLogger($"{nameof(BlinkLightningClient)}:{walletId}"));
|
|
||||||
(Network Network, string DefaultWalletId, string DefaultWalletCurrency) res;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
res = bclient.GetNetworkAndDefaultWallet().GetAwaiter().GetResult();
|
|
||||||
if (res.Network != network)
|
|
||||||
{
|
|
||||||
error = $"The wallet is not on the right network ({res.Network.Name} instead of {network.Name})";
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (walletId is null && string.IsNullOrEmpty(res.DefaultWalletId))
|
BlinkCurrency? currency = null;
|
||||||
{
|
if (kv.TryGetValue("currency", out var v))
|
||||||
error = $"The wallet-id is not set and no default wallet is set";
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
error = $"Invalid server or api key";
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (walletId is null)
|
|
||||||
{
|
|
||||||
bclient.WalletId = res.DefaultWalletId;
|
|
||||||
bclient.WalletCurrency = res.DefaultWalletCurrency;
|
|
||||||
bclient.Logger = _loggerFactory.CreateLogger($"{nameof(BlinkLightningClient)}:{walletId}");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
bclient.GetBalance().GetAwaiter().GetResult();
|
currency = BlinkLightningClient.ParseBlinkCurrency(v);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (FormatException e)
|
||||||
{
|
{
|
||||||
error = "Invalid wallet id";
|
error = e.Message;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return bclient;
|
return new BlinkLightningClient(apiKey, uri, walletId, currency, network, client, _loggerFactory.CreateLogger(nameof(BlinkLightningClient)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
<div class="accordion-body">
|
<div class="accordion-body">
|
||||||
<ul class="pb-2">
|
<ul class="pb-2">
|
||||||
<li>
|
<li>
|
||||||
<code><b>type=</b>blink;<b>server=</b>https://api.blink.sv/graphql;<b>api-key</b>=blink_...;<b>wallet-id=</b>xyz</code>
|
<code><b>type=</b>blink;<b>server=</b>https://api.blink.sv/graphql;<b>api-key</b>=blink_...;<b>wallet-id=</b>xyz;<b>currency=</b>BTC;</code>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p class="my-2">Head over to the <a href="https://dashboard.blink.sv" target="_blank" rel="noreferrer noopener" >Blink dashboard</a> and create an api key. The server is optional and will use the default Blink instance.The wallet-id is optional and will use the default wallet otherwise.</p>
|
<p class="my-2">Head over to the <a href="https://dashboard.blink.sv" target="_blank" rel="noreferrer noopener" >Blink dashboard</a> and create an api key. The server is optional and will use the default Blink instance.The wallet-id is optional and will use the default wallet otherwise.</p>
|
||||||
|
|||||||
Reference in New Issue
Block a user