Skip to content

Commit

Permalink
Unit tests for walletOperations, and cypress test (#97)
Browse files Browse the repository at this point in the history
* add test, and data-cy

* add invest project test

* test for not enough funds

funds are null

* temp commit

* fix test, and add test for UnspentOutputs

* add test for CalculateTransactionFee

not sure about the values, @dangershony can you check if the logic is ok?

* remove unwanted comments

* remove flaky line for now
  • Loading branch information
itailiors committed May 23, 2024
1 parent 4486df3 commit a9c63b6
Show file tree
Hide file tree
Showing 8 changed files with 357 additions and 15 deletions.
272 changes: 265 additions & 7 deletions src/Angor.Test/WalletOperationsTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
using Angor.Shared.Services;
using Money = Blockcore.NBitcoin.Money;
using uint256 = Blockcore.NBitcoin.uint256;
using Blockcore.Consensus.TransactionInfo;
using Blockcore.Networks;
using Microsoft.Extensions.Logging;
using Blockcore.NBitcoin.BIP32;

namespace Angor.Test;

Expand All @@ -21,6 +25,7 @@ public class WalletOperationsTest : AngorTestData
private readonly Mock<IIndexerService> _indexerService;
private readonly InvestorTransactionActions _investorTransactionActions;
private readonly FounderTransactionActions _founderTransactionActions;
private readonly IHdOperations _hdOperations;

public WalletOperationsTest()
{
Expand Down Expand Up @@ -58,15 +63,15 @@ private void AddCoins(AccountInfo accountInfo, int utxos, long amount)
});

