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

Add manually_frozen tag #393

Merged
merged 3 commits into from
Sep 5, 2024
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
1 change: 1 addition & 0 deletions src/Helpers/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ public class Constants
public static decimal MAX_TX_FEE_RATIO = 0.5m;

public const string IsFrozenTag = "frozen";
public const string IsManuallyFrozenTag = "manually_frozen";

// Constants for the NBXplorer API
public static int SCAN_GAP_LIMIT = 1000;
Expand Down
18 changes: 13 additions & 5 deletions src/Pages/Wallets.razor
Original file line number Diff line number Diff line change
Expand Up @@ -1154,8 +1154,16 @@
.Select((u) => (
u.Outpoint,
u,
tagsMap[u.Outpoint].Where(t => t.Key != Constants.IsFrozenTag).ToList(),
tagsMap[u.Outpoint].Any(t => t.Key == Constants.IsFrozenTag && t.Value == "true")
tagsMap[u.Outpoint]
.Where(t => t.Key != Constants.IsFrozenTag && t.Key != Constants.IsManuallyFrozenTag)
.ToList(),
// Check if the UTXO is frozen, has been manually frozen or has been manually unfrozen
(tagsMap[u.Outpoint]
.Any(t => t.Key == Constants.IsFrozenTag && t.Value == "true") ||
tagsMap[u.Outpoint]
.Any(t => t.Key == Constants.IsManuallyFrozenTag && t.Value == "true")) &&
!tagsMap[u.Outpoint]
.Any(t => t.Key == Constants.IsManuallyFrozenTag && t.Value == "false")
)).ToList();

