Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatic coin selection strategies #195

Merged
merged 3 commits into from
Jun 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 20 additions & 10 deletions src/Data/DbInitializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ public static void Initialize(IServiceProvider serviceProvider)
{
ChannelAdminMacaroon =
"0201036c6e6402f801030a108cdfeb2614b8335c11aebb358f888d6d1201301a160a0761646472657373120472656164120577726974651a130a04696e666f120472656164120577726974651a170a08696e766f69636573120472656164120577726974651a210a086d616361726f6f6e120867656e6572617465120472656164120577726974651a160a076d657373616765120472656164120577726974651a170a086f6666636861696e120472656164120577726974651a160a076f6e636861696e120472656164120577726974651a140a057065657273120472656164120577726974651a180a067369676e6572120867656e657261746512047265616400000620c999e1a30842cbae3f79bd633b19d5ec0d2b6ebdc4880f6f5d5c230ce38f26ab",
Endpoint = Constants.ALICE_HOST,
Endpoint = Constants.ALICE_HOST,
Name = "Alice",
CreationDatetime = DateTimeOffset.UtcNow,
PubKey = "02dc2ae598a02fc1e9709a23b68cd51d7fa14b1132295a4d75aa4f5acd23ee9527",
Expand All @@ -240,7 +240,7 @@ public static void Initialize(IServiceProvider serviceProvider)
};

_ = Task.Run(() => nodeRepository.AddAsync(carol)).Result;

//Bob node from Polar (BOB) LND 0.15.5 -> check devnetwork.zip polar file
var bob = new Node
{
Expand All @@ -253,7 +253,7 @@ public static void Initialize(IServiceProvider serviceProvider)
Users = new List<ApplicationUser>(),
AutosweepEnabled = false
};

_ = Task.Run(() => nodeRepository.AddAsync(bob)).Result;

//Add user to the channel
Expand All @@ -262,13 +262,13 @@ public static void Initialize(IServiceProvider serviceProvider)

var carolUpdateResult = applicationDbContext.Update(adminUser);
}

var internalWallet = applicationDbContext.InternalWallets.FirstOrDefault();
if (internalWallet == null)
{
//Default Internal Wallet
internalWallet = CreateWallet.CreateInternalWallet(logger);

applicationDbContext.Add(internalWallet);
applicationDbContext.SaveChanges();
}
Expand All @@ -280,14 +280,14 @@ public static void Initialize(IServiceProvider serviceProvider)
"social mango annual basic work brain economy one safe physical junk other toy valid load cook napkin maple runway island oil fan legend stem";
var wallet2Seed =
"solar goat auto bachelor chronic input twin depth fork scale divorce fury mushroom column image sauce car public artist announce treat spend jacket physical";

logger?.LogInformation("Wallet 1 seed: {MnemonicString}", wallet1Seed);
logger?.LogInformation("Wallet 2 seed: {MnemonicString}", wallet2Seed);

var user1Key = CreateWallet.CreateUserKey("Key 1", adminUser.Id, wallet1Seed);
var user2Key = CreateWallet.CreateUserKey("Key 2", financeUser.Id, wallet2Seed);
var testingLegacyMultisigWallet = CreateWallet.LegacyMultiSig(internalWallet, "1'", user1Key, user2Key);

var testingLegacyMultisigWallet = CreateWallet.LegacyMultiSig(internalWallet, "1'", user1Key, user2Key);
var testingMultisigWallet = CreateWallet.MultiSig(internalWallet, "0", user1Key, user2Key);
var testingSinglesigWallet = CreateWallet.SingleSig(internalWallet, "1");
var testingSingleSigBIP39Wallet = CreateWallet.BIP39Singlesig();
Expand Down Expand Up @@ -329,6 +329,16 @@ public static void Initialize(IServiceProvider serviceProvider)
minerRPC.SendToAddress(singlesigAddress, singlesigFundCoins);
minerRPC.SendToAddress(singleSigBIP39Address, singleSigBIP39FundCoins);

// Create a lot of utxos and send them to the single sig wallet
// Random r = new Random();
// for (var i = 0; i < 1000; i++)
// {
// var keypath = nbxplorerClient.GetUnused(singlesigDerivationStrategy, DerivationFeature.Deposit);
// decimal coin = r.Next(536, 10000000);
// var randomCoint = Money.Coins(coin / 100000000); //20BTC
// minerRPC.SendToAddress(keypath.Address, randomCoint);
// }

//6 blocks to confirm
minerRPC.Generate(6);

