diff --git a/Directory.Packages.props b/Directory.Packages.props
index 3594807..c3068dc 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -5,17 +5,17 @@
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
+
diff --git a/README.md b/README.md
index c5e857d..3c21558 100644
--- a/README.md
+++ b/README.md
@@ -7,9 +7,11 @@ A .NET 9 library that provides comprehensive utilities and services for working
- **HD Wallet Management**: Generate and restore hierarchical deterministic wallets
- **Balance Services**: Query native token and ERC20/ERC721 token balances
- **Transaction Services**: Send and track blockchain transactions
+- **Transaction Status**: Detect the correct lifecycle state of a transaction (pending, confirmed, reverted, replaced, dropped)
- **Gas Estimation**: Estimate gas costs for various transaction types
- **Transfer Services**: Simplified token transfer operations (native, ERC20, ERC721)
- **Block Services**: Query blockchain block information
+- **Network Health**: Check blockchain network and RPC endpoint health before sending transactions
- **Web3 Provider**: Easy Web3 instance creation with various configurations
## Prerequisites
@@ -272,6 +274,64 @@ var blockService = serviceProvider.GetRequiredService();
var latestBlock = await blockService.GetLastBlockAsync(web3, cancellationToken);
```
+#### Transaction Status Service
+
+Detect the lifecycle state of a transaction:
+
+```csharp
+using BlockChainTools.Interfaces;
+using BlockChainTools.DataTransferObjects;
+
+var txStatusService = serviceProvider.GetRequiredService();
+
+// Check the state of a transaction by hash
+var stateInfo = await txStatusService.GetTransactionStateAsync(
+ web3,
+ "0xTransactionHash...",
+ cancellationToken
+);
+
+// stateInfo.State can be:
+// TransactionState.Pending – known to node but not mined
+// TransactionState.ConfirmedSuccess – receipt exists, status == 1
+// TransactionState.ConfirmedReverted– receipt exists, status == 0 (on-chain revert)
+// TransactionState.Replaced – another tx with the same nonce was mined
+// TransactionState.StalePending – node no longer knows the tx (likely dropped)
+
+if (stateInfo.State == TransactionState.ConfirmedSuccess)
+{
+ Console.WriteLine($"Mined in block {stateInfo.BlockNumber}");
+}
+```
+
+#### Network Health Service
+
+Check blockchain network health before sending transactions:
+
+```csharp
+using BlockChainTools.Interfaces;
+using Nethereum.Signer;
+
+var healthService = serviceProvider.GetRequiredService();
+
+// Full health check with expected chain and freshness threshold
+var health = await healthService.CheckNetworkHealthAsync(
+ web3,
+ expectedChain: Chain.Polygon,
+ maxBlockAgeSeconds: 120,
+ cancellationToken
+);
+
+if (!health.IsHealthy)
+{
+ foreach (var issue in health.Issues)
+ Console.WriteLine($"Issue: {issue}");
+}
+
+// Quick check without chain ID validation
+var quickHealth = await healthService.CheckNetworkHealthAsync(web3);
+```
+
## Available Services
### IWeb3ProviderService
@@ -308,9 +368,15 @@ var latestBlock = await blockService.GetLastBlockAsync(web3, cancellationToken);
### ITransactionService
- `GetTransactionByHashAsync(Web3, string hash)` - Get transaction details
+### ITransactionStatusService
+- `GetTransactionStateAsync(Web3, string transactionHash)` - Determine the lifecycle state of a transaction (Pending, ConfirmedSuccess, ConfirmedReverted, Replaced, StalePending)
+
### IBlockService
- `GetLastBlockAsync(Web3)` - Get latest block number
+### INetworkHealthService
+- `CheckNetworkHealthAsync(Web3, Chain? expectedChain, int maxBlockAgeSeconds)` - Check network health (block freshness, gas price availability, chain ID match)
+
## Project Structure
```
@@ -323,7 +389,9 @@ BlockChainTools/
│ │ ├── BlockService.cs
│ │ ├── GasService.cs
│ │ ├── HdWalletService.cs
+│ │ ├── NetworkHealthService.cs
│ │ ├── TransactionService.cs
+│ │ ├── TransactionStatusService.cs
│ │ ├── TransferService.cs
│ │ └── Web3ProviderService.cs
│ ├── Interfaces/ # Service contracts
@@ -334,12 +402,12 @@ BlockChainTools/
## Dependencies
-- **Nethereum.Web3** (5.0.0) - Ethereum Web3 implementation
-- **Nethereum.Accounts** (5.0.0) - Account management
-- **Nethereum.Signer** (5.0.0) - Transaction signing
-- **Nethereum.HdWallet** (5.0.0) - HD wallet support
-- **Microsoft.Extensions.DependencyInjection** (9.0.10) - DI support
-- **Tricksfor.DistributedNonce** (9.0.0) - Distributed nonce management
+- **Nethereum.Web3** (6.0.4) - Ethereum Web3 implementation
+- **Nethereum.Accounts** (6.0.4) - Account management
+- **Nethereum.Signer** (6.0.4) - Transaction signing
+- **Nethereum.HdWallet** (6.0.4) - HD wallet support
+- **Microsoft.Extensions.DependencyInjection** (10.0.5) - DI support
+- **Tricksfor.DistributedNonce** (10.0.2) - Distributed nonce management
## Testing
diff --git a/src/BlockChainTools/Configuration.cs b/src/BlockChainTools/Configuration.cs
index f7678cc..0478ecd 100644
--- a/src/BlockChainTools/Configuration.cs
+++ b/src/BlockChainTools/Configuration.cs
@@ -16,6 +16,8 @@ public static void AddBlockChainTools(this IServiceCollection services)
services.AddScoped();
services.AddScoped();
services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
services.AddDistributedNonce();
}
diff --git a/src/BlockChainTools/DataTransferObjects/NetworkHealthInfo.cs b/src/BlockChainTools/DataTransferObjects/NetworkHealthInfo.cs
new file mode 100644
index 0000000..612f221
--- /dev/null
+++ b/src/BlockChainTools/DataTransferObjects/NetworkHealthInfo.cs
@@ -0,0 +1,56 @@
+using System.Numerics;
+
+namespace BlockChainTools.DataTransferObjects;
+
+///
+/// Contains the results of a blockchain network health check.
+///
+public class NetworkHealthInfo
+{
+ ///
+ /// Whether the network is considered healthy overall.
+ /// True when blocks are advancing, timestamps are fresh, gas estimates succeed, and chain ID matches.
+ ///
+ public required bool IsHealthy { get; init; }
+
+ ///
+ /// The latest block number reported by the RPC endpoint.
+ ///
+ public BigInteger? LatestBlockNumber { get; init; }
+
+ ///
+ /// The timestamp of the latest block as a UTC DateTime, if available.
+ ///
+ public DateTime? LatestBlockTimestamp { get; init; }
+
+ ///
+ /// Whether the latest block timestamp is within the acceptable freshness threshold.
+ ///
+ public bool IsBlockTimestampFresh { get; init; }
+
+ ///
+ /// The current gas price returned by the node, if available.
+ ///
+ public BigInteger? GasPrice { get; init; }
+
+ ///
+ /// Whether a gas price was successfully retrieved and is non-zero.
+ ///
+ public bool IsGasPriceAvailable { get; init; }
+
+ ///
+ /// The chain ID returned by the RPC endpoint, if available.
+ ///
+ public BigInteger? ChainId { get; init; }
+
+ ///
+ /// Whether the returned chain ID matches the expected chain ID (when an expected value was provided).
+ ///
+ public bool ChainIdMatches { get; init; }
+
+ ///
+ /// Human-readable summary of any issues detected during the health check.
+ /// Empty when fully healthy.
+ ///
+ public required IReadOnlyList Issues { get; init; }
+}
diff --git a/src/BlockChainTools/DataTransferObjects/TransactionState.cs b/src/BlockChainTools/DataTransferObjects/TransactionState.cs
new file mode 100644
index 0000000..47948a2
--- /dev/null
+++ b/src/BlockChainTools/DataTransferObjects/TransactionState.cs
@@ -0,0 +1,35 @@
+namespace BlockChainTools.DataTransferObjects;
+
+///
+/// Represents the lifecycle state of an EVM transaction.
+///
+public enum TransactionState
+{
+ ///
+ /// The transaction is known to the node (returned by eth_getTransactionByHash)
+ /// but has not yet been mined (no receipt).
+ ///
+ Pending,
+
+ ///
+ /// A receipt exists and receipt.Status == 1 — the transaction executed successfully on-chain.
+ ///
+ ConfirmedSuccess,
+
+ ///
+ /// A receipt exists and receipt.Status == 0 — the transaction was mined but reverted on-chain.
+ ///
+ ConfirmedReverted,
+
+ ///
+ /// Another transaction with the same nonce from the same sender has been mined,
+ /// meaning this transaction was replaced or cancelled.
+ ///
+ Replaced,
+
+ ///
+ /// No receipt exists and the node no longer returns the transaction,
+ /// indicating it was likely dropped from the mempool.
+ ///
+ StalePending
+}
diff --git a/src/BlockChainTools/DataTransferObjects/TransactionStateInfo.cs b/src/BlockChainTools/DataTransferObjects/TransactionStateInfo.cs
new file mode 100644
index 0000000..2fb8a65
--- /dev/null
+++ b/src/BlockChainTools/DataTransferObjects/TransactionStateInfo.cs
@@ -0,0 +1,39 @@
+using System.Numerics;
+
+namespace BlockChainTools.DataTransferObjects;
+
+///
+/// Contains the resolved state of a transaction along with supporting metadata.
+///
+public class TransactionStateInfo
+{
+ ///
+ /// The resolved lifecycle state of the transaction.
+ ///
+ public required TransactionState State { get; init; }
+
+ ///
+ /// The transaction hash that was queried.
+ ///
+ public required string TransactionHash { get; init; }
+
+ ///
+ /// The receipt status value (1 = success, 0 = revert) when a receipt exists; otherwise null.
+ ///
+ public BigInteger? ReceiptStatus { get; init; }
+
+ ///
+ /// The block number in which the transaction was mined, if available.
+ ///
+ public BigInteger? BlockNumber { get; init; }
+
+ ///
+ /// The nonce of the transaction, if the transaction data was available from the node.
+ ///
+ public BigInteger? Nonce { get; init; }
+
+ ///
+ /// The confirmed on-chain nonce for the sender address, if checked.
+ ///
+ public BigInteger? ConfirmedNonce { get; init; }
+}
diff --git a/src/BlockChainTools/Interfaces/INetworkHealthService.cs b/src/BlockChainTools/Interfaces/INetworkHealthService.cs
new file mode 100644
index 0000000..2cb2cee
--- /dev/null
+++ b/src/BlockChainTools/Interfaces/INetworkHealthService.cs
@@ -0,0 +1,25 @@
+using BlockChainTools.DataTransferObjects;
+using Nethereum.Signer;
+using Nethereum.Web3;
+
+namespace BlockChainTools.Interfaces;
+
+public interface INetworkHealthService
+{
+ ///
+ /// Performs a health check on the blockchain network via the given Web3 endpoint.
+ /// Verifies that blocks are being produced, timestamps are fresh, gas price is available,
+ /// and the chain ID matches expectations.
+ ///
+ /// The Web3 instance to query against.
+ ///
+ /// Optional expected chain. When provided, the health check verifies the endpoint returns the correct chain ID.
+ ///
+ ///
+ /// Maximum acceptable age (in seconds) of the latest block timestamp.
+ /// Defaults to 120 seconds. If the latest block is older than this, the network is considered unhealthy.
+ ///
+ /// Cancellation token.
+ /// A describing the health status of the network.
+ Task CheckNetworkHealthAsync(Web3 web3, Chain? expectedChain = null, int maxBlockAgeSeconds = 120, CancellationToken cancellationToken = default);
+}
diff --git a/src/BlockChainTools/Interfaces/ITransactionStatusService.cs b/src/BlockChainTools/Interfaces/ITransactionStatusService.cs
new file mode 100644
index 0000000..917ba37
--- /dev/null
+++ b/src/BlockChainTools/Interfaces/ITransactionStatusService.cs
@@ -0,0 +1,19 @@
+using BlockChainTools.DataTransferObjects;
+using Nethereum.Web3;
+
+namespace BlockChainTools.Interfaces;
+
+public interface ITransactionStatusService
+{
+ ///
+ /// Determines the current lifecycle state of a transaction by checking its receipt and mempool presence.
+ /// Uses receipt status as the canonical success/failure signal when a receipt exists.
+ /// When no receipt is found, checks whether the transaction is still known to the node
+ /// and whether the sender's on-chain nonce has advanced past the transaction nonce (indicating replacement).
+ ///
+ /// The Web3 instance to query against.
+ /// The hash of the transaction to check.
+ /// Cancellation token.
+ /// A describing the current state and metadata of the transaction.
+ Task GetTransactionStateAsync(Web3 web3, string transactionHash, CancellationToken cancellationToken = default);
+}
diff --git a/src/BlockChainTools/README.md b/src/BlockChainTools/README.md
index d69d37f..b6aa5fd 100644
--- a/src/BlockChainTools/README.md
+++ b/src/BlockChainTools/README.md
@@ -9,6 +9,8 @@ A .NET 9 library built on Nethereum that provides ready-to-use services for comm
- **Gas Estimation** - Estimate gas costs for various transaction types
- **Transfer Services** - Simplified transfers for native tokens, ERC20, and ERC721 (NFTs)
- **Transaction & Block Services** - Query blockchain data
+- **Transaction Status** - Detect lifecycle state of transactions (pending, confirmed, reverted, replaced, dropped)
+- **Network Health** - Check blockchain network and RPC endpoint health
- **Web3 Provider** - Easy Web3 instance creation with distributed nonce support
## Installation
@@ -87,7 +89,9 @@ var txHash = await transferService.TransferAsync(web3Tx, txInput);
- **ITransferService** - Transfer native tokens, ERC20, and ERC721 (NFTs)
- **IHdWalletService** - Generate/restore HD wallets and derive accounts
- **ITransactionService** - Query transaction details
+- **ITransactionStatusService** - Detect transaction lifecycle state (pending, confirmed, reverted, replaced, dropped)
- **IBlockService** - Query blockchain block information
+- **INetworkHealthService** - Check network health (block freshness, gas price, chain ID)
## Distributed Nonce Management
diff --git a/src/BlockChainTools/Services/NetworkHealthService.cs b/src/BlockChainTools/Services/NetworkHealthService.cs
new file mode 100644
index 0000000..f132060
--- /dev/null
+++ b/src/BlockChainTools/Services/NetworkHealthService.cs
@@ -0,0 +1,112 @@
+using System.Numerics;
+using BlockChainTools.DataTransferObjects;
+using BlockChainTools.Interfaces;
+using Nethereum.Hex.HexTypes;
+using Nethereum.Signer;
+using Nethereum.Web3;
+
+namespace BlockChainTools.Services;
+
+public class NetworkHealthService : INetworkHealthService
+{
+ public async Task CheckNetworkHealthAsync(Web3 web3, Chain? expectedChain = null, int maxBlockAgeSeconds = 120, CancellationToken cancellationToken = default)
+ {
+ var issues = new List();
+ BigInteger? latestBlockNumber = null;
+ DateTime? latestBlockTimestamp = null;
+ bool isBlockTimestampFresh = false;
+ BigInteger? gasPrice = null;
+ bool isGasPriceAvailable = false;
+ BigInteger? chainId = null;
+ bool chainIdMatches = true;
+
+ // 1. Check latest block number and timestamp
+ try
+ {
+ var block = await web3.Eth.Blocks.GetBlockWithTransactionsHashesByNumber.SendRequestAsync(
+ Nethereum.RPC.Eth.DTOs.BlockParameter.CreateLatest());
+
+ if (block is not null)
+ {
+ latestBlockNumber = block.Number?.Value;
+ (latestBlockTimestamp, isBlockTimestampFresh) = EvaluateBlockFreshness(block.Timestamp, maxBlockAgeSeconds);
+
+ if (latestBlockTimestamp is null)
+ issues.Add("Latest block has no timestamp.");
+ else if (!isBlockTimestampFresh)
+ issues.Add($"Latest block timestamp is {(DateTime.UtcNow - latestBlockTimestamp.Value).TotalSeconds:F0}s old, exceeding the {maxBlockAgeSeconds}s threshold.");
+ }
+ else
+ {
+ issues.Add("Unable to retrieve the latest block.");
+ }
+ }
+ catch (Exception ex)
+ {
+ issues.Add($"Failed to retrieve latest block: {ex.Message}");
+ }
+
+ // 2. Check gas price
+ try
+ {
+ var gasPriceResult = await web3.Eth.GasPrice.SendRequestAsync();
+ gasPrice = gasPriceResult?.Value;
+ isGasPriceAvailable = gasPrice is not null && gasPrice > 0;
+
+ if (!isGasPriceAvailable)
+ {
+ issues.Add("Gas price is unavailable or zero.");
+ }
+ }
+ catch (Exception ex)
+ {
+ issues.Add($"Failed to retrieve gas price: {ex.Message}");
+ }
+
+ // 3. Check chain ID
+ try
+ {
+ var chainIdResult = await web3.Eth.ChainId.SendRequestAsync();
+ chainId = chainIdResult?.Value;
+
+ if (expectedChain is not null)
+ {
+ chainIdMatches = chainId == (int)expectedChain;
+ if (!chainIdMatches)
+ {
+ issues.Add($"Chain ID mismatch: expected {(int)expectedChain}, got {chainId}.");
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ chainIdMatches = false;
+ issues.Add($"Failed to retrieve chain ID: {ex.Message}");
+ }
+
+ var isHealthy = issues.Count == 0;
+
+ return new NetworkHealthInfo
+ {
+ IsHealthy = isHealthy,
+ LatestBlockNumber = latestBlockNumber,
+ LatestBlockTimestamp = latestBlockTimestamp,
+ IsBlockTimestampFresh = isBlockTimestampFresh,
+ GasPrice = gasPrice,
+ IsGasPriceAvailable = isGasPriceAvailable,
+ ChainId = chainId,
+ ChainIdMatches = chainIdMatches,
+ Issues = issues.AsReadOnly()
+ };
+ }
+
+ private static (DateTime? Timestamp, bool IsFresh) EvaluateBlockFreshness(HexBigInteger? blockTimestamp, int maxBlockAgeSeconds)
+ {
+ if (blockTimestamp?.Value is null)
+ return (null, false);
+
+ var timestamp = DateTimeOffset.FromUnixTimeSeconds((long)blockTimestamp.Value).UtcDateTime;
+ var isFresh = (DateTime.UtcNow - timestamp).TotalSeconds <= maxBlockAgeSeconds;
+ return (timestamp, isFresh);
+ }
+}
diff --git a/src/BlockChainTools/Services/TransactionStatusService.cs b/src/BlockChainTools/Services/TransactionStatusService.cs
new file mode 100644
index 0000000..61596f7
--- /dev/null
+++ b/src/BlockChainTools/Services/TransactionStatusService.cs
@@ -0,0 +1,81 @@
+using BlockChainTools.DataTransferObjects;
+using BlockChainTools.Interfaces;
+using Nethereum.RPC.Eth.DTOs;
+using Nethereum.Web3;
+
+namespace BlockChainTools.Services;
+
+public class TransactionStatusService : ITransactionStatusService
+{
+ public async Task GetTransactionStateAsync(Web3 web3, string transactionHash, CancellationToken cancellationToken = default)
+ {
+ // 1. Try to get the receipt — this is the canonical success/failure signal
+ TransactionReceipt? receipt = await web3.Eth.Transactions.GetTransactionReceipt.SendRequestAsync(transactionHash);
+
+ if (receipt is not null)
+ {
+ var succeeded = receipt.Status?.Value == 1;
+ return new TransactionStateInfo
+ {
+ State = succeeded ? TransactionState.ConfirmedSuccess : TransactionState.ConfirmedReverted,
+ TransactionHash = transactionHash,
+ ReceiptStatus = receipt.Status?.Value,
+ BlockNumber = receipt.BlockNumber?.Value,
+ Nonce = null,
+ ConfirmedNonce = null
+ };
+ }
+
+ // 2. No receipt — check if the node still knows about the transaction
+ var transaction = await web3.Eth.Transactions.GetTransactionByHash.SendRequestAsync(transactionHash);
+
+ if (transaction is not null)
+ {
+ // Transaction exists in the node but is not yet mined — check for replacement
+ var senderAddress = transaction.From;
+ var txNonce = transaction.Nonce?.Value;
+
+ if (senderAddress is not null && txNonce is not null)
+ {
+ // Get the confirmed (mined) nonce for the sender
+ var confirmedNonce = await web3.Eth.Transactions.GetTransactionCount.SendRequestAsync(
+ senderAddress, BlockParameter.CreateLatest());
+
+ if (confirmedNonce?.Value > txNonce)
+ {
+ // On-chain nonce has advanced past this tx's nonce — it was replaced
+ return new TransactionStateInfo
+ {
+ State = TransactionState.Replaced,
+ TransactionHash = transactionHash,
+ ReceiptStatus = null,
+ BlockNumber = null,
+ Nonce = txNonce,
+ ConfirmedNonce = confirmedNonce.Value
+ };
+ }
+ }
+
+ return new TransactionStateInfo
+ {
+ State = TransactionState.Pending,
+ TransactionHash = transactionHash,
+ ReceiptStatus = null,
+ BlockNumber = null,
+ Nonce = txNonce,
+ ConfirmedNonce = null
+ };
+ }
+
+ // 3. Node does not know the transaction and no receipt — likely dropped
+ return new TransactionStateInfo
+ {
+ State = TransactionState.StalePending,
+ TransactionHash = transactionHash,
+ ReceiptStatus = null,
+ BlockNumber = null,
+ Nonce = null,
+ ConfirmedNonce = null
+ };
+ }
+}
diff --git a/tests/BlockChainTools.Tests/NetworkHealthServiceTests.cs b/tests/BlockChainTools.Tests/NetworkHealthServiceTests.cs
new file mode 100644
index 0000000..dace814
--- /dev/null
+++ b/tests/BlockChainTools.Tests/NetworkHealthServiceTests.cs
@@ -0,0 +1,156 @@
+using System.Numerics;
+using BlockChainTools.DataTransferObjects;
+using BlockChainTools.Interfaces;
+using Nethereum.Signer;
+using Nethereum.Web3;
+using Nethereum.JsonRpc.Client;
+using NUnit.Framework;
+using NSubstitute;
+
+namespace BlockChainTools.Tests;
+
+[TestFixture]
+public class NetworkHealthServiceTests
+{
+ private INetworkHealthService? _service;
+ private Web3? _web3;
+
+ [SetUp]
+ public void SetUp()
+ {
+ _service = Substitute.For();
+ _web3 = new Web3(Substitute.For());
+ }
+
+ [Test]
+ public async Task CheckNetworkHealthAsync_Healthy_ReturnsHealthy()
+ {
+ var expected = new NetworkHealthInfo
+ {
+ IsHealthy = true,
+ LatestBlockNumber = new BigInteger(50000),
+ LatestBlockTimestamp = DateTime.UtcNow,
+ IsBlockTimestampFresh = true,
+ GasPrice = new BigInteger(30000000000),
+ IsGasPriceAvailable = true,
+ ChainId = new BigInteger(137),
+ ChainIdMatches = true,
+ Issues = []
+ };
+ _service!.CheckNetworkHealthAsync(_web3!, Chain.Polygon, 120, Arg.Any()).Returns(expected);
+ var result = await _service.CheckNetworkHealthAsync(_web3!, Chain.Polygon, 120);
+ Assert.That(result.IsHealthy, Is.True);
+ Assert.That(result.IsBlockTimestampFresh, Is.True);
+ Assert.That(result.IsGasPriceAvailable, Is.True);
+ Assert.That(result.ChainIdMatches, Is.True);
+ Assert.That(result.Issues, Is.Empty);
+ }
+
+ [Test]
+ public async Task CheckNetworkHealthAsync_StaleBlock_ReturnsUnhealthy()
+ {
+ var expected = new NetworkHealthInfo
+ {
+ IsHealthy = false,
+ LatestBlockNumber = new BigInteger(50000),
+ LatestBlockTimestamp = DateTime.UtcNow.AddMinutes(-10),
+ IsBlockTimestampFresh = false,
+ GasPrice = new BigInteger(30000000000),
+ IsGasPriceAvailable = true,
+ ChainId = new BigInteger(137),
+ ChainIdMatches = true,
+ Issues = ["Latest block timestamp is 600s old, exceeding the 120s threshold."]
+ };
+ _service!.CheckNetworkHealthAsync(_web3!, Chain.Polygon, 120, Arg.Any()).Returns(expected);
+ var result = await _service.CheckNetworkHealthAsync(_web3!, Chain.Polygon, 120);
+ Assert.That(result.IsHealthy, Is.False);
+ Assert.That(result.IsBlockTimestampFresh, Is.False);
+ Assert.That(result.Issues, Has.Count.EqualTo(1));
+ }
+
+ [Test]
+ public async Task CheckNetworkHealthAsync_ChainIdMismatch_ReturnsUnhealthy()
+ {
+ var expected = new NetworkHealthInfo
+ {
+ IsHealthy = false,
+ LatestBlockNumber = new BigInteger(50000),
+ LatestBlockTimestamp = DateTime.UtcNow,
+ IsBlockTimestampFresh = true,
+ GasPrice = new BigInteger(30000000000),
+ IsGasPriceAvailable = true,
+ ChainId = new BigInteger(1),
+ ChainIdMatches = false,
+ Issues = ["Chain ID mismatch: expected 137, got 1."]
+ };
+ _service!.CheckNetworkHealthAsync(_web3!, Chain.Polygon, 120, Arg.Any()).Returns(expected);
+ var result = await _service.CheckNetworkHealthAsync(_web3!, Chain.Polygon, 120);
+ Assert.That(result.IsHealthy, Is.False);
+ Assert.That(result.ChainIdMatches, Is.False);
+ Assert.That(result.Issues, Has.Count.EqualTo(1));
+ }
+
+ [Test]
+ public async Task CheckNetworkHealthAsync_NoGasPrice_ReturnsUnhealthy()
+ {
+ var expected = new NetworkHealthInfo
+ {
+ IsHealthy = false,
+ LatestBlockNumber = new BigInteger(50000),
+ LatestBlockTimestamp = DateTime.UtcNow,
+ IsBlockTimestampFresh = true,
+ GasPrice = null,
+ IsGasPriceAvailable = false,
+ ChainId = new BigInteger(137),
+ ChainIdMatches = true,
+ Issues = ["Gas price is unavailable or zero."]
+ };
+ _service!.CheckNetworkHealthAsync(_web3!, Chain.Polygon, 120, Arg.Any()).Returns(expected);
+ var result = await _service.CheckNetworkHealthAsync(_web3!, Chain.Polygon, 120);
+ Assert.That(result.IsHealthy, Is.False);
+ Assert.That(result.IsGasPriceAvailable, Is.False);
+ Assert.That(result.Issues, Has.Count.EqualTo(1));
+ }
+
+ [Test]
+ public async Task CheckNetworkHealthAsync_WithoutExpectedChainId_SkipsChainIdCheck()
+ {
+ var expected = new NetworkHealthInfo
+ {
+ IsHealthy = true,
+ LatestBlockNumber = new BigInteger(50000),
+ LatestBlockTimestamp = DateTime.UtcNow,
+ IsBlockTimestampFresh = true,
+ GasPrice = new BigInteger(30000000000),
+ IsGasPriceAvailable = true,
+ ChainId = new BigInteger(137),
+ ChainIdMatches = true,
+ Issues = []
+ };
+ _service!.CheckNetworkHealthAsync(_web3!, null, 120, Arg.Any()).Returns(expected);
+ var result = await _service.CheckNetworkHealthAsync(_web3!, null, 120);
+ Assert.That(result.IsHealthy, Is.True);
+ Assert.That(result.ChainIdMatches, Is.True);
+ }
+
+ [Test]
+ public async Task CheckNetworkHealthAsync_MultipleIssues_ReportsAll()
+ {
+ var expected = new NetworkHealthInfo
+ {
+ IsHealthy = false,
+ LatestBlockNumber = null,
+ LatestBlockTimestamp = null,
+ IsBlockTimestampFresh = false,
+ GasPrice = null,
+ IsGasPriceAvailable = false,
+ ChainId = null,
+ ChainIdMatches = false,
+ Issues = ["Unable to retrieve the latest block.", "Gas price is unavailable or zero.", "Failed to retrieve chain ID: timeout"]
+ };
+ _service!.CheckNetworkHealthAsync(_web3!, Chain.Polygon, 120, Arg.Any()).Returns(expected);
+ var result = await _service.CheckNetworkHealthAsync(_web3!, Chain.Polygon, 120);
+ Assert.That(result.IsHealthy, Is.False);
+ Assert.That(result.Issues, Has.Count.EqualTo(3));
+ }
+}
diff --git a/tests/BlockChainTools.Tests/TransactionStatusServiceTests.cs b/tests/BlockChainTools.Tests/TransactionStatusServiceTests.cs
new file mode 100644
index 0000000..d0f33f2
--- /dev/null
+++ b/tests/BlockChainTools.Tests/TransactionStatusServiceTests.cs
@@ -0,0 +1,110 @@
+using System.Numerics;
+using BlockChainTools.DataTransferObjects;
+using BlockChainTools.Interfaces;
+using Nethereum.Web3;
+using Nethereum.JsonRpc.Client;
+using NUnit.Framework;
+using NSubstitute;
+
+namespace BlockChainTools.Tests;
+
+[TestFixture]
+public class TransactionStatusServiceTests
+{
+ private ITransactionStatusService? _service;
+ private Web3? _web3;
+
+ [SetUp]
+ public void SetUp()
+ {
+ _service = Substitute.For();
+ _web3 = new Web3(Substitute.For());
+ }
+
+ [Test]
+ public async Task GetTransactionStateAsync_ConfirmedSuccess_ReturnsCorrectState()
+ {
+ var expected = new TransactionStateInfo
+ {
+ State = TransactionState.ConfirmedSuccess,
+ TransactionHash = "0xabc",
+ ReceiptStatus = BigInteger.One,
+ BlockNumber = new BigInteger(100)
+ };
+ _service!.GetTransactionStateAsync(_web3!, "0xabc", Arg.Any()).Returns(expected);
+ var result = await _service.GetTransactionStateAsync(_web3!, "0xabc");
+ Assert.That(result.State, Is.EqualTo(TransactionState.ConfirmedSuccess));
+ Assert.That(result.ReceiptStatus, Is.EqualTo(BigInteger.One));
+ Assert.That(result.BlockNumber, Is.EqualTo(new BigInteger(100)));
+ Assert.That(result.TransactionHash, Is.EqualTo("0xabc"));
+ }
+
+ [Test]
+ public async Task GetTransactionStateAsync_ConfirmedReverted_ReturnsCorrectState()
+ {
+ var expected = new TransactionStateInfo
+ {
+ State = TransactionState.ConfirmedReverted,
+ TransactionHash = "0xdef",
+ ReceiptStatus = BigInteger.Zero,
+ BlockNumber = new BigInteger(200)
+ };
+ _service!.GetTransactionStateAsync(_web3!, "0xdef", Arg.Any()).Returns(expected);
+ var result = await _service.GetTransactionStateAsync(_web3!, "0xdef");
+ Assert.That(result.State, Is.EqualTo(TransactionState.ConfirmedReverted));
+ Assert.That(result.ReceiptStatus, Is.EqualTo(BigInteger.Zero));
+ }
+
+ [Test]
+ public async Task GetTransactionStateAsync_Pending_ReturnsCorrectState()
+ {
+ var expected = new TransactionStateInfo
+ {
+ State = TransactionState.Pending,
+ TransactionHash = "0xpending",
+ Nonce = new BigInteger(5)
+ };
+ _service!.GetTransactionStateAsync(_web3!, "0xpending", Arg.Any()).Returns(expected);
+ var result = await _service.GetTransactionStateAsync(_web3!, "0xpending");
+ Assert.That(result.State, Is.EqualTo(TransactionState.Pending));
+ Assert.That(result.ReceiptStatus, Is.Null);
+ Assert.That(result.BlockNumber, Is.Null);
+ Assert.That(result.Nonce, Is.EqualTo(new BigInteger(5)));
+ }
+
+ [Test]
+ public async Task GetTransactionStateAsync_Replaced_ReturnsCorrectState()
+ {
+ var expected = new TransactionStateInfo
+ {
+ State = TransactionState.Replaced,
+ TransactionHash = "0xreplaced",
+ Nonce = new BigInteger(3),
+ ConfirmedNonce = new BigInteger(5)
+ };
+ _service!.GetTransactionStateAsync(_web3!, "0xreplaced", Arg.Any()).Returns(expected);
+ var result = await _service.GetTransactionStateAsync(_web3!, "0xreplaced");
+ Assert.That(result.State, Is.EqualTo(TransactionState.Replaced));
+ Assert.That(result.Nonce, Is.Not.Null);
+ Assert.That(result.ConfirmedNonce, Is.Not.Null);
+ Assert.That(result.ConfirmedNonce!.Value, Is.GreaterThan(result.Nonce!.Value));
+ }
+
+ [Test]
+ public async Task GetTransactionStateAsync_StalePending_ReturnsCorrectState()
+ {
+ var expected = new TransactionStateInfo
+ {
+ State = TransactionState.StalePending,
+ TransactionHash = "0xstale"
+ };
+ _service!.GetTransactionStateAsync(_web3!, "0xstale", Arg.Any()).Returns(expected);
+ var result = await _service.GetTransactionStateAsync(_web3!, "0xstale");
+ Assert.That(result.State, Is.EqualTo(TransactionState.StalePending));
+ Assert.That(result.ReceiptStatus, Is.Null);
+ Assert.That(result.BlockNumber, Is.Null);
+ Assert.That(result.Nonce, Is.Null);
+ Assert.That(result.ConfirmedNonce, Is.Null);
+ }
+
+}