_detailsTransactions = await NBXplorerService.GetTransactionsAsync(derivationStrategyBase);
Expand Down Expand Up @@ -1411,7 +1419,7 @@
var withdrawalRequest = new WalletWithdrawalRequest
{
UserRequestorId = LoggedUser != null ? LoggedUser.Id : string.Empty,
Description = @$"Funds transferred from {_sourceWalletName} to {_targetWalletName}",
Description = $"Funds transferred from {_sourceWalletName} to {_targetWalletName}",
WithdrawAllFunds = _transferAllFunds,
DestinationAddress = targetBitcoinAddress.ToString(),
MempoolRecommendedFeesType = MempoolRecommendedFeesType.HourFee,
Expand Down Expand Up @@ -1703,8 +1711,8 @@

private async Task ToggleUtxoFreeze(bool newValue, UTXO utxo)
{
var tag = await UTXOTagRepository.GetByKeyAndOutpoint(Constants.IsFrozenTag, utxo.Outpoint.ToString());
var (saved, _) = UTXOTagRepository.Update(new UTXOTag {Key = Constants.IsFrozenTag, Value = newValue ? "true" : "false", Outpoint = utxo.Outpoint.ToString(), Id = tag?.Id ?? 0 });
var tag = await UTXOTagRepository.GetByKeyAndOutpoint(Constants.IsManuallyFrozenTag, utxo.Outpoint.ToString());
var (saved, _) = UTXOTagRepository.Update(new UTXOTag {Key = Constants.IsManuallyFrozenTag, Value = newValue ? "true" : "false", Outpoint = utxo.Outpoint.ToString(), Id = tag?.Id ?? 0 });
if (!saved)
{
ToastService.ShowError("Error while updating the UTXO status");
Expand Down
4 changes: 1 addition & 3 deletions src/Rpc/NodeGuardService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -951,11 +951,9 @@ public override async Task<GetUtxosResponse> GetAvailableUtxos(GetAvailableUtxos
}

var lockedUtxos = await _fmutxoRepository.GetLockedUTXOs();
var frozenUtxos = await _utxoTagRepository.GetByKeyValue(Constants.IsFrozenTag, "true");

var ignoreOutpoints = new List<string>();
var listLocked = lockedUtxos.Select(utxo => $"{utxo.TxId}-{utxo.OutputIndex}").ToList();
var listFrozen = frozenUtxos.Select(utxo => utxo.Outpoint).ToList();
var listFrozen = await _coinSelectionService.GetFrozenUTXOs();
ignoreOutpoints.AddRange(listLocked);
ignoreOutpoints.AddRange(listFrozen);

Expand Down
27 changes: 25 additions & 2 deletions src/Services/CoinSelectionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ public interface ICoinSelectionService
/// <param name="bitcoinRequest"></param>
/// <param name="requestType"></param>
public Task<List<UTXO>> GetLockedUTXOsForRequest(IBitcoinRequest bitcoinRequest, BitcoinRequestType requestType);

/// <summary>
/// Gets the frozen UTXOs
/// </summary>
public Task<List<string>> GetFrozenUTXOs();

public Task<(List<ICoin> coins, List<UTXO> selectedUTXOs)> GetTxInputCoins(
List<UTXO> availableUTXOs,
Expand Down Expand Up @@ -146,9 +151,8 @@ public async Task<List<UTXO>> GetLockedUTXOsForRequest(IBitcoinRequest bitcoinRe
private async Task<List<UTXO>> FilterLockedFrozenUTXOs(UTXOChanges? utxoChanges)
{
var lockedUTXOs = await _fmutxoRepository.GetLockedUTXOs();
var frozenUTXOs = await _utxoTagRepository.GetByKeyValue(Constants.IsFrozenTag, "true");
var listLocked = lockedUTXOs.Select(utxo => $"{utxo.TxId}-{utxo.OutputIndex}").ToList();
var listFrozen = frozenUTXOs.Select(utxo => utxo.Outpoint).ToList();
var listFrozen = await GetFrozenUTXOs();
var frozenAndLockedOutpoints = new List<string>();
frozenAndLockedOutpoints.AddRange(listLocked);
frozenAndLockedOutpoints.AddRange(listFrozen);
Expand All @@ -171,6 +175,25 @@ private async Task<List<UTXO>> FilterLockedFrozenUTXOs(UTXOChanges? utxoChanges)

return availableUTXOs;
}

public async Task<List<string>> GetFrozenUTXOs()
{
var frozenUTXOs = await _utxoTagRepository.GetByKeyValue(Constants.IsFrozenTag, "true");
var manuallyFrozenUTXOs = await _utxoTagRepository.GetByKeyValue(Constants.IsManuallyFrozenTag, "true");
var manuallyUnfrozenUTXOs = await _utxoTagRepository.GetByKeyValue(Constants.IsManuallyFrozenTag, "false");
var listFrozen = frozenUTXOs.Select(utxo => utxo.Outpoint).ToList();
var listManuallyFrozen = manuallyFrozenUTXOs.Select(utxo => utxo.Outpoint).ToList();
var listManuallyUnfrozen = manuallyUnfrozenUTXOs.Select(utxo => utxo.Outpoint).ToList();

// Merge manually frozen and frozen UTXOs and remove manually unfrozen UTXOs
List<string> frozenUTXOsList =
listFrozen
.Union(listManuallyFrozen)
.Except(listManuallyUnfrozen)
.ToList();

return frozenUTXOsList;
}

public async Task<List<UTXO>> GetAvailableUTXOsAsync(DerivationStrategyBase derivationStrategy)
{
Expand Down
190 changes: 188 additions & 2 deletions test/NodeGuard.Tests/Services/BitcoinServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using NBitcoin;
using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
using NSubstitute;
using NSubstitute.Exceptions;
using Key = NodeGuard.Data.Models.Key;

Expand Down Expand Up @@ -402,7 +403,7 @@ async Task GenerateTemplatePSBT_SingleSigFailsFrozenUTXO()
.Setup(x => x.GetLockedUTXOs(null, null))
.ReturnsAsync(new List<FMUTXO>());
utxoTagRepository
.Setup(x => x.GetByKeyValue(It.IsAny<string>(), It.IsAny<string>()))
.SetupSequence(x => x.GetByKeyValue(It.IsAny<string>(), It.IsAny<string>()))
.ReturnsAsync(new List<UTXOTag>()
{
new UTXOTag()
Expand All @@ -411,7 +412,9 @@ async Task GenerateTemplatePSBT_SingleSigFailsFrozenUTXO()
Value = "true",
Outpoint = "00000000000000000000000000000000000000000000000000000000000004d2-1"
}
});
})
.ReturnsAsync(new List<UTXOTag>())
.ReturnsAsync(new List<UTXOTag>());

var coinSelectionService = new CoinSelectionService(_logger, mapper.Object, fmutxoRepository.Object, nbXplorerService.Object, null, walletWithdrawalRequestRepository.Object, utxoTagRepository.Object);

Expand All @@ -427,6 +430,189 @@ await act
.WithMessage("Exception of type 'NodeGuard.Helpers.NoUTXOsAvailableException' was thrown.");
}

[Fact]
async Task GenerateTemplatePSBT_SingleSigSuccessManuallyUnfrozenUTXO()
{
// Arrange
var wallet = CreateWallet.SingleSig(_internalWallet);
var withdrawalRequest = new WalletWithdrawalRequest()
{
Id = 1,
Status = WalletWithdrawalRequestStatus.Pending,
Wallet = wallet,
WalletWithdrawalRequestPSBTs = new List<WalletWithdrawalRequestPSBT>(),
Amount = 0.01m,
DestinationAddress = "bcrt1q8k3av6q5yp83rn332lx8a90k6kukhg28hs5qw7krdq95t629hgsqk6ztmf"
};

var walletWithdrawalRequestRepository = new Mock<IWalletWithdrawalRequestRepository>();
var walletWithdrawalRequestPsbtRepository = new Mock<IWalletWithdrawalRequestPsbtRepository>();
var fmutxoRepository = new Mock<IFMUTXORepository>();
var nbXplorerService = new Mock<INBXplorerService>();
var utxoTagRepository = new Mock<IUTXOTagRepository>();
var mapper = new Mock<IMapper>();
walletWithdrawalRequestRepository
.Setup((w) => w.GetById(It.IsAny<int>()))
.ReturnsAsync(withdrawalRequest);
walletWithdrawalRequestRepository
.Setup((w) => w.AddUTXOs(It.IsAny<WalletWithdrawalRequest>(), It.IsAny<List<FMUTXO>>()))
.ReturnsAsync((true, null));
walletWithdrawalRequestPsbtRepository
.Setup((w) => w.AddAsync(It.IsAny<WalletWithdrawalRequestPSBT>()))
.ReturnsAsync((true, null));
nbXplorerService
.Setup(x => x.GetStatusAsync(default))
.ReturnsAsync(new StatusResult() { IsFullySynched = true });
nbXplorerService
.Setup(x => x.GetUnusedAsync(It.IsAny<DerivationStrategyBase>(), DerivationFeature.Change, 0, false, default))
.ReturnsAsync(new KeyPathInformation() { Address = BitcoinAddress.Create("bcrt1q83ml8tve8vh672wsm83getxfzetaquq352jr6t423tdwjvdz3f3qe4r4t7", Network.RegTest) });
nbXplorerService
.Setup(x => x.GetUTXOsAsync(It.IsAny<DerivationStrategyBase>(), default))
.ReturnsAsync(new UTXOChanges()
{
Confirmed = new UTXOChange()
{
UTXOs = new List<UTXO>()
{
new UTXO()
{
Outpoint = new OutPoint(1234, 1),
Value = new Money((long)10000000),
ScriptPubKey = wallet.GetDerivationStrategy().GetDerivation(KeyPath.Parse("0/0")).ScriptPubKey,
KeyPath = KeyPath.Parse("0/0")
}
}
}
});
fmutxoRepository
.Setup(x => x.GetLockedUTXOs(null, null))
.ReturnsAsync(new List<FMUTXO>());
utxoTagRepository
.SetupSequence(x => x.GetByKeyValue(It.IsAny<string>(), It.IsAny<string>()))
.ReturnsAsync(new List<UTXOTag>()
{
new UTXOTag()
{
Key = Constants.IsFrozenTag,
Value = "false",
Outpoint = "00000000000000000000000000000000000000000000000000000000000004d2-1"
}
})
.ReturnsAsync(new List<UTXOTag>())
.ReturnsAsync(new List<UTXOTag>()
{
new UTXOTag()
{
Key = Constants.IsManuallyFrozenTag,
Value = "true",
Outpoint = "00000000000000000000000000000000000000000000000000000000000004d2-1"
}
});

var coinSelectionService = new CoinSelectionService(_logger, mapper.Object, fmutxoRepository.Object, nbXplorerService.Object, null, walletWithdrawalRequestRepository.Object, utxoTagRepository.Object);

var bitcoinService = new BitcoinService(_logger, mapper.Object, walletWithdrawalRequestRepository.Object, walletWithdrawalRequestPsbtRepository.Object, null, null, nbXplorerService.Object, coinSelectionService);

// Act
var result = await bitcoinService.GenerateTemplatePSBT(withdrawalRequest);

// Assert
var psbt = PSBT.Parse("cHNidP8BAIkBAAAAAdIEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAD/////AkBCDwAAAAAAIgAgPaPWaBQgTxHOMVfMfpX21blroUe8KAd6w2gLRelFuiCsUYkAAAAAACIAIDx3862ZOy+vKdDZ4oysyRZX0HARoqQ9LqqK2ukxoopiAAAAAE8BBDWHzwN9uUaNAAAAAYPR/OiA1LbTzxbLPvbXvtAwckIG3g+0T1zblR/ZodaiA5zBFsigPpL8htN/KJ/Ph8SPvQA/K+mSNXTSA0hgvPNuEO0CEMgwAACAAQAAgAEAAAAAAQEfgJaYAAAAAAAWABTpOvUBMqNMfl7P81etji6x4fXrMyIGA3uD9HVjgF5E+eQhHp+Na6femVYpc4bCA4DmimehAdWcGO0CEMgwAACAAQAAgAEAAAAAAAAAAAAAAAAAAA==", Network.RegTest);
result.Should().BeEquivalentTo(psbt);
}

[Fact]
async Task GenerateTemplatePSBT_SingleSigFailsManuallyFrozenUTXO()
{
// Arrange
var wallet = CreateWallet.SingleSig(_internalWallet);
var withdrawalRequest = new WalletWithdrawalRequest()
{
Id = 1,
Status = WalletWithdrawalRequestStatus.Pending,
Wallet = wallet,
WalletWithdrawalRequestPSBTs = new List<WalletWithdrawalRequestPSBT>(),
Amount = 0.01m,
DestinationAddress = "bcrt1q8k3av6q5yp83rn332lx8a90k6kukhg28hs5qw7krdq95t629hgsqk6ztmf"
};

var walletWithdrawalRequestRepository = new Mock<IWalletWithdrawalRequestRepository>();
var walletWithdrawalRequestPsbtRepository = new Mock<IWalletWithdrawalRequestPsbtRepository>();
var fmutxoRepository = new Mock<IFMUTXORepository>();
var nbXplorerService = new Mock<INBXplorerService>();
var utxoTagRepository = new Mock<IUTXOTagRepository>();
var mapper = new Mock<IMapper>();
walletWithdrawalRequestRepository
.Setup((w) => w.GetById(It.IsAny<int>()))
.ReturnsAsync(withdrawalRequest);
walletWithdrawalRequestRepository
.Setup((w) => w.AddUTXOs(It.IsAny<WalletWithdrawalRequest>(), It.IsAny<List<FMUTXO>>()))
.ReturnsAsync((true, null));
walletWithdrawalRequestPsbtRepository
.Setup((w) => w.AddAsync(It.IsAny<WalletWithdrawalRequestPSBT>()))
.ReturnsAsync((true, null));
nbXplorerService
.Setup(x => x.GetStatusAsync(default))
.ReturnsAsync(new StatusResult() { IsFullySynched = true });
nbXplorerService
.Setup(x => x.GetUnusedAsync(It.IsAny<DerivationStrategyBase>(), DerivationFeature.Change, 0, false, default))
.ReturnsAsync(new KeyPathInformation() { Address = BitcoinAddress.Create("bcrt1q83ml8tve8vh672wsm83getxfzetaquq352jr6t423tdwjvdz3f3qe4r4t7", Network.RegTest) });
nbXplorerService
.Setup(x => x.GetUTXOsAsync(It.IsAny<DerivationStrategyBase>(), default))
.ReturnsAsync(new UTXOChanges()
{
Confirmed = new UTXOChange()
{
UTXOs = new List<UTXO>()
{
new UTXO()
{
Outpoint = new OutPoint(1234, 1),
Value = new Money((long)10000000),
ScriptPubKey = wallet.GetDerivationStrategy().GetDerivation(KeyPath.Parse("0/0")).ScriptPubKey,
KeyPath = KeyPath.Parse("0/0")
}
}
}
});
fmutxoRepository
.Setup(x => x.GetLockedUTXOs(null, null))
.ReturnsAsync(new List<FMUTXO>());
utxoTagRepository
.SetupSequence(x => x.GetByKeyValue(It.IsAny<string>(), It.IsAny<string>()))
.ReturnsAsync(new List<UTXOTag>()
{
new UTXOTag()
{
Key = Constants.IsFrozenTag,
Value = "false",
Outpoint = "00000000000000000000000000000000000000000000000000000000000004d2-1"
}
})
.ReturnsAsync(new List<UTXOTag>()
{
new UTXOTag()
{
Key = Constants.IsManuallyFrozenTag,
Value = "true",
Outpoint = "00000000000000000000000000000000000000000000000000000000000004d2-1"
}
})
.ReturnsAsync(new List<UTXOTag>());
var coinSelectionService = new CoinSelectionService(_logger, mapper.Object, fmutxoRepository.Object, nbXplorerService.Object, null, walletWithdrawalRequestRepository.Object, utxoTagRepository.Object);

var bitcoinService = new BitcoinService(_logger, mapper.Object, walletWithdrawalRequestRepository.Object, walletWithdrawalRequestPsbtRepository.Object, null, null, nbXplorerService.Object, coinSelectionService);

// Act
var act = () => bitcoinService.GenerateTemplatePSBT(withdrawalRequest);

// Assert
await act
.Should()
.ThrowAsync<NoUTXOsAvailableException>()
.WithMessage("Exception of type 'NodeGuard.Helpers.NoUTXOsAvailableException' was thrown.");
}

[Fact]
async Task GenerateTemplatePSBT_Changeless_SingleSigSucceeds()
{
Expand Down
Loading