Skip to content

Commit

Permalink
Manual coin selection (#181)
Browse files Browse the repository at this point in the history
* Changeless lnd funding

* Refactor on coin selection

* Manual coin selection

* Changeless coin selection

* Preserve selection

* Fixed validation

* Refactor on utxo selection

* Fixed validations for mainnet, added fee settings

* Fixed changeful operations

* Fixed tests for multisig wallets

* Added descending ordering to tables by default

* Readded disappearing code

* Fixed finding output for change

---------

Co-authored-by: José A.P <[email protected]>
Co-authored-by: Rodrigo Sanchez <[email protected]>
  • Loading branch information
3 people authored May 31, 2023
1 parent c812ccd commit 8b3a83b
Show file tree
Hide file tree
Showing 25 changed files with 1,696 additions and 186 deletions.
5 changes: 5 additions & 0 deletions src/Data/Models/ChannelOperationRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,11 @@ public decimal Amount
/// </summary>
public string? JobId { get; set; }

/// <summary>
/// This indicates if the user requested a changeless operation by selecting UTXOs
/// </summary>
public bool Changeless { get; set; }

/// <summary>
/// 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
/// </summary>
Expand Down
22 changes: 22 additions & 0 deletions src/Data/Repositories/ChannelOperationRequestRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,28 @@ public async Task<List<ChannelOperationRequest>> GetUnsignedPendingRequestsByUse
return result;
}

public async Task<(bool, List<FMUTXO>?)> GetUTXOs(IBitcoinRequest request)
{
await using var applicationDbContext = await _dbContextFactory.CreateDbContextAsync();

(bool, List<FMUTXO>?) result = (true, null);
try
{
var channelOperationRequest = await applicationDbContext.ChannelOperationRequests
.Include(r => r.Utxos)
.FirstOrDefaultAsync(r => r.Id == request.Id);

result.Item2 = channelOperationRequest.Utxos;
}
catch (Exception e)
{
_logger.LogError(e, "Error while getting UTXOs from channel op request: {RequestId}", request.Id);
result.Item1 = false;
}

return result;
}