Expand Down Expand Up @@ -368,7 +378,7 @@ public static void Initialize(IServiceProvider serviceProvider)
applicationDbContext.Add(testingSingleSigBIP39Wallet);
}
}

applicationDbContext.SaveChanges();
}

Expand Down
3 changes: 3 additions & 0 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 NBXPLORER_ENABLE_CUSTOM_BACKEND = false;

// Connections
public static readonly string POSTGRES_CONNECTIONSTRING = "Host=localhost;Port=5432;Database=fundsmanager;Username=rw_dev;Password=rw_dev";
Expand Down Expand Up @@ -102,6 +103,8 @@ static Constants()

ENABLE_HW_SUPPORT = Environment.GetEnvironmentVariable("ENABLE_HW_SUPPORT") != "false"; // We default to true

NBXPLORER_ENABLE_CUSTOM_BACKEND = Environment.GetEnvironmentVariable("NBXPLORER_ENABLE_CUSTOM_BACKEND") == "true";

// Connections
POSTGRES_CONNECTIONSTRING = Environment.GetEnvironmentVariable("POSTGRES_CONNECTIONSTRING") ?? POSTGRES_CONNECTIONSTRING;

Expand Down
10 changes: 6 additions & 4 deletions src/Helpers/ValidationHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,16 @@ public static void ValidateUsername(ValidatorEventArgs obj, List<ApplicationUser
public static void ValidateChannelCapacity(ValidatorEventArgs obj)
{
obj.Status = ValidationStatus.Success;
if ((long)obj.Value < Constants.MINIMUM_CHANNEL_CAPACITY_SATS)
var minimumChannelValue = new Money(Constants.MINIMUM_CHANNEL_CAPACITY_SATS).ToUnit(MoneyUnit.BTC);
var maxChannelRegtestValue = new Money(Constants.MAXIMUM_CHANNEL_CAPACITY_SATS_REGTEST).ToUnit(MoneyUnit.BTC);
if ((decimal)obj.Value < minimumChannelValue)
{
obj.ErrorText = "The amount selected must be greater than 20.000";
obj.ErrorText = $"The amount selected must be greater than {minimumChannelValue:f8} BTC";
obj.Status = ValidationStatus.Error;
}
else if ((long)obj.Value > Constants.MAXIMUM_CHANNEL_CAPACITY_SATS_REGTEST && network == Network.RegTest)
else if ((decimal)obj.Value > maxChannelRegtestValue && network == Network.RegTest)
{
obj.ErrorText = "The amount selected must be lower than 16.777.215";
obj.ErrorText = $"The amount selected must be lower than {maxChannelRegtestValue:f8} BTC";
obj.Status = ValidationStatus.Error;
}
}
Expand Down
20 changes: 11 additions & 9 deletions src/Pages/ChannelRequests.razor
Original file line number Diff line number Diff line change
Expand Up @@ -131,15 +131,15 @@
</DisplayTemplate>
<EditTemplate>
<Validation Validator="@ValidationHelper.ValidateChannelCapacity" @ref="_capacityValidation">
<NumericPicker TValue="long" @bind-Value="@_amount" CurrencySymbol=" SATS" CurrencySymbolPlacement="CurrencySymbolPlacement.Suffix" Disabled="@(SelectedUTXOs.Count > 0)">
<NumericPicker TValue="decimal" @bind-Value="@_amount" Min="0" Decimals="8" Step="0.00001m" CurrencySymbol=" BTC" CurrencySymbolPlacement="CurrencySymbolPlacement.Suffix" Disabled="@(SelectedUTXOs.Count > 0)">
<Feedback>
<ValidationError/>
</Feedback>
</NumericPicker>
<FieldHelp>
@{
@($"Amount in Satoshis. Minimum 20.000. Current amount: {Math.Round(PriceConversionHelper.SatToUsdConversion(_amount, _btcPrice), 2)} USD")
}

@($"Amount in Satoshis. Minimum {_minimumChannelCapacity:f8}. Current amount: {Math.Round(PriceConversionHelper.SatToUsdConversion(new Money(_amount, MoneyUnit.BTC).Satoshi, _btcPrice), 2)} USD")

</FieldHelp>
</Validation>
<div class="mb-3">
Expand Down Expand Up @@ -423,7 +423,8 @@
private Node? _selectedDestNode;
private int? _selectedWalletId;
private string? _destNodeName;
private long _amount = Constants.MINIMUM_CHANNEL_CAPACITY_SATS;
private static readonly decimal _minimumChannelCapacity = new Money(Constants.MINIMUM_CHANNEL_CAPACITY_SATS).ToUnit(MoneyUnit.BTC);
private decimal _amount { get; set; } = _minimumChannelCapacity;
private bool _selectedPrivate = false;

//Validation
Expand Down Expand Up @@ -475,7 +476,8 @@
_destNodeName = "";
_selectedDestNode = null;
_selectedPrivate = false;
_amount = Constants.MINIMUM_CHANNEL_CAPACITY_SATS;
_selectedWalletId = null;
_amount = _minimumChannelCapacity;
}

