From 6147b6f952f7fcde997984633e77502eaa7d0d28 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Mar 2026 12:16:47 +0000 Subject: [PATCH 1/7] Initial plan From d2bb36abbda70cafc7b4826b4a6d06f8dfce0ed5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Mar 2026 12:24:34 +0000 Subject: [PATCH 2/7] Add transaction state detection and network health check services - Add TransactionState enum (Broadcasted, Pending, ConfirmedSuccess, ConfirmedReverted, Replaced, StalePending) - Add TransactionStateInfo DTO with state and metadata - Add ITransactionStatusService interface with GetTransactionStateAsync - Add TransactionStatusService using receipt polling and mempool/nonce checks - Add NetworkHealthInfo DTO for health check results - Add INetworkHealthService interface with CheckNetworkHealthAsync - Add NetworkHealthService checking block freshness, gas price, chain ID - Register new services in Configuration.cs - Add unit tests for both new services (12 new tests) Co-authored-by: Phelecks <47243363+Phelecks@users.noreply.github.com> Agent-Logs-Url: https://github.com/Tricksfor-Organization/BlockChainTools/sessions/33c42eb9-2ed0-4d7f-a7ce-51a18903f68c --- src/BlockChainTools/Configuration.cs | 2 + .../DataTransferObjects/NetworkHealthInfo.cs | 56 +++++++ .../DataTransferObjects/TransactionState.cs | 40 +++++ .../TransactionStateInfo.cs | 39 +++++ .../Interfaces/INetworkHealthService.cs | 25 +++ .../Interfaces/ITransactionStatusService.cs | 19 +++ .../Services/NetworkHealthService.cs | 109 ++++++++++++ .../Services/TransactionStatusService.cs | 82 +++++++++ .../NetworkHealthServiceTests.cs | 155 ++++++++++++++++++ .../TransactionStatusServiceTests.cs | 122 ++++++++++++++ 10 files changed, 649 insertions(+) create mode 100644 src/BlockChainTools/DataTransferObjects/NetworkHealthInfo.cs create mode 100644 src/BlockChainTools/DataTransferObjects/TransactionState.cs create mode 100644 src/BlockChainTools/DataTransferObjects/TransactionStateInfo.cs create mode 100644 src/BlockChainTools/Interfaces/INetworkHealthService.cs create mode 100644 src/BlockChainTools/Interfaces/ITransactionStatusService.cs create mode 100644 src/BlockChainTools/Services/NetworkHealthService.cs create mode 100644 src/BlockChainTools/Services/TransactionStatusService.cs create mode 100644 tests/BlockChainTools.Tests/NetworkHealthServiceTests.cs create mode 100644 tests/BlockChainTools.Tests/TransactionStatusServiceTests.cs 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..ee540eb --- /dev/null +++ b/src/BlockChainTools/DataTransferObjects/TransactionState.cs @@ -0,0 +1,40 @@ +namespace BlockChainTools.DataTransferObjects; + +/// +/// Represents the lifecycle state of an EVM transaction. +/// +public enum TransactionState +{ + /// + /// A transaction hash was obtained but no further information is available yet. + /// + Broadcasted, + + /// + /// 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..1184d34 --- /dev/null +++ b/src/BlockChainTools/Interfaces/INetworkHealthService.cs @@ -0,0 +1,25 @@ +using System.Numerics; +using BlockChainTools.DataTransferObjects; +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 ID. When provided, the health check verifies the endpoint returns the correct chain. + /// + /// + /// 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, BigInteger? expectedChainId = 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/Services/NetworkHealthService.cs b/src/BlockChainTools/Services/NetworkHealthService.cs new file mode 100644 index 0000000..b7d988a --- /dev/null +++ b/src/BlockChainTools/Services/NetworkHealthService.cs @@ -0,0 +1,109 @@ +using System.Numerics; +using BlockChainTools.DataTransferObjects; +using BlockChainTools.Interfaces; +using Nethereum.Web3; + +namespace BlockChainTools.Services; + +public class NetworkHealthService : INetworkHealthService +{ + public async Task CheckNetworkHealthAsync(Web3 web3, BigInteger? expectedChainId = 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.GetBlockWithTransactionsByNumber.SendRequestAsync( + Nethereum.RPC.Eth.DTOs.BlockParameter.CreateLatest()); + + if (block is not null) + { + latestBlockNumber = block.Number?.Value; + if (block.Timestamp?.Value is not null) + { + latestBlockTimestamp = DateTimeOffset.FromUnixTimeSeconds((long)block.Timestamp.Value).UtcDateTime; + var age = DateTime.UtcNow - latestBlockTimestamp.Value; + isBlockTimestampFresh = age.TotalSeconds <= maxBlockAgeSeconds; + + if (!isBlockTimestampFresh) + { + issues.Add($"Latest block timestamp is {age.TotalSeconds:F0}s old, exceeding the {maxBlockAgeSeconds}s threshold."); + } + } + else + { + issues.Add("Latest block has no timestamp."); + } + } + 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 (expectedChainId is not null) + { + chainIdMatches = chainId == expectedChainId; + if (!chainIdMatches) + { + issues.Add($"Chain ID mismatch: expected {expectedChainId}, 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() + }; + } +} diff --git a/src/BlockChainTools/Services/TransactionStatusService.cs b/src/BlockChainTools/Services/TransactionStatusService.cs new file mode 100644 index 0000000..9401b55 --- /dev/null +++ b/src/BlockChainTools/Services/TransactionStatusService.cs @@ -0,0 +1,82 @@ +using BlockChainTools.DataTransferObjects; +using BlockChainTools.Interfaces; +using Nethereum.Hex.HexTypes; +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..a0d380d --- /dev/null +++ b/tests/BlockChainTools.Tests/NetworkHealthServiceTests.cs @@ -0,0 +1,155 @@ +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 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!, new BigInteger(137), 120, Arg.Any()).Returns(expected); + var result = await _service.CheckNetworkHealthAsync(_web3!, new BigInteger(137), 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!, new BigInteger(137), 120, Arg.Any()).Returns(expected); + var result = await _service.CheckNetworkHealthAsync(_web3!, new BigInteger(137), 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!, new BigInteger(137), 120, Arg.Any()).Returns(expected); + var result = await _service.CheckNetworkHealthAsync(_web3!, new BigInteger(137), 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!, new BigInteger(137), 120, Arg.Any()).Returns(expected); + var result = await _service.CheckNetworkHealthAsync(_web3!, new BigInteger(137), 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!, new BigInteger(137), 120, Arg.Any()).Returns(expected); + var result = await _service.CheckNetworkHealthAsync(_web3!, new BigInteger(137), 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..9bb0c8c --- /dev/null +++ b/tests/BlockChainTools.Tests/TransactionStatusServiceTests.cs @@ -0,0 +1,122 @@ +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); + } + + [Test] + public async Task GetTransactionStateAsync_Broadcasted_ReturnsCorrectState() + { + var expected = new TransactionStateInfo + { + State = TransactionState.Broadcasted, + TransactionHash = "0xbroadcast" + }; + _service!.GetTransactionStateAsync(_web3!, "0xbroadcast", Arg.Any()).Returns(expected); + var result = await _service.GetTransactionStateAsync(_web3!, "0xbroadcast"); + Assert.That(result.State, Is.EqualTo(TransactionState.Broadcasted)); + } +} From ee487883c0209f83f0a9c754ecbe9f1201949ba8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Mar 2026 13:21:41 +0000 Subject: [PATCH 3/7] plan: fix test file and upgrade NuGet packages Co-authored-by: Phelecks <47243363+Phelecks@users.noreply.github.com> Agent-Logs-Url: https://github.com/Tricksfor-Organization/BlockChainTools/sessions/24dee835-ccb3-4b1c-b35e-2092ffefd0c1 --- .../Interfaces/INetworkHealthService.cs | 8 ++++---- .../Services/NetworkHealthService.cs | 4 +++- .../NetworkHealthServiceTests.cs | 19 +++++++++---------- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/BlockChainTools/Interfaces/INetworkHealthService.cs b/src/BlockChainTools/Interfaces/INetworkHealthService.cs index 1184d34..2cb2cee 100644 --- a/src/BlockChainTools/Interfaces/INetworkHealthService.cs +++ b/src/BlockChainTools/Interfaces/INetworkHealthService.cs @@ -1,5 +1,5 @@ -using System.Numerics; using BlockChainTools.DataTransferObjects; +using Nethereum.Signer; using Nethereum.Web3; namespace BlockChainTools.Interfaces; @@ -12,8 +12,8 @@ public interface INetworkHealthService /// and the chain ID matches expectations. /// /// The Web3 instance to query against. - /// - /// Optional expected chain ID. When provided, the health check verifies the endpoint returns the correct chain. + /// + /// 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. @@ -21,5 +21,5 @@ public interface INetworkHealthService /// /// Cancellation token. /// A describing the health status of the network. - Task CheckNetworkHealthAsync(Web3 web3, BigInteger? expectedChainId = null, int maxBlockAgeSeconds = 120, CancellationToken cancellationToken = default); + Task CheckNetworkHealthAsync(Web3 web3, Chain? expectedChain = null, int maxBlockAgeSeconds = 120, CancellationToken cancellationToken = default); } diff --git a/src/BlockChainTools/Services/NetworkHealthService.cs b/src/BlockChainTools/Services/NetworkHealthService.cs index b7d988a..7a20dea 100644 --- a/src/BlockChainTools/Services/NetworkHealthService.cs +++ b/src/BlockChainTools/Services/NetworkHealthService.cs @@ -1,14 +1,16 @@ using System.Numerics; using BlockChainTools.DataTransferObjects; using BlockChainTools.Interfaces; +using Nethereum.Signer; using Nethereum.Web3; namespace BlockChainTools.Services; public class NetworkHealthService : INetworkHealthService { - public async Task CheckNetworkHealthAsync(Web3 web3, BigInteger? expectedChainId = null, int maxBlockAgeSeconds = 120, CancellationToken cancellationToken = default) + public async Task CheckNetworkHealthAsync(Web3 web3, Chain? expectedChain = null, int maxBlockAgeSeconds = 120, CancellationToken cancellationToken = default) { + BigInteger? expectedChainId = expectedChain is not null ? (int)expectedChain : null; var issues = new List(); BigInteger? latestBlockNumber = null; DateTime? latestBlockTimestamp = null; diff --git a/tests/BlockChainTools.Tests/NetworkHealthServiceTests.cs b/tests/BlockChainTools.Tests/NetworkHealthServiceTests.cs index a0d380d..b6dcb64 100644 --- a/tests/BlockChainTools.Tests/NetworkHealthServiceTests.cs +++ b/tests/BlockChainTools.Tests/NetworkHealthServiceTests.cs @@ -1,6 +1,7 @@ using System.Numerics; using BlockChainTools.DataTransferObjects; using BlockChainTools.Interfaces; +using Nethereum.Signer; using Nethereum.Web3; using Nethereum.JsonRpc.Client; using NUnit.Framework; @@ -36,8 +37,8 @@ public async Task CheckNetworkHealthAsync_Healthy_ReturnsHealthy() ChainIdMatches = true, Issues = [] }; - _service!.CheckNetworkHealthAsync(_web3!, new BigInteger(137), 120, Arg.Any()).Returns(expected); - var result = await _service.CheckNetworkHealthAsync(_web3!, new BigInteger(137), 120); + _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); @@ -60,8 +61,8 @@ public async Task CheckNetworkHealthAsync_StaleBlock_ReturnsUnhealthy() ChainIdMatches = true, Issues = ["Latest block timestamp is 600s old, exceeding the 120s threshold."] }; - _service!.CheckNetworkHealthAsync(_web3!, new BigInteger(137), 120, Arg.Any()).Returns(expected); - var result = await _service.CheckNetworkHealthAsync(_web3!, new BigInteger(137), 120); + _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)); @@ -82,8 +83,8 @@ public async Task CheckNetworkHealthAsync_ChainIdMismatch_ReturnsUnhealthy() ChainIdMatches = false, Issues = ["Chain ID mismatch: expected 137, got 1."] }; - _service!.CheckNetworkHealthAsync(_web3!, new BigInteger(137), 120, Arg.Any()).Returns(expected); - var result = await _service.CheckNetworkHealthAsync(_web3!, new BigInteger(137), 120); + _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)); @@ -104,10 +105,8 @@ public async Task CheckNetworkHealthAsync_NoGasPrice_ReturnsUnhealthy() ChainIdMatches = true, Issues = ["Gas price is unavailable or zero."] }; - _service!.CheckNetworkHealthAsync(_web3!, new BigInteger(137), 120, Arg.Any()).Returns(expected); - var result = await _service.CheckNetworkHealthAsync(_web3!, new BigInteger(137), 120); - Assert.That(result.IsHealthy, Is.False); - Assert.That(result.IsGasPriceAvailable, Is.False); + _service!.CheckNetworkHealthAsync(_web3!, Chain.Polygon, 120, Arg.Any()).Returns(expected); + var result = await _service.CheckNetworkHealthAsync(_web3!, Chain.Polygon, 120); Assert.That(result.Issues, Has.Count.EqualTo(1)); } From b00d03edbd733247e9e6f6faba39e2fd1ae7bb9d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Mar 2026 13:25:50 +0000 Subject: [PATCH 4/7] Use Chain enum instead of BigInteger for expectedChainId, upgrade all NuGet packages, update READMEs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Change INetworkHealthService parameter from BigInteger? to Chain? - Upgrade Nethereum.* 5.8.0 → 6.0.4 - Upgrade Microsoft.Extensions.DependencyInjection 10.0.1 → 10.0.5 - Upgrade NUnit 4.4.0 → 4.5.1, NUnit3TestAdapter 6.1.0 → 6.2.0 - Upgrade Microsoft.NET.Test.Sdk 18.0.1 → 18.3.0 - Upgrade Testcontainers 4.10.0 → 4.11.0 - Upgrade Aspire.StackExchange.Redis.DistributedCaching 13.1.0 → 13.1.3 - Update both README.md files with new services and correct dependency versions Co-authored-by: Phelecks <47243363+Phelecks@users.noreply.github.com> Agent-Logs-Url: https://github.com/Tricksfor-Organization/BlockChainTools/sessions/24dee835-ccb3-4b1c-b35e-2092ffefd0c1 --- Directory.Packages.props | 20 ++--- README.md | 81 +++++++++++++++++-- src/BlockChainTools/README.md | 4 + .../NetworkHealthServiceTests.cs | 6 +- 4 files changed, 93 insertions(+), 18 deletions(-) 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..adc79fc 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,65 @@ 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.Broadcasted – tx hash obtained, no further info yet +// 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 +369,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 (Broadcasted, 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 +390,9 @@ BlockChainTools/ │ │ ├── BlockService.cs │ │ ├── GasService.cs │ │ ├── HdWalletService.cs +│ │ ├── NetworkHealthService.cs │ │ ├── TransactionService.cs +│ │ ├── TransactionStatusService.cs │ │ ├── TransferService.cs │ │ └── Web3ProviderService.cs │ ├── Interfaces/ # Service contracts @@ -334,12 +403,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/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/tests/BlockChainTools.Tests/NetworkHealthServiceTests.cs b/tests/BlockChainTools.Tests/NetworkHealthServiceTests.cs index b6dcb64..dace814 100644 --- a/tests/BlockChainTools.Tests/NetworkHealthServiceTests.cs +++ b/tests/BlockChainTools.Tests/NetworkHealthServiceTests.cs @@ -107,6 +107,8 @@ public async Task CheckNetworkHealthAsync_NoGasPrice_ReturnsUnhealthy() }; _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)); } @@ -146,8 +148,8 @@ public async Task CheckNetworkHealthAsync_MultipleIssues_ReportsAll() ChainIdMatches = false, Issues = ["Unable to retrieve the latest block.", "Gas price is unavailable or zero.", "Failed to retrieve chain ID: timeout"] }; - _service!.CheckNetworkHealthAsync(_web3!, new BigInteger(137), 120, Arg.Any()).Returns(expected); - var result = await _service.CheckNetworkHealthAsync(_web3!, new BigInteger(137), 120); + _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)); } From 4ac40b39a72f83b92b361f2ff96756cd79d8ca3d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Mar 2026 14:06:20 +0000 Subject: [PATCH 5/7] Remove unreachable TransactionState.Broadcasted, refactor CheckNetworkHealthAsync to reduce cognitive complexity - Remove Broadcasted from TransactionState enum (never returned by GetTransactionStateAsync) - Remove Broadcasted test case - Extract EvaluateBlockFreshness helper; inline Chain cast to eliminate ternary - Update README to reflect actual returned states Co-authored-by: Phelecks <47243363+Phelecks@users.noreply.github.com> Agent-Logs-Url: https://github.com/Tricksfor-Organization/BlockChainTools/sessions/1d8b147d-a32f-4525-84c8-d0ff31b40763 --- README.md | 3 +- .../DataTransferObjects/TransactionState.cs | 5 --- .../Services/NetworkHealthService.cs | 35 ++++++++++--------- .../TransactionStatusServiceTests.cs | 12 ------- 4 files changed, 19 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index adc79fc..3c21558 100644 --- a/README.md +++ b/README.md @@ -292,7 +292,6 @@ var stateInfo = await txStatusService.GetTransactionStateAsync( ); // stateInfo.State can be: -// TransactionState.Broadcasted – tx hash obtained, no further info yet // TransactionState.Pending – known to node but not mined // TransactionState.ConfirmedSuccess – receipt exists, status == 1 // TransactionState.ConfirmedReverted– receipt exists, status == 0 (on-chain revert) @@ -370,7 +369,7 @@ var quickHealth = await healthService.CheckNetworkHealthAsync(web3); - `GetTransactionByHashAsync(Web3, string hash)` - Get transaction details ### ITransactionStatusService -- `GetTransactionStateAsync(Web3, string transactionHash)` - Determine the lifecycle state of a transaction (Broadcasted, Pending, ConfirmedSuccess, ConfirmedReverted, Replaced, StalePending) +- `GetTransactionStateAsync(Web3, string transactionHash)` - Determine the lifecycle state of a transaction (Pending, ConfirmedSuccess, ConfirmedReverted, Replaced, StalePending) ### IBlockService - `GetLastBlockAsync(Web3)` - Get latest block number diff --git a/src/BlockChainTools/DataTransferObjects/TransactionState.cs b/src/BlockChainTools/DataTransferObjects/TransactionState.cs index ee540eb..47948a2 100644 --- a/src/BlockChainTools/DataTransferObjects/TransactionState.cs +++ b/src/BlockChainTools/DataTransferObjects/TransactionState.cs @@ -5,11 +5,6 @@ namespace BlockChainTools.DataTransferObjects; /// public enum TransactionState { - /// - /// A transaction hash was obtained but no further information is available yet. - /// - Broadcasted, - /// /// The transaction is known to the node (returned by eth_getTransactionByHash) /// but has not yet been mined (no receipt). diff --git a/src/BlockChainTools/Services/NetworkHealthService.cs b/src/BlockChainTools/Services/NetworkHealthService.cs index 7a20dea..83bb54d 100644 --- a/src/BlockChainTools/Services/NetworkHealthService.cs +++ b/src/BlockChainTools/Services/NetworkHealthService.cs @@ -1,6 +1,7 @@ using System.Numerics; using BlockChainTools.DataTransferObjects; using BlockChainTools.Interfaces; +using Nethereum.Hex.HexTypes; using Nethereum.Signer; using Nethereum.Web3; @@ -10,7 +11,6 @@ public class NetworkHealthService : INetworkHealthService { public async Task CheckNetworkHealthAsync(Web3 web3, Chain? expectedChain = null, int maxBlockAgeSeconds = 120, CancellationToken cancellationToken = default) { - BigInteger? expectedChainId = expectedChain is not null ? (int)expectedChain : null; var issues = new List(); BigInteger? latestBlockNumber = null; DateTime? latestBlockTimestamp = null; @@ -29,21 +29,12 @@ public async Task CheckNetworkHealthAsync(Web3 web3, Chain? e if (block is not null) { latestBlockNumber = block.Number?.Value; - if (block.Timestamp?.Value is not null) - { - latestBlockTimestamp = DateTimeOffset.FromUnixTimeSeconds((long)block.Timestamp.Value).UtcDateTime; - var age = DateTime.UtcNow - latestBlockTimestamp.Value; - isBlockTimestampFresh = age.TotalSeconds <= maxBlockAgeSeconds; + (latestBlockTimestamp, isBlockTimestampFresh) = EvaluateBlockFreshness(block.Timestamp, maxBlockAgeSeconds); - if (!isBlockTimestampFresh) - { - issues.Add($"Latest block timestamp is {age.TotalSeconds:F0}s old, exceeding the {maxBlockAgeSeconds}s threshold."); - } - } - else - { + 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 { @@ -78,12 +69,12 @@ public async Task CheckNetworkHealthAsync(Web3 web3, Chain? e var chainIdResult = await web3.Eth.ChainId.SendRequestAsync(); chainId = chainIdResult?.Value; - if (expectedChainId is not null) + if (expectedChain is not null) { - chainIdMatches = chainId == expectedChainId; + chainIdMatches = chainId == (int)expectedChain; if (!chainIdMatches) { - issues.Add($"Chain ID mismatch: expected {expectedChainId}, got {chainId}."); + issues.Add($"Chain ID mismatch: expected {(int)expectedChain}, got {chainId}."); } } } @@ -108,4 +99,14 @@ public async Task CheckNetworkHealthAsync(Web3 web3, Chain? e 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/tests/BlockChainTools.Tests/TransactionStatusServiceTests.cs b/tests/BlockChainTools.Tests/TransactionStatusServiceTests.cs index 9bb0c8c..d0f33f2 100644 --- a/tests/BlockChainTools.Tests/TransactionStatusServiceTests.cs +++ b/tests/BlockChainTools.Tests/TransactionStatusServiceTests.cs @@ -107,16 +107,4 @@ public async Task GetTransactionStateAsync_StalePending_ReturnsCorrectState() Assert.That(result.ConfirmedNonce, Is.Null); } - [Test] - public async Task GetTransactionStateAsync_Broadcasted_ReturnsCorrectState() - { - var expected = new TransactionStateInfo - { - State = TransactionState.Broadcasted, - TransactionHash = "0xbroadcast" - }; - _service!.GetTransactionStateAsync(_web3!, "0xbroadcast", Arg.Any()).Returns(expected); - var result = await _service.GetTransactionStateAsync(_web3!, "0xbroadcast"); - Assert.That(result.State, Is.EqualTo(TransactionState.Broadcasted)); - } } From 3091cc47cf8ef848ef2ea801a823d0076f6afc82 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Mar 2026 14:49:21 +0000 Subject: [PATCH 6/7] Use lighter GetBlockWithTransactionsHashesByNumber in health check The health check only needs block number and timestamp, not full transaction objects. GetBlockWithTransactionsHashesByNumber returns only tx hashes, reducing RPC payload size and latency on high-throughput networks. Co-authored-by: Phelecks <47243363+Phelecks@users.noreply.github.com> Agent-Logs-Url: https://github.com/Tricksfor-Organization/BlockChainTools/sessions/6c6da43a-8e70-4bfa-8cd3-c11c93a6c907 --- src/BlockChainTools/Services/NetworkHealthService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BlockChainTools/Services/NetworkHealthService.cs b/src/BlockChainTools/Services/NetworkHealthService.cs index 83bb54d..f132060 100644 --- a/src/BlockChainTools/Services/NetworkHealthService.cs +++ b/src/BlockChainTools/Services/NetworkHealthService.cs @@ -23,7 +23,7 @@ public async Task CheckNetworkHealthAsync(Web3 web3, Chain? e // 1. Check latest block number and timestamp try { - var block = await web3.Eth.Blocks.GetBlockWithTransactionsByNumber.SendRequestAsync( + var block = await web3.Eth.Blocks.GetBlockWithTransactionsHashesByNumber.SendRequestAsync( Nethereum.RPC.Eth.DTOs.BlockParameter.CreateLatest()); if (block is not null) From dff8054ce78c95b4932d64d714811061d1b0c0ce Mon Sep 17 00:00:00 2001 From: Hamed Ansarian Date: Sun, 22 Mar 2026 15:54:32 +0100 Subject: [PATCH 7/7] Update src/BlockChainTools/Services/TransactionStatusService.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/BlockChainTools/Services/TransactionStatusService.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/BlockChainTools/Services/TransactionStatusService.cs b/src/BlockChainTools/Services/TransactionStatusService.cs index 9401b55..61596f7 100644 --- a/src/BlockChainTools/Services/TransactionStatusService.cs +++ b/src/BlockChainTools/Services/TransactionStatusService.cs @@ -1,6 +1,5 @@ using BlockChainTools.DataTransferObjects; using BlockChainTools.Interfaces; -using Nethereum.Hex.HexTypes; using Nethereum.RPC.Eth.DTOs; using Nethereum.Web3;