mirror of
https://github.com/aljazceru/BTCPayServerPlugins.git
synced 2025-12-17 07:34:24 +01:00
fix breez and add blink
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<LangVersion>10</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Plugin specific properties -->
|
||||
<PropertyGroup>
|
||||
<Product>Blink</Product>
|
||||
<Description>Brink Lightning support</Description>
|
||||
<Version>1.0.0</Version>
|
||||
<RootNamespace>BTCPayServer.Plugins.Blink</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<!-- Plugin development properties -->
|
||||
<PropertyGroup>
|
||||
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
|
||||
<PreserveCompilationContext>false</PreserveCompilationContext>
|
||||
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- This will make sure that referencing BTCPayServer doesn't put any artifact in the published directory -->
|
||||
<ItemDefinitionGroup>
|
||||
<ProjectReference>
|
||||
<Properties>StaticWebAssetsEnabled=false</Properties>
|
||||
<Private>false</Private>
|
||||
<ExcludeAssets>runtime;native;build;buildTransitive;contentFiles</ExcludeAssets>
|
||||
</ProjectReference>
|
||||
</ItemDefinitionGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Resources\**" />
|
||||
<ProjectReference Include="..\..\submodules\btcpayserver\BTCPayServer\BTCPayServer.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="Views\Shared\Blink\LNPaymentMethodSetupTab.cshtml" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="GraphQL.Client" Version="6.0.2" />
|
||||
<PackageReference Include="GraphQL.Client.Serializer.Newtonsoft" Version="6.0.2" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
645
Plugins/BTCPayServer.Plugins.Blink/BlinkLightningClient.cs
Normal file
645
Plugins/BTCPayServer.Plugins.Blink/BlinkLightningClient.cs
Normal file
@@ -0,0 +1,645 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Lightning.LndHub;
|
||||
using GraphQL;
|
||||
using GraphQL.Client.Http;
|
||||
using GraphQL.Client.Serializer.Newtonsoft;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Network = NBitcoin.Network;
|
||||
|
||||
namespace BTCPayServer.Plugins.Blink;
|
||||
|
||||
public class BlinkLightningClient : ILightningClient
|
||||
{
|
||||
private readonly string _apiKey;
|
||||
private readonly Uri _apiEndpoint;
|
||||
private readonly string _walletId;
|
||||
private readonly Network _network;
|
||||
private readonly NBXplorerDashboard _nbXplorerDashboard;
|
||||
private readonly GraphQLHttpClient _client;
|
||||
|
||||
public BlinkLightningClient(string apiKey, Uri apiEndpoint, string walletId, Network network,
|
||||
NBXplorerDashboard nbXplorerDashboard, HttpClient httpClient)
|
||||
{
|
||||
_apiKey = apiKey;
|
||||
_apiEndpoint = apiEndpoint;
|
||||
_walletId = walletId;
|
||||
_network = network;
|
||||
_nbXplorerDashboard = nbXplorerDashboard;
|
||||
_client = new GraphQLHttpClient(new GraphQLHttpClientOptions() {EndPoint = _apiEndpoint}, new NewtonsoftJsonSerializer(), httpClient);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"type=blink;server={_apiEndpoint};api-key={_apiKey};wallet-id={_walletId}";
|
||||
}
|
||||
|
||||
public async Task<LightningInvoice?> GetInvoice(string invoiceId,
|
||||
CancellationToken cancellation = new CancellationToken())
|
||||
{
|
||||
|
||||
var reques = new GraphQLRequest
|
||||
{
|
||||
Query = @"
|
||||
query InvoiceByPaymentHash($paymentHash: PaymentHash!, $walletId: WalletId!) {
|
||||
me {
|
||||
defaultAccount {
|
||||
walletById(walletId: $walletId) {
|
||||
invoiceByPaymentHash(paymentHash: $paymentHash) {
|
||||
createdAt
|
||||
paymentHash
|
||||
paymentRequest
|
||||
paymentSecret
|
||||
paymentStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}",
|
||||
OperationName = "InvoiceByPaymentHash",
|
||||
Variables = new
|
||||
{
|
||||
walletId = _walletId,
|
||||
paymentHash = invoiceId
|
||||
}
|
||||
};
|
||||
var response = await _client.SendQueryAsync<dynamic>(reques, cancellation);
|
||||
|
||||
|
||||
return response.Data is null ? null : ToInvoice(response.Data.me.defaultAccount.walletById.invoiceByPaymentHash);
|
||||
}
|
||||
|
||||
public LightningInvoice? ToInvoice(JObject invoice)
|
||||
{
|
||||
var bolt11 = BOLT11PaymentRequest.Parse(invoice["paymentRequest"].Value<string>(), _network);
|
||||
return new LightningInvoice()
|
||||
{
|
||||
Id = invoice["paymentHash"].Value<string>(),
|
||||
Amount = invoice["satoshis"] is null? bolt11.MinimumAmount: LightMoney.Satoshis(invoice["satoshis"].Value<long>()),
|
||||
Preimage = invoice["paymentSecret"].Value<string>(),
|
||||
PaidAt = (invoice["paymentStatus"].Value<string>()) == "PAID"? DateTimeOffset.UtcNow : null,
|
||||
Status = (invoice["paymentStatus"].Value<string>()) switch
|
||||
{
|
||||
"EXPIRED" => LightningInvoiceStatus.Expired,
|
||||
"PAID" => LightningInvoiceStatus.Paid,
|
||||
"PENDING" => LightningInvoiceStatus.Unpaid
|
||||
},
|
||||
BOLT11 = invoice["paymentRequest"].Value<string>(),
|
||||
PaymentHash = invoice["paymentHash"].Value<string>(),
|
||||
ExpiresAt = bolt11.ExpiryDate
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<LightningInvoice?> GetInvoice(uint256 paymentHash,
|
||||
CancellationToken cancellation = new CancellationToken())
|
||||
{
|
||||
return await GetInvoice(paymentHash.ToString(), cancellation);
|
||||
}
|
||||
|
||||
public async Task<LightningInvoice[]> ListInvoices(CancellationToken cancellation = new CancellationToken())
|
||||
{
|
||||
return await ListInvoices(new ListInvoicesParams(), cancellation);
|
||||
}
|
||||
|
||||
public async Task<LightningInvoice[]> ListInvoices(ListInvoicesParams request,
|
||||
CancellationToken cancellation = new CancellationToken())
|
||||
{
|
||||
var reques = new GraphQLRequest
|
||||
{
|
||||
Query = @"
|
||||
query Invoices($walletId: WalletId!) {
|
||||
me {
|
||||
defaultAccount {
|
||||
walletById(walletId: $walletId) {
|
||||
invoices {
|
||||
edges {
|
||||
node {
|
||||
createdAt
|
||||
paymentHash
|
||||
paymentRequest
|
||||
paymentSecret
|
||||
paymentStatus
|
||||
... on LnInvoice {
|
||||
satoshis
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}",
|
||||
OperationName = "Invoices",
|
||||
Variables = new
|
||||
{
|
||||
walletId = _walletId
|
||||
}
|
||||
};
|
||||
var response = await _client.SendQueryAsync<dynamic>(reques, cancellation);
|
||||
|
||||
var result = ((JArray)response.Data.me.defaultAccount.walletById.invoices.edges).Select(o => ToInvoice((JObject) o["node"] )).Where(o => o is not null || request.PendingOnly is not true || o.Status == LightningInvoiceStatus.Unpaid).ToArray();
|
||||
return (LightningInvoice[]) result;
|
||||
}
|
||||
|
||||
public async Task<LightningPayment?> GetPayment(string paymentHash,
|
||||
CancellationToken cancellation = new CancellationToken())
|
||||
{
|
||||
var reques = new GraphQLRequest
|
||||
{
|
||||
Query = @"
|
||||
query TransactionsByPaymentHash($paymentHash: PaymentHash!, $walletId: WalletId!) {
|
||||
me {
|
||||
defaultAccount {
|
||||
walletById(walletId: $walletId) {
|
||||
transactionsByPaymentHash(paymentHash: $paymentHash) {
|
||||
createdAt
|
||||
direction
|
||||
id
|
||||
initiationVia {
|
||||
... on InitiationViaLn {
|
||||
paymentHash
|
||||
paymentRequest
|
||||
}
|
||||
}
|
||||
memo
|
||||
settlementAmount
|
||||
settlementCurrency
|
||||
settlementVia {
|
||||
... on SettlementViaLn {
|
||||
preImage
|
||||
}
|
||||
}
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}",
|
||||
OperationName = "TransactionsByPaymentHash",
|
||||
Variables = new
|
||||
{
|
||||
walletId = _walletId,
|
||||
paymentHash = paymentHash
|
||||
}
|
||||
};
|
||||
var response = await _client.SendQueryAsync<dynamic>(reques, cancellation);
|
||||
var item = (JArray) response.Data.me.defaultAccount.walletById.transactionsByPaymentHash;
|
||||
return item.Any()? ToLightningPayment((JObject)item.First()): null;
|
||||
}
|
||||
|
||||
public LightningPayment? ToLightningPayment(JObject transaction)
|
||||
{
|
||||
if ((string)transaction["direction"] == "RECEIVE")
|
||||
return null;
|
||||
|
||||
var initiationVia = transaction["initiationVia"];
|
||||
if (initiationVia["paymentHash"] == null)
|
||||
return null;
|
||||
|
||||
var bolt11 = BOLT11PaymentRequest.Parse((string)initiationVia["paymentRequest"], _network);
|
||||
|
||||
return new LightningPayment()
|
||||
{
|
||||
Amount = bolt11.MinimumAmount,
|
||||
Status = transaction["status"].ToString() switch
|
||||
{
|
||||
"FAILURE" => LightningPaymentStatus.Failed,
|
||||
"PENDING" => LightningPaymentStatus.Pending,
|
||||
"SUCCESS" => LightningPaymentStatus.Complete,
|
||||
_ => LightningPaymentStatus.Unknown
|
||||
},
|
||||
BOLT11 = (string)initiationVia["paymentRequest"],
|
||||
Id = (string)initiationVia["paymentHash"],
|
||||
PaymentHash = (string)initiationVia["paymentHash"],
|
||||
CreatedAt = DateTimeOffset.FromUnixTimeSeconds(transaction["createdAt"].Value<long>()),
|
||||
AmountSent = bolt11.MinimumAmount,
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<LightningPayment[]> ListPayments(CancellationToken cancellation = new CancellationToken())
|
||||
{
|
||||
return await ListPayments(new ListPaymentsParams(), cancellation);
|
||||
}
|
||||
|
||||
public async Task<LightningPayment[]> ListPayments(ListPaymentsParams request,
|
||||
CancellationToken cancellation = new CancellationToken())
|
||||
{
|
||||
|
||||
var reques = new GraphQLRequest
|
||||
{
|
||||
Query = @"
|
||||
query Transactions($walletId: WalletId!) {
|
||||
me {
|
||||
defaultAccount {
|
||||
walletById(walletId: $walletId) {
|
||||
transactions {
|
||||
edges {
|
||||
node {
|
||||
createdAt
|
||||
direction
|
||||
id
|
||||
initiationVia {
|
||||
... on InitiationViaLn {
|
||||
paymentHash
|
||||
paymentRequest
|
||||
}
|
||||
}
|
||||
memo
|
||||
settlementAmount
|
||||
settlementCurrency
|
||||
settlementVia {
|
||||
... on SettlementViaLn {
|
||||
preImage
|
||||
}
|
||||
}
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}",
|
||||
OperationName = "Transactions",
|
||||
Variables = new
|
||||
{
|
||||
walletId = _walletId
|
||||
}
|
||||
};
|
||||
var response = await _client.SendQueryAsync<dynamic>(reques, cancellation);
|
||||
|
||||
|
||||
|
||||
var result = ((JArray)response.Data.me.defaultAccount.walletById.transactions.edges).Select(o => ToLightningPayment((JObject) o["node"])).Where(o => o is not null && (request.IncludePending is not true || o.Status!= LightningPaymentStatus.Pending)).ToArray();
|
||||
return (LightningPayment[]) result;
|
||||
}
|
||||
|
||||
public async Task<LightningInvoice> CreateInvoice(LightMoney amount, string description, TimeSpan expiry,
|
||||
CancellationToken cancellation = new())
|
||||
{
|
||||
return await CreateInvoice(new CreateInvoiceParams(amount, description, expiry), cancellation);
|
||||
}
|
||||
|
||||
public async Task<LightningInvoice> CreateInvoice(CreateInvoiceParams createInvoiceRequest,
|
||||
CancellationToken cancellation = new())
|
||||
{
|
||||
var reques = new GraphQLRequest
|
||||
{
|
||||
Query = @"
|
||||
mutation LnInvoiceCreate($input: LnInvoiceCreateInput!) {
|
||||
lnInvoiceCreate(input: $input) {
|
||||
invoice {
|
||||
createdAt
|
||||
paymentHash
|
||||
paymentRequest
|
||||
paymentSecret
|
||||
paymentStatus
|
||||
satoshis
|
||||
|
||||
}
|
||||
}
|
||||
}",
|
||||
OperationName = "LnInvoiceCreate",
|
||||
Variables = new
|
||||
{
|
||||
input = new
|
||||
{
|
||||
walletId = _walletId,
|
||||
memo = createInvoiceRequest.Description?? createInvoiceRequest.DescriptionHash?.ToString(),
|
||||
amount = (long)createInvoiceRequest.Amount.ToUnit(LightMoneyUnit.Satoshi),
|
||||
expiresIn = (int)createInvoiceRequest.Expiry.TotalMinutes
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
var response = await _client.SendQueryAsync<dynamic>(reques, cancellation);
|
||||
|
||||
|
||||
return ToInvoice(response.Data.lnInvoiceCreate.invoice);
|
||||
}
|
||||
|
||||
public async Task<ILightningInvoiceListener> Listen(CancellationToken cancellation = new CancellationToken())
|
||||
{
|
||||
return new BlinkListener(this, cancellation);
|
||||
}
|
||||
|
||||
|
||||
public class BlinkListener : ILightningInvoiceListener
|
||||
{
|
||||
private readonly ILightningClient _client;
|
||||
private readonly Channel<LightningInvoice> _invoices = Channel.CreateUnbounded<LightningInvoice>();
|
||||
private readonly CancellationTokenSource _cts;
|
||||
private HttpClient _httpClient;
|
||||
private HttpResponseMessage _response;
|
||||
private Stream _body;
|
||||
private StreamReader _reader;
|
||||
private Task _listenLoop;
|
||||
private readonly List<string> _paidInvoiceIds;
|
||||
|
||||
public BlinkListener(ILightningClient client, CancellationToken cancellation)
|
||||
{
|
||||
_cts = CancellationTokenSource.CreateLinkedTokenSource(cancellation);
|
||||
_client = client;
|
||||
_paidInvoiceIds = new List<string>();
|
||||
_listenLoop = ListenLoop();
|
||||
}
|
||||
|
||||
public async Task<LightningInvoice> WaitInvoice(CancellationToken cancellation)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _invoices.Reader.ReadAsync(cancellation);
|
||||
}
|
||||
catch (ChannelClosedException ex) when (ex.InnerException == null)
|
||||
{
|
||||
throw new OperationCanceledException();
|
||||
}
|
||||
catch (ChannelClosedException ex)
|
||||
{
|
||||
ExceptionDispatchInfo.Capture(ex.InnerException).Throw();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
}
|
||||
|
||||
static readonly AsyncDuplicateLock _locker = new();
|
||||
static readonly ConcurrentDictionary<string, LightningInvoice[]> _activeListeners = new();
|
||||
|
||||
private async Task ListenLoop()
|
||||
{
|
||||
try
|
||||
{
|
||||
var releaser = await _locker.LockOrBustAsync(_client.ToString(), _cts.Token);
|
||||
if (releaser is null)
|
||||
{
|
||||
while (!_cts.IsCancellationRequested && releaser is null)
|
||||
{
|
||||
if (_activeListeners.TryGetValue(_client.ToString(), out var invoicesData))
|
||||
{
|
||||
await HandleInvoicesData(invoicesData);
|
||||
}
|
||||
|
||||
releaser = await _locker.LockOrBustAsync(_client.ToString(), _cts.Token);
|
||||
|
||||
if (releaser is null)
|
||||
await Task.Delay(2500, _cts.Token);
|
||||
}
|
||||
}
|
||||
|
||||
using (releaser)
|
||||
{
|
||||
while (!_cts.IsCancellationRequested)
|
||||
{
|
||||
var invoicesData = await _client.ListInvoices(_cts.Token);
|
||||
_activeListeners.AddOrReplace(_client.ToString(), invoicesData);
|
||||
await HandleInvoicesData(invoicesData);
|
||||
|
||||
await Task.Delay(2500, _cts.Token);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch when (_cts.IsCancellationRequested)
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_invoices.Writer.TryComplete(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_activeListeners.TryRemove(_client.ToString(), out _);
|
||||
Dispose(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleInvoicesData(IEnumerable<LightningInvoice> invoicesData)
|
||||
{
|
||||
foreach (var data in invoicesData)
|
||||
{
|
||||
var invoice = data;
|
||||
if (invoice.PaidAt != null && !_paidInvoiceIds.Contains(invoice.Id))
|
||||
{
|
||||
await _invoices.Writer.WriteAsync(invoice, _cts.Token);
|
||||
_paidInvoiceIds.Add(invoice.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Dispose(bool waitLoop)
|
||||
{
|
||||
if (_cts.IsCancellationRequested)
|
||||
return;
|
||||
_cts.Cancel();
|
||||
_reader?.Dispose();
|
||||
_reader = null;
|
||||
_body?.Dispose();
|
||||
_body = null;
|
||||
_response?.Dispose();
|
||||
_response = null;
|
||||
_httpClient?.Dispose();
|
||||
_httpClient = null;
|
||||
if (waitLoop)
|
||||
_listenLoop?.Wait();
|
||||
_invoices.Writer.TryComplete();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<LightningNodeInformation> GetInfo(CancellationToken cancellation = new CancellationToken())
|
||||
{
|
||||
|
||||
var reques = new GraphQLRequest
|
||||
{
|
||||
Query = @"
|
||||
query Globals {
|
||||
globals {
|
||||
nodesIds
|
||||
}
|
||||
}",
|
||||
OperationName = "Globals"
|
||||
};
|
||||
var response = await _client.SendQueryAsync<dynamic>(reques, cancellation);
|
||||
var result = new LightningNodeInformation()
|
||||
{
|
||||
BlockHeight = _nbXplorerDashboard.Get("BTC").Status.ChainHeight,
|
||||
Alias = "Blink",
|
||||
|
||||
};
|
||||
result.NodeInfoList.AddRange(((JArray)response.Data.globals.nodesIds).Select(s => new NodeInfo(new PubKey(s.Value<string>()), "galoy.com", 69)));
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<LightningNodeBalance> GetBalance(CancellationToken cancellation = new())
|
||||
{
|
||||
var request = new GraphQLRequest
|
||||
{
|
||||
Query = @"
|
||||
query GetWallet($walletId: WalletId!) {
|
||||
me {
|
||||
defaultAccount {
|
||||
walletById(walletId: $walletId) {
|
||||
id
|
||||
balance
|
||||
walletCurrency
|
||||
}
|
||||
}
|
||||
}
|
||||
}",
|
||||
OperationName = "GetWallet",
|
||||
Variables = new {
|
||||
walletId = _walletId
|
||||
}
|
||||
};
|
||||
|
||||
var response = await _client.SendQueryAsync<dynamic>(request, cancellation);
|
||||
|
||||
if (response.Data.me.defaultAccount.walletById.walletCurrency == "BTC")
|
||||
{
|
||||
return new LightningNodeBalance()
|
||||
{
|
||||
OffchainBalance = new OffchainBalance()
|
||||
{
|
||||
Local = LightMoney.Satoshis((long)response.Data.me.defaultAccount.walletById.balance)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return new LightningNodeBalance();
|
||||
}
|
||||
|
||||
|
||||
|
||||
public async Task<PayResponse> Pay(PayInvoiceParams payParams,
|
||||
CancellationToken cancellation = new CancellationToken())
|
||||
{
|
||||
return await Pay(null, new PayInvoiceParams(), cancellation);
|
||||
}
|
||||
|
||||
public async Task<PayResponse> Pay(string bolt11, PayInvoiceParams payParams,
|
||||
CancellationToken cancellation = new CancellationToken())
|
||||
{
|
||||
|
||||
var request = new GraphQLRequest
|
||||
{
|
||||
Query = @"
|
||||
mutation LnInvoicePaymentSend($input: LnInvoicePaymentInput!) {
|
||||
lnInvoicePaymentSend(input: $input) {
|
||||
transaction {
|
||||
createdAt
|
||||
direction
|
||||
id
|
||||
initiationVia {
|
||||
... on InitiationViaLn {
|
||||
paymentHash
|
||||
paymentRequest
|
||||
}
|
||||
}
|
||||
memo
|
||||
settlementAmount
|
||||
settlementCurrency
|
||||
settlementVia {
|
||||
... on SettlementViaLn {
|
||||
preImage
|
||||
}
|
||||
}
|
||||
status
|
||||
}
|
||||
errors {
|
||||
message
|
||||
}
|
||||
status
|
||||
}
|
||||
}",
|
||||
OperationName = "LnInvoicePaymentSend",
|
||||
Variables = new {
|
||||
input = new {
|
||||
walletId = _walletId,
|
||||
paymentRequest = bolt11,
|
||||
}
|
||||
}
|
||||
};
|
||||
CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellation,
|
||||
new CancellationTokenSource(payParams?.SendTimeout ?? PayInvoiceParams.DefaultSendTimeout).Token);
|
||||
var response =(JObject) (await _client.SendQueryAsync<dynamic>(request, cts.Token)).Data.lnInvoicePaymentSend;
|
||||
|
||||
var result = new PayResponse();
|
||||
result.Result = response["status"].Value<string>() switch
|
||||
{
|
||||
"ALREADY_PAID" => PayResult.Ok,
|
||||
"FAILURE" => PayResult.Error,
|
||||
"PENDING"=> PayResult.Unknown,
|
||||
"SUCCESS" => PayResult.Ok,
|
||||
null => PayResult.Unknown,
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
if (response["transaction"]?.Value<JObject>() is not null)
|
||||
{
|
||||
result.Details = new PayDetails()
|
||||
{
|
||||
PaymentHash = new uint256(response["transaction"]["initiationVia"]["paymentHash"].Value<string>()),
|
||||
Status = response["status"].Value<string>() switch
|
||||
{
|
||||
"ALREADY_PAID" => LightningPaymentStatus.Complete,
|
||||
"FAILURE" => LightningPaymentStatus.Failed,
|
||||
"PENDING" => LightningPaymentStatus.Pending,
|
||||
"SUCCESS" => LightningPaymentStatus.Complete,
|
||||
null => LightningPaymentStatus.Unknown,
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
},
|
||||
Preimage = response["transaction"]["settlementVia"]?["preImage"].Value<string>() is null? null: new uint256(response["transaction"]["settlementVia"]["preImage"].Value<string>()),
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<PayResponse> Pay(string bolt11, CancellationToken cancellation = new CancellationToken())
|
||||
{
|
||||
return await Pay(bolt11, new PayInvoiceParams(), cancellation);
|
||||
}
|
||||
|
||||
|
||||
public async Task CancelInvoice(string invoiceId, CancellationToken cancellation = new CancellationToken())
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public async Task<BitcoinAddress> GetDepositAddress(CancellationToken cancellation = new CancellationToken())
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public async Task<OpenChannelResponse> OpenChannel(OpenChannelRequest openChannelRequest,
|
||||
CancellationToken cancellation = new CancellationToken())
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public async Task<ConnectionResult> ConnectTo(NodeInfo nodeInfo,
|
||||
CancellationToken cancellation = new CancellationToken())
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public async Task<LightningChannel[]> ListChannels(CancellationToken cancellation = new CancellationToken())
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Lightning;
|
||||
using Network = NBitcoin.Network;
|
||||
|
||||
namespace BTCPayServer.Plugins.Blink;
|
||||
|
||||
|
||||
public class BlinkLightningConnectionStringHandler : ILightningConnectionStringHandler
|
||||
{
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly NBXplorerDashboard _nbXplorerDashboard;
|
||||
|
||||
public BlinkLightningConnectionStringHandler(IHttpClientFactory httpClientFactory, NBXplorerDashboard nbXplorerDashboard)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_nbXplorerDashboard = nbXplorerDashboard;
|
||||
}
|
||||
|
||||
|
||||
public ILightningClient? Create(string connectionString, Network network, out string? error)
|
||||
{
|
||||
var kv = LightningConnectionStringHelper.ExtractValues(connectionString, out var type);
|
||||
if (type != "blink")
|
||||
{
|
||||
error = null;
|
||||
return null;
|
||||
}
|
||||
if (!kv.TryGetValue("server", out var server))
|
||||
{
|
||||
error = $"The key 'server' is mandatory for blink connection strings";
|
||||
return null;
|
||||
}
|
||||
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 null;
|
||||
}
|
||||
bool allowInsecure = false;
|
||||
if (kv.TryGetValue("allowinsecure", out var allowinsecureStr))
|
||||
{
|
||||
var allowedValues = new[] {"true", "false"};
|
||||
if (!allowedValues.Any(v => v.Equals(allowinsecureStr, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
error = "The key 'allowinsecure' should be true or false";
|
||||
return null;
|
||||
}
|
||||
|
||||
allowInsecure = allowinsecureStr.Equals("true", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
if (!LightningConnectionStringHelper.VerifySecureEndpoint(uri, allowInsecure))
|
||||
{
|
||||
error = "The key 'allowinsecure' is false, but server's Uri is not using https";
|
||||
return null;
|
||||
}
|
||||
if (!kv.TryGetValue("api-key", out var apiKey))
|
||||
{
|
||||
error = "The key 'api-key' is not found";
|
||||
return null;
|
||||
}
|
||||
if (!kv.TryGetValue("wallet-id", out var walletId))
|
||||
{
|
||||
error = "The key 'wallet-id' is not found";
|
||||
return null;
|
||||
}
|
||||
|
||||
error = null;
|
||||
|
||||
var client = _httpClientFactory.CreateClient();
|
||||
|
||||
client.DefaultRequestHeaders.Add("X-Api-Key", apiKey);
|
||||
|
||||
client.BaseAddress = uri;
|
||||
|
||||
network = Network.Main;
|
||||
var bclient = new BlinkLightningClient( apiKey, uri, walletId, network, _nbXplorerDashboard, client);
|
||||
var result = bclient.GetBalance().GetAwaiter().GetResult();
|
||||
if (result is null)
|
||||
{
|
||||
error = "Invalid credentials";
|
||||
return null;
|
||||
}
|
||||
return bclient;
|
||||
|
||||
}
|
||||
}
|
||||
29
Plugins/BTCPayServer.Plugins.Blink/BlinkPlugin.cs
Normal file
29
Plugins/BTCPayServer.Plugins.Blink/BlinkPlugin.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
#nullable enable
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Abstractions.Services;
|
||||
using BTCPayServer.Lightning;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace BTCPayServer.Plugins.Blink
|
||||
{
|
||||
|
||||
public class BlinkPlugin : BaseBTCPayServerPlugin
|
||||
{
|
||||
public override IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } =
|
||||
{
|
||||
new() {Identifier = nameof(BTCPayServer), Condition = ">=1.12.0"}
|
||||
|
||||
};
|
||||
|
||||
public override void Execute(IServiceCollection applicationBuilder)
|
||||
{
|
||||
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("Blink/LNPaymentMethodSetupTab", "ln-payment-method-setup-tab"));
|
||||
applicationBuilder.AddSingleton<ILightningConnectionStringHandler>(provider => provider.GetRequiredService<BlinkLightningConnectionStringHandler>());
|
||||
applicationBuilder.AddSingleton<BlinkLightningConnectionStringHandler>();
|
||||
|
||||
base.Execute(applicationBuilder);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
@model BTCPayServer.Models.StoreViewModels.LightningNodeViewModel
|
||||
@{
|
||||
var storeId = Model.StoreId;
|
||||
if (Model.CryptoCode != "BTC")
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
|
||||
const customNodeAccordian = document.getElementById("CustomNodeSupport");
|
||||
const template = document.getElementById("blink");
|
||||
customNodeAccordian.appendChild(template.content.cloneNode(true));
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
<template id="blink">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="CustomBlinkHeader">
|
||||
<button type="button" class="accordion-button collapsed" data-bs-toggle="collapse" data-bs-target="#CustomBlinkContent" aria-controls="CustomBlinkContent" aria-expanded="false">
|
||||
<span><strong>Blink</strong> via GraphQL</span>
|
||||
<vc:icon symbol="caret-down"/>
|
||||
</button>
|
||||
</h2>
|
||||
<div id="CustomBlinkContent" class="accordion-collapse collapse" aria-labelledby="CustomBlinkHeader" data-bs-parent="#CustomNodeSupport">
|
||||
<div class="accordion-body">
|
||||
<ul class="pb-2">
|
||||
<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>
|
||||
</li>
|
||||
</ul>
|
||||
<p class="my-2">Head over to the dashboard for the wallet id and and create an api key.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
@inject BTCPayServer.Abstractions.Services.Safe Safe
|
||||
@addTagHelper *, BTCPayServer.Abstractions
|
||||
@addTagHelper *, BTCPayServer.TagHelpers
|
||||
@addTagHelper *, BTCPayServer.Views.TagHelpers
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@addTagHelper *, BTCPayServer
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<LangVersion>10</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -33,10 +33,7 @@
|
||||
<ProjectReference Include="..\..\submodules\btcpayserver\BTCPayServer\BTCPayServer.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="Resources" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Breez.Sdk" Version="0.2.7" />
|
||||
<PackageReference Include="Breez.Sdk" Version="0.2.10" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
|
||||
@@ -326,7 +326,7 @@ public class BreezController : Controller
|
||||
|
||||
viewModel ??= new PaymentsViewModel();
|
||||
|
||||
viewModel.Payments = client.Sdk.ListPayments(new ListPaymentsRequest(PaymentTypeFilter.ALL, null, null, true,
|
||||
viewModel.Payments = client.Sdk.ListPayments(new ListPaymentsRequest(null, null, null, true,
|
||||
(uint?) viewModel.Skip, (uint?) viewModel.Count));
|
||||
|
||||
return View(viewModel);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -135,7 +136,7 @@ public class BreezLightningClient : ILightningClient, IDisposable, EventListener
|
||||
public async Task<LightningInvoice[]> ListInvoices(ListInvoicesParams request,
|
||||
CancellationToken cancellation = default)
|
||||
{
|
||||
return Sdk.ListPayments(new ListPaymentsRequest(PaymentTypeFilter.RECEIVED, null, null,
|
||||
return Sdk.ListPayments(new ListPaymentsRequest(new List<PaymentTypeFilter>(){PaymentTypeFilter.RECEIVED}, null, null,
|
||||
request?.PendingOnly is not true, (uint?) request?.OffsetIndex, null))
|
||||
.Select(FromPayment).ToArray();
|
||||
}
|
||||
@@ -153,7 +154,7 @@ public class BreezLightningClient : ILightningClient, IDisposable, EventListener
|
||||
public async Task<LightningPayment[]> ListPayments(ListPaymentsParams request,
|
||||
CancellationToken cancellation = default)
|
||||
{
|
||||
return Sdk.ListPayments(new ListPaymentsRequest(PaymentTypeFilter.RECEIVED, null, null, null,
|
||||
return Sdk.ListPayments(new ListPaymentsRequest(new List<PaymentTypeFilter>(){PaymentTypeFilter.RECEIVED}, null, null, null,
|
||||
(uint?) request?.OffsetIndex, null))
|
||||
.Select(ToLightningPayment).ToArray();
|
||||
}
|
||||
|
||||
@@ -8,9 +8,11 @@
|
||||
@using BTCPayServer.Payments
|
||||
@using BTCPayServer.Plugins.Breez
|
||||
@using BTCPayServer.Security
|
||||
@using BTCPayServer.Services
|
||||
@using NBitcoin
|
||||
@inject BreezService BreezService
|
||||
@inject BTCPayNetworkProvider BtcPayNetworkProvider
|
||||
@inject TransactionLinkProviders TransactionLinkProviders
|
||||
@{
|
||||
ViewData.SetActivePage("Breez", "Swap In", "SwapIn");
|
||||
string storeId = Model switch
|
||||
@@ -36,6 +38,7 @@
|
||||
var deriv = Context.GetStoreData().GetDerivationSchemeSettings(BtcPayNetworkProvider, "BTC");
|
||||
var ni = sdk.NodeInfo();
|
||||
var f = sdk.RecommendedFees();
|
||||
var pmi = new PaymentMethodId("BTC", PaymentTypes.BTCLike);
|
||||
|
||||
|
||||
}
|
||||
@@ -104,7 +107,7 @@
|
||||
<span class="text-nowrap">Unconfirmed transactions</span>
|
||||
@foreach (var txId in inProgressSwap.unconfirmedTxIds)
|
||||
{
|
||||
<vc:truncate-center text="@txId" link="@BitcoinPaymentType.Instance.GetTransactionLink(BtcPayNetworkProvider.BTC, txId)" classes="truncate-center-id"/>
|
||||
<vc:truncate-center text="@txId" link="@TransactionLinkProviders.GetTransactionLink(pmi, txId)" classes="truncate-center-id"/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@@ -115,7 +118,7 @@
|
||||
<span class="text-nowrap">Confirmed transactions</span>
|
||||
@foreach (var txId in inProgressSwap.confirmedTxIds)
|
||||
{
|
||||
<vc:truncate-center text="@txId" link="@BitcoinPaymentType.Instance.GetTransactionLink(BtcPayNetworkProvider.BTC, txId)" classes="truncate-center-id"/>
|
||||
<vc:truncate-center text="@txId" link="@TransactionLinkProviders.GetTransactionLink(pmi, txId)" classes="truncate-center-id"/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@@ -156,7 +159,7 @@
|
||||
<span class="text-nowrap">Unconfirmed transactions</span>
|
||||
@foreach (var txId in refund.unconfirmedTxIds)
|
||||
{
|
||||
<vc:truncate-center text="@txId" link="@BitcoinPaymentType.Instance.GetTransactionLink(BtcPayNetworkProvider.BTC, txId)" classes="truncate-center-id"/>
|
||||
<vc:truncate-center text="@txId" link="@TransactionLinkProviders.GetTransactionLink(pmi, txId)" classes="truncate-center-id"/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@@ -167,7 +170,7 @@
|
||||
<span class="text-nowrap">Confirmed transactions</span>
|
||||
@foreach (var txId in refund.confirmedTxIds)
|
||||
{
|
||||
<vc:truncate-center text="@txId" link="@BitcoinPaymentType.Instance.GetTransactionLink(BtcPayNetworkProvider.BTC, txId)" classes="truncate-center-id"/>
|
||||
<vc:truncate-center text="@txId" link="@TransactionLinkProviders.GetTransactionLink(pmi, txId)" classes="truncate-center-id"/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@@ -178,7 +181,7 @@
|
||||
<span class="text-nowrap">Refund transactions</span>
|
||||
@foreach (var txId in refund.refundTxIds)
|
||||
{
|
||||
<vc:truncate-center text="@txId" link="@BitcoinPaymentType.Instance.GetTransactionLink(BtcPayNetworkProvider.BTC, txId)" classes="truncate-center-id"/>
|
||||
<vc:truncate-center text="@txId" link="@TransactionLinkProviders.GetTransactionLink(pmi, txId)" classes="truncate-center-id"/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
if (sdk is null)
|
||||
return;
|
||||
|
||||
data = sdk.ListPayments(new ListPaymentsRequest(PaymentTypeFilter.ALL, null, null, null, 0, 10));
|
||||
data = sdk.ListPayments(new ListPaymentsRequest(null, null, null, null, 0, 10));
|
||||
}
|
||||
|
||||
var isDashboard = Model is StoreDashboardViewModel;
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="NNostr.Client" Version="0.0.37" />
|
||||
<PackageReference Include="NNostr.Client" Version="0.0.38" > </PackageReference>
|
||||
<PackageReference Include="WabiSabi" Version="1.0.1.2" />
|
||||
</ItemGroup>
|
||||
<Target Name="DeleteExampleFile" AfterTargets="Publish">
|
||||
|
||||
Reference in New Issue
Block a user