private async Task ResetChannelCancelModal()
Expand Down Expand Up @@ -578,7 +580,7 @@
var amount = SelectedUTXOs.Count > 0 ? SelectedUTXOsValue() : _amount;
var request = new ChannelOperationRequest()
{
SatsAmount = amount,
SatsAmount = new Money(amount, MoneyUnit.BTC).Satoshi,
RequestType = OperationRequestType.Open,
Description = "Created by user via Funds Manager",
WalletId = _selectedWalletId,
Expand Down Expand Up @@ -913,9 +915,9 @@
StateHasChanged();
}

private long SelectedUTXOsValue()
private decimal SelectedUTXOsValue()
{
return SelectedUTXOs.Sum(x => ((Money)x.Value).Satoshi);
return SelectedUTXOs.Sum(x => ((Money)x.Value).ToUnit(MoneyUnit.BTC));
}

private void ClearSelectedUTXOs()
Expand Down
2 changes: 1 addition & 1 deletion src/Pages/_Layout.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
<script src="https://cdn.onesignal.com/sdks/OneSignalSDK.js" async=""></script>
<script>
if ("@Constants.PUSH_NOTIFICATIONS_ONESIGNAL_ENABLED")
{
{
window.OneSignal = window.OneSignal || [];
OneSignal.push(function() {
OneSignal.init({
Expand Down
1 change: 1 addition & 0 deletions src/Properties/launchSettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"POSTGRES_CONNECTIONSTRING": "Host=127.0.0.1;Port=5432;Database=fundsmanager;Username=rw_dev;Password=rw_dev",
"BITCOIN_NETWORK": "REGTEST",
"MAXIMUM_WITHDRAWAL_BTC_AMOUNT": "21000000",
"NBXPLORER_ENABLE_CUSTOM_BACKEND": "true",
"NBXPLORER_URI": "http://127.0.0.1:32838",
"NBXPLORER_BTCRPCUSER": "polaruser",
"NBXPLORER_BTCRPCPASSWORD": "polarpass",
Expand Down
34 changes: 32 additions & 2 deletions src/Services/CoinSelectionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,17 @@ public interface ICoinSelectionService
/// <param name="derivationStrategy"></param>
public Task<List<UTXO>> GetAvailableUTXOsAsync(DerivationStrategyBase derivationStrategy);

/// <summary>
/// Gets the UTXOs for a wallet that are not locked in other transactions, but with a limit
/// </summary>
/// <param name="derivationStrategy"></param>
/// <param name="strategy"></param>
/// <param name="limit"></param>
/// <param name="amount"></param>
/// <param name="tolerance"></param>
/// <param name="closestTo"></param>
public Task<List<UTXO>> GetAvailableUTXOsAsync(DerivationStrategyBase derivationStrategy, CoinSelectionStrategy strategy, int limit, long amount, long closestTo);

/// <summary>
/// Locks the UTXOs for using in a specific transaction
/// </summary>
Expand Down Expand Up @@ -121,10 +132,9 @@ public async Task<List<UTXO>> GetLockedUTXOsForRequest(IBitcoinRequest bitcoinRe
return utxos.Confirmed.UTXOs.Where(utxo => lockedUTXOsList.Contains(utxo.Outpoint.Hash.ToString())).ToList();
}

public async Task<List<UTXO>> GetAvailableUTXOsAsync(DerivationStrategyBase derivationStrategy)
private async Task<List<UTXO>> FilterUnlockedUTXOs(UTXOChanges? utxoChanges)
{
var lockedUTXOs = await _fmutxoRepository.GetLockedUTXOs();
var utxoChanges = await _nbXplorerService.GetUTXOsAsync(derivationStrategy);
utxoChanges.RemoveDuplicateUTXOs();

var availableUTXOs = new List<UTXO>();
Expand All @@ -145,6 +155,26 @@ public async Task<List<UTXO>> GetAvailableUTXOsAsync(DerivationStrategyBase deri
return availableUTXOs;
}

public async Task<List<UTXO>> GetAvailableUTXOsAsync(DerivationStrategyBase derivationStrategy)
{
var utxoChanges = await _nbXplorerService.GetUTXOsAsync(derivationStrategy);
return await FilterUnlockedUTXOs(utxoChanges);
}

public async Task<List<UTXO>> GetAvailableUTXOsAsync(DerivationStrategyBase derivationStrategy, CoinSelectionStrategy strategy, int limit, long amount, long closestTo)
{
UTXOChanges utxoChanges;
if (Constants.NBXPLORER_ENABLE_CUSTOM_BACKEND)
{
utxoChanges = await _nbXplorerService.GetUTXOsByLimitAsync(derivationStrategy, strategy, limit, amount, closestTo);
}
else
{
utxoChanges = await _nbXplorerService.GetUTXOsAsync(derivationStrategy);
}
return await FilterUnlockedUTXOs(utxoChanges);
}

/// <summary>
/// Gets UTXOs confirmed from the wallet of the request
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion src/Services/LightningService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1200,7 +1200,7 @@ await foreach (var response in closeChannelResult.ResponseStream.ReadAllAsync())
}
catch (Exception e)
{
_logger.LogError(e, "Error while getting wallet balance for wallet: {WalletId}", wallet.Id);
_logger.LogError(e, "Error while getting wallet address for wallet: {WalletId}", wallet.Id);
}

var result = keyPathInformation?.Address ?? null;
Expand Down
53 changes: 53 additions & 0 deletions src/Services/NBXplorerService.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
using System.Text.Json;
using FundsManager.Helpers;
using Microsoft.AspNetCore.WebUtilities;
using NBitcoin;
using NBXplorer;
using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
using Newtonsoft.Json;

namespace FundsManager.Services;

Expand All @@ -23,6 +27,8 @@ public interface INBXplorerService

public Task<UTXOChanges> GetUTXOsAsync(DerivationStrategyBase extKey, CancellationToken cancellation = default);

public Task<UTXOChanges> GetUTXOsByLimitAsync(DerivationStrategyBase extKey, CoinSelectionStrategy strategy = CoinSelectionStrategy.SmallestFirst, int limit = 0, long amount = 0, long closestTo = 0, CancellationToken cancellation = default);

public Task<GetFeeRateResult> GetFeeRateAsync(int blockCount, FeeRate fallbackFeeRate,
CancellationToken cancellation = default);

Expand Down Expand Up @@ -63,6 +69,14 @@ public class MempoolRecommendedFees
public int MinimumFee { get; set; }
}

[JsonConverter(typeof(System.Text.Json.Serialization.JsonStringEnumConverter))]
public enum CoinSelectionStrategy
{
SmallestFirst,
BiggestFirst,
ClosestToTargetFirst
}

/// <summary>
/// Wrapper for the NBXplorer client to support DI
/// </summary>
Expand Down Expand Up @@ -130,6 +144,45 @@ public async Task TrackAsync(TrackedSource trackedSource, CancellationToken canc
return await client.GetUTXOsAsync(extKey, cancellation);
}

public async Task<UTXOChanges> GetUTXOsByLimitAsync(DerivationStrategyBase extKey,
CoinSelectionStrategy strategy = CoinSelectionStrategy.SmallestFirst,
int limit = 0,
long amount = 0,
long closestTo = 0,
CancellationToken cancellation = default)
{
try
{
var requestUri = $"{Constants.NBXPLORER_URI}/v1/cryptos/btc/derivations/{TrackedSource.Create(extKey).DerivationStrategy}/selectutxos";

IDictionary<string, string?> keyValuePairs = new Dictionary<string, string?>();
keyValuePairs.Add("strategy", strategy.ToString());
keyValuePairs.Add("limit", limit.ToString());
keyValuePairs.Add("amount", amount.ToString());
if (strategy == CoinSelectionStrategy.ClosestToTargetFirst)
{
keyValuePairs.Add("closestTo", closestTo.ToString());
}

var url = QueryHelpers.AddQueryString(requestUri, keyValuePairs);
var response = await _httpClient.GetAsync(url, cancellation);

if (response.IsSuccessStatusCode)
{
var client = await LightningHelper.CreateNBExplorerClient();

return client.Serializer.ToObject<UTXOChanges>(await response.Content.ReadAsStringAsync().ConfigureAwait(false));
}
}
catch (Exception e)
{
_logger.LogError(e.ToString());
throw e;
}

return new UTXOChanges();
}

public async Task<GetFeeRateResult> GetFeeRateAsync(int blockCount, FeeRate fallbackFeeRate,
CancellationToken cancellation = default)
{
Expand Down
Loading
Loading