int outputIndex = 0;
_indexerService.Setup(_ => _.FetchUtxoAsync(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>())).Returns<string,int, int>((address, limit, offset) =>
_indexerService.Setup(_ => _.FetchUtxoAsync(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>())).Returns<string, int, int>((address, limit, offset) =>
{
var res = new List<UtxoData>
{
new ()
{
address =address,
value = Money.Satoshis(amount).Satoshi,
outpoint = new Outpoint( uint256.Zero.ToString(),outputIndex++ ),
address =address,
value = Money.Satoshis(amount).Satoshi,
outpoint = new Outpoint( uint256.Zero.ToString(),outputIndex++ ),
scriptHex = new Blockcore.NBitcoin.BitcoinWitPubKeyAddress(address,network).ScriptPubKey.ToHex()
}
};
Expand All @@ -79,6 +84,20 @@ private void AddCoins(AccountInfo accountInfo, int utxos, long amount)
_sut.UpdateAccountInfoWithNewAddressesAsync(accountInfo).Wait();
}

private string GenerateScriptHex(string address, Network network)
{
try
{
var segwitAddress = new Blockcore.NBitcoin.BitcoinWitPubKeyAddress(address, network);
return segwitAddress.ScriptPubKey.ToHex();
}
catch (FormatException ex)
{
Console.WriteLine($"Error: Invalid address format. Details: {ex.Message}");
throw;
}
}

[Fact]
public void AddFeeAndSignTransaction_test()
{
Expand Down Expand Up @@ -137,7 +156,7 @@ public void AddInputsAndSignTransaction()
projectInfo.TargetAmount = 3;
projectInfo.StartDate = DateTime.UtcNow;
projectInfo.ExpiryDate = DateTime.UtcNow.AddDays(5);
projectInfo.PenaltyDays= 10;
projectInfo.PenaltyDays = 10;
projectInfo.Stages = new List<Stage>
{
new Stage { AmountToRelease = 1, ReleaseDate = DateTime.UtcNow.AddDays(1) },
Expand Down Expand Up @@ -183,10 +202,249 @@ public void AddInputsAndSignTransaction()
//add all utxos as coins (easier)
foreach (var utxo in accountInfo.AddressesInfo.Concat(accountInfo.ChangeAddressesInfo).SelectMany(s => s.UtxoData))
{
coins.Add(new Blockcore.NBitcoin.Coin(Blockcore.NBitcoin.uint256.Parse(utxo.outpoint.transactionId), (uint)utxo.outpoint.outputIndex,
new Money(utxo.value), Blockcore.Consensus.ScriptInfo.Script.FromHex(utxo.scriptHex))); //Adding fee inputs
coins.Add(new Blockcore.NBitcoin.Coin(Blockcore.NBitcoin.uint256.Parse(utxo.outpoint.transactionId), (uint)utxo.outpoint.outputIndex,
new Money(utxo.value), Blockcore.Consensus.ScriptInfo.Script.FromHex(utxo.scriptHex))); //Adding fee inputs
}

TransactionValidation.ThanTheTransactionHasNoErrors(signedRecoveryTransaction.Transaction, coins);
}

[Fact]
public void GenerateWalletWords_ReturnsCorrectFormat()
{
// Arrange
var walletOps = new WalletOperations(_indexerService.Object, _hdOperations, NullLogger<WalletOperations>.Instance, _networkConfiguration.Object);

// Act
var result = walletOps.GenerateWalletWords();

// Assert
Assert.NotNull(result);
Assert.Equal(12, result.Split(' ').Length); // Assuming a 12-word mnemonic
}

[Fact]

public async Task transaction_fails_due_to_insufficient_funds() // funds are null
{
// Arrange
var mockNetworkConfiguration = new Mock<INetworkConfiguration>();
var mockIndexerService = new Mock<IIndexerService>();
var mockHdOperations = new Mock<IHdOperations>();
var mockLogger = new Mock<ILogger<WalletOperations>>();
var network = _networkConfiguration.Object.GetNetwork();
mockNetworkConfiguration.Setup(x => x.GetNetwork()).Returns(network);
mockIndexerService.Setup(x => x.PublishTransactionAsync(It.IsAny<string>())).ReturnsAsync(string.Empty);

var walletOperations = new WalletOperations(mockIndexerService.Object, mockHdOperations.Object, mockLogger.Object, mockNetworkConfiguration.Object);

var words = new WalletWords { Words = "sorry poet adapt sister barely loud praise spray option oxygen hero surround" };
string address = "tb1qeu7wvxjg7ft4fzngsdxmv0pphdux2uthq4z679";
AccountInfo accountInfo = _sut.BuildAccountInfoForWalletWords(words);
string scriptHex = GenerateScriptHex(address, network);
var sendInfo = new SendInfo
{
SendAmount = 0.01m,
SendUtxos = new Dictionary<string, UtxoDataWithPath>
{
{
"key", new UtxoDataWithPath
{
UtxoData = new UtxoData
{
value = 500, // insufficient to cover the send amount and fees
address = address,
scriptHex = scriptHex,
outpoint = new Outpoint(), // Ensure Outpoint is also correctly initialized
blockIndex = 1,
PendingSpent = false
},
HdPath = "your_hd_path_here"
}
}
}
};

// Act & Assert
var exception = await Assert.ThrowsAsync<ApplicationException>(() => walletOperations.SendAmountToAddress(words, sendInfo));
Assert.Contains("not enough funds", exception.Message);
}




[Fact]
public async Task TransactionSucceeds_WithSufficientFundsWallet()
{
// Arrange
var mockNetworkConfiguration = new Mock<INetworkConfiguration>();
var mockIndexerService = new Mock<IIndexerService>();
var mockHdOperations = new Mock<IHdOperations>();
var mockLogger = new Mock<ILogger<WalletOperations>>();
var network = _networkConfiguration.Object.GetNetwork();
mockNetworkConfiguration.Setup(x => x.GetNetwork()).Returns(network);
mockHdOperations.Setup(x => x.GetAccountHdPath(It.IsAny<int>(), It.IsAny<int>(), It.IsAny<int>()))
.Returns("m/0/0");
var expectedExtendedKey = new ExtKey();
mockHdOperations.Setup(x => x.GetExtendedKey(It.IsAny<string>(), It.IsAny<string>())).Returns(expectedExtendedKey);

var walletOperations = new WalletOperations(mockIndexerService.Object, mockHdOperations.Object, mockLogger.Object, mockNetworkConfiguration.Object);

var words = new WalletWords { Words = "suspect lesson reduce catalog melt lucky decade harvest plastic output hello panel", Passphrase = "" };
string address = "tb1qeu7wvxjg7ft4fzngsdxmv0pphdux2uthq4z679";
string scriptHex = GenerateScriptHex(address, network);
var sendInfo = new SendInfo
{
SendToAddress = "tb1qw4vvm955kq5vrnx48m3x6kq8rlpgcauzzx63sr",
ChangeAddress = "tb1qw4vvm955kq5vrnx48m3x6kq8rlpgcauzzx63sr",
SendAmount = 0.01m,
SendUtxos = new Dictionary<string, UtxoDataWithPath>
{
{
"key", new UtxoDataWithPath
{
UtxoData = new UtxoData
{
value = 1500000, // Sufficient to cover the send amount and estimated fees
address = address,
scriptHex = scriptHex,
outpoint = new Outpoint("0000000000000000000000000000000000000000000000000000000000000000", 0),
blockIndex = 1,
PendingSpent = false
},
HdPath = "m/0/0"
}
}
}
};

// Act
var operationResult = await walletOperations.SendAmountToAddress(words, sendInfo);

// Assert
Assert.True(operationResult.Success);
Assert.NotNull(operationResult.Data); // ensure data is saved
}


[Fact]
public void GetUnspentOutputsForTransaction_ReturnsCorrectOutputs()
{
// Arrange
var mockHdOperations = new Mock<IHdOperations>();
var walletWords = new WalletWords { Words = "suspect lesson reduce catalog melt lucky decade harvest plastic output hello panel", Passphrase = "" };
var utxos = new List<UtxoDataWithPath>
{
new UtxoDataWithPath
{
UtxoData = new UtxoData
{
value = 1500000,
address = "tb1qeu7wvxjg7ft4fzngsdxmv0pphdux2uthq4z679",
scriptHex = "0014b7d165bb8b25f567f05c57d3b484159582ac2827",
outpoint = new Outpoint("0000000000000000000000000000000000000000000000000000000000000000", 0),
blockIndex = 1,
PendingSpent = false
},
HdPath = "m/0/0"
}
};

var expectedExtKey = new ExtKey();
mockHdOperations.Setup(x => x.GetExtendedKey(walletWords.Words, walletWords.Passphrase)).Returns(expectedExtKey);

var walletOperations = new WalletOperations(null, mockHdOperations.Object, null, null);

// Act
var (coins, keys) = walletOperations.GetUnspentOutputsForTransaction(walletWords, utxos);

// Assert
Assert.Single(coins);
Assert.Single(keys);
Assert.Equal((uint)0, coins[0].Outpoint.N);
Assert.Equal(1500000, coins[0].Amount.Satoshi);
Assert.Equal(expectedExtKey.Derive(new KeyPath("m/0/0")).PrivateKey, keys[0]);
}

[Fact]
public void CalculateTransactionFee_WithMultipleScenarios()
{
// Arrange common elements
var mockNetworkConfiguration = new Mock<INetworkConfiguration>();
var mockIndexerService = new Mock<IIndexerService>();
var mockHdOperations = new Mock<IHdOperations>();
var mockLogger = new Mock<ILogger<WalletOperations>>();
var network = _networkConfiguration.Object.GetNetwork();
mockNetworkConfiguration.Setup(x => x.GetNetwork()).Returns(network);

var walletOperations = new WalletOperations(mockIndexerService.Object, mockHdOperations.Object, mockLogger.Object, mockNetworkConfiguration.Object);

var words = new WalletWords { Words = "suspect lesson reduce catalog melt lucky decade harvest plastic output hello panel", Passphrase = "" };
var address = "tb1qeu7wvxjg7ft4fzngsdxmv0pphdux2uthq4z679";
var scriptHex = "0014b7d165bb8b25f567f05c57d3b484159582ac2827";
var accountInfo = new AccountInfo();
long feeRate = 10;

// Scenario 1: Sufficient funds
var sendInfoSufficientFunds = new SendInfo
{
SendToAddress = "tb1qw4vvm955kq5vrnx48m3x6kq8rlpgcauzzx63sr",
ChangeAddress = "tb1qw4vvm955kq5vrnx48m3x6kq8rlpgcauzzx63sr",
SendAmount = 0.0001m, // Lower amount for successful fee calculation
SendUtxos = new Dictionary<string, UtxoDataWithPath>
{
{
"key", new UtxoDataWithPath
{
UtxoData = new UtxoData
{
value = 150000000, // Sufficient to cover the send amount and estimated fees
address = address,
scriptHex = scriptHex,
outpoint = new Outpoint("0000000000000000000000000000000000000000000000000000000000000000", 0),
blockIndex = 1,
PendingSpent = false
},
HdPath = "m/0/0"
}
}
}
};

// Act & Assert for sufficient funds
var calculatedFeeSufficient = walletOperations.CalculateTransactionFee(sendInfoSufficientFunds, accountInfo, feeRate);
Assert.True(calculatedFeeSufficient > 0);
Assert.Equal(0.00000001m, calculatedFeeSufficient); // Assuming an expected fee for validation

// Scenario 2: Insufficient funds
var sendInfoInsufficientFunds = new SendInfo
{
SendToAddress = sendInfoSufficientFunds.SendToAddress,
ChangeAddress = sendInfoSufficientFunds.ChangeAddress,
SendAmount = 10000m, // High amount to trigger insufficient funds
SendUtxos = new Dictionary<string, UtxoDataWithPath>
{
{
"key", new UtxoDataWithPath
{
UtxoData = new UtxoData
{
value = 500, // low to ensure insufficient funds
address = address,
scriptHex = scriptHex,
outpoint = new Outpoint("0000000000000000000000000000000000000000000000000000000000000000", 0),
blockIndex = 1,
PendingSpent = false
},
HdPath = "m/0/0"
}
}
}
};

// Act & Assert for insufficient funds
var exception = Assert.Throws<Blockcore.Consensus.TransactionInfo.NotEnoughFundsException>(() => walletOperations.CalculateTransactionFee(sendInfoInsufficientFunds, accountInfo, feeRate));
Assert.Equal("Not enough funds to cover the target with missing amount 9999.99999500", exception.Message);
}

}
2 changes: 1 addition & 1 deletion src/Angor/Client/Pages/Invest.razor
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@
}
else
{
<span>Next</span>
<span data-cy="NEXT_BUTTON">Next</span>
}
</button>
</EditForm>
Expand Down
2 changes: 1 addition & 1 deletion src/Angor/Client/Pages/View.razor
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@
}
else
{
<button class="btn btn-primary" @onclick="InvestInProject">Invest</button>
<button class="btn btn-primary" data-cy="INVEST_BUTTON" @onclick="InvestInProject">Invest</button>
}
</div>
</div>
Expand Down
2 changes: 2 additions & 0 deletions src/Angor/Shared/WalletOperations.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Diagnostics;
using Angor.Shared.Models;
using Angor.Shared.Services;
using Blockcore.Consensus.ScriptInfo;
Expand Down Expand Up @@ -265,6 +266,7 @@ public List<UtxoDataWithPath> FindOutputsForTransaction(long sendAmountat, Accou
return (coins,keys);
}


public AccountInfo BuildAccountInfoForWalletWords(WalletWords walletWords)
{
ExtKey.UseBCForHMACSHA512 = true;
Expand Down

0 comments on commit a9c63b6

Please sign in to comment.