public async Task<List<ChannelOperationRequest>> GetPendingRequests()
{
await using var applicationDbContext = await _dbContextFactory.CreateDbContextAsync();
Expand Down
2 changes: 2 additions & 0 deletions src/Data/Repositories/Interfaces/IBitcoinRequestRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,6 @@ public interface IBitcoinRequestRepository
/// <param name="utxos"></param>
/// <returns></returns>
Task<(bool, string?)> AddUTXOs(IBitcoinRequest type, List<FMUTXO> utxos);

public Task<(bool, List<FMUTXO>?)> GetUTXOs(IBitcoinRequest type);
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,6 @@ public interface IChannelOperationRequestRepository : IBitcoinRequestRepository

(bool, string?) Update(ChannelOperationRequest type);

/// <summary>
/// Adds on the many-to-many collection the list of utxos provided
/// </summary>
/// <param name="type"></param>
/// <param name="utxos"></param>
/// <returns></returns>
Task<(bool, string?)> AddUTXOs(IBitcoinRequest type, List<FMUTXO> utxos);

/// <summary>
/// Returns those requests that can have a PSBT locked until they are confirmed / rejected / cancelled
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

namespace FundsManager.Data.Repositories.Interfaces;

public interface IWalletWithdrawalRequestRepository
public interface IWalletWithdrawalRequestRepository: IBitcoinRequestRepository
{
Task<WalletWithdrawalRequest?> GetById(int id);

Expand All @@ -39,8 +39,6 @@ public interface IWalletWithdrawalRequestRepository

(bool, string?) Update(WalletWithdrawalRequest type);

Task<(bool, string?)> AddUTXOs(WalletWithdrawalRequest type, List<FMUTXO> utxos);

Task<List<WalletWithdrawalRequest>> GetPendingRequests();

Task<List<WalletWithdrawalRequest>> GetOnChainPendingWithdrawals();
Expand Down
41 changes: 32 additions & 9 deletions src/Data/Repositories/WalletWithdrawalRequestRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
using Humanizer;
using Microsoft.EntityFrameworkCore;
using NBitcoin;
using Exception = System.Exception;

namespace FundsManager.Data.Repositories
{
Expand All @@ -38,8 +39,8 @@ public class WalletWithdrawalRequestRepository : IWalletWithdrawalRequestReposit

public WalletWithdrawalRequestRepository(IRepository<WalletWithdrawalRequest> repository,
ILogger<WalletWithdrawalRequestRepository> logger,
IDbContextFactory<ApplicationDbContext> dbContextFactory,
IMapper mapper,
IDbContextFactory<ApplicationDbContext> dbContextFactory,
IMapper mapper,
NotificationService notificationService,
INBXplorerService nBXplorerService
)
Expand Down Expand Up @@ -101,22 +102,22 @@ public async Task<List<WalletWithdrawalRequest>> GetUnsignedPendingRequestsByUse

type.SetCreationDatetime();
type.SetUpdateDatetime();

//Verify that the wallet has enough funds calling nbxplorer
var wallet = await applicationDbContext.Wallets.Include(x=> x.Keys).SingleOrDefaultAsync(x => x.Id == type.WalletId);

if (wallet == null)
{
return (false, "The wallet could not be found.");
}

var derivationStrategyBase = wallet.GetDerivationStrategy();

if (derivationStrategyBase == null)
{
return (false, "The wallet does not have a derivation strategy.");
}

var balance = await _nBXplorerService.GetBalanceAsync(derivationStrategyBase, default);

if (balance == null)
Expand All @@ -125,12 +126,12 @@ public async Task<List<WalletWithdrawalRequest>> GetUnsignedPendingRequestsByUse
}

var requestMoneyAmount = new Money(type.Amount, MoneyUnit.BTC);

if ((Money) balance.Confirmed < requestMoneyAmount)
{
return (false, $"The wallet {type.Wallet.Name} does not have enough funds to complete this withdrawal request. The wallet has {balance.Confirmed} BTC and the withdrawal request is for {requestMoneyAmount} BTC.");
}


var valueTuple = await _repository.AddAsync(type, applicationDbContext);
if (!wallet.IsHotWallet)
Expand Down Expand Up @@ -171,7 +172,7 @@ public async Task<List<WalletWithdrawalRequest>> GetUnsignedPendingRequestsByUse
return _repository.Update(strippedType, applicationDbContext);
}

public async Task<(bool, string?)> AddUTXOs(WalletWithdrawalRequest type, List<FMUTXO> utxos)
public async Task<(bool, string?)> AddUTXOs(IBitcoinRequest type, List<FMUTXO> utxos)
{
if (type == null) throw new ArgumentNullException(nameof(type));
if (utxos.Count == 0) throw new ArgumentException("Value cannot be an empty collection.", nameof(utxos));
Expand Down Expand Up @@ -210,6 +211,28 @@ public async Task<List<WalletWithdrawalRequest>> GetUnsignedPendingRequestsByUse
return result;
}

public async Task<(bool, List<FMUTXO>?)> GetUTXOs(IBitcoinRequest request)
{
await using var applicationDbContext = await _dbContextFactory.CreateDbContextAsync();

(bool, List<FMUTXO>?) result = (true, null);
try
{
var walletWithdrawalRequest = await applicationDbContext.WalletWithdrawalRequests
.Include(r => r.UTXOs)
.FirstOrDefaultAsync(r => r.Id == request.Id);

result.Item2 = walletWithdrawalRequest.UTXOs;
}
catch (Exception e)
{
_logger.LogError(e, "Error while getting UTXOs from wallet withdrawal request: {RequestId}", request.Id);
result.Item1 = false;
}

return result;
}

public async Task<List<WalletWithdrawalRequest>> GetPendingRequests()
{
await using var applicationDbContext = await _dbContextFactory.CreateDbContextAsync();
Expand Down
6 changes: 4 additions & 2 deletions src/Helpers/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public class Constants
public static readonly bool ENABLE_REMOTE_SIGNER;
public static readonly bool PUSH_NOTIFICATIONS_ONESIGNAL_ENABLED;
public static readonly bool ENABLE_HW_SUPPORT;
public static readonly bool FEE_SELECTION_ENABLED = false; // Incomplete feature, remove this line when it's ready

// Connections
public static readonly string POSTGRES_CONNECTIONSTRING = "Host=localhost;Port=5432;Database=fundsmanager;Username=rw_dev;Password=rw_dev";
Expand Down Expand Up @@ -62,6 +63,7 @@ public class Constants
// Usage
public static readonly string BITCOIN_NETWORK;
public static readonly long MINIMUM_CHANNEL_CAPACITY_SATS = 20_000;
public static readonly long MAXIMUM_CHANNEL_CAPACITY_SATS_REGTEST = 16_777_215;
public static readonly decimal MINIMUM_WITHDRAWAL_BTC_AMOUNT = 0.0m;
public static readonly decimal MAXIMUM_WITHDRAWAL_BTC_AMOUNT = 21_000_000;
public static readonly int TRANSACTION_CONFIRMATION_MINIMUM_BLOCKS;
Expand Down Expand Up @@ -102,7 +104,7 @@ static Constants()
ALICE_HOST = Environment.GetEnvironmentVariable("ALICE_HOST") ?? "host.docker.internal:10001";

CAROL_HOST = Environment.GetEnvironmentVariable("CAROL_HOST") ?? "host.docker.internal:10003";

BOB_HOST = Environment.GetEnvironmentVariable("BOB_HOST") ?? "host.docker.internal:10002";

FUNDSMANAGER_ENDPOINT = Environment.GetEnvironmentVariable("FUNDSMANAGER_ENDPOINT");
Expand Down Expand Up @@ -136,7 +138,7 @@ static Constants()
AWS_ACCESS_KEY_ID = GetEnvironmentalVariableOrThrowIfNotTesting("AWS_ACCESS_KEY_ID", "if ENABLE_REMOTE_SIGNER is set, AWS_ACCESS_KEY_ID");

AWS_SECRET_ACCESS_KEY = GetEnvironmentalVariableOrThrowIfNotTesting("AWS_SECRET_ACCESS_KEY", "if ENABLE_REMOTE_SIGNER is set, AWS_SECRET_ACCESS_KEY");

REMOTE_SIGNER_ENDPOINT = GetEnvironmentalVariableOrThrowIfNotTesting("REMOTE_SIGNER_ENDPOINT", "if ENABLE_REMOTE_SIGNER is set, REMOTE_SIGNER_ENDPOINT");
}

Expand Down
29 changes: 22 additions & 7 deletions src/Helpers/LightningHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,14 @@ public static void RemoveDuplicateUTXOs(this UTXOChanges utxoChanges)
var rootedKeyPath = key.GetRootedKeyPath();

//Global xpubs field addition
result.GlobalXPubs.Add(
bitcoinExtPubKey,
rootedKeyPath
);
if (!result.GlobalXPubs.ContainsKey(bitcoinExtPubKey))
{
result.GlobalXPubs.Add(
bitcoinExtPubKey,
rootedKeyPath
);
}


foreach (var selectedUtxo in selectedUtxOs)
{
Expand All @@ -79,13 +83,24 @@ public static void RemoveDuplicateUTXOs(this UTXOChanges utxoChanges)
input?.GetCoin()?.Outpoint == selectedUtxo.Outpoint);
var coin = coins.FirstOrDefault(x => x.Outpoint == selectedUtxo.Outpoint);

if (coin != null && input != null &&
if (input == null)
{
var errorMessage = $"Couldn't get coin for: {selectedUtxo.Outpoint}";
logger?.LogError(errorMessage);
throw new ArgumentException(errorMessage, nameof(derivedPubKey));
}

if (coin != null &&
(
wallet.IsHotWallet && (coin as Coin).ScriptPubKey == derivedPubKey.WitHash.ScriptPubKey ||
!wallet.IsHotWallet && (coin as ScriptCoin).Redeem.GetAllPubKeys().Contains(derivedPubKey))
)
{
input.AddKeyPath(derivedPubKey, addressRootedKeyPath);
if (!input.HDKeyPaths.ContainsKey(derivedPubKey))
{
input.AddKeyPath(derivedPubKey, addressRootedKeyPath);

}
}
else
{
Expand Down Expand Up @@ -227,7 +242,7 @@ public static async Task<GetFeeRateResult> GetFeeRateResult(Network nbXplorerNet
{
//TODO Maybe the block confirmation count can be a parameter.
feeRateResult =
await nbxplorerClient.GetFeeRateAsync(6, default);
await nbxplorerClient.GetFeeRateAsync(1, default);
}

return feeRateResult;
Expand Down
18 changes: 12 additions & 6 deletions src/Helpers/ValidationHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,17 @@
*
*/

using System.Globalization;
using System.Text.RegularExpressions;
using Blazorise;
using FundsManager.Data.Models;
using FundsManager.Helpers;
using NBitcoin;

namespace FundsManager.Helpers;

public static class ValidationHelper
{
private static Network network = CurrentNetworkHelper.GetCurrentNetwork();

/// <summary>
/// Validates that the name of the item introduced in the form is not null and is not only whitespaces.
/// </summary>
Expand All @@ -41,7 +42,7 @@ public static void ValidateName(ValidatorEventArgs obj)
obj.Status = ValidationStatus.Error;
}
}

public static void ValidateUsername(ValidatorEventArgs obj, List<ApplicationUser> users, string currentUserId)
{
obj.Status = ValidationStatus.Success;
Expand All @@ -57,15 +58,20 @@ public static void ValidateUsername(ValidatorEventArgs obj, List<ApplicationUser
obj.Status = ValidationStatus.Error;
return;
}

}

public static void ValidateChannelCapacity(ValidatorEventArgs obj)
{
obj.Status = ValidationStatus.Success;
if (((long)obj.Value) < Constants.MINIMUM_CHANNEL_CAPACITY_SATS)
if ((long)obj.Value < Constants.MINIMUM_CHANNEL_CAPACITY_SATS)
{
obj.ErrorText = "The amount selected must be greater than 20.000";
obj.Status = ValidationStatus.Error;
}
else if ((long)obj.Value > Constants.MAXIMUM_CHANNEL_CAPACITY_SATS_REGTEST && network == Network.RegTest)
{
obj.ErrorText = "The amount must be greater than 20.000";
obj.ErrorText = "The amount selected must be lower than 16.777.215";
obj.Status = ValidationStatus.Error;
}
}
Expand Down
Loading

0 comments on commit 8b3a83b

Please sign in to comment.