< Summary - Combined Code Coverage

Information
Class: NLightning.Client.Ipc.NamedPipeIpcClient
Assembly: NLightning.Client
File(s): /home/runner/work/NLightning/NLightning/src/NLightning.Client/Ipc/NamedPipeIpcClient.cs
Tag: 57_24045730253
Line coverage
0%
Covered lines: 0
Uncovered lines: 203
Coverable lines: 203
Total lines: 279
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 34
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/NLightning/NLightning/src/NLightning.Client/Ipc/NamedPipeIpcClient.cs

#LineLine coverage
 1using System.Buffers;
 2using System.IO.Pipes;
 3using MessagePack;
 4using NLightning.Domain.Channels.ValueObjects;
 5
 6namespace NLightning.Client.Ipc;
 7
 8using Domain.Bitcoin.Enums;
 9using Domain.Client.Enums;
 10using Domain.Money;
 11using Domain.Node.ValueObjects;
 12using Transport.Ipc;
 13using Transport.Ipc.Requests;
 14using Transport.Ipc.Responses;
 15
 16public sealed class NamedPipeIpcClient : IAsyncDisposable
 17{
 18    private readonly string _namedPipeFilePath;
 19    private readonly string _cookieFilePath;
 20    private readonly string _server;
 21
 022    public NamedPipeIpcClient(string namedPipeFilePath, string cookieFilePath, string server = ".")
 023    {
 024        _namedPipeFilePath = namedPipeFilePath;
 025        _cookieFilePath = cookieFilePath;
 026        _server = server;
 027    }
 28
 29    public async Task<NodeInfoIpcResponse> GetNodeInfoAsync(CancellationToken ct = default)
 030    {
 031        var req = new NodeInfoIpcRequest();
 032        var payload = MessagePackSerializer.Serialize(req, cancellationToken: ct);
 033        var env = new IpcEnvelope
 034        {
 035            Version = 1,
 036            Command = ClientCommand.NodeInfo,
 037            CorrelationId = Guid.NewGuid(),
 038            AuthToken = await GetAuthTokenAsync(ct),
 039            Payload = payload,
 040            Kind = 0
 041        };
 42
 043        var respEnv = await SendAsync(env, ct);
 044        if (respEnv.Kind != IpcEnvelopeKind.Error)
 045            return MessagePackSerializer.Deserialize<NodeInfoIpcResponse>(respEnv.Payload, cancellationToken: ct);
 46
 047        var err = MessagePackSerializer.Deserialize<IpcError>(respEnv.Payload, cancellationToken: ct);
 048        throw new InvalidOperationException($"IPC error {err.Code}: {err.Message}");
 049    }
 50
 51    public async Task<ConnectPeerIpcResponse> ConnectPeerAsync(string address, CancellationToken ct = default)
 052    {
 053        var req = new ConnectPeerIpcRequest
 054        {
 055            Address = new PeerAddressInfo(address)
 056        };
 057        var payload = MessagePackSerializer.Serialize(req, cancellationToken: ct);
 058        var env = new IpcEnvelope
 059        {
 060            Version = 1,
 061            Command = ClientCommand.ConnectPeer,
 062            CorrelationId = Guid.NewGuid(),
 063            AuthToken = await GetAuthTokenAsync(ct),
 064            Payload = payload,
 065            Kind = 0
 066        };
 67
 068        var respEnv = await SendAsync(env, ct);
 069        if (respEnv.Kind != IpcEnvelopeKind.Error)
 070            return MessagePackSerializer.Deserialize<ConnectPeerIpcResponse>(respEnv.Payload, cancellationToken: ct);
 71
 072        var err = MessagePackSerializer.Deserialize<IpcError>(respEnv.Payload, cancellationToken: ct);
 073        throw new InvalidOperationException($"IPC error {err.Code}: {err.Message}");
 074    }
 75
 76    public async Task<ListPeersIpcResponse> ListPeersAsync(CancellationToken ct = default)
 077    {
 078        var req = new ListPeersIpcRequest();
 079        var payload = MessagePackSerializer.Serialize(req, cancellationToken: ct);
 080        var env = new IpcEnvelope
 081        {
 082            Version = 1,
 083            Command = ClientCommand.ListPeers,
 084            CorrelationId = Guid.NewGuid(),
 085            AuthToken = await GetAuthTokenAsync(ct),
 086            Payload = payload,
 087            Kind = IpcEnvelopeKind.Request
 088        };
 89
 090        var respEnv = await SendAsync(env, ct);
 091        if (respEnv.Kind != IpcEnvelopeKind.Error)
 092            return MessagePackSerializer.Deserialize<ListPeersIpcResponse>(respEnv.Payload, cancellationToken: ct);
 93
 094        var err = MessagePackSerializer.Deserialize<IpcError>(respEnv.Payload, cancellationToken: ct);
 095        throw new InvalidOperationException($"IPC error {err.Code}: {err.Message}");
 096    }
 97
 98    public async Task<GetAddressIpcResponse> GetAddressAsync(string? addressTypeString, CancellationToken ct = default)
 099    {
 0100        var addressType = AddressType.P2Tr;
 0101        if (!string.IsNullOrWhiteSpace(addressTypeString))
 0102        {
 0103            addressType = addressTypeString.ToLowerInvariant() switch
 0104            {
 0105                "p2tr" => AddressType.P2Tr,
 0106                "p2wpkh" => AddressType.P2Wpkh,
 0107                "all" => AddressType.P2Tr | AddressType.P2Wpkh,
 0108                _ => throw new ArgumentOutOfRangeException(nameof(addressTypeString), addressTypeString,
 0109                                                           "Address has to be `p2tr`, `p2wpkh`, or `all`.")
 0110            };
 0111        }
 112
 0113        var req = new GetAddressIpcRequest { AddressType = addressType };
 0114        var payload = MessagePackSerializer.Serialize(req, cancellationToken: ct);
 0115        var env = new IpcEnvelope
 0116        {
 0117            Version = 1,
 0118            Command = ClientCommand.GetAddress,
 0119            CorrelationId = Guid.NewGuid(),
 0120            AuthToken = await GetAuthTokenAsync(ct),
 0121            Payload = payload,
 0122            Kind = IpcEnvelopeKind.Request
 0123        };
 124
 0125        var respEnv = await SendAsync(env, ct);
 0126        if (respEnv.Kind != IpcEnvelopeKind.Error)
 0127            return MessagePackSerializer.Deserialize<GetAddressIpcResponse>(respEnv.Payload, cancellationToken: ct);
 128
 0129        var err = MessagePackSerializer.Deserialize<IpcError>(respEnv.Payload, cancellationToken: ct);
 0130        throw new InvalidOperationException($"IPC error {err.Code}: {err.Message}");
 0131    }
 132
 133    public async Task<WalletBalanceIpcResponse> GetWalletBalance(CancellationToken ct)
 0134    {
 0135        var req = new WalletBalanceIpcRequest();
 0136        var payload = MessagePackSerializer.Serialize(req, cancellationToken: ct);
 0137        var env = new IpcEnvelope
 0138        {
 0139            Version = 1,
 0140            Command = ClientCommand.WalletBalance,
 0141            CorrelationId = Guid.NewGuid(),
 0142            AuthToken = await GetAuthTokenAsync(ct),
 0143            Payload = payload,
 0144            Kind = 0
 0145        };
 146
 0147        var respEnv = await SendAsync(env, ct);
 0148        if (respEnv.Kind != IpcEnvelopeKind.Error)
 0149            return MessagePackSerializer.Deserialize<WalletBalanceIpcResponse>(respEnv.Payload, cancellationToken: ct);
 150
 0151        var err = MessagePackSerializer.Deserialize<IpcError>(respEnv.Payload, cancellationToken: ct);
 0152        throw new InvalidOperationException($"IPC error {err.Code}: {err.Message}");
 0153    }
 154
 155    public async Task<OpenChannelIpcResponse> OpenChannelAsync(string nodeInfo, string amountSats,
 156                                                               CancellationToken ct = default)
 0157    {
 0158        var req = new OpenChannelIpcRequest
 0159        {
 0160            NodeInfo = nodeInfo,
 0161            Amount = LightningMoney.Satoshis(Convert.ToInt64(amountSats))
 0162        };
 0163        var payload = MessagePackSerializer.Serialize(req, cancellationToken: ct);
 0164        var env = new IpcEnvelope
 0165        {
 0166            Version = 1,
 0167            Command = ClientCommand.OpenChannel,
 0168            CorrelationId = Guid.NewGuid(),
 0169            AuthToken = await GetAuthTokenAsync(ct),
 0170            Payload = payload,
 0171            Kind = 0
 0172        };
 173
 0174        var respEnv = await SendAsync(env, ct);
 0175        if (respEnv.Kind != IpcEnvelopeKind.Error)
 0176            return MessagePackSerializer.Deserialize<OpenChannelIpcResponse>(respEnv.Payload, cancellationToken: ct);
 177
 0178        var err = MessagePackSerializer.Deserialize<IpcError>(respEnv.Payload, cancellationToken: ct);
 0179        throw new InvalidOperationException($"IPC error {err.Code}: {err.Message}");
 0180    }
 181
 182    public async Task<OpenChannelSubscriptionIpcResponse> OpenChannelSubscriptionAsync(
 183        ChannelId channelId, CancellationToken ct = default)
 0184    {
 0185        var req = new OpenChannelSubscriptionIpcRequest
 0186        {
 0187            ChannelId = channelId
 0188        };
 0189        var payload = MessagePackSerializer.Serialize(req, cancellationToken: ct);
 0190        var env = new IpcEnvelope
 0191        {
 0192            Version = 1,
 0193            Command = ClientCommand.OpenChannelSubscription,
 0194            CorrelationId = Guid.NewGuid(),
 0195            AuthToken = await GetAuthTokenAsync(ct),
 0196            Payload = payload,
 0197            Kind = 0
 0198        };
 199
 0200        var respEnv = await SendAsync(env, ct);
 0201        if (respEnv.Kind != IpcEnvelopeKind.Error)
 0202            return MessagePackSerializer.Deserialize<OpenChannelSubscriptionIpcResponse>(
 0203                respEnv.Payload, cancellationToken: ct);
 204
 0205        var err = MessagePackSerializer.Deserialize<IpcError>(respEnv.Payload, cancellationToken: ct);
 0206        throw new InvalidOperationException($"IPC error {err.Code}: {err.Message}");
 0207    }
 208
 209    private async Task<IpcEnvelope> SendAsync(IpcEnvelope envelope, CancellationToken ct)
 0210    {
 0211        await using var client =
 0212            new NamedPipeClientStream(_server, _namedPipeFilePath, PipeDirection.InOut, PipeOptions.Asynchronous);
 213
 214        try
 0215        {
 0216            await client.ConnectAsync(TimeSpan.FromSeconds(2), ct);
 0217        }
 0218        catch (TimeoutException)
 0219        {
 0220            throw new IOException(
 0221                "Could not connect to NLightning node IPC pipe. Ensure the node is running and listening for IPC.");
 222        }
 223
 224        // Send request
 0225        var bytes = MessagePackSerializer.Serialize(envelope, cancellationToken: ct);
 0226        var lenPrefix = BitConverter.GetBytes(bytes.Length);
 0227        await client.WriteAsync(lenPrefix, ct);
 0228        await client.WriteAsync(bytes, ct);
 0229        await client.FlushAsync(ct);
 230
 231        // Read response length
 0232        var header = new byte[4];
 0233        await ReadExactAsync(client, header, ct);
 0234        var respLen = BitConverter.ToInt32(header, 0);
 0235        if (respLen is <= 0 or > 10_000_000)
 0236            throw new IOException("Invalid IPC response length.");
 237
 238        // Read payload
 0239        var respBuf = ArrayPool<byte>.Shared.Rent(respLen);
 240        try
 0241        {
 0242            await ReadExactAsync(client, respBuf.AsMemory(0, respLen), ct);
 0243            var env = MessagePackSerializer.Deserialize<IpcEnvelope>(respBuf.AsMemory(0, respLen),
 0244                                                                     cancellationToken: ct);
 0245            return env;
 246        }
 247        finally
 0248        {
 0249            ArrayPool<byte>.Shared.Return(respBuf);
 0250        }
 0251    }
 252
 253    private static async Task ReadExactAsync(Stream stream, Memory<byte> buffer, CancellationToken ct)
 0254    {
 0255        var total = 0;
 0256        while (total < buffer.Length)
 0257        {
 0258            var read = await stream.ReadAsync(buffer[total..], ct);
 0259            if (read == 0) throw new EndOfStreamException();
 0260            total += read;
 0261        }
 0262    }
 263
 264    private static async Task ReadExactAsync(Stream stream, byte[] buffer, CancellationToken ct)
 0265        => await ReadExactAsync(stream, buffer.AsMemory(), ct);
 266
 267    private async Task<string> GetAuthTokenAsync(CancellationToken ct)
 0268    {
 0269        if (!File.Exists(_cookieFilePath))
 0270            throw new IOException(
 0271                "Authentication cookie file not found. Ensure the node is running and the cookie file path is correct.")
 272
 0273        var content = await File.ReadAllTextAsync(_cookieFilePath, ct);
 0274        return content.Trim();
 0275    }
 276
 277    public ValueTask DisposeAsync()
 0278        => ValueTask.CompletedTask;
 279}