diff --git a/src/Areas/Identity/Pages/Account/LoginWith2fa.cshtml.cs b/src/Areas/Identity/Pages/Account/LoginWith2fa.cshtml.cs index d92bd239..0da3749a 100644 --- a/src/Areas/Identity/Pages/Account/LoginWith2fa.cshtml.cs +++ b/src/Areas/Identity/Pages/Account/LoginWith2fa.cshtml.cs @@ -2,18 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable -using System; using System.ComponentModel.DataAnnotations; -using System.Threading.Tasks; using FundsManager.Data.Models; -using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; -using Microsoft.Extensions.Logging; using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.RazorPages; -using Microsoft.Extensions.Logging; namespace FundsManager.Areas.Identity.Pages.Account { diff --git a/src/Data/Models/ChannelOperationRequest.cs b/src/Data/Models/ChannelOperationRequest.cs index 3edadc34..3b2e8018 100644 --- a/src/Data/Models/ChannelOperationRequest.cs +++ b/src/Data/Models/ChannelOperationRequest.cs @@ -64,7 +64,7 @@ public enum OperationRequestType Close = 2 } - public class ChannelOperationRequest : Entity, IEquatable + public class ChannelOperationRequest : Entity, IEquatable, IBitcoinRequest { /// /// Amount in satoshis @@ -78,9 +78,7 @@ public class ChannelOperationRequest : Entity, IEquatable new Money(SatsAmount, MoneyUnit.Satoshi).ToDecimal(MoneyUnit.BTC); - set - { - } + set { } } public string? Description { get; set; } @@ -130,7 +128,7 @@ public decimal Amount public bool AreAllRequiredHumanSignaturesCollected => CheckSignatures(); [NotMapped] - public int NumberOfSignaturesCollected => ChannelOperationRequestPsbts == null ? 0 : ChannelOperationRequestPsbts.Count(x =>!x.IsFinalisedPSBT && !x.IsTemplatePSBT && !x.IsInternalWalletPSBT); + public int NumberOfSignaturesCollected => ChannelOperationRequestPsbts == null ? 0 : ChannelOperationRequestPsbts.Count(x => !x.IsFinalisedPSBT && !x.IsTemplatePSBT && !x.IsInternalWalletPSBT); /// /// This is the JobId provided by Quartz of the job executing this request. @@ -150,7 +148,7 @@ private bool CheckSignatures() var userPSBTsCount = NumberOfSignaturesCollected; //We add the internal Wallet signature - if (Wallet != null && Wallet.IsHotWallet) return ChannelOperationRequestPsbts.Count(x=> x.IsTemplatePSBT) == 1; + if (Wallet != null && Wallet.IsHotWallet) return ChannelOperationRequestPsbts.Count(x => x.IsTemplatePSBT) == 1; userPSBTsCount++; if (userPSBTsCount == Wallet.MofN) @@ -158,7 +156,7 @@ private bool CheckSignatures() result = true; } } - + return result; } diff --git a/src/Data/Models/Interfaces/IBitcoinRequest.cs b/src/Data/Models/Interfaces/IBitcoinRequest.cs new file mode 100644 index 00000000..eec660fd --- /dev/null +++ b/src/Data/Models/Interfaces/IBitcoinRequest.cs @@ -0,0 +1,38 @@ +/* + * NodeGuard + * Copyright (C) 2023 Elenpay + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +namespace FundsManager.Data.Models; + +public enum BitcoinRequestType +{ + ChannelOperation, + WalletWithdrawal +} + +public interface IBitcoinRequest +{ + public int Id { get; set; } + + /// + /// Amount in satoshis + /// + public long SatsAmount { get; } + + public Wallet? Wallet { get; set; } +} \ No newline at end of file diff --git a/src/Data/Models/WalletWithdrawalRequest.cs b/src/Data/Models/WalletWithdrawalRequest.cs index e1d2d530..697adeaa 100644 --- a/src/Data/Models/WalletWithdrawalRequest.cs +++ b/src/Data/Models/WalletWithdrawalRequest.cs @@ -17,7 +17,7 @@ * */ -using System.ComponentModel.DataAnnotations.Schema; +using System.ComponentModel.DataAnnotations.Schema; using NBitcoin; namespace FundsManager.Data.Models @@ -63,7 +63,7 @@ public enum WalletWithdrawalRequestStatus /// /// Requests to withdraw funds from a FM-managed multisig wallet /// - public class WalletWithdrawalRequest : Entity, IEquatable + public class WalletWithdrawalRequest : Entity, IEquatable, IBitcoinRequest { public WalletWithdrawalRequestStatus Status { get; set; } @@ -118,7 +118,7 @@ public class WalletWithdrawalRequest : Entity, IEquatable public string? RequestMetadata { get; set; } - + /// /// Check that the number of signatures (not finalised psbt nor internal wallet psbt or template psbt are gathered and increases by one to count on the internal wallet signature /// @@ -131,7 +131,7 @@ private bool CheckSignatures() { return true; } - + if (WalletWithdrawalRequestPSBTs != null && WalletWithdrawalRequestPSBTs.Any()) { var numberOfSignaturesCollected = NumberOfSignaturesCollected; diff --git a/src/Data/Repositories/ChannelOperationRequestRepository.cs b/src/Data/Repositories/ChannelOperationRequestRepository.cs index b8968e43..55af0f12 100644 --- a/src/Data/Repositories/ChannelOperationRequestRepository.cs +++ b/src/Data/Repositories/ChannelOperationRequestRepository.cs @@ -118,6 +118,7 @@ public async Task> GetUnsignedPendingRequestsByUse { await _notificationService.NotifyRequestSigners(type.WalletId.Value, "/channel-requests"); } + return valueTuple; } @@ -153,7 +154,7 @@ public async Task> GetUnsignedPendingRequestsByUse return _repository.Update(strippedType, applicationDbContext); } - public async Task<(bool, string?)> AddUTXOs(ChannelOperationRequest type, List utxos) + public async Task<(bool, string?)> AddUTXOs(IBitcoinRequest type, List utxos) { if (type == null) throw new ArgumentNullException(nameof(type)); if (utxos.Count == 0) throw new ArgumentException("Value cannot be an empty collection.", nameof(utxos)); diff --git a/src/Data/Repositories/Interfaces/IBitcoinRequestRepository.cs b/src/Data/Repositories/Interfaces/IBitcoinRequestRepository.cs new file mode 100644 index 00000000..f4a77c08 --- /dev/null +++ b/src/Data/Repositories/Interfaces/IBitcoinRequestRepository.cs @@ -0,0 +1,33 @@ +/* + * NodeGuard + * Copyright (C) 2023 Elenpay + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +using FundsManager.Data.Models; + +namespace FundsManager.Data.Repositories.Interfaces; + +public interface IBitcoinRequestRepository +{ + /// + /// Adds to the many-to-many collection the list of utxos provided + /// + /// + /// + /// + Task<(bool, string?)> AddUTXOs(IBitcoinRequest type, List utxos); +} \ No newline at end of file diff --git a/src/Data/Repositories/Interfaces/IChannelOperationRequestRepository.cs b/src/Data/Repositories/Interfaces/IChannelOperationRequestRepository.cs index 12075dcf..7440d9b2 100644 --- a/src/Data/Repositories/Interfaces/IChannelOperationRequestRepository.cs +++ b/src/Data/Repositories/Interfaces/IChannelOperationRequestRepository.cs @@ -17,11 +17,11 @@ * */ -using FundsManager.Data.Models; +using FundsManager.Data.Models; namespace FundsManager.Data.Repositories.Interfaces; -public interface IChannelOperationRequestRepository +public interface IChannelOperationRequestRepository : IBitcoinRequestRepository { Task GetById(int id); @@ -45,7 +45,7 @@ public interface IChannelOperationRequestRepository /// /// /// - Task<(bool, string?)> AddUTXOs(ChannelOperationRequest type, List utxos); + Task<(bool, string?)> AddUTXOs(IBitcoinRequest type, List utxos); /// /// Returns those requests that can have a PSBT locked until they are confirmed / rejected / cancelled diff --git a/src/Data/Repositories/Interfaces/IFMUTXORepository.cs b/src/Data/Repositories/Interfaces/IFMUTXORepository.cs index a5ae5504..82671670 100644 --- a/src/Data/Repositories/Interfaces/IFMUTXORepository.cs +++ b/src/Data/Repositories/Interfaces/IFMUTXORepository.cs @@ -41,6 +41,5 @@ public interface IFMUTXORepository /// Gets the current list of UTXOs locked on requests ChannelOperationRequest / WalletWithdrawalRequest by passing its id if wants to remove it from the resulting set /// /// - Task> GetLockedUTXOs(int? ignoredWalletWithdrawalRequestId = null, - int? ignoredChannelOperationRequestId = null); + Task> GetLockedUTXOs(int? ignoredWalletWithdrawalRequestId = null, int? ignoredChannelOperationRequestId = null); } \ No newline at end of file diff --git a/src/Helpers/LightningHelper.cs b/src/Helpers/LightningHelper.cs index f6ce545f..87d3d616 100644 --- a/src/Helpers/LightningHelper.cs +++ b/src/Helpers/LightningHelper.cs @@ -17,15 +17,12 @@ * */ -using AutoMapper; using FundsManager.Data.Models; using FundsManager.Services; using Google.Protobuf; using NBitcoin; using NBXplorer; using NBXplorer.Models; -using Key = FundsManager.Data.Models.Key; -using Unmockable; namespace FundsManager.Helpers { @@ -77,7 +74,7 @@ public static void RemoveDuplicateUTXOs(this UTXOChanges utxoChanges) var utxoDerivationPath = key.DeriveUtxoKeyPath(selectedUtxo.KeyPath); var derivedPubKey = key.DeriveUtxoPubKey(nbXplorerNetwork, selectedUtxo.KeyPath); var addressRootedKeyPath = key.GetAddressRootedKeyPath(utxoDerivationPath); - + var input = result.Inputs.FirstOrDefault(input => input?.GetCoin()?.Outpoint == selectedUtxo.Outpoint); var coin = coins.FirstOrDefault(x => x.Outpoint == selectedUtxo.Outpoint); @@ -86,7 +83,7 @@ public static void RemoveDuplicateUTXOs(this UTXOChanges utxoChanges) ( wallet.IsHotWallet && (coin as Coin).ScriptPubKey == derivedPubKey.WitHash.ScriptPubKey || !wallet.IsHotWallet && (coin as ScriptCoin).Redeem.GetAllPubKeys().Contains(derivedPubKey)) - ) + ) { input.AddKeyPath(derivedPubKey, addressRootedKeyPath); } @@ -108,7 +105,7 @@ public static void RemoveDuplicateUTXOs(this UTXOChanges utxoChanges) /// /// /// - public static async Task CreateNBExplorerClient() + public static async Task CreateNBExplorerClient() { //Nbxplorer api client var nbXplorerNetwork = CurrentNetworkHelper.GetCurrentNetwork(); @@ -117,69 +114,46 @@ public static async Task CreateNBExplorerClient() var nbxplorerClient = new ExplorerClient( provider.GetFromCryptoCode(nbXplorerNetwork.NetworkSet.CryptoCode), new Uri(Constants.NBXPLORER_URI)); - return nbxplorerClient; + return nbxplorerClient; } /// - /// Helper to select coins from a wallet for requests (Withdrawals, ChannelOperationRequest). FIFO is the coin selection + /// Helper to select utxos from a wallet for requests (Withdrawals, ChannelOperationRequest) by oldest /// /// /// - /// - /// + /// /// - /// /// - public static async Task<(List coins, List selectedUTXOs)> SelectCoins( - Wallet wallet, long satsAmount, UTXOChanges utxoChanges, List lockedUTXOs, ILogger logger, - IMapper mapper) + public static async Task> SelectUTXOsByOldest( + Wallet wallet, long satsAmount, List availableUTXOs, ILogger logger) { if (wallet == null) throw new ArgumentNullException(nameof(wallet)); - if (utxoChanges == null) throw new ArgumentNullException(nameof(utxoChanges)); - if (lockedUTXOs == null) throw new ArgumentNullException(nameof(lockedUTXOs)); if (logger == null) throw new ArgumentNullException(nameof(logger)); - if (mapper == null) throw new ArgumentNullException(nameof(mapper)); if (wallet == null) throw new ArgumentNullException(nameof(wallet)); if (satsAmount <= 0) throw new ArgumentOutOfRangeException(nameof(satsAmount)); - var derivationStrategy = wallet.GetDerivationStrategy(); var selectedUTXOs = new List(); - var coins = new List(); - - var availableUTXOs = new List(); - foreach (var utxo in utxoChanges.Confirmed.UTXOs) - { - var fmUtxo = mapper.Map(utxo); - - if (lockedUTXOs.Contains(fmUtxo)) - { - logger.LogInformation("Removing UTXO: {Utxo} from UTXO set as it is locked", fmUtxo.ToString()); - } - else - { - availableUTXOs.Add(utxo); - } - } if (!availableUTXOs.Any()) { logger.LogError("The PSBT cannot be generated, no UTXOs are available for walletId: {WalletId}", wallet.Id); - return (coins, selectedUTXOs); + return selectedUTXOs; } var utxosStack = new Stack(availableUTXOs.OrderByDescending(x => x.Confirmations)); //FIFO Algorithm to match the amount, oldest UTXOs are first taken - var totalUTXOsConfirmedSats = utxosStack.Sum(x => ((Money) x.Value).Satoshi); + var totalUTXOsConfirmedSats = utxosStack.Sum(x => ((Money)x.Value).Satoshi); if (totalUTXOsConfirmedSats < satsAmount) { logger.LogError( "Error, the total UTXOs set balance for walletid: {WalletId} ({AvailableSats} sats) is less than the amount in the request ({RequestedSats} sats)", wallet.Id, totalUTXOsConfirmedSats, satsAmount); - return (coins, selectedUTXOs); + return selectedUTXOs; } var utxosSatsAmountAccumulator = 0M; @@ -190,7 +164,7 @@ public static async Task CreateNBExplorerClient() if (utxosStack.TryPop(out var utxo)) { selectedUTXOs.Add(utxo); - utxosSatsAmountAccumulator += ((Money) utxo.Value).Satoshi; + utxosSatsAmountAccumulator += ((Money)utxo.Value).Satoshi; } iterations++; @@ -201,19 +175,34 @@ public static async Task CreateNBExplorerClient() } } + return selectedUTXOs; + } + + /// + /// Helper to select coins from a wallet for requests (Withdrawals, ChannelOperationRequest). FIFO is the coin selection + /// + /// + /// + /// + public static async Task> SelectCoins(Wallet wallet, List selectedUTXOs) + { + if (wallet == null) throw new ArgumentNullException(nameof(wallet)); + if (wallet == null) throw new ArgumentNullException(nameof(wallet)); + + var derivationStrategy = wallet.GetDerivationStrategy(); + //UTXOS to Enumerable of ICOINS - coins = selectedUTXOs.Select(x => + return selectedUTXOs.Select(x => { var coin = x.AsCoin(derivationStrategy); if (wallet.IsHotWallet) { return coin; } + return coin.ToScriptCoin(x.ScriptPubKey); }) .ToList(); - - return (coins, selectedUTXOs); } /// @@ -223,7 +212,7 @@ public static async Task CreateNBExplorerClient() /// /// public static async Task GetFeeRateResult(Network nbXplorerNetwork, - INBXplorerService nbxplorerClient) + INBXplorerService nbxplorerClient) { GetFeeRateResult feeRateResult; if (nbXplorerNetwork == Network.RegTest) @@ -272,7 +261,7 @@ public static async Task GetFeeRateResult(Network nbXplorerNet return combinedPSBT; } - + /// /// Helper for decoding bytestring-based LND representation of TxIds /// diff --git a/src/Program.cs b/src/Program.cs index 1b436f76..c8455eef 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -71,10 +71,7 @@ public static void Main(string[] args) //Identity builder.Services - .AddDefaultIdentity(options => - { - options.SignIn.RequireConfirmedAccount = false; - }) + .AddDefaultIdentity(options => { options.SignIn.RequireConfirmedAccount = false; }) .AddRoles() .AddRoleManager>() .AddEntityFrameworkStores() @@ -108,6 +105,7 @@ public static void Main(string[] args) builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); + builder.Services.AddTransient(); //BlazoredToast builder.Services.AddBlazoredToast(); @@ -125,15 +123,15 @@ public static void Main(string[] args) //options.EnableSensitiveDataLogging(); //options.EnableDetailedErrors(); options.UseNpgsql(Constants.POSTGRES_CONNECTIONSTRING, options => - { - options.UseQuerySplittingBehavior(QuerySplittingBehavior - .SingleQuery); // Slower but integrity is ensured - }); + { + options.UseQuerySplittingBehavior(QuerySplittingBehavior + .SingleQuery); // Slower but integrity is ensured + }); }, ServiceLifetime.Transient); - + //HTTPClient factory builder.Services.AddHttpClient(); - + //gRPC builder.Services.AddGrpc(options => { @@ -146,7 +144,7 @@ public static void Main(string[] args) // Setup a HTTP/2 endpoint without TLS. options.ListenAnyIP(50051, o => o.Protocols = HttpProtocols.Http2); - options.ListenAnyIP(int.Parse(Environment.GetEnvironmentVariable("HTTP1_LISTEN_PORT") ?? "80") , o => o.Protocols = + options.ListenAnyIP(int.Parse(Environment.GetEnvironmentVariable("HTTP1_LISTEN_PORT") ?? "80"), o => o.Protocols = HttpProtocols.Http1); }); @@ -180,12 +178,9 @@ public static void Main(string[] args) options.UsePostgres(Constants.POSTGRES_CONNECTIONSTRING); options.UseJsonSerializer(); }); - - q.UseDedicatedThreadPool(x => - { - x.MaxConcurrency = 500; - }); - + + q.UseDedicatedThreadPool(x => { x.MaxConcurrency = 500; }); + //This allows DI in jobs q.UseMicrosoftDependencyInjectionJobFactory(); @@ -199,10 +194,7 @@ public static void Main(string[] args) q.AddTrigger(opts => { opts.ForJob(nameof(SweepAllNodesWalletsJob)).WithIdentity($"{nameof(SweepAllNodesWalletsJob)}Trigger") - .StartNow().WithSimpleSchedule(scheduleBuilder => - { - scheduleBuilder.WithIntervalInMinutes(1).RepeatForever(); - }); + .StartNow().WithSimpleSchedule(scheduleBuilder => { scheduleBuilder.WithIntervalInMinutes(1).RepeatForever(); }); }); //Monitor Withdrawals Job @@ -231,13 +223,10 @@ public static void Main(string[] args) opts.ForJob(nameof(ChannelAcceptorJob)).WithIdentity($"{nameof(ChannelAcceptorJob)}Trigger") .StartNow(); }); - + // NodeChannelSubscribeJob - q.AddJob(opts => - { - opts.WithIdentity(nameof(NodeSubscriptorJob)); - }); - + q.AddJob(opts => { opts.WithIdentity(nameof(NodeSubscriptorJob)); }); + q.AddTrigger(opts => { opts.ForJob(nameof(NodeSubscriptorJob)).WithIdentity($"{nameof(NodeSubscriptorJob)}Trigger") @@ -258,42 +247,40 @@ public static void Main(string[] args) if (Constants.OTEL_EXPORTER_ENDPOINT != null) { - builder.Services - .AddOpenTelemetryTracing((builder) => builder - // Configure the resource attribute `service.name` to MyServiceName - //.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("BtcPayServer")) - .SetResourceBuilder(ResourceBuilder.CreateEmpty().AddEnvironmentVariableDetector()) - // Add tracing of the AspNetCore instrumentation library - .AddAspNetCoreInstrumentation() - .AddOtlpExporter(options => - { - options.Protocol = OtlpExportProtocol.Grpc; - options.ExportProcessorType = OpenTelemetry.ExportProcessorType.Batch; - options.Endpoint = new Uri(Constants.OTEL_EXPORTER_ENDPOINT); - }) - .AddEntityFrameworkCoreInstrumentation() - .AddQuartzInstrumentation() + builder.Services + .AddOpenTelemetryTracing((builder) => builder + // Configure the resource attribute `service.name` to MyServiceName + //.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("BtcPayServer")) + .SetResourceBuilder(ResourceBuilder.CreateEmpty().AddEnvironmentVariableDetector()) + // Add tracing of the AspNetCore instrumentation library + .AddAspNetCoreInstrumentation() + .AddOtlpExporter(options => + { + options.Protocol = OtlpExportProtocol.Grpc; + options.ExportProcessorType = OpenTelemetry.ExportProcessorType.Batch; + options.Endpoint = new Uri(Constants.OTEL_EXPORTER_ENDPOINT); + }) + .AddEntityFrameworkCoreInstrumentation() + .AddQuartzInstrumentation() ); - builder.Services - .AddOpenTelemetryMetrics(builder => builder - // Configure the resource attribute `service.name` to MyServiceName - //.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("BtcPayServer")) - // Add metrics from the AspNetCore instrumentation library - .SetResourceBuilder(ResourceBuilder.CreateEmpty().AddEnvironmentVariableDetector()) - .AddAspNetCoreInstrumentation() - .AddRuntimeInstrumentation() - .AddOtlpExporter(options => - { - options.Protocol = OtlpExportProtocol.Grpc; - options.ExportProcessorType = OpenTelemetry.ExportProcessorType.Batch; - options.Endpoint = new Uri(Constants.OTEL_EXPORTER_ENDPOINT); - }) + builder.Services + .AddOpenTelemetryMetrics(builder => builder + // Configure the resource attribute `service.name` to MyServiceName + //.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("BtcPayServer")) + // Add metrics from the AspNetCore instrumentation library + .SetResourceBuilder(ResourceBuilder.CreateEmpty().AddEnvironmentVariableDetector()) + .AddAspNetCoreInstrumentation() + .AddRuntimeInstrumentation() + .AddOtlpExporter(options => + { + options.Protocol = OtlpExportProtocol.Grpc; + options.ExportProcessorType = OpenTelemetry.ExportProcessorType.Batch; + options.Endpoint = new Uri(Constants.OTEL_EXPORTER_ENDPOINT); + }) ); } - - - + var app = builder.Build(); @@ -338,7 +325,7 @@ public static void Main(string[] args) app.MapControllers(); app.MapBlazorHub(); app.MapFallbackToPage("/_Host"); - + //Grpc services //TODO Auth in the future, DAPR(?) app.MapGrpcService(); diff --git a/src/Properties/launchSettings.json b/src/Properties/launchSettings.json index bf0e8187..7c92b2c2 100644 --- a/src/Properties/launchSettings.json +++ b/src/Properties/launchSettings.json @@ -34,8 +34,9 @@ "SWEEPNODEWALLETSJOB_CRON": "0 */1 * * * ?", "ANCHOR_CLOSINGS_MINIMUM_SATS": "100000", "ALICE_HOST": "localhost:10001", + "BOB_HOST": "localhost:10002", "CAROL_HOST": "localhost:10003", - "ENABLE_HW_SUPPORT": "false", + "ENABLE_HW_SUPPORT": "true", "MINIMUM_WITHDRAWAL_BTC_AMOUNT": "0.001", "MINIMUM_CHANNEL_CAPACITY_SATS": "20000", "MEMPOOL_ENDPOINT": "https://mempool-staging.elenpay.tech", diff --git a/src/Services/BitcoinService.cs b/src/Services/BitcoinService.cs index 52728917..50570f53 100644 --- a/src/Services/BitcoinService.cs +++ b/src/Services/BitcoinService.cs @@ -39,32 +39,32 @@ public class BitcoinService : IBitcoinService { private readonly ILogger _logger; - private readonly IFMUTXORepository _fmutxoRepository; private readonly IMapper _mapper; private readonly IWalletWithdrawalRequestRepository _walletWithdrawalRequestRepository; private readonly IWalletWithdrawalRequestPsbtRepository _walletWithdrawalRequestPsbtRepository; private readonly INodeRepository _nodeRepository; private readonly IRemoteSignerService _remoteSignerService; private readonly INBXplorerService _nbXplorerService; + private readonly ICoinSelectionService _coinSelectionService; public BitcoinService(ILogger logger, - IFMUTXORepository fmutxoRepository, IMapper mapper, IWalletWithdrawalRequestRepository walletWithdrawalRequestRepository, IWalletWithdrawalRequestPsbtRepository walletWithdrawalRequestPsbtRepository, INodeRepository nodeRepository, IRemoteSignerService remoteSignerService, - INBXplorerService nbXplorerService - ) + INBXplorerService nbXplorerService, + ICoinSelectionService coinSelectionService + ) { _logger = logger; - _fmutxoRepository = fmutxoRepository; _mapper = mapper; _walletWithdrawalRequestRepository = walletWithdrawalRequestRepository; _walletWithdrawalRequestPsbtRepository = walletWithdrawalRequestPsbtRepository; _nodeRepository = nodeRepository; _remoteSignerService = remoteSignerService; _nbXplorerService = nbXplorerService; + _coinSelectionService = coinSelectionService; } public async Task<(decimal, long)> GetWalletConfirmedBalance(Wallet wallet) @@ -72,7 +72,7 @@ INBXplorerService nbXplorerService if (wallet == null) throw new ArgumentNullException(nameof(wallet)); var balance = await _nbXplorerService.GetBalanceAsync(wallet.GetDerivationStrategy(), default); - var confirmedBalanceMoney = (Money) balance.Confirmed; + var confirmedBalanceMoney = (Money)balance.Confirmed; return (confirmedBalanceMoney.ToUnit(MoneyUnit.BTC), confirmedBalanceMoney.Satoshi); } @@ -100,7 +100,7 @@ public async Task GenerateTemplatePSBT(WalletWithdrawalRequest walletWithd var derivationStrategy = walletWithdrawalRequest.Wallet.GetDerivationStrategy(); if (derivationStrategy == null) { - var message = $"Error while getting the derivation strategy scheme for wallet: {walletWithdrawalRequest.Wallet.Id}"; + var message = $"Error while getting the derivation strategy scheme for wallet: {walletWithdrawalRequest.Wallet.Id}"; _logger.LogError(message); throw new ArgumentNotFoundException(message); } @@ -143,18 +143,12 @@ public async Task GenerateTemplatePSBT(WalletWithdrawalRequest walletWithd } } - var utxoChanges = await _nbXplorerService.GetUTXOsAsync(derivationStrategy); - utxoChanges.RemoveDuplicateUTXOs(); - - var lockedUtxOs = - await _fmutxoRepository.GetLockedUTXOs(ignoredWalletWithdrawalRequestId: walletWithdrawalRequest.Id); - //If the request is a full funds withdrawal, calculate the amount to the existing balance if (walletWithdrawalRequest.WithdrawAllFunds) { var balanceResponse = await _nbXplorerService.GetBalanceAsync(derivationStrategy); - walletWithdrawalRequest.Amount = ((Money) balanceResponse.Confirmed).ToUnit(MoneyUnit.BTC); + walletWithdrawalRequest.Amount = ((Money)balanceResponse.Confirmed).ToUnit(MoneyUnit.BTC); var update = _walletWithdrawalRequestRepository.Update(walletWithdrawalRequest); if (!update.Item1) @@ -163,11 +157,8 @@ public async Task GenerateTemplatePSBT(WalletWithdrawalRequest walletWithd } } - var (scriptCoins, selectedUTXOs) = await LightningHelper.SelectCoins(walletWithdrawalRequest.Wallet, - walletWithdrawalRequest.SatsAmount, - utxoChanges, - lockedUtxOs, - _logger, _mapper); + var availableUTXOs = await _coinSelectionService.GetAvailableUTXOsAsync(derivationStrategy); + var (scriptCoins, selectedUTXOs) = await _coinSelectionService.GetTxInputCoins(availableUTXOs, walletWithdrawalRequest, derivationStrategy); if (scriptCoins == null || !scriptCoins.Any()) { @@ -194,7 +185,7 @@ public async Task GenerateTemplatePSBT(WalletWithdrawalRequest walletWithd if (changeAddress == null) { var message = String.Format("Change address was not found for wallet: {WalletId}", - walletWithdrawalRequest.Wallet.Id); + walletWithdrawalRequest.Wallet.Id); _logger.LogError(message); throw new ArgumentNullException(message); } @@ -211,7 +202,7 @@ public async Task GenerateTemplatePSBT(WalletWithdrawalRequest walletWithd // We preserve the output order when testing so the psbt doesn't change var command = Assembly.GetEntryAssembly()?.GetName().Name?.ToLowerInvariant(); - builder.ShuffleOutputs = command != "ef" && (command != null && !command.Contains("test"));; + builder.ShuffleOutputs = command != "ef" && (command != null && !command.Contains("test")); if (walletWithdrawalRequest.WithdrawAllFunds) { @@ -224,8 +215,8 @@ public async Task GenerateTemplatePSBT(WalletWithdrawalRequest walletWithd } result = builder.BuildPSBT(false); - - //Additional fields to support PSBT signing with a HW or the Remote Signer + + //Additional fields to support PSBT signing with a HW or the Remote Signer result = LightningHelper.AddDerivationData(walletWithdrawalRequest.Wallet, result, selectedUTXOs, scriptCoins, _logger); } catch (Exception e) @@ -246,7 +237,7 @@ public async Task GenerateTemplatePSBT(WalletWithdrawalRequest walletWithd if (result == null) { - throw new Exception("Error while generating base PSBT"); + throw new Exception("Error while generating base PSBT"); } // The template PSBT is saved for later reuse @@ -287,10 +278,10 @@ public async Task PerformWithdrawal(WalletWithdrawalRequest walletWithdrawalRequ //Update walletWithdrawalRequest = await _walletWithdrawalRequestRepository.GetById(walletWithdrawalRequest.Id) ?? throw new InvalidOperationException(); - + PSBT? psbtToSign = null; //If it is a hot wallet or a BIP39 imported wallet, we dont need to combine the PSBTs - if(walletWithdrawalRequest.Wallet.IsHotWallet || walletWithdrawalRequest.Wallet.IsBIP39Imported) + if (walletWithdrawalRequest.Wallet.IsHotWallet || walletWithdrawalRequest.Wallet.IsBIP39Imported) { psbtToSign = PSBT.Parse(walletWithdrawalRequest.WalletWithdrawalRequestPSBTs .Single(x => x.IsTemplatePSBT) @@ -335,7 +326,6 @@ public async Task PerformWithdrawal(WalletWithdrawalRequest walletWithdrawalRequ signedCombinedPSBT = await SignPSBTWithEmbeddedSigner(walletWithdrawalRequest, _nbXplorerService, derivationStrategyBase, psbtToSign, CurrentNetworkHelper.GetCurrentNetwork(), _logger); } - } else { @@ -462,7 +452,7 @@ private async Task SignPSBTWithEmbeddedSigner(WalletWithdrawalRequest wall errorKeypathsForTheUtxosUsedInThisTxAreNotFound); } - Dictionary privateKeysForUsedUTXOs; + Dictionary privateKeysForUsedUTXOs; try { privateKeysForUsedUTXOs = txInKeyPathDictionary.ToDictionary(x => x.Key.PrevOut, diff --git a/src/Services/CoinSelectionService.cs b/src/Services/CoinSelectionService.cs new file mode 100644 index 00000000..75dc7c19 --- /dev/null +++ b/src/Services/CoinSelectionService.cs @@ -0,0 +1,131 @@ +/* + * NodeGuard + * Copyright (C) 2023 Elenpay + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +using AutoMapper; +using FundsManager.Data.Models; +using FundsManager.Data.Repositories.Interfaces; +using FundsManager.Helpers; +using Humanizer; +using NBitcoin; +using NBXplorer.DerivationStrategy; +using NBXplorer.Models; + +namespace FundsManager.Services; + +public interface ICoinSelectionService +{ + /// + /// Gets the UTXOs for a wallet that are not locked in other transactions + /// + /// + public Task> GetAvailableUTXOsAsync(DerivationStrategyBase derivationStrategy); + + /// + /// Locks the UTXOs for using in a specific transaction + /// + /// + /// + public Task LockUTXOs(List selectedUTXOs, IBitcoinRequest bitcoinRequest, IBitcoinRequestRepository bitcoinRequestRepository); + + public Task<(List coins, List selectedUTXOs)> GetTxInputCoins( + List availableUTXOs, + IBitcoinRequest request, + DerivationStrategyBase derivationStrategy); +} + +public class CoinSelectionService: ICoinSelectionService +{ + private readonly ILogger _logger; + private readonly IMapper _mapper; + private readonly IFMUTXORepository _fmutxoRepository; + private readonly INBXplorerService _nbXplorerService; + + public CoinSelectionService( + ILogger logger, + IMapper mapper, + IFMUTXORepository fmutxoRepository, + INBXplorerService nbXplorerService + ) + { + _logger = logger; + _mapper = mapper; + _fmutxoRepository = fmutxoRepository; + _nbXplorerService = nbXplorerService; + } + + public async Task LockUTXOs(List selectedUTXOs, IBitcoinRequest bitcoinRequest, IBitcoinRequestRepository bitcoinRequestRepository) + { + // We "lock" the PSBT to the channel operation request by adding to its UTXOs collection for later checking + var utxos = selectedUTXOs.Select(x => _mapper.Map(x)).ToList(); + + var addUTXOSOperation = await bitcoinRequestRepository.AddUTXOs(bitcoinRequest, utxos); + if (!addUTXOSOperation.Item1) + { + _logger.LogError( + $"Could not add the following utxos({utxos.Humanize()}) to op request:{bitcoinRequest.Id}"); + } + } + + public async Task> GetAvailableUTXOsAsync(DerivationStrategyBase derivationStrategy) + { + var lockedUTXOs = await _fmutxoRepository.GetLockedUTXOs(); + var utxoChanges = await _nbXplorerService.GetUTXOsAsync(derivationStrategy); + utxoChanges.RemoveDuplicateUTXOs(); + + var availableUTXOs = new List(); + foreach (var utxo in utxoChanges.Confirmed.UTXOs) + { + var fmUtxo = _mapper.Map(utxo); + + if (lockedUTXOs.Contains(fmUtxo)) + { + _logger.LogInformation("Removing UTXO: {Utxo} from UTXO set as it is locked", fmUtxo.ToString()); + } + else + { + availableUTXOs.Add(utxo); + } + } + + return availableUTXOs; + } + + /// + /// Gets UTXOs confirmed from the wallet of the request + /// + /// + /// + /// + /// + public async Task<(List coins, List selectedUTXOs)> GetTxInputCoins( + List availableUTXOs, + IBitcoinRequest request, + DerivationStrategyBase derivationStrategy) + { + var utxoChanges = await _nbXplorerService.GetUTXOsAsync(derivationStrategy, default); + utxoChanges.RemoveDuplicateUTXOs(); + + var satsAmount = request.SatsAmount; + + var selectedUTXOs = await LightningHelper.SelectUTXOsByOldest(request.Wallet, satsAmount, availableUTXOs, _logger); + var coins = await LightningHelper.SelectCoins(request.Wallet, selectedUTXOs); + + return (coins, selectedUTXOs); + } +} \ No newline at end of file diff --git a/src/Services/LightningService.cs b/src/Services/LightningService.cs index 6a74dab8..4b12cfe4 100644 --- a/src/Services/LightningService.cs +++ b/src/Services/LightningService.cs @@ -24,20 +24,15 @@ using Grpc.Net.Client; using Lnrpc; using NBitcoin; -using NBXplorer; using NBXplorer.DerivationStrategy; using NBXplorer.Models; using System.Security.Cryptography; using AutoMapper; -using Blazored.Toast.Services; using FundsManager.Data; -using FundsManager.Data.Repositories; using FundsManager.Helpers; -using Humanizer; using Microsoft.EntityFrameworkCore; using Channel = FundsManager.Data.Models.Channel; using Transaction = NBitcoin.Transaction; -using UTXO = NBXplorer.Models.UTXO; using Unmockable; // ReSharper disable InconsistentNaming @@ -112,7 +107,7 @@ public interface ILightningService /// /// /// - public void CancelPendingChannel(Node source, byte[] pendingChannelId, IUnmockable? client = null); + public void CancelPendingChannel(Node source, byte[] pendingChannelId, IUnmockable? client = null); } public class LightningService : ILightningService @@ -128,6 +123,7 @@ public class LightningService : ILightningService private readonly IChannelRepository _channelRepository; private readonly IRemoteSignerService _remoteSignerService; private readonly INBXplorerService _nbXplorerService; + private readonly ICoinSelectionService _coinSelectionService; public LightningService(ILogger logger, IChannelOperationRequestRepository channelOperationRequestRepository, @@ -139,8 +135,9 @@ public LightningService(ILogger logger, IChannelOperationRequestPSBTRepository channelOperationRequestPsbtRepository, IChannelRepository channelRepository, IRemoteSignerService remoteSignerService, - INBXplorerService nbXplorerService - ) + INBXplorerService nbXplorerService, + ICoinSelectionService coinSelectionService + ) { _logger = logger; @@ -154,6 +151,7 @@ INBXplorerService nbXplorerService _channelRepository = channelRepository; _remoteSignerService = remoteSignerService; _nbXplorerService = nbXplorerService; + _coinSelectionService = coinSelectionService; } /// @@ -164,6 +162,7 @@ INBXplorerService nbXplorerService /// /// public record RemoteSignerRequest(string Psbt, SigHash? EnforcedSighash, string Network); + /// /// Record used to match AWS SignPSBT funciton output /// @@ -231,12 +230,13 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest) //For now, we only rely on pure tcp IPV4 connections var addr = remoteNodeInfo.Addresses.FirstOrDefault(x => x.Network == "tcp").Addr; - if(addr == null) + if (addr == null) { _logger.LogError("Error, remote node with {Pubkey} has no tcp IPV4 address", channelOperationRequest.DestNode?.PubKey); throw new InvalidOperationException(); } + var isPeerAlreadyConnected = false; ConnectPeerResponse connectPeerResponse = null; @@ -244,11 +244,11 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest) { connectPeerResponse = await client.Execute(x => x.ConnectPeerAsync(new ConnectPeerRequest { - Addr = new LightningAddress {Host = addr, Pubkey = remoteNodeInfo.PubKey}, + Addr = new LightningAddress { Host = addr, Pubkey = remoteNodeInfo.PubKey }, Perm = true }, new Metadata { - {"macaroon", source.ChannelAdminMacaroon} + { "macaroon", source.ChannelAdminMacaroon } }, null, default)); } //We avoid to stop the method if the peer is already connected @@ -286,7 +286,7 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest) if (source.ChannelAdminMacaroon != null) { var openStatusUpdateStream = client.Execute(x => x.OpenChannel(openChannelRequest, - new Metadata {{"macaroon", source.ChannelAdminMacaroon}}, null, default + new Metadata { { "macaroon", source.ChannelAdminMacaroon } }, null, default )); await foreach (var response in openStatusUpdateStream.ResponseStream.ReadAllAsync()) @@ -321,10 +321,10 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest) //Get the channels to find the channelId, not the temporary one var channels = await client.Execute(x => x.ListChannelsAsync(new ListChannelsRequest(), - new Metadata {{"macaroon", source.ChannelAdminMacaroon}}, null, default)); - var currentChannel = channels.Channels.SingleOrDefault(x=> x.ChannelPoint == $"{fundingTx}:{response.ChanOpen.ChannelPoint.OutputIndex}"); + new Metadata { { "macaroon", source.ChannelAdminMacaroon } }, null, default)); + var currentChannel = channels.Channels.SingleOrDefault(x => x.ChannelPoint == $"{fundingTx}:{response.ChanOpen.ChannelPoint.OutputIndex}"); - if(currentChannel == null) + if (currentChannel == null) { _logger.LogError("Error, channel not found for channel point: {ChannelPoint}", response.ChanOpen.ChannelPoint.ToString()); @@ -582,7 +582,7 @@ public static PSBT GetCombinedPsbt(ChannelOperationRequest channelOperationReque { //PSBT Combine var signedPsbts = channelOperationRequest.ChannelOperationRequestPsbts.Where(x => - channelOperationRequest.Wallet != null && !x.IsFinalisedPSBT && !x.IsInternalWalletPSBT && (channelOperationRequest.Wallet.IsHotWallet || !x.IsTemplatePSBT)); + channelOperationRequest.Wallet != null && !x.IsFinalisedPSBT && !x.IsInternalWalletPSBT && (channelOperationRequest.Wallet.IsHotWallet || !x.IsTemplatePSBT)); var signedPsbts2 = signedPsbts.Select(x => x.PSBT); var combinedPSBT = LightningHelper.CombinePSBTs(signedPsbts2, _logger); @@ -598,12 +598,12 @@ public static PSBT GetCombinedPsbt(ChannelOperationRequest channelOperationReque public static async Task GetCloseAddress(ChannelOperationRequest channelOperationRequest, DerivationStrategyBase derivationStrategyBase, INBXplorerService nbXplorerService, ILogger? _logger = null) { - var closeAddress =await - nbXplorerService.GetUnusedAsync(derivationStrategyBase, DerivationFeature.Deposit, 0, true, default); + var closeAddress = await + nbXplorerService.GetUnusedAsync(derivationStrategyBase, DerivationFeature.Deposit, 0, true, default); if (closeAddress != null) return closeAddress; - var closeAddressNull = $"Closing address was null for an operation on wallet:{channelOperationRequest.Wallet.Id}"; + var closeAddressNull = $"Closing address was null for an operation on wallet:{channelOperationRequest.Wallet.Id}"; _logger?.LogError(closeAddressNull); throw new ArgumentException(closeAddressNull); @@ -675,8 +675,8 @@ public static (Node, Node) CheckNodesAreValid(ChannelOperationRequest channelOpe /// /// public static async Task SignPSBTWithEmbeddedSigner( - ChannelOperationRequest channelOperationRequest, INBXplorerService nbXplorerService, - DerivationStrategyBase derivationStrategyBase, Transaction channelfundingTx, Network network, PSBT changeFixedPSBT, ILogger? logger = null) + ChannelOperationRequest channelOperationRequest, INBXplorerService nbXplorerService, + DerivationStrategyBase derivationStrategyBase, Transaction channelfundingTx, Network network, PSBT changeFixedPSBT, ILogger? logger = null) { //We get the UTXO keyPath / derivation path from nbxplorer @@ -704,7 +704,7 @@ public static async Task SignPSBTWithEmbeddedSigner( } - Dictionary privateKeysForUsedUTXOs; + Dictionary privateKeysForUsedUTXOs; try { privateKeysForUsedUTXOs = txInKeyPathDictionary.ToDictionary(x => x.Key.PrevOut, x => @@ -736,7 +736,7 @@ public static async Task SignPSBTWithEmbeddedSigner( //We should have added a signature for each input, plus already existing signatures var expectedPartialSigs = partialSigsCount + changeFixedPSBT.Inputs.Count; - + if (partialSigsCountAfterSignature == 0 || partialSigsCountAfterSignature != expectedPartialSigs) { @@ -765,6 +765,7 @@ public void CancelPendingChannel(Node source, byte[] pendingChannelId, IUnmockab { client = CreateLightningClient(source.Endpoint); } + if (pendingChannelId != null) { var cancelRequest = new FundingShimCancel @@ -775,9 +776,9 @@ public void CancelPendingChannel(Node source, byte[] pendingChannelId, IUnmockab if (source.ChannelAdminMacaroon != null) { var cancelResult = client.Execute(x => x.FundingStateStep(new FundingTransitionMsg - { - ShimCancel = cancelRequest, - }, + { + ShimCancel = cancelRequest, + }, new Metadata { { "macaroon", source.ChannelAdminMacaroon } }, null, default)); } } @@ -840,7 +841,7 @@ public void CancelPendingChannel(Node source, byte[] pendingChannelId, IUnmockab //If there is already a PSBT as template with the inputs as still valid UTXOs we avoid generating the whole process again to //avoid non-deterministic issues (e.g. Input order and other potential errors) var templatePSBT = - channelOperationRequest.ChannelOperationRequestPsbts.Where(x => x.IsTemplatePSBT).OrderBy(x => x.Id).LastOrDefault(); + channelOperationRequest.ChannelOperationRequestPsbts.Where(x => x.IsTemplatePSBT).MaxBy(x => x.Id); if (templatePSBT != null && PSBT.TryParse(templatePSBT.PSBT, CurrentNetworkHelper.GetCurrentNetwork(), out var parsedTemplatePSBT)) @@ -871,8 +872,8 @@ public void CancelPendingChannel(Node source, byte[] pendingChannelId, IUnmockab } } - var (multisigCoins, selectedUtxOs) = - await GetTxInputCoins(channelOperationRequest, _nbXplorerService, derivationStrategy); + var availableUTXOs = await _coinSelectionService.GetAvailableUTXOsAsync(derivationStrategy); + var (multisigCoins, selectedUtxOs) = await _coinSelectionService.GetTxInputCoins(availableUTXOs, channelOperationRequest, derivationStrategy); if (multisigCoins == null || !multisigCoins.Any()) { @@ -926,15 +927,7 @@ public void CancelPendingChannel(Node source, byte[] pendingChannelId, IUnmockab _logger.LogError(e, "Error while generating base PSBT"); } - // We "lock" the PSBT to the channel operation request by adding to its UTXOs collection for later checking - var utxos = selectedUtxOs.Select(x => _mapper.Map(x)).ToList(); - - var addUTXOSOperation = await _channelOperationRequestRepository.AddUTXOs(channelOperationRequest, utxos); - if (!addUTXOSOperation.Item1) - { - _logger.LogError( - $"Could not add the following utxos({utxos.Humanize()}) to op request:{channelOperationRequest.Id}"); - } + await _coinSelectionService.LockUTXOs(selectedUtxOs, channelOperationRequest, _channelOperationRequestRepository); // The template PSBT is saved for later reuse if (result.Item1 != null) @@ -959,35 +952,6 @@ public void CancelPendingChannel(Node source, byte[] pendingChannelId, IUnmockab return result; } - - /// - /// Gets UTXOs confirmed from the wallet of the request - /// - /// - /// - /// - /// - private async Task<(List coins, List selectedUTXOs)> GetTxInputCoins( - ChannelOperationRequest channelOperationRequest, - INBXplorerService nbXplorerService, - DerivationStrategyBase derivationStrategy) - { - var utxoChanges = await nbXplorerService.GetUTXOsAsync(derivationStrategy, default); - utxoChanges.RemoveDuplicateUTXOs(); - - var satsAmount = channelOperationRequest.SatsAmount; - var lockedUTXOs = await _ifmutxoRepository.GetLockedUTXOs(ignoredChannelOperationRequestId: channelOperationRequest.Id); - - var (coins, selectedUTXOs) = await LightningHelper.SelectCoins(channelOperationRequest.Wallet, - satsAmount, - utxoChanges, - lockedUTXOs, - _logger, - _mapper); - - return (coins, selectedUTXOs); - } - public async Task CloseChannel(ChannelOperationRequest channelOperationRequest, bool forceClose = false) { CheckArgumentsAreValid(channelOperationRequest, OperationRequestType.Close, _logger); @@ -1007,8 +971,6 @@ public async Task CloseChannel(ChannelOperationRequest channelOperationRequest, if (channel != null && node.ChannelAdminMacaroon != null) { - - var client = CreateLightningClient(node.Endpoint); //Time to close the channel @@ -1163,7 +1125,7 @@ public async Task CloseChannel(ChannelOperationRequest channelOperationRequest, KeyPathInformation? keyPathInformation = null; try { - keyPathInformation =await _nbXplorerService.GetUnusedAsync(wallet.GetDerivationStrategy(), + keyPathInformation = await _nbXplorerService.GetUnusedAsync(wallet.GetDerivationStrategy(), derivationFeature, default, false, default); } catch (Exception e) @@ -1194,7 +1156,6 @@ public async Task CloseChannel(ChannelOperationRequest channelOperationRequest, } - var client = CreateLightningClient(node.Endpoint); try { @@ -1226,12 +1187,13 @@ public async Task CloseChannel(ChannelOperationRequest channelOperationRequest, client = CreateLightningClient(node.Endpoint); var result = client.Execute(x => x.ListChannels(new ListChannelsRequest(), - new Metadata { - {"macaroon", node.ChannelAdminMacaroon} - }, null, default)); + new Metadata + { + { "macaroon", node.ChannelAdminMacaroon } + }, null, default)); var chan = result.Channels.FirstOrDefault(x => x.ChanId == channel.ChanId); - if(chan == null) + if (chan == null) return (null, null); var htlcsLocal = chan.PendingHtlcs.Where(x => x.Incoming == true).Sum(x => x.Amount); diff --git a/test/FundsManager.Tests/Data/Repositories/FUTXORepositoryTests.cs b/test/FundsManager.Tests/Data/Repositories/FUTXORepositoryTests.cs new file mode 100644 index 00000000..a84439ec --- /dev/null +++ b/test/FundsManager.Tests/Data/Repositories/FUTXORepositoryTests.cs @@ -0,0 +1,164 @@ +using FluentAssertions; +using FundsManager.Data.Models; +using Microsoft.EntityFrameworkCore; + +namespace FundsManager.Data.Repositories; + +public class FUTXORepositoryTests +{ + private readonly Random _random = new(); + + private Mock> SetupDbContextFactory() + { + var dbContextFactory = new Mock>(); + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: "FUTXORepositoryTest" + _random.Next()) + .Options; + var context = ()=> new ApplicationDbContext(options); + dbContextFactory.Setup(x => x.CreateDbContext()).Returns(context); + dbContextFactory.Setup(x => x.CreateDbContextAsync(default)).ReturnsAsync(context); + return dbContextFactory; + } + + [Fact] + public async Task GetLockedUTXOs_emptyArgs() + { + var dbContextFactory = SetupDbContextFactory(); + var futxoRepository = new FUTXORepository(null, null, dbContextFactory.Object); + + var context = await dbContextFactory.Object.CreateDbContextAsync(); + + context.WalletWithdrawalRequests.Add(new WalletWithdrawalRequest + { + Description = "1", + DestinationAddress = "1", + Status = WalletWithdrawalRequestStatus.Pending, + UTXOs = new List { new () { TxId = "1"} } + }); + context.ChannelOperationRequests.Add(new ChannelOperationRequest + { + Status = ChannelOperationRequestStatus.Pending, + Utxos = new List { new () { TxId = "2"} } + }); + await context.SaveChangesAsync(); + + var result = await futxoRepository.GetLockedUTXOs(); + result.Should().HaveCount(2); + } + + [Fact] + public async Task GetLockedUTXOs_ignoreWithdrawals() + { + var dbContextFactory = SetupDbContextFactory(); + var futxoRepository = new FUTXORepository(null, null, dbContextFactory.Object); + + var context = dbContextFactory.Object.CreateDbContext(); + + context.WalletWithdrawalRequests.Add(new WalletWithdrawalRequest + { + Id = 1, + Description = "1", + DestinationAddress = "1", + Status = WalletWithdrawalRequestStatus.Pending, + UTXOs = new List { new () { TxId = "1"} } + }); + context.ChannelOperationRequests.Add(new ChannelOperationRequest + { + Id = 2, + Status = ChannelOperationRequestStatus.Pending, + Utxos = new List { new () { TxId = "2"} } + }); + await context.SaveChangesAsync(); + + var result = await futxoRepository.GetLockedUTXOs(1); + result.Should().HaveCount(1); + result[0].Id.Should().Be(2); + } + + [Fact] + public async Task GetLockedUTXOs_ignoreChannels() + { + var dbContextFactory = SetupDbContextFactory(); + var futxoRepository = new FUTXORepository(null, null, dbContextFactory.Object); + + var context = dbContextFactory.Object.CreateDbContext(); + + context.WalletWithdrawalRequests.Add(new WalletWithdrawalRequest + { + Id = 1, + Description = "1", + DestinationAddress = "1", + Status = WalletWithdrawalRequestStatus.Pending, + UTXOs = new List { new () { TxId = "1"} } + }); + context.ChannelOperationRequests.Add(new ChannelOperationRequest + { + Id = 2, + Status = ChannelOperationRequestStatus.Pending, + Utxos = new List { new () { TxId = "2"} } + }); + await context.SaveChangesAsync(); + + var result = await futxoRepository.GetLockedUTXOs( null, 2); + result.Should().HaveCount(1); + result[0].Id.Should().Be(1); + } + + [Fact] + public async Task GetLockedUTXOs_failedChannels() + { + var dbContextFactory = SetupDbContextFactory(); + var futxoRepository = new FUTXORepository(null, null, dbContextFactory.Object); + + var context = dbContextFactory.Object.CreateDbContext(); + + context.WalletWithdrawalRequests.Add(new WalletWithdrawalRequest + { + Id = 1, + Description = "1", + DestinationAddress = "1", + Status = WalletWithdrawalRequestStatus.Failed, + UTXOs = new List { new () { TxId = "1"} } + }); + context.ChannelOperationRequests.Add(new ChannelOperationRequest + { + Id = 2, + Status = ChannelOperationRequestStatus.Pending, + Utxos = new List { new () { TxId = "2"} } + }); + await context.SaveChangesAsync(); + + var result = await futxoRepository.GetLockedUTXOs(); + result.Should().HaveCount(1); + result[0].Id.Should().Be(2); + } + + [Fact] + public async Task GetLockedUTXOs_failedCWithdrawals() + { + var dbContextFactory = SetupDbContextFactory(); + var futxoRepository = new FUTXORepository(null, null, dbContextFactory.Object); + + var context = dbContextFactory.Object.CreateDbContext(); + + context.WalletWithdrawalRequests.Add(new WalletWithdrawalRequest + { + Id = 1, + Description = "1", + DestinationAddress = "1", + Status = WalletWithdrawalRequestStatus.Pending, + UTXOs = new List { new () { TxId = "1"} } + }); + context.ChannelOperationRequests.Add(new ChannelOperationRequest + { + Id = 2, + Status = ChannelOperationRequestStatus.Failed, + Utxos = new List { new () { TxId = "2"} } + }); + await context.SaveChangesAsync(); + + var result = await futxoRepository.GetLockedUTXOs(); + result.Should().HaveCount(1); + result[0].Id.Should().Be(1); + } +} \ No newline at end of file diff --git a/test/FundsManager.Tests/Services/BitcoinServiceTests.cs b/test/FundsManager.Tests/Services/BitcoinServiceTests.cs index 2483834d..0eccd104 100644 --- a/test/FundsManager.Tests/Services/BitcoinServiceTests.cs +++ b/test/FundsManager.Tests/Services/BitcoinServiceTests.cs @@ -53,7 +53,7 @@ async Task GenerateTemplatePSBT_RequestNotPending(WalletWithdrawalRequestStatus .Setup((w) => w.GetById(It.IsAny())) .ReturnsAsync(withdrawalRequest); - var bitcoinService = new BitcoinService(_logger, null, null, walletWithdrawalRequestRepository.Object, null, null, null, null); + var bitcoinService = new BitcoinService(_logger, null, walletWithdrawalRequestRepository.Object, null, null, null, null, null); // Act var act = () => bitcoinService.GenerateTemplatePSBT(withdrawalRequest); @@ -83,7 +83,7 @@ async Task GenerateTemplatePSBT_NBXplorerNotFullySynced() .Setup(x => x.GetStatusAsync(default)) .ReturnsAsync(new StatusResult() { IsFullySynched = false }); - var bitcoinService = new BitcoinService(_logger, null, null, walletWithdrawalRequestRepository.Object, null, null, null, nbXplorerService.Object); + var bitcoinService = new BitcoinService(_logger, null, walletWithdrawalRequestRepository.Object, null, null, null, nbXplorerService.Object, null); // Act var act = () => bitcoinService.GenerateTemplatePSBT(withdrawalRequest); @@ -115,7 +115,7 @@ async Task GenerateTemplatePSBT_NoDerivationStrategy() .Setup(x => x.GetStatusAsync(default)) .ReturnsAsync(new StatusResult() { IsFullySynched = true }); - var bitcoinService = new BitcoinService(_logger, null, null, walletWithdrawalRequestRepository.Object, null, null, null, nbXplorerService.Object); + var bitcoinService = new BitcoinService(_logger, null, walletWithdrawalRequestRepository.Object, null, null, null, nbXplorerService.Object, null); // Act var act = () => bitcoinService.GenerateTemplatePSBT(withdrawalRequest); @@ -126,7 +126,7 @@ await act .ThrowAsync() .WithMessage("Error while getting the derivation strategy scheme for wallet: 0"); } - + [Fact] async Task GenerateTemplatePSBT_LegacyMultiSigSucceeds() { @@ -181,10 +181,11 @@ async Task GenerateTemplatePSBT_LegacyMultiSigSucceeds() } }); fmutxoRepository - .Setup(x => x.GetLockedUTXOs(It.IsAny(), null)) + .Setup(x => x.GetLockedUTXOs(null, null)) .ReturnsAsync(new List()); + var coinSelectionService = new CoinSelectionService(_logger, mapper.Object, fmutxoRepository.Object, nbXplorerService.Object); - var bitcoinService = new BitcoinService(_logger, fmutxoRepository.Object, mapper.Object, walletWithdrawalRequestRepository.Object, walletWithdrawalRequestPsbtRepository.Object, null, null, nbXplorerService.Object); + var bitcoinService = new BitcoinService(_logger, mapper.Object, walletWithdrawalRequestRepository.Object, walletWithdrawalRequestPsbtRepository.Object, null, null, nbXplorerService.Object, coinSelectionService); // Act var result = await bitcoinService.GenerateTemplatePSBT(withdrawalRequest); @@ -193,7 +194,7 @@ async Task GenerateTemplatePSBT_LegacyMultiSigSucceeds() var psbt = PSBT.Parse("cHNidP8BAIkBAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/////wD/////AkBCDwAAAAAAIgAgPaPWaBQgTxHOMVfMfpX21blroUe8KAd6w2gLRelFuiAYUYkAAAAAACIAIDx3862ZOy+vKdDZ4oysyRZX0HARoqQ9LqqK2ukxoopiAAAAAE8BBDWHzwMvESQsgAAAAfw77kI6AYzrbSJqBmMojtD7XuD6nXkKs3DQMOBHMObIA4COLhzUgr3QcZaUPFqBM9Fpr4YCK2uwOBdxZE7AdETXEB/M5N4wAACAAQAAgAEAAIBPAQQ1h88DVqwD9IAAAAH5CK5KZrD/oasUtVrwzkjypwIly5AQkC1pAa+QuT6PgQJRrxXgW7i36sGJWz9fR//v7NgyGgLvIimPidCiA33wYBBg86CzMAAAgAEAAIABAACATwEENYfPA325Ro2AAAAB9SJwx2h6Ovs1HvTxuaMMEPO205IXBoOuqUiME5oRyZgDIiOFIzjqZ/v9jcNSqyYl55ondkYhI2vxwCEwkNNInp8Q7QIQyDAAAIABAACAAQAAgAABASuAlpgAAAAAACIAILNTGKQyViCBs/y3kcG+Q/3NcIIypkqLb3/EMmN57BDEAQVpUiEC2FTFYM/mwE4L60Q0G2p5QElV7YlMD7fcgoJEH79pLLEhAwJn/wsRl0hvcYj5Y3Bv3uQlxZ57pBZ9KSeuEPVNmjS/IQMaU3fyWsF+N0FpN8hSusDj6bESvd9YR509kdgWMLKLj1OuIgYC2FTFYM/mwE4L60Q0G2p5QElV7YlMD7fcgoJEH79pLLEYH8zk3jAAAIABAACAAQAAgAAAAAAAAAAAIgYDAmf/CxGXSG9xiPljcG/e5CXFnnukFn0pJ64Q9U2aNL8YYPOgszAAAIABAACAAQAAgAAAAAAAAAAAIgYDGlN38lrBfjdBaTfIUrrA4+mxEr3fWEedPZHYFjCyi48Y7QIQyDAAAIABAACAAQAAgAAAAAAAAAAAAAAA", Network.RegTest); result.Should().BeEquivalentTo(psbt); } - + [Fact] async Task GenerateTemplatePSBT_MultiSigSucceeds() { @@ -248,10 +249,12 @@ async Task GenerateTemplatePSBT_MultiSigSucceeds() } }); fmutxoRepository - .Setup(x => x.GetLockedUTXOs(It.IsAny(), null)) + .Setup(x => x.GetLockedUTXOs(null , null)) .ReturnsAsync(new List()); - var bitcoinService = new BitcoinService(_logger, fmutxoRepository.Object, mapper.Object, walletWithdrawalRequestRepository.Object, walletWithdrawalRequestPsbtRepository.Object, null, null, nbXplorerService.Object); + var coinSelectionService = new CoinSelectionService(_logger, mapper.Object, fmutxoRepository.Object, nbXplorerService.Object); + + var bitcoinService = new BitcoinService(_logger, mapper.Object, walletWithdrawalRequestRepository.Object, walletWithdrawalRequestPsbtRepository.Object, null, null, nbXplorerService.Object, coinSelectionService); // Act var result = await bitcoinService.GenerateTemplatePSBT(withdrawalRequest); @@ -314,10 +317,12 @@ async Task GenerateTemplatePSBT_SingleSigSucceeds() } }); fmutxoRepository - .Setup(x => x.GetLockedUTXOs(It.IsAny(), null)) + .Setup(x => x.GetLockedUTXOs(null, null)) .ReturnsAsync(new List()); - var bitcoinService = new BitcoinService(_logger, fmutxoRepository.Object, mapper.Object, walletWithdrawalRequestRepository.Object, walletWithdrawalRequestPsbtRepository.Object, null, null, nbXplorerService.Object); + var coinSelectionService = new CoinSelectionService(_logger, mapper.Object, fmutxoRepository.Object, nbXplorerService.Object); + + var bitcoinService = new BitcoinService(_logger, mapper.Object, walletWithdrawalRequestRepository.Object, walletWithdrawalRequestPsbtRepository.Object, null, null, nbXplorerService.Object, coinSelectionService); // Act var result = await bitcoinService.GenerateTemplatePSBT(withdrawalRequest); @@ -366,7 +371,7 @@ async Task PerformWithdrawal_SingleSigSucceeds() .ReturnsAsync(withdrawalRequest); walletWithdrawalRequestRepository .Setup((w) => w.Update(It.IsAny())) - .Returns((true, null)); + .Returns((true, null)); nbXplorerService .Setup(x => x.GetUTXOsAsync(It.IsAny(), default)) .ReturnsAsync(new UTXOChanges() @@ -390,7 +395,7 @@ async Task PerformWithdrawal_SingleSigSucceeds() nodeRepository .Setup(x => x.GetAllManagedByNodeGuard()) .Returns(Task.FromResult(new List() {node})); - var bitcoinService = new BitcoinService(_logger, null, null, walletWithdrawalRequestRepository.Object, null, nodeRepository.Object, null, nbXplorerService.Object); + var bitcoinService = new BitcoinService(_logger, null, walletWithdrawalRequestRepository.Object, null, nodeRepository.Object, null, nbXplorerService.Object, null); // Act var act = () => bitcoinService.PerformWithdrawal(withdrawalRequest); @@ -398,7 +403,7 @@ async Task PerformWithdrawal_SingleSigSucceeds() // Assert await act.Should().NotThrowAsync(); } - + [Fact] async Task PerformWithdrawal_MultiSigSucceeds() { @@ -446,7 +451,7 @@ async Task PerformWithdrawal_MultiSigSucceeds() .ReturnsAsync(withdrawalRequest); walletWithdrawalRequestRepository .Setup((w) => w.Update(It.IsAny())) - .Returns((true, null)); + .Returns((true, null)); nbXplorerService .Setup(x => x.GetUTXOsAsync(It.IsAny(), default)) .ReturnsAsync(new UTXOChanges() @@ -470,7 +475,7 @@ async Task PerformWithdrawal_MultiSigSucceeds() nodeRepository .Setup(x => x.GetAllManagedByNodeGuard()) .Returns(Task.FromResult(new List() {node})); - var bitcoinService = new BitcoinService(_logger, null, null, walletWithdrawalRequestRepository.Object, null, nodeRepository.Object, null, nbXplorerService.Object); + var bitcoinService = new BitcoinService(_logger, null, walletWithdrawalRequestRepository.Object, null, nodeRepository.Object, null, nbXplorerService.Object, null); // Act var act = () => bitcoinService.PerformWithdrawal(withdrawalRequest); @@ -478,7 +483,7 @@ async Task PerformWithdrawal_MultiSigSucceeds() // Assert await act.Should().NotThrowAsync(); } - + [Fact] async Task PerformWithdrawal_LegacyMultiSigSucceeds() { @@ -526,7 +531,7 @@ async Task PerformWithdrawal_LegacyMultiSigSucceeds() .ReturnsAsync(withdrawalRequest); walletWithdrawalRequestRepository .Setup((w) => w.Update(It.IsAny())) - .Returns((true, null)); + .Returns((true, null)); nbXplorerService .Setup(x => x.GetUTXOsAsync(It.IsAny(), default)) .ReturnsAsync(new UTXOChanges() @@ -550,7 +555,7 @@ async Task PerformWithdrawal_LegacyMultiSigSucceeds() nodeRepository .Setup(x => x.GetAllManagedByNodeGuard()) .Returns(Task.FromResult(new List() {node})); - var bitcoinService = new BitcoinService(_logger, null, null, walletWithdrawalRequestRepository.Object, null, nodeRepository.Object, null, nbXplorerService.Object); + var bitcoinService = new BitcoinService(_logger, null, walletWithdrawalRequestRepository.Object, null, nodeRepository.Object, null, nbXplorerService.Object, null); // Act var act = () => bitcoinService.PerformWithdrawal(withdrawalRequest); diff --git a/test/FundsManager.Tests/Services/LightningServiceTests.cs b/test/FundsManager.Tests/Services/LightningServiceTests.cs index f8b5c153..d785261e 100644 --- a/test/FundsManager.Tests/Services/LightningServiceTests.cs +++ b/test/FundsManager.Tests/Services/LightningServiceTests.cs @@ -83,7 +83,7 @@ public async Task OpenChannel_ChannelOperationRequestNotFound() .ReturnsAsync(null as ChannelOperationRequest); var lightningService = new LightningService(_logger, channelOperationRequestRepository.Object, null, - dbContextFactory.Object, null, null, null, null, null, null, new Mock().Object); + dbContextFactory.Object, null, null, null, null, null, null, new Mock().Object, null); var operationRequest = new ChannelOperationRequest { @@ -594,7 +594,8 @@ public async Task OpenChannel_SuccessLegacyMultiSig() channelOperationRequestPsbtRepository.Object, channelRepository.Object, null, - GetNBXplorerServiceFullyMocked(utxoChanges).Object); + GetNBXplorerServiceFullyMocked(utxoChanges).Object, + null); // Act var act = async () => await lightningService.OpenChannel(operationRequest); @@ -824,7 +825,8 @@ public async Task OpenChannel_SuccessMultiSig() channelOperationRequestPsbtRepository.Object, channelRepository.Object, null, - GetNBXplorerServiceFullyMocked(utxoChanges).Object); + GetNBXplorerServiceFullyMocked(utxoChanges).Object, + null); // Act var act = async () => await lightningService.OpenChannel(operationRequest); @@ -1054,7 +1056,8 @@ public async Task OpenChannel_SuccessSingleSigBip39() channelOperationRequestPsbtRepository.Object, channelRepository.Object, null, - GetNBXplorerServiceFullyMocked(utxoChanges).Object); + GetNBXplorerServiceFullyMocked(utxoChanges).Object, + null); // Act var act = async () => await lightningService.OpenChannel(operationRequest); @@ -1283,7 +1286,8 @@ public async Task OpenChannel_SuccessSingleSig() channelOperationRequestPsbtRepository.Object, channelRepository.Object, null, - GetNBXplorerServiceFullyMocked(utxoChanges).Object); + GetNBXplorerServiceFullyMocked(utxoChanges).Object, + null); // Act var act = async () => await lightningService.OpenChannel(operationRequest); @@ -1373,6 +1377,7 @@ public async Task CloseChannel_Succeeds() null, channelRepository.Object, null, + null, null); // Act