From bf9ec11da8231a4259e2469b2c9072cbc9dc32f3 Mon Sep 17 00:00:00 2001 From: Rodrigo Sanchez Date: Fri, 14 Jul 2023 17:34:03 +0200 Subject: [PATCH 1/4] Check upfront shutdown script feature --- src/Services/LightningService.cs | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/Services/LightningService.cs b/src/Services/LightningService.cs index 972ecd2f..ba61531f 100644 --- a/src/Services/LightningService.cs +++ b/src/Services/LightningService.cs @@ -176,7 +176,6 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest) var network = CurrentNetworkHelper.GetCurrentNetwork(); - var closeAddress = await GetCloseAddress(channelOperationRequest, derivationStrategyBase, _nbXplorerService, _logger); _logger.LogInformation("Channel open request for request id: {RequestId} from node: {SourceNodeName} to node: {DestinationNodeName}", channelOperationRequest.Id, @@ -193,11 +192,11 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest) try { var humanSignaturesCount = channelOperationRequest.ChannelOperationRequestPsbts.Count( - x => channelOperationRequest.Wallet != null && - !x.IsFinalisedPSBT && - !x.IsInternalWalletPSBT && + x => channelOperationRequest.Wallet != null && + !x.IsFinalisedPSBT && + !x.IsInternalWalletPSBT && !x.IsTemplatePSBT); - + //If it is a hot wallet, we dont check the number of (human) signatures if (channelOperationRequest.Wallet != null && !channelOperationRequest.Wallet.IsHotWallet && channelOperationRequest.Wallet != null && humanSignaturesCount != channelOperationRequest.Wallet.MofN -1) { @@ -236,7 +235,6 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest) } }, LocalFundingAmount = fundingAmount, - CloseAddress = closeAddress.Address.ToString(), Private = channelOperationRequest.IsChannelPrivate, NodePubkey = ByteString.CopyFrom(Convert.FromHexString(destination.PubKey)), }; @@ -250,8 +248,19 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest) throw new InvalidOperationException(); } + // Check features to see if we need or is allowed to add a close address + var upfrontShutdownScriptOpt = remoteNodeInfo.Features.ContainsKey((uint)FeatureBit.UpfrontShutdownScriptOpt); + var upfrontShutdownScriptReq = remoteNodeInfo.Features.ContainsKey((uint)FeatureBit.UpfrontShutdownScriptReq); + string? closeAddress = null; + if (upfrontShutdownScriptOpt || upfrontShutdownScriptReq) + { + var keyPathInformation = await GetCloseAddress(channelOperationRequest, derivationStrategyBase, _nbXplorerService, _logger); + closeAddress = keyPathInformation?.Address?.ToString(); + openChannelRequest.CloseAddress = closeAddress; + } + //For now, we only rely on pure tcp IPV4 connections - var addr = remoteNodeInfo.Addresses.FirstOrDefault(x => x.Network == "tcp").Addr; + var addr = remoteNodeInfo.Addresses.FirstOrDefault(x => x.Network == "tcp")?.Addr; if (addr == null) { @@ -368,7 +377,7 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest) CreationDatetime = DateTimeOffset.Now, FundingTx = fundingTx, FundingTxOutputIndex = response.ChanOpen.ChannelPoint.OutputIndex, - BtcCloseAddress = closeAddress?.Address.ToString(), + BtcCloseAddress = closeAddress, SatsAmount = channelOperationRequest.SatsAmount, UpdateDatetime = DateTimeOffset.Now, Status = Channel.ChannelStatus.Open, From 8b2293efa2ef6cdeb7aa455a20a1ed480983046b Mon Sep 17 00:00:00 2001 From: Rodrigo Sanchez Date: Fri, 14 Jul 2023 17:57:14 +0200 Subject: [PATCH 2/4] Check IsKnown flag --- src/Services/LightningService.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Services/LightningService.cs b/src/Services/LightningService.cs index ba61531f..39645606 100644 --- a/src/Services/LightningService.cs +++ b/src/Services/LightningService.cs @@ -252,7 +252,8 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest) var upfrontShutdownScriptOpt = remoteNodeInfo.Features.ContainsKey((uint)FeatureBit.UpfrontShutdownScriptOpt); var upfrontShutdownScriptReq = remoteNodeInfo.Features.ContainsKey((uint)FeatureBit.UpfrontShutdownScriptReq); string? closeAddress = null; - if (upfrontShutdownScriptOpt || upfrontShutdownScriptReq) + if (upfrontShutdownScriptOpt && remoteNodeInfo.Features[(uint)FeatureBit.UpfrontShutdownScriptOpt] is { IsKnown: true } || + upfrontShutdownScriptReq && remoteNodeInfo.Features[(uint)FeatureBit.UpfrontShutdownScriptReq] is { IsKnown: true }) { var keyPathInformation = await GetCloseAddress(channelOperationRequest, derivationStrategyBase, _nbXplorerService, _logger); closeAddress = keyPathInformation?.Address?.ToString(); From 68c7a51b3c3ac5fc8b374f1febebcb5abb41c0f1 Mon Sep 17 00:00:00 2001 From: Rodrigo <39995243+RodriFS@users.noreply.github.com> Date: Thu, 20 Jul 2023 11:10:19 +0200 Subject: [PATCH 3/4] Changed GetCloseAddress signature --- src/Services/LightningService.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Services/LightningService.cs b/src/Services/LightningService.cs index 39645606..29bff930 100644 --- a/src/Services/LightningService.cs +++ b/src/Services/LightningService.cs @@ -255,8 +255,8 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest) if (upfrontShutdownScriptOpt && remoteNodeInfo.Features[(uint)FeatureBit.UpfrontShutdownScriptOpt] is { IsKnown: true } || upfrontShutdownScriptReq && remoteNodeInfo.Features[(uint)FeatureBit.UpfrontShutdownScriptReq] is { IsKnown: true }) { - var keyPathInformation = await GetCloseAddress(channelOperationRequest, derivationStrategyBase, _nbXplorerService, _logger); - closeAddress = keyPathInformation?.Address?.ToString(); + var address = await GetCloseAddress(channelOperationRequest, derivationStrategyBase, _nbXplorerService, _logger); + closeAddress = address.Address.ToString(); openChannelRequest.CloseAddress = closeAddress; } @@ -682,7 +682,7 @@ public static PSBT GetCombinedPsbt(ChannelOperationRequest channelOperationReque throw new ArgumentException(invalidPsbtNullToBeUsedForTheRequest, nameof(combinedPSBT)); } - public static async Task GetCloseAddress(ChannelOperationRequest channelOperationRequest, + public static async Task GetCloseAddress(ChannelOperationRequest channelOperationRequest, DerivationStrategyBase derivationStrategyBase, INBXplorerService nbXplorerService, ILogger? _logger = null) { var closeAddress = await From 75cd33f26f366ecd45ca77df7d4876754ec3abb0 Mon Sep 17 00:00:00 2001 From: Rodrigo <39995243+RodriFS@users.noreply.github.com> Date: Thu, 20 Jul 2023 12:15:49 +0200 Subject: [PATCH 4/4] Added unit testing for closing address --- src/Services/LightningService.cs | 112 ++++++++------- .../Services/LightningServiceTests.cs | 127 ++++++++++++++++-- 2 files changed, 180 insertions(+), 59 deletions(-) diff --git a/src/Services/LightningService.cs b/src/Services/LightningService.cs index 29bff930..a581d07d 100644 --- a/src/Services/LightningService.cs +++ b/src/Services/LightningService.cs @@ -198,47 +198,18 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest) !x.IsTemplatePSBT); //If it is a hot wallet, we dont check the number of (human) signatures - if (channelOperationRequest.Wallet != null && !channelOperationRequest.Wallet.IsHotWallet && channelOperationRequest.Wallet != null && humanSignaturesCount != channelOperationRequest.Wallet.MofN -1) + if (channelOperationRequest.Wallet != null && !channelOperationRequest.Wallet.IsHotWallet && channelOperationRequest.Wallet != null && humanSignaturesCount != channelOperationRequest.Wallet.MofN - 1) { - _logger.LogError("The number of human signatures does not match the number of signatures required for this wallet, expected {MofN} but got {HumanSignaturesCount}", channelOperationRequest.Wallet.MofN-1, humanSignaturesCount); + _logger.LogError("The number of human signatures does not match the number of signatures required for this wallet, expected {MofN} but got {HumanSignaturesCount}", channelOperationRequest.Wallet.MofN - 1, humanSignaturesCount); throw new InvalidOperationException("The number of human signatures does not match the number of signatures required for this wallet"); } - if (!combinedPSBT.TryGetVirtualSize(out var estimatedVsize)) - { - _logger.LogError("Could not estimate virtual size of the PSBT"); - throw new InvalidOperationException("Could not estimate virtual size of the PSBT"); - } - if(channelOperationRequest.Changeless && combinedPSBT.Outputs.Any()) + if (channelOperationRequest.Changeless && combinedPSBT.Outputs.Any()) { _logger.LogError("Changeless channel operation request cannot have outputs at this stage"); throw new InvalidOperationException("Changeless channel operation request cannot have outputs at this stage"); } - var changelessVSize = channelOperationRequest.Changeless ? 43 : 0; // 8 value + 1 script pub key size + 34 script pub key hash (Segwit output 2-0f-2 multisig) - var outputVirtualSize = estimatedVsize + changelessVSize; // We add the change output if needed - var initialFeeRate = channelOperationRequest.FeeRate ?? (await LightningHelper.GetFeeRateResult(network, _nbXplorerService)).FeeRate.SatoshiPerByte;; - - var totalFees = new Money(outputVirtualSize * initialFeeRate, MoneyUnit.Satoshi); - - long fundingAmount = channelOperationRequest.Changeless ? channelOperationRequest.SatsAmount - totalFees : channelOperationRequest.SatsAmount; - //We prepare the request (shim) with the base PSBT we had presigned with the UTXOs to fund the channel - var openChannelRequest = new OpenChannelRequest - { - FundingShim = new FundingShim - { - PsbtShim = new PsbtShim - { - BasePsbt = ByteString.FromBase64(combinedPSBT.ToBase64()), - NoPublish = false, - PendingChanId = ByteString.CopyFrom(pendingChannelId) - } - }, - LocalFundingAmount = fundingAmount, - Private = channelOperationRequest.IsChannelPrivate, - NodePubkey = ByteString.CopyFrom(Convert.FromHexString(destination.PubKey)), - }; - //Prior to opening the channel, we add the remote node as a peer var remoteNodeInfo = await GetNodeInfo(channelOperationRequest.DestNode?.PubKey); if (remoteNodeInfo == null) @@ -248,17 +219,12 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest) throw new InvalidOperationException(); } - // Check features to see if we need or is allowed to add a close address - var upfrontShutdownScriptOpt = remoteNodeInfo.Features.ContainsKey((uint)FeatureBit.UpfrontShutdownScriptOpt); - var upfrontShutdownScriptReq = remoteNodeInfo.Features.ContainsKey((uint)FeatureBit.UpfrontShutdownScriptReq); - string? closeAddress = null; - if (upfrontShutdownScriptOpt && remoteNodeInfo.Features[(uint)FeatureBit.UpfrontShutdownScriptOpt] is { IsKnown: true } || - upfrontShutdownScriptReq && remoteNodeInfo.Features[(uint)FeatureBit.UpfrontShutdownScriptReq] is { IsKnown: true }) - { - var address = await GetCloseAddress(channelOperationRequest, derivationStrategyBase, _nbXplorerService, _logger); - closeAddress = address.Address.ToString(); - openChannelRequest.CloseAddress = closeAddress; - } + var initialFeeRate = channelOperationRequest.FeeRate ?? (await LightningHelper.GetFeeRateResult(network, _nbXplorerService)).FeeRate.SatoshiPerByte; + ; + + var fundingAmount = GetFundingAmount(channelOperationRequest, combinedPSBT, initialFeeRate); + + var openChannelRequest = await CreateOpenChannelRequest(channelOperationRequest, combinedPSBT, remoteNodeInfo, fundingAmount, pendingChannelId, derivationStrategyBase); //For now, we only rely on pure tcp IPV4 connections var addr = remoteNodeInfo.Addresses.FirstOrDefault(x => x.Network == "tcp")?.Addr; @@ -291,6 +257,7 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest) { throw new PeerNotOnlineException($"$peer {destination.PubKey} is not online"); } + if (!e.Message.Contains("already connected to peer")) { throw; @@ -356,6 +323,7 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest) { channelOperationRequest.StatusLogs.Add(ChannelStatusLog.Info($"Channel opened successfully 🎉")); } + _channelOperationRequestRepository.Update(channelOperationRequest); var fundingTx = LightningHelper.DecodeTxId(response.ChanOpen.ChannelPoint.FundingTxidBytes); @@ -378,7 +346,7 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest) CreationDatetime = DateTimeOffset.Now, FundingTx = fundingTx, FundingTxOutputIndex = response.ChanOpen.ChannelPoint.OutputIndex, - BtcCloseAddress = closeAddress, + BtcCloseAddress = openChannelRequest.CloseAddress, SatsAmount = channelOperationRequest.SatsAmount, UpdateDatetime = DateTimeOffset.Now, Status = Channel.ChannelStatus.Open, @@ -536,7 +504,7 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest) } //if the fee is too high, we throw an exception - var finalizedTotalIn = finalizedPSBT.Inputs.Sum(x => (long) x.GetCoin()?.Amount); + var finalizedTotalIn = finalizedPSBT.Inputs.Sum(x => (long)x.GetCoin()?.Amount); if (finalizedPSBT.GetFee().Satoshi >= finalizedTotalIn * Constants.MAX_TX_FEE_RATIO) { @@ -636,10 +604,12 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest) // TODO: Make exception message pretty throw new RemoteCanceledFundingException(e.Message); } + if (e.Message.Contains("is not online")) { throw new PeerNotOnlineException($"$peer {destination.PubKey} is not online"); } + throw; } } @@ -665,6 +635,58 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest) return new Lightning.LightningClient(grpcChannel).Wrap(); }; + public long GetFundingAmount(ChannelOperationRequest channelOperationRequest, PSBT combinedPSBT, decimal initialFeeRate) + { + if (!combinedPSBT.TryGetVirtualSize(out var estimatedVsize)) + { + _logger.LogError("Could not estimate virtual size of the PSBT"); + throw new InvalidOperationException("Could not estimate virtual size of the PSBT"); + } + + var changelessVSize = channelOperationRequest.Changeless ? 43 : 0; // 8 value + 1 script pub key size + 34 script pub key hash (Segwit output 2-0f-2 multisig) + var outputVirtualSize = estimatedVsize + changelessVSize; // We add the change output if needed + + var totalFees = new Money(outputVirtualSize * initialFeeRate, MoneyUnit.Satoshi); + return channelOperationRequest.Changeless ? channelOperationRequest.SatsAmount - totalFees : channelOperationRequest.SatsAmount; + } + + public async Task CreateOpenChannelRequest(ChannelOperationRequest channelOperationRequest, PSBT? combinedPSBT, LightningNode? remoteNodeInfo, long fundingAmount, byte[] pendingChannelId, DerivationStrategyBase? derivationStrategyBase) + { + if (combinedPSBT == null) throw new ArgumentNullException(nameof(combinedPSBT)); + if (remoteNodeInfo == null) throw new ArgumentNullException(nameof(remoteNodeInfo)); + if (derivationStrategyBase == null) throw new ArgumentNullException(nameof(derivationStrategyBase)); + + //We prepare the request (shim) with the base PSBT we had presigned with the UTXOs to fund the channel + var openChannelRequest = new OpenChannelRequest + { + FundingShim = new FundingShim + { + PsbtShim = new PsbtShim + { + BasePsbt = ByteString.FromBase64(combinedPSBT.ToBase64()), + NoPublish = false, + PendingChanId = ByteString.CopyFrom(pendingChannelId) + } + }, + LocalFundingAmount = fundingAmount, + Private = channelOperationRequest.IsChannelPrivate, + NodePubkey = ByteString.CopyFrom(Convert.FromHexString(remoteNodeInfo.PubKey)), + }; + + // Check features to see if we need or is allowed to add a close address + var upfrontShutdownScriptOpt = remoteNodeInfo.Features.ContainsKey((uint)FeatureBit.UpfrontShutdownScriptOpt); + var upfrontShutdownScriptReq = remoteNodeInfo.Features.ContainsKey((uint)FeatureBit.UpfrontShutdownScriptReq); + if (upfrontShutdownScriptOpt && remoteNodeInfo.Features[(uint)FeatureBit.UpfrontShutdownScriptOpt] is { IsKnown: true } || + upfrontShutdownScriptReq && remoteNodeInfo.Features[(uint)FeatureBit.UpfrontShutdownScriptReq] is { IsKnown: true }) + { + var address = await GetCloseAddress(channelOperationRequest, derivationStrategyBase, _nbXplorerService, _logger); + openChannelRequest.CloseAddress = address.Address.ToString(); + ; + } + + return openChannelRequest; + } + public static PSBT GetCombinedPsbt(ChannelOperationRequest channelOperationRequest, ILogger? _logger = null) { //PSBT Combine diff --git a/test/FundsManager.Tests/Services/LightningServiceTests.cs b/test/FundsManager.Tests/Services/LightningServiceTests.cs index ac9d813f..24f26da2 100644 --- a/test/FundsManager.Tests/Services/LightningServiceTests.cs +++ b/test/FundsManager.Tests/Services/LightningServiceTests.cs @@ -17,6 +17,7 @@ * */ +using System.Security.Cryptography; using Microsoft.Extensions.Logging; using FundsManager.Data; using FundsManager.Data.Models; @@ -297,7 +298,7 @@ private static Mock GetNBXplorerServiceFullyMocked(UTXOChange var nbXplorerMock = new Mock(); //Mock to return a wallet address var keyPathInformation = new KeyPathInformation() - {Address = BitcoinAddress.Create("bcrt1q590shaxaf5u08ml8jwlzghz99dup3z9592vxal", Network.RegTest)}; + { Address = BitcoinAddress.Create("bcrt1q590shaxaf5u08ml8jwlzghz99dup3z9592vxal", Network.RegTest) }; nbXplorerMock .Setup(x => x.GetUnusedAsync(It.IsAny(), It.IsAny(), @@ -426,7 +427,7 @@ public async Task OpenChannel_SuccessLegacyMultiSig() .Setup(x => x.GetById(It.IsAny())) .ReturnsAsync(operationRequest); - var nodes = new List {destinationNode}; + var nodes = new List { destinationNode }; nodeRepository .Setup(x => x.GetAllManagedByNodeGuard()) @@ -527,7 +528,7 @@ public async Task OpenChannel_SuccessLegacyMultiSig() }, }; - utxoChanges.Confirmed = new UTXOChange() {UTXOs = utxoList}; + utxoChanges.Confirmed = new UTXOChange() { UTXOs = utxoList }; var channelOperationRequestPsbtRepository = new Mock(); channelOperationRequestPsbtRepository @@ -654,7 +655,7 @@ public async Task OpenChannel_SuccessMultiSig() .Setup(x => x.GetById(It.IsAny())) .ReturnsAsync(operationRequest); - var nodes = new List {destinationNode}; + var nodes = new List { destinationNode }; nodeRepository .Setup(x => x.GetAllManagedByNodeGuard()) @@ -755,7 +756,7 @@ public async Task OpenChannel_SuccessMultiSig() }, }; - utxoChanges.Confirmed = new UTXOChange() {UTXOs = utxoList}; + utxoChanges.Confirmed = new UTXOChange() { UTXOs = utxoList }; var channelOperationRequestPsbtRepository = new Mock(); channelOperationRequestPsbtRepository @@ -882,7 +883,7 @@ public async Task OpenChannel_SuccessSingleSigBip39() .Setup(x => x.GetById(It.IsAny())) .ReturnsAsync(operationRequest); - var nodes = new List {destinationNode}; + var nodes = new List { destinationNode }; nodeRepository .Setup(x => x.GetAllManagedByNodeGuard()) @@ -983,7 +984,7 @@ public async Task OpenChannel_SuccessSingleSigBip39() }, }; - utxoChanges.Confirmed = new UTXOChange() {UTXOs = utxoList}; + utxoChanges.Confirmed = new UTXOChange() { UTXOs = utxoList }; var channelOperationRequestPsbtRepository = new Mock(); channelOperationRequestPsbtRepository @@ -1059,7 +1060,7 @@ public async Task OpenChannel_SuccessSingleSigBip39() //TODO Remove hack LightningService.CreateLightningClient = originalCreateLightningClient; } - + /// /// This tests makes sure that if a multisig wallet is used, the number of signatures is correct. /// This means that we need in in a m-of-n multisig, m-1 signatures so nodeguard is that last one to sign to avoid leaking signatures with SIGHASH_NONE @@ -1086,9 +1087,9 @@ public async Task OpenChannel_FailedIncorrectNumberOfHumanSigs() { PSBT = userSignedPSBT, }); - + //Lets add a second signed "human" PSBT - + channelOpReqPsbts.Add(new ChannelOperationRequestPSBT() { PSBT = userSignedPSBT, @@ -1120,7 +1121,7 @@ public async Task OpenChannel_FailedIncorrectNumberOfHumanSigs() .Setup(x => x.GetById(It.IsAny())) .ReturnsAsync(operationRequest); - var nodes = new List {destinationNode}; + var nodes = new List { destinationNode }; nodeRepository .Setup(x => x.GetAllManagedByNodeGuard()) @@ -1221,7 +1222,7 @@ public async Task OpenChannel_FailedIncorrectNumberOfHumanSigs() }, }; - utxoChanges.Confirmed = new UTXOChange() {UTXOs = utxoList}; + utxoChanges.Confirmed = new UTXOChange() { UTXOs = utxoList }; var channelOperationRequestPsbtRepository = new Mock(); channelOperationRequestPsbtRepository @@ -1347,7 +1348,7 @@ public async Task OpenChannel_SuccessSingleSig() .Setup(x => x.GetById(It.IsAny())) .ReturnsAsync(operationRequest); - var nodes = new List {destinationNode}; + var nodes = new List { destinationNode }; nodeRepository .Setup(x => x.GetAllManagedByNodeGuard()) @@ -1448,7 +1449,7 @@ public async Task OpenChannel_SuccessSingleSig() }, }; - utxoChanges.Confirmed = new UTXOChange() {UTXOs = utxoList}; + utxoChanges.Confirmed = new UTXOChange() { UTXOs = utxoList }; var channelOperationRequestPsbtRepository = new Mock(); channelOperationRequestPsbtRepository @@ -1612,5 +1613,103 @@ public async Task CloseChannel_Succeeds() //TODO Remove hack LightningService.CreateLightningClient = originalCreateLightningClient; } + + [Fact] + public async Task? CreateOpenChannelRequest_CreatesRequestWithoutClosingAddress() + { + // Arrange + var wallet = CreateWallet.SingleSig(_internalWallet); + var channelOperationRequest = new ChannelOperationRequest + { + Wallet = wallet + }; + var psbt = + "cHNidP8BAFIBAAAAAeh7YDXyZE11vXb0yRqCkrxY7VpHH1WVMHwaCWYMv/pCAQAAAAD/////AUjf9QUAAAAAFgAULTCtUNMojFQZ8oa6fpbXbDhK2EYAAAAATwEENYfPA325Ro0AAAABg9H86IDUttPPFss+9te+0DByQgbeD7RPXNuVH9mh1qIDnMEWyKA+kvyG038on8+HxI+9AD8r6ZI1dNIDSGC8824Q7QIQyDAAAIABAACAAQAAAAABAR8A4fUFAAAAABYAFOk69QEyo0x+Xs/zV62OLrHh9eszAQMEAgAAAAAA"; + + var combinedPsbt = LightningHelper.CombinePSBTs(new[] { psbt }); + var lightningService = new LightningService(_logger, null, null, null, null, null, null, null, null); + var pendingChannelId = RandomNumberGenerator.GetBytes(32); + var derivationStrategyBase = LightningService.GetDerivationStrategyBase(channelOperationRequest); + var node = new LightningNode() + { + PubKey = "03650f49929d84d9a6d9b5a66235c603a1a0597dd609f7cd3b15052382cf9bb1b4" + }; + + // Act + var openChannelRequest = await lightningService.CreateOpenChannelRequest(channelOperationRequest, combinedPsbt, node, 1000, pendingChannelId, derivationStrategyBase); + + // Assert + openChannelRequest.Should().Be(new OpenChannelRequest() + { + FundingShim = new FundingShim + { + PsbtShim = new PsbtShim + { + BasePsbt = ByteString.FromBase64(combinedPsbt.ToBase64()), + NoPublish = false, + PendingChanId = ByteString.CopyFrom(pendingChannelId) + } + }, + LocalFundingAmount = 1000, + Private = false, + NodePubkey = ByteString.CopyFrom(Convert.FromHexString("03650f49929d84d9a6d9b5a66235c603a1a0597dd609f7cd3b15052382cf9bb1b4")), + CloseAddress = "" + }); + } + + [Fact] + public async Task? CreateOpenChannelRequest_CreatesRequestWithClosingAddress() + { + // Arrange + var nbXplorerMock = new Mock(); + + nbXplorerMock.Setup(x => x.GetUnusedAsync(It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(Task.FromResult(new KeyPathInformation() { Address = BitcoinAddress.Create("bcrt1q590shaxaf5u08ml8jwlzghz99dup3z9592vxal", Network.RegTest) })); + + + var wallet = CreateWallet.SingleSig(_internalWallet); + var channelOperationRequest = new ChannelOperationRequest + { + Wallet = wallet + }; + var psbt = + "cHNidP8BAFIBAAAAAeh7YDXyZE11vXb0yRqCkrxY7VpHH1WVMHwaCWYMv/pCAQAAAAD/////AUjf9QUAAAAAFgAULTCtUNMojFQZ8oa6fpbXbDhK2EYAAAAATwEENYfPA325Ro0AAAABg9H86IDUttPPFss+9te+0DByQgbeD7RPXNuVH9mh1qIDnMEWyKA+kvyG038on8+HxI+9AD8r6ZI1dNIDSGC8824Q7QIQyDAAAIABAACAAQAAAAABAR8A4fUFAAAAABYAFOk69QEyo0x+Xs/zV62OLrHh9eszAQMEAgAAAAAA"; + + var combinedPsbt = LightningHelper.CombinePSBTs(new[] { psbt }); + var lightningService = new LightningService(_logger, null, null, null, null, null, null, nbXplorerMock.Object, null); + var pendingChannelId = RandomNumberGenerator.GetBytes(32); + var derivationStrategyBase = LightningService.GetDerivationStrategyBase(channelOperationRequest); + + var node = new LightningNode() + { + PubKey = "03650f49929d84d9a6d9b5a66235c603a1a0597dd609f7cd3b15052382cf9bb1b4", + }; + node.Features.Add((uint)FeatureBit.UpfrontShutdownScriptOpt, new Feature() { Name = "upfront-shutdown-script", IsKnown = true, IsRequired = false }); + + // Act + var openChannelRequest = await lightningService.CreateOpenChannelRequest(channelOperationRequest, combinedPsbt, node, 1000, pendingChannelId, derivationStrategyBase); + + // Assert + openChannelRequest.Should().Be(new OpenChannelRequest() + { + FundingShim = new FundingShim + { + PsbtShim = new PsbtShim + { + BasePsbt = ByteString.FromBase64(combinedPsbt.ToBase64()), + NoPublish = false, + PendingChanId = ByteString.CopyFrom(pendingChannelId) + } + }, + LocalFundingAmount = 1000, + Private = false, + NodePubkey = ByteString.CopyFrom(Convert.FromHexString("03650f49929d84d9a6d9b5a66235c603a1a0597dd609f7cd3b15052382cf9bb1b4")), + CloseAddress = "bcrt1q590shaxaf5u08ml8jwlzghz99dup3z9592vxal" + }); + } } } \ No newline at end of file