Files
BTCPayServerPlugins/Plugins/BTCPayServer.Plugins.Breez/BreezLightningClient.cs
2023-12-07 10:50:10 +01:00

372 lines
14 KiB
C#

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Breez.Sdk;
using BTCPayServer.Lightning;
using NBitcoin;
using Network = Breez.Sdk.Network;
namespace BTCPayServer.Plugins.Breez;
public class BreezLightningClient : ILightningClient, IDisposable, EventListener
{
public override string ToString()
{
return $"type=breez;key={PaymentKey}";
}
private readonly NBitcoin.Network _network;
public readonly string PaymentKey;
public BreezLightningClient(string inviteCode, string apiKey, string workingDir, NBitcoin.Network network,
string mnemonic, string paymentKey)
{
_network = network;
PaymentKey = paymentKey;
var nodeConfig = new NodeConfig.Greenlight(
new GreenlightNodeConfig(null, inviteCode)
);
var config = BreezSdkMethods.DefaultConfig(
network == NBitcoin.Network.Main ? EnvironmentType.PRODUCTION : EnvironmentType.STAGING,
apiKey,
nodeConfig
) with
{
workingDir = workingDir,
network = network == NBitcoin.Network.Main ? Network.BITCOIN :
network == NBitcoin.Network.TestNet ? Network.TESTNET :
network == NBitcoin.Network.RegTest ? Network.REGTEST : Network.SIGNET
};
var seed = BreezSdkMethods.MnemonicToSeed(mnemonic);
Sdk = BreezSdkMethods.Connect(config, seed, this);
}
public BlockingBreezServices Sdk { get; }
public event EventHandler<BreezEvent> EventReceived;
public void OnEvent(BreezEvent e)
{
EventReceived?.Invoke(this, e);
}
public Task<LightningInvoice> GetInvoice(string invoiceId, CancellationToken cancellation = default)
{
return GetInvoice(uint256.Parse(invoiceId), cancellation);
}
private LightningPayment ToLightningPayment(Payment payment)
{
if (payment?.details is not PaymentDetails.Ln lnPaymentDetails)
{
return null;
}
return new LightningPayment()
{
Amount = LightMoney.MilliSatoshis(payment.amountMsat),
Id = lnPaymentDetails.data.paymentHash,
Preimage = lnPaymentDetails.data.paymentPreimage,
PaymentHash = lnPaymentDetails.data.paymentHash,
BOLT11 = lnPaymentDetails.data.bolt11,
Status = payment.status switch
{
PaymentStatus.FAILED => LightningPaymentStatus.Failed,
PaymentStatus.COMPLETE => LightningPaymentStatus.Complete,
PaymentStatus.PENDING => LightningPaymentStatus.Pending,
_ => throw new ArgumentOutOfRangeException()
},
CreatedAt = DateTimeOffset.FromUnixTimeMilliseconds(payment.paymentTime),
Fee = LightMoney.MilliSatoshis(payment.feeMsat),
AmountSent = LightMoney.MilliSatoshis(payment.amountMsat)
};
}
private LightningInvoice FromPayment(Payment p)
{
if (p?.details is not PaymentDetails.Ln lnPaymentDetails)
{
return null;
}
var bolt11 = BOLT11PaymentRequest.Parse(lnPaymentDetails.data.bolt11, _network);
return new LightningInvoice()
{
Amount = LightMoney.MilliSatoshis(p.amountMsat),
Id = lnPaymentDetails.data.paymentHash,
Preimage = lnPaymentDetails.data.paymentPreimage,
PaymentHash = lnPaymentDetails.data.paymentHash,
BOLT11 = lnPaymentDetails.data.bolt11,
Status = p.status switch
{
PaymentStatus.FAILED => LightningInvoiceStatus.Expired,
PaymentStatus.COMPLETE => LightningInvoiceStatus.Paid,
_ => LightningInvoiceStatus.Unpaid
},
PaidAt = DateTimeOffset.FromUnixTimeSeconds(p.paymentTime),
ExpiresAt = bolt11.ExpiryDate
};
}
public async Task<LightningInvoice> GetInvoice(uint256 paymentHash, CancellationToken cancellation = default)
{
var p = Sdk.PaymentByHash(paymentHash.ToString()!);
if(p is null)
return new LightningInvoice()
{
Id = paymentHash.ToString(),
PaymentHash = paymentHash.ToString(),
Status = LightningInvoiceStatus.Expired,
};
return FromPayment(p);
}
public async Task<LightningInvoice[]> ListInvoices(CancellationToken cancellation = default)
{
return await ListInvoices(null, cancellation);
}
public async Task<LightningInvoice[]> ListInvoices(ListInvoicesParams request,
CancellationToken cancellation = default)
{
return Sdk.ListPayments(new ListPaymentsRequest(new List<PaymentTypeFilter>(){PaymentTypeFilter.RECEIVED}, null, null,
request?.PendingOnly is not true, (uint?) request?.OffsetIndex, null))
.Select(FromPayment).ToArray();
}
public async Task<LightningPayment> GetPayment(string paymentHash, CancellationToken cancellation = default)
{
return ToLightningPayment(Sdk.PaymentByHash(paymentHash));
}
public async Task<LightningPayment[]> ListPayments(CancellationToken cancellation = default)
{
return await ListPayments(null, cancellation);
}
public async Task<LightningPayment[]> ListPayments(ListPaymentsParams request,
CancellationToken cancellation = default)
{
return Sdk.ListPayments(new ListPaymentsRequest(new List<PaymentTypeFilter>(){PaymentTypeFilter.RECEIVED}, null, null, null,
(uint?) request?.OffsetIndex, null))
.Select(ToLightningPayment).ToArray();
}
public async Task<LightningInvoice> CreateInvoice(LightMoney amount, string description, TimeSpan expiry,
CancellationToken cancellation = default)
{
var expiryS = expiry == TimeSpan.Zero ? (uint?) null : Math.Max(0, (uint) expiry.TotalSeconds);
var p = Sdk.ReceivePayment(new ReceivePaymentRequest((ulong) amount.MilliSatoshi, description, null, null,
false, expiryS));
return FromPR(p);
}
public LightningInvoice FromPR(ReceivePaymentResponse response)
{
return new LightningInvoice()
{
Amount = LightMoney.MilliSatoshis(response.lnInvoice.amountMsat ?? 0),
Id = response.lnInvoice.paymentHash,
Preimage = ConvertHelper.ToHexString(response.lnInvoice.paymentSecret.ToArray()),
PaymentHash = response.lnInvoice.paymentHash,
BOLT11 = response.lnInvoice.bolt11,
Status = LightningInvoiceStatus.Unpaid,
ExpiresAt = DateTimeOffset.FromUnixTimeSeconds((long) response.lnInvoice.expiry)
};
}
public async Task<LightningInvoice> CreateInvoice(CreateInvoiceParams createInvoiceRequest,
CancellationToken cancellation = default)
{
var expiryS = createInvoiceRequest.Expiry == TimeSpan.Zero
? (uint?) null
: Math.Max(0, (uint) createInvoiceRequest.Expiry.TotalSeconds);
var p = Sdk.ReceivePayment(new ReceivePaymentRequest((ulong) createInvoiceRequest.Amount.MilliSatoshi,
(createInvoiceRequest.Description ?? createInvoiceRequest.DescriptionHash.ToString())!, null, null,
createInvoiceRequest.DescriptionHashOnly, expiryS));
return FromPR(p);
}
public async Task<ILightningInvoiceListener> Listen(CancellationToken cancellation = default)
{
return new BreezInvoiceListener(this, cancellation);
}
public async Task<LightningNodeInformation> GetInfo(CancellationToken cancellation = default)
{
var ni = Sdk.NodeInfo();
return new LightningNodeInformation()
{
PeersCount = ni.connectedPeers.Count,
Alias = $"greenlight {ni.id}",
NodeInfoList =
{
new NodeInfo(new PubKey(ni.id), "blockstrean.com", 69)
}, //we have to fake this as btcpay currently requires this to enable the payment method
BlockHeight = (int) ni.blockHeight
};
}
public async Task<LightningNodeBalance> GetBalance(CancellationToken cancellation = default)
{
var ni = Sdk.NodeInfo();
return new LightningNodeBalance()
{
OnchainBalance =
new OnchainBalance()
{
Confirmed = Money.Coins(LightMoney.MilliSatoshis(ni.onchainBalanceMsat)
.ToUnit(LightMoneyUnit.BTC))
},
OffchainBalance = new OffchainBalance()
{
Local = LightMoney.MilliSatoshis(ni.channelsBalanceMsat),
Remote = LightMoney.MilliSatoshis(ni.inboundLiquidityMsats),
}
};
}
public async Task<PayResponse> Pay(PayInvoiceParams payParams, CancellationToken cancellation = default)
{
return await Pay(null, payParams, cancellation);
}
public async Task<PayResponse> Pay(string bolt11, PayInvoiceParams payParams,
CancellationToken cancellation = default)
{
SendPaymentResponse result;
try
{
if (bolt11 is null)
{
result = Sdk.SendSpontaneousPayment(new SendSpontaneousPaymentRequest(payParams.Destination.ToString(),
(ulong) payParams.Amount.MilliSatoshi));
}
else
{
result = Sdk.SendPayment(new SendPaymentRequest(bolt11, (ulong?) payParams.Amount?.MilliSatoshi));
}
var details = result.payment.details as PaymentDetails.Ln;
return new PayResponse()
{
Result = result.payment.status switch
{
PaymentStatus.FAILED => PayResult.Error,
PaymentStatus.COMPLETE => PayResult.Ok,
PaymentStatus.PENDING => PayResult.Unknown,
_ => throw new ArgumentOutOfRangeException()
},
Details = new PayDetails()
{
Status = result.payment.status switch
{
PaymentStatus.FAILED => LightningPaymentStatus.Failed,
PaymentStatus.COMPLETE => LightningPaymentStatus.Complete,
PaymentStatus.PENDING => LightningPaymentStatus.Pending,
_ => LightningPaymentStatus.Unknown
},
Preimage =
details.data.paymentPreimage is null ? null : uint256.Parse(details.data.paymentPreimage),
PaymentHash = details.data.paymentHash is null ? null : uint256.Parse(details.data.paymentHash),
FeeAmount = result.payment.feeMsat,
TotalAmount = LightMoney.MilliSatoshis(result.payment.amountMsat + result.payment.feeMsat),
}
};
}
catch (Exception e)
{
return new PayResponse(PayResult.Error, e.Message);
}
}
public async Task<PayResponse> Pay(string bolt11, CancellationToken cancellation = default)
{
return await Pay(bolt11, null, cancellation);
}
public async Task<OpenChannelResponse> OpenChannel(OpenChannelRequest openChannelRequest,
CancellationToken cancellation = default)
{
throw new NotImplementedException();
}
public async Task<BitcoinAddress> GetDepositAddress(CancellationToken cancellation = default)
{
throw new NotImplementedException();
}
public async Task<ConnectionResult> ConnectTo(NodeInfo nodeInfo, CancellationToken cancellation = default)
{
throw new NotImplementedException();
}
public async Task CancelInvoice(string invoiceId, CancellationToken cancellation = default)
{
throw new NotImplementedException();
}
public async Task<LightningChannel[]> ListChannels(CancellationToken cancellation = default)
{
throw new NotImplementedException();
}
public void Dispose()
{
Sdk.Dispose();
Sdk.Dispose();
}
public class BreezInvoiceListener : ILightningInvoiceListener
{
private readonly BreezLightningClient _breezLightningClient;
private readonly CancellationToken _cancellationToken;
public BreezInvoiceListener(BreezLightningClient breezLightningClient, CancellationToken cancellationToken)
{
_breezLightningClient = breezLightningClient;
_cancellationToken = cancellationToken;
breezLightningClient.EventReceived += BreezLightningClientOnEventReceived;
}
private readonly ConcurrentQueue<Task<LightningInvoice>> _invoices = new();
private void BreezLightningClientOnEventReceived(object sender, BreezEvent e)
{
if (e is BreezEvent.InvoicePaid pre)
{
_invoices.Enqueue(_breezLightningClient.GetInvoice(pre.details.paymentHash, _cancellationToken));
}
}
public void Dispose()
{
_breezLightningClient.EventReceived -= BreezLightningClientOnEventReceived;
}
public async Task<LightningInvoice> WaitInvoice(CancellationToken cancellation)
{
while (cancellation.IsCancellationRequested is not true)
{
if (_invoices.TryDequeue(out var task))
{
return await task.WithCancellation(cancellation);
}
await Task.Delay(100, cancellation);
}
cancellation.ThrowIfCancellationRequested();
return null;
}
}
}