From a669dc459326143e3388194b2e890e2044d525e3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Mar 2026 20:59:07 +0000 Subject: [PATCH 1/7] Initial plan From 7eebacaf4c6679acef804cddeb08efc958efd8c8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Mar 2026 21:05:41 +0000 Subject: [PATCH 2/7] Enrich TransactionStateInfo with Receipt and transaction-only properties - Add TransactionReceipt? Receipt to TransactionStateInfo - Add BigInteger? Value (wei amount) to TransactionStateInfo - Add BigInteger? Gas (gas limit) to TransactionStateInfo - Update Nonce XML doc to clarify it is sourced from the transaction object - TransactionStatusService: for confirmed txs also fetch the transaction to populate Nonce, Value, Gas (fields absent from TransactionReceipt) - Tests: assert Receipt, Nonce, Value, Gas on confirmed-state results; add test for graceful null handling when transaction data is unavailable Agent-Logs-Url: https://github.com/Tricksfor-Organization/BlockChainTools/sessions/b1b0a706-e15f-4776-8aa0-d9e1adf38d8d Co-authored-by: Phelecks <47243363+Phelecks@users.noreply.github.com> --- .../TransactionStateInfo.cs | 20 ++++++ .../Services/TransactionStatusService.cs | 9 ++- .../TransactionStatusServiceTests.cs | 62 ++++++++++++++++++- 3 files changed, 88 insertions(+), 3 deletions(-) diff --git a/src/BlockChainTools/DataTransferObjects/TransactionStateInfo.cs b/src/BlockChainTools/DataTransferObjects/TransactionStateInfo.cs index 2fb8a65..5ec9e4a 100644 --- a/src/BlockChainTools/DataTransferObjects/TransactionStateInfo.cs +++ b/src/BlockChainTools/DataTransferObjects/TransactionStateInfo.cs @@ -1,4 +1,5 @@ using System.Numerics; +using Nethereum.RPC.Eth.DTOs; namespace BlockChainTools.DataTransferObjects; @@ -17,6 +18,12 @@ public class TransactionStateInfo /// public required string TransactionHash { get; init; } + /// + /// The full transaction receipt when a receipt exists (ConfirmedSuccess or ConfirmedReverted states); otherwise null. + /// Contains canonical on-chain data such as gas used, logs, contract address, and effective gas price. + /// + public TransactionReceipt? Receipt { get; init; } + /// /// The receipt status value (1 = success, 0 = revert) when a receipt exists; otherwise null. /// @@ -29,6 +36,7 @@ public class TransactionStateInfo /// /// The nonce of the transaction, if the transaction data was available from the node. + /// The nonce is not included in the transaction receipt and is sourced from the transaction object. /// public BigInteger? Nonce { get; init; } @@ -36,4 +44,16 @@ public class TransactionStateInfo /// The confirmed on-chain nonce for the sender address, if checked. /// public BigInteger? ConfirmedNonce { get; init; } + + /// + /// The amount of native currency (in wei) sent with the transaction, if the transaction data was available. + /// This property is not included in the transaction receipt and is sourced from the transaction object. + /// + public BigInteger? Value { get; init; } + + /// + /// The gas limit provided by the sender, if the transaction data was available. + /// This property is not included in the transaction receipt and is sourced from the transaction object. + /// + public BigInteger? Gas { get; init; } } diff --git a/src/BlockChainTools/Services/TransactionStatusService.cs b/src/BlockChainTools/Services/TransactionStatusService.cs index 61596f7..cbff859 100644 --- a/src/BlockChainTools/Services/TransactionStatusService.cs +++ b/src/BlockChainTools/Services/TransactionStatusService.cs @@ -15,13 +15,20 @@ public async Task GetTransactionStateAsync(Web3 web3, stri if (receipt is not null) { var succeeded = receipt.Status?.Value == 1; + + // Fetch the transaction to populate fields not included in the receipt (Nonce, Value, Gas) + var confirmedTx = await web3.Eth.Transactions.GetTransactionByHash.SendRequestAsync(transactionHash); + return new TransactionStateInfo { State = succeeded ? TransactionState.ConfirmedSuccess : TransactionState.ConfirmedReverted, TransactionHash = transactionHash, + Receipt = receipt, ReceiptStatus = receipt.Status?.Value, BlockNumber = receipt.BlockNumber?.Value, - Nonce = null, + Nonce = confirmedTx?.Nonce?.Value, + Value = confirmedTx?.Value?.Value, + Gas = confirmedTx?.Gas?.Value, ConfirmedNonce = null }; } diff --git a/tests/BlockChainTools.Tests/TransactionStatusServiceTests.cs b/tests/BlockChainTools.Tests/TransactionStatusServiceTests.cs index d0f33f2..980f43d 100644 --- a/tests/BlockChainTools.Tests/TransactionStatusServiceTests.cs +++ b/tests/BlockChainTools.Tests/TransactionStatusServiceTests.cs @@ -1,6 +1,8 @@ using System.Numerics; using BlockChainTools.DataTransferObjects; using BlockChainTools.Interfaces; +using Nethereum.Hex.HexTypes; +using Nethereum.RPC.Eth.DTOs; using Nethereum.Web3; using Nethereum.JsonRpc.Client; using NUnit.Framework; @@ -24,12 +26,21 @@ public void SetUp() [Test] public async Task GetTransactionStateAsync_ConfirmedSuccess_ReturnsCorrectState() { + var receipt = new TransactionReceipt + { + Status = new HexBigInteger(1), + BlockNumber = new HexBigInteger(100) + }; var expected = new TransactionStateInfo { State = TransactionState.ConfirmedSuccess, TransactionHash = "0xabc", + Receipt = receipt, ReceiptStatus = BigInteger.One, - BlockNumber = new BigInteger(100) + BlockNumber = new BigInteger(100), + Nonce = new BigInteger(7), + Value = new BigInteger(1000000000000000000), + Gas = new BigInteger(21000) }; _service!.GetTransactionStateAsync(_web3!, "0xabc", Arg.Any()).Returns(expected); var result = await _service.GetTransactionStateAsync(_web3!, "0xabc"); @@ -37,22 +48,37 @@ public async Task GetTransactionStateAsync_ConfirmedSuccess_ReturnsCorrectState( Assert.That(result.ReceiptStatus, Is.EqualTo(BigInteger.One)); Assert.That(result.BlockNumber, Is.EqualTo(new BigInteger(100))); Assert.That(result.TransactionHash, Is.EqualTo("0xabc")); + Assert.That(result.Receipt, Is.Not.Null); + Assert.That(result.Receipt!.Status?.Value, Is.EqualTo(BigInteger.One)); + Assert.That(result.Nonce, Is.EqualTo(new BigInteger(7))); + Assert.That(result.Value, Is.EqualTo(new BigInteger(1000000000000000000))); + Assert.That(result.Gas, Is.EqualTo(new BigInteger(21000))); } [Test] public async Task GetTransactionStateAsync_ConfirmedReverted_ReturnsCorrectState() { + var receipt = new TransactionReceipt + { + Status = new HexBigInteger(0), + BlockNumber = new HexBigInteger(200) + }; var expected = new TransactionStateInfo { State = TransactionState.ConfirmedReverted, TransactionHash = "0xdef", + Receipt = receipt, ReceiptStatus = BigInteger.Zero, - BlockNumber = new BigInteger(200) + BlockNumber = new BigInteger(200), + Nonce = new BigInteger(3) }; _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)); + Assert.That(result.Receipt, Is.Not.Null); + Assert.That(result.Receipt!.Status?.Value, Is.EqualTo(BigInteger.Zero)); + Assert.That(result.Nonce, Is.EqualTo(new BigInteger(3))); } [Test] @@ -107,4 +133,36 @@ public async Task GetTransactionStateAsync_StalePending_ReturnsCorrectState() Assert.That(result.ConfirmedNonce, Is.Null); } + [Test] + public async Task GetTransactionStateAsync_ConfirmedSuccess_TransactionDataUnavailable_NullableFieldsAreNull() + { + // Simulates the case where a receipt is found but the transaction fetch returns null + // (e.g., the node has pruned the transaction data). Receipt is still populated; + // Nonce, Value, and Gas are gracefully null. + var receipt = new TransactionReceipt + { + Status = new HexBigInteger(1), + BlockNumber = new HexBigInteger(50) + }; + var expected = new TransactionStateInfo + { + State = TransactionState.ConfirmedSuccess, + TransactionHash = "0xpruned", + Receipt = receipt, + ReceiptStatus = BigInteger.One, + BlockNumber = new BigInteger(50), + Nonce = null, + Value = null, + Gas = null + }; + _service!.GetTransactionStateAsync(_web3!, "0xpruned", Arg.Any()).Returns(expected); + var result = await _service.GetTransactionStateAsync(_web3!, "0xpruned"); + Assert.That(result.State, Is.EqualTo(TransactionState.ConfirmedSuccess)); + Assert.That(result.Receipt, Is.Not.Null); + Assert.That(result.ReceiptStatus, Is.EqualTo(BigInteger.One)); + Assert.That(result.Nonce, Is.Null); + Assert.That(result.Value, Is.Null); + Assert.That(result.Gas, Is.Null); + } + } From 7427af5b990bfae5d2b06d356d31a97469dfc2df Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Mar 2026 22:31:15 +0000 Subject: [PATCH 3/7] Remove redundant explicit null assignments from TransactionStatusService - Drop ConfirmedNonce = null, ReceiptStatus = null, BlockNumber = null, Nonce = null from every object initializer where they add no value; nullable properties default to null in C# - Sharpen ConfirmedNonce XML doc: explicitly states it is only populated for the Replaced state, null for all other states - Remove redundant Nonce/Value/Gas = null from the pruned-transaction test setup for consistency Agent-Logs-Url: https://github.com/Tricksfor-Organization/BlockChainTools/sessions/bcf678a3-75e5-4dd3-9b9b-fce8cd71a977 Co-authored-by: Phelecks <47243363+Phelecks@users.noreply.github.com> --- .../DataTransferObjects/TransactionStateInfo.cs | 4 +++- .../Services/TransactionStatusService.cs | 16 +++------------- .../TransactionStatusServiceTests.cs | 5 +---- 3 files changed, 7 insertions(+), 18 deletions(-) diff --git a/src/BlockChainTools/DataTransferObjects/TransactionStateInfo.cs b/src/BlockChainTools/DataTransferObjects/TransactionStateInfo.cs index 5ec9e4a..41b492d 100644 --- a/src/BlockChainTools/DataTransferObjects/TransactionStateInfo.cs +++ b/src/BlockChainTools/DataTransferObjects/TransactionStateInfo.cs @@ -41,7 +41,9 @@ public class TransactionStateInfo public BigInteger? Nonce { get; init; } /// - /// The confirmed on-chain nonce for the sender address, if checked. + /// The confirmed on-chain nonce for the sender address at the time the replacement was detected. + /// Only populated when is ; + /// null for all other states. /// public BigInteger? ConfirmedNonce { get; init; } diff --git a/src/BlockChainTools/Services/TransactionStatusService.cs b/src/BlockChainTools/Services/TransactionStatusService.cs index cbff859..9ef8c09 100644 --- a/src/BlockChainTools/Services/TransactionStatusService.cs +++ b/src/BlockChainTools/Services/TransactionStatusService.cs @@ -28,8 +28,7 @@ public async Task GetTransactionStateAsync(Web3 web3, stri BlockNumber = receipt.BlockNumber?.Value, Nonce = confirmedTx?.Nonce?.Value, Value = confirmedTx?.Value?.Value, - Gas = confirmedTx?.Gas?.Value, - ConfirmedNonce = null + Gas = confirmedTx?.Gas?.Value }; } @@ -55,8 +54,6 @@ public async Task GetTransactionStateAsync(Web3 web3, stri { State = TransactionState.Replaced, TransactionHash = transactionHash, - ReceiptStatus = null, - BlockNumber = null, Nonce = txNonce, ConfirmedNonce = confirmedNonce.Value }; @@ -67,10 +64,7 @@ public async Task GetTransactionStateAsync(Web3 web3, stri { State = TransactionState.Pending, TransactionHash = transactionHash, - ReceiptStatus = null, - BlockNumber = null, - Nonce = txNonce, - ConfirmedNonce = null + Nonce = txNonce }; } @@ -78,11 +72,7 @@ public async Task GetTransactionStateAsync(Web3 web3, stri return new TransactionStateInfo { State = TransactionState.StalePending, - TransactionHash = transactionHash, - ReceiptStatus = null, - BlockNumber = null, - Nonce = null, - ConfirmedNonce = null + TransactionHash = transactionHash }; } } diff --git a/tests/BlockChainTools.Tests/TransactionStatusServiceTests.cs b/tests/BlockChainTools.Tests/TransactionStatusServiceTests.cs index 980f43d..c546477 100644 --- a/tests/BlockChainTools.Tests/TransactionStatusServiceTests.cs +++ b/tests/BlockChainTools.Tests/TransactionStatusServiceTests.cs @@ -150,10 +150,7 @@ public async Task GetTransactionStateAsync_ConfirmedSuccess_TransactionDataUnava TransactionHash = "0xpruned", Receipt = receipt, ReceiptStatus = BigInteger.One, - BlockNumber = new BigInteger(50), - Nonce = null, - Value = null, - Gas = null + BlockNumber = new BigInteger(50) }; _service!.GetTransactionStateAsync(_web3!, "0xpruned", Arg.Any()).Returns(expected); var result = await _service.GetTransactionStateAsync(_web3!, "0xpruned"); From 7a3cb7c9f07d07e06718883fe45d62d70301b914 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Mar 2026 22:34:49 +0000 Subject: [PATCH 4/7] Populate Value and Gas for Pending and Replaced states The transaction object is already fetched from the node for both states but Value and Gas were left unpopulated. Now every state with available transaction data (ConfirmedSuccess, ConfirmedReverted, Pending, Replaced) fills Value and Gas; only StalePending leaves them null. Update XML docs and tests accordingly. Agent-Logs-Url: https://github.com/Tricksfor-Organization/BlockChainTools/sessions/bcf678a3-75e5-4dd3-9b9b-fce8cd71a977 Co-authored-by: Phelecks <47243363+Phelecks@users.noreply.github.com> --- .../DataTransferObjects/TransactionStateInfo.cs | 12 ++++++++++-- .../Services/TransactionStatusService.cs | 6 +++++- .../TransactionStatusServiceTests.cs | 10 +++++++++- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/BlockChainTools/DataTransferObjects/TransactionStateInfo.cs b/src/BlockChainTools/DataTransferObjects/TransactionStateInfo.cs index 41b492d..444bb85 100644 --- a/src/BlockChainTools/DataTransferObjects/TransactionStateInfo.cs +++ b/src/BlockChainTools/DataTransferObjects/TransactionStateInfo.cs @@ -48,13 +48,21 @@ public class TransactionStateInfo public BigInteger? ConfirmedNonce { get; init; } /// - /// The amount of native currency (in wei) sent with the transaction, if the transaction data was available. + /// The amount of native currency (in wei) sent with the transaction. + /// Populated whenever the transaction data is available from the node: + /// , , + /// , and . + /// Null for (node no longer holds the transaction). /// This property is not included in the transaction receipt and is sourced from the transaction object. /// public BigInteger? Value { get; init; } /// - /// The gas limit provided by the sender, if the transaction data was available. + /// The gas limit provided by the sender. + /// Populated whenever the transaction data is available from the node: + /// , , + /// , and . + /// Null for (node no longer holds the transaction). /// This property is not included in the transaction receipt and is sourced from the transaction object. /// public BigInteger? Gas { get; init; } diff --git a/src/BlockChainTools/Services/TransactionStatusService.cs b/src/BlockChainTools/Services/TransactionStatusService.cs index 9ef8c09..5940f0c 100644 --- a/src/BlockChainTools/Services/TransactionStatusService.cs +++ b/src/BlockChainTools/Services/TransactionStatusService.cs @@ -55,6 +55,8 @@ public async Task GetTransactionStateAsync(Web3 web3, stri State = TransactionState.Replaced, TransactionHash = transactionHash, Nonce = txNonce, + Value = transaction.Value?.Value, + Gas = transaction.Gas?.Value, ConfirmedNonce = confirmedNonce.Value }; } @@ -64,7 +66,9 @@ public async Task GetTransactionStateAsync(Web3 web3, stri { State = TransactionState.Pending, TransactionHash = transactionHash, - Nonce = txNonce + Nonce = txNonce, + Value = transaction.Value?.Value, + Gas = transaction.Gas?.Value }; } diff --git a/tests/BlockChainTools.Tests/TransactionStatusServiceTests.cs b/tests/BlockChainTools.Tests/TransactionStatusServiceTests.cs index c546477..732d53e 100644 --- a/tests/BlockChainTools.Tests/TransactionStatusServiceTests.cs +++ b/tests/BlockChainTools.Tests/TransactionStatusServiceTests.cs @@ -88,7 +88,9 @@ public async Task GetTransactionStateAsync_Pending_ReturnsCorrectState() { State = TransactionState.Pending, TransactionHash = "0xpending", - Nonce = new BigInteger(5) + Nonce = new BigInteger(5), + Value = new BigInteger(500000000000000000), + Gas = new BigInteger(21000) }; _service!.GetTransactionStateAsync(_web3!, "0xpending", Arg.Any()).Returns(expected); var result = await _service.GetTransactionStateAsync(_web3!, "0xpending"); @@ -96,6 +98,8 @@ public async Task GetTransactionStateAsync_Pending_ReturnsCorrectState() Assert.That(result.ReceiptStatus, Is.Null); Assert.That(result.BlockNumber, Is.Null); Assert.That(result.Nonce, Is.EqualTo(new BigInteger(5))); + Assert.That(result.Value, Is.EqualTo(new BigInteger(500000000000000000))); + Assert.That(result.Gas, Is.EqualTo(new BigInteger(21000))); } [Test] @@ -106,6 +110,8 @@ public async Task GetTransactionStateAsync_Replaced_ReturnsCorrectState() State = TransactionState.Replaced, TransactionHash = "0xreplaced", Nonce = new BigInteger(3), + Value = new BigInteger(250000000000000000), + Gas = new BigInteger(21000), ConfirmedNonce = new BigInteger(5) }; _service!.GetTransactionStateAsync(_web3!, "0xreplaced", Arg.Any()).Returns(expected); @@ -114,6 +120,8 @@ public async Task GetTransactionStateAsync_Replaced_ReturnsCorrectState() Assert.That(result.Nonce, Is.Not.Null); Assert.That(result.ConfirmedNonce, Is.Not.Null); Assert.That(result.ConfirmedNonce!.Value, Is.GreaterThan(result.Nonce!.Value)); + Assert.That(result.Value, Is.EqualTo(new BigInteger(250000000000000000))); + Assert.That(result.Gas, Is.EqualTo(new BigInteger(21000))); } [Test] From 409d3835965587860d4d57c1746dc682496a513a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Mar 2026 22:37:39 +0000 Subject: [PATCH 5/7] Update README: document TransactionStateInfo fields and per-state population table Replace the minimal single-if example with a full switch covering all 5 states, demonstrating every populated field per state. Add a Markdown table showing exactly which properties are set for each TransactionState, and a short prose summary explaining the rules. Agent-Logs-Url: https://github.com/Tricksfor-Organization/BlockChainTools/sessions/18d9ec2b-f293-4298-8547-d7924195aa7d Co-authored-by: Phelecks <47243363+Phelecks@users.noreply.github.com> --- README.md | 45 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3c21558..bc8ea3e 100644 --- a/README.md +++ b/README.md @@ -295,15 +295,54 @@ var stateInfo = await txStatusService.GetTransactionStateAsync( // 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.Replaced – another tx with the same nonce was mined first // TransactionState.StalePending – node no longer knows the tx (likely dropped) -if (stateInfo.State == TransactionState.ConfirmedSuccess) +switch (stateInfo.State) { - Console.WriteLine($"Mined in block {stateInfo.BlockNumber}"); + case TransactionState.ConfirmedSuccess: + case TransactionState.ConfirmedReverted: + Console.WriteLine($"Mined in block {stateInfo.BlockNumber}"); + Console.WriteLine($"Receipt status: {stateInfo.ReceiptStatus}"); // 1 or 0 + Console.WriteLine($"Gas used: {stateInfo.Receipt!.GasUsed}"); // full receipt available + Console.WriteLine($"Nonce: {stateInfo.Nonce}, Value: {stateInfo.Value}, Gas limit: {stateInfo.Gas}"); + break; + + case TransactionState.Replaced: + // The sender's confirmed nonce has already passed this tx's nonce + Console.WriteLine($"Tx nonce {stateInfo.Nonce} was replaced; sender's confirmed nonce is now {stateInfo.ConfirmedNonce}"); + Console.WriteLine($"Original value: {stateInfo.Value}, gas limit: {stateInfo.Gas}"); + break; + + case TransactionState.Pending: + Console.WriteLine($"Still pending – nonce {stateInfo.Nonce}, value {stateInfo.Value}, gas limit {stateInfo.Gas}"); + break; + + case TransactionState.StalePending: + Console.WriteLine("Transaction dropped from the mempool"); + break; } ``` +##### `TransactionStateInfo` property population by state + +| Property | ConfirmedSuccess / ConfirmedReverted | Pending | Replaced | StalePending | +|---|---|---|---|---| +| `State` | ✅ | ✅ | ✅ | ✅ | +| `TransactionHash` | ✅ | ✅ | ✅ | ✅ | +| `Receipt` | ✅ | — | — | — | +| `ReceiptStatus` | ✅ | — | — | — | +| `BlockNumber` | ✅ | — | — | — | +| `Nonce` | ✅ | ✅ | ✅ | — | +| `Value` | ✅ | ✅ | ✅ | — | +| `Gas` | ✅ | ✅ | ✅ | — | +| `ConfirmedNonce` | — | — | ✅ | — | + +`Receipt`, `ReceiptStatus`, and `BlockNumber` are only available when a receipt exists (confirmed states). +`Nonce`, `Value`, and `Gas` are populated whenever the node still holds the transaction object. +`ConfirmedNonce` is exclusively set for the `Replaced` state — it holds the sender's current on-chain nonce at the point replacement was detected. +All properties are `null` for states where the data source is unavailable. + #### Network Health Service Check blockchain network health before sending transactions: From 777dcf18b0901175237d267a678f38420519c697 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Mar 2026 23:24:30 +0000 Subject: [PATCH 6/7] Rewrite TransactionStatusServiceTests: instantiate service directly, stub IClient RPC responses Replace interface-level mocking (Substitute.For) with direct instantiation of TransactionStatusService and NSubstitute stubs on IClient (keyed on eth_getTransaction* method names). All 6 tests now exercise the real service logic including receipt/tx fetch paths, Replaced vs Pending nonce comparison, and the pruned-transaction null-fallback case. Agent-Logs-Url: https://github.com/Tricksfor-Organization/BlockChainTools/sessions/ff398c7f-10f8-434d-85a4-2853ff8b65b7 Co-authored-by: Phelecks <47243363+Phelecks@users.noreply.github.com> --- .../TransactionStatusServiceTests.cs | 173 ++++++++++-------- 1 file changed, 99 insertions(+), 74 deletions(-) diff --git a/tests/BlockChainTools.Tests/TransactionStatusServiceTests.cs b/tests/BlockChainTools.Tests/TransactionStatusServiceTests.cs index 732d53e..779d6a9 100644 --- a/tests/BlockChainTools.Tests/TransactionStatusServiceTests.cs +++ b/tests/BlockChainTools.Tests/TransactionStatusServiceTests.cs @@ -1,26 +1,50 @@ using System.Numerics; using BlockChainTools.DataTransferObjects; -using BlockChainTools.Interfaces; +using BlockChainTools.Services; using Nethereum.Hex.HexTypes; +using Nethereum.JsonRpc.Client; using Nethereum.RPC.Eth.DTOs; using Nethereum.Web3; -using Nethereum.JsonRpc.Client; -using NUnit.Framework; using NSubstitute; +using NUnit.Framework; namespace BlockChainTools.Tests; [TestFixture] public class TransactionStatusServiceTests { - private ITransactionStatusService? _service; - private Web3? _web3; + private TransactionStatusService _service = null!; [SetUp] - public void SetUp() + public void SetUp() => _service = new TransactionStatusService(); + + /// + /// Builds a whose underlying is stubbed so that + /// each Ethereum JSON-RPC method returns the supplied value (or null when omitted). + /// + private static Web3 BuildWeb3( + TransactionReceipt? receipt = null, + Transaction? tx = null, + HexBigInteger? transactionCount = null) { - _service = Substitute.For(); - _web3 = new Web3(Substitute.For()); + var client = Substitute.For(); + + client.SendRequestAsync( + Arg.Is(r => r.Method == "eth_getTransactionReceipt"), + Arg.Any()) + .Returns(receipt!); + + client.SendRequestAsync( + Arg.Is(r => r.Method == "eth_getTransactionByHash"), + Arg.Any()) + .Returns(tx!); + + client.SendRequestAsync( + Arg.Is(r => r.Method == "eth_getTransactionCount"), + Arg.Any()) + .Returns(transactionCount!); + + return new Web3(client); } [Test] @@ -31,28 +55,26 @@ public async Task GetTransactionStateAsync_ConfirmedSuccess_ReturnsCorrectState( Status = new HexBigInteger(1), BlockNumber = new HexBigInteger(100) }; - var expected = new TransactionStateInfo + var tx = new Transaction { - State = TransactionState.ConfirmedSuccess, - TransactionHash = "0xabc", - Receipt = receipt, - ReceiptStatus = BigInteger.One, - BlockNumber = new BigInteger(100), - Nonce = new BigInteger(7), - Value = new BigInteger(1000000000000000000), - Gas = new BigInteger(21000) + Nonce = new HexBigInteger(7), + Value = new HexBigInteger(1_000_000_000_000_000_000L), + Gas = new HexBigInteger(21000) }; - _service!.GetTransactionStateAsync(_web3!, "0xabc", Arg.Any()).Returns(expected); - var result = await _service.GetTransactionStateAsync(_web3!, "0xabc"); + var web3 = BuildWeb3(receipt: receipt, tx: tx); + + 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")); Assert.That(result.Receipt, Is.Not.Null); Assert.That(result.Receipt!.Status?.Value, Is.EqualTo(BigInteger.One)); + Assert.That(result.ReceiptStatus, Is.EqualTo(BigInteger.One)); + Assert.That(result.BlockNumber, Is.EqualTo(new BigInteger(100))); Assert.That(result.Nonce, Is.EqualTo(new BigInteger(7))); - Assert.That(result.Value, Is.EqualTo(new BigInteger(1000000000000000000))); + Assert.That(result.Value, Is.EqualTo(new BigInteger(1_000_000_000_000_000_000L))); Assert.That(result.Gas, Is.EqualTo(new BigInteger(21000))); + Assert.That(result.ConfirmedNonce, Is.Null); } [Test] @@ -63,111 +85,114 @@ public async Task GetTransactionStateAsync_ConfirmedReverted_ReturnsCorrectState Status = new HexBigInteger(0), BlockNumber = new HexBigInteger(200) }; - var expected = new TransactionStateInfo + var tx = new Transaction { - State = TransactionState.ConfirmedReverted, - TransactionHash = "0xdef", - Receipt = receipt, - ReceiptStatus = BigInteger.Zero, - BlockNumber = new BigInteger(200), - Nonce = new BigInteger(3) + Nonce = new HexBigInteger(3), + Value = new HexBigInteger(0), + Gas = new HexBigInteger(21000) }; - _service!.GetTransactionStateAsync(_web3!, "0xdef", Arg.Any()).Returns(expected); - var result = await _service.GetTransactionStateAsync(_web3!, "0xdef"); + var web3 = BuildWeb3(receipt: receipt, tx: tx); + + var result = await _service.GetTransactionStateAsync(web3, "0xdef"); + Assert.That(result.State, Is.EqualTo(TransactionState.ConfirmedReverted)); - Assert.That(result.ReceiptStatus, Is.EqualTo(BigInteger.Zero)); Assert.That(result.Receipt, Is.Not.Null); Assert.That(result.Receipt!.Status?.Value, Is.EqualTo(BigInteger.Zero)); + Assert.That(result.ReceiptStatus, Is.EqualTo(BigInteger.Zero)); + Assert.That(result.BlockNumber, Is.EqualTo(new BigInteger(200))); Assert.That(result.Nonce, Is.EqualTo(new BigInteger(3))); + Assert.That(result.Value, Is.EqualTo(BigInteger.Zero)); + Assert.That(result.Gas, Is.EqualTo(new BigInteger(21000))); + Assert.That(result.ConfirmedNonce, Is.Null); } [Test] public async Task GetTransactionStateAsync_Pending_ReturnsCorrectState() { - var expected = new TransactionStateInfo + var tx = new Transaction { - State = TransactionState.Pending, - TransactionHash = "0xpending", - Nonce = new BigInteger(5), - Value = new BigInteger(500000000000000000), - Gas = new BigInteger(21000) + From = "0xsender", + Nonce = new HexBigInteger(5), + Value = new HexBigInteger(500_000_000_000_000_000L), + Gas = new HexBigInteger(21000) }; - _service!.GetTransactionStateAsync(_web3!, "0xpending", Arg.Any()).Returns(expected); - var result = await _service.GetTransactionStateAsync(_web3!, "0xpending"); + // Confirmed nonce equals tx nonce — not yet replaced, still pending + var web3 = BuildWeb3(tx: tx, transactionCount: new HexBigInteger(5)); + + var result = await _service.GetTransactionStateAsync(web3, "0xpending"); + Assert.That(result.State, Is.EqualTo(TransactionState.Pending)); + Assert.That(result.Receipt, Is.Null); Assert.That(result.ReceiptStatus, Is.Null); Assert.That(result.BlockNumber, Is.Null); Assert.That(result.Nonce, Is.EqualTo(new BigInteger(5))); - Assert.That(result.Value, Is.EqualTo(new BigInteger(500000000000000000))); + Assert.That(result.Value, Is.EqualTo(new BigInteger(500_000_000_000_000_000L))); Assert.That(result.Gas, Is.EqualTo(new BigInteger(21000))); + Assert.That(result.ConfirmedNonce, Is.Null); } [Test] public async Task GetTransactionStateAsync_Replaced_ReturnsCorrectState() { - var expected = new TransactionStateInfo + var tx = new Transaction { - State = TransactionState.Replaced, - TransactionHash = "0xreplaced", - Nonce = new BigInteger(3), - Value = new BigInteger(250000000000000000), - Gas = new BigInteger(21000), - ConfirmedNonce = new BigInteger(5) + From = "0xsender", + Nonce = new HexBigInteger(3), + Value = new HexBigInteger(250_000_000_000_000_000L), + Gas = new HexBigInteger(21000) }; - _service!.GetTransactionStateAsync(_web3!, "0xreplaced", Arg.Any()).Returns(expected); - var result = await _service.GetTransactionStateAsync(_web3!, "0xreplaced"); + // Confirmed nonce (5) > tx nonce (3) — the tx was replaced by another + var web3 = BuildWeb3(tx: tx, transactionCount: new HexBigInteger(5)); + + 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.Nonce, Is.EqualTo(new BigInteger(3))); + Assert.That(result.ConfirmedNonce, Is.EqualTo(new BigInteger(5))); Assert.That(result.ConfirmedNonce!.Value, Is.GreaterThan(result.Nonce!.Value)); - Assert.That(result.Value, Is.EqualTo(new BigInteger(250000000000000000))); + Assert.That(result.Value, Is.EqualTo(new BigInteger(250_000_000_000_000_000L))); Assert.That(result.Gas, Is.EqualTo(new BigInteger(21000))); } [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"); + // Node returns null for both receipt and transaction — likely dropped from mempool + var web3 = BuildWeb3(); + + var result = await _service.GetTransactionStateAsync(web3, "0xstale"); + Assert.That(result.State, Is.EqualTo(TransactionState.StalePending)); + Assert.That(result.TransactionHash, Is.EqualTo("0xstale")); + Assert.That(result.Receipt, Is.Null); Assert.That(result.ReceiptStatus, Is.Null); Assert.That(result.BlockNumber, Is.Null); Assert.That(result.Nonce, Is.Null); + Assert.That(result.Value, Is.Null); + Assert.That(result.Gas, Is.Null); Assert.That(result.ConfirmedNonce, Is.Null); } [Test] public async Task GetTransactionStateAsync_ConfirmedSuccess_TransactionDataUnavailable_NullableFieldsAreNull() { - // Simulates the case where a receipt is found but the transaction fetch returns null - // (e.g., the node has pruned the transaction data). Receipt is still populated; - // Nonce, Value, and Gas are gracefully null. + // Receipt found but the node has pruned the transaction object. + // Nonce, Value, and Gas fall back to null gracefully; Receipt and ReceiptStatus are still populated. var receipt = new TransactionReceipt { Status = new HexBigInteger(1), BlockNumber = new HexBigInteger(50) }; - var expected = new TransactionStateInfo - { - State = TransactionState.ConfirmedSuccess, - TransactionHash = "0xpruned", - Receipt = receipt, - ReceiptStatus = BigInteger.One, - BlockNumber = new BigInteger(50) - }; - _service!.GetTransactionStateAsync(_web3!, "0xpruned", Arg.Any()).Returns(expected); - var result = await _service.GetTransactionStateAsync(_web3!, "0xpruned"); + var web3 = BuildWeb3(receipt: receipt); // tx = null (pruned) + + var result = await _service.GetTransactionStateAsync(web3, "0xpruned"); + Assert.That(result.State, Is.EqualTo(TransactionState.ConfirmedSuccess)); Assert.That(result.Receipt, Is.Not.Null); Assert.That(result.ReceiptStatus, Is.EqualTo(BigInteger.One)); + Assert.That(result.BlockNumber, Is.EqualTo(new BigInteger(50))); Assert.That(result.Nonce, Is.Null); Assert.That(result.Value, Is.Null); Assert.That(result.Gas, Is.Null); } - } From 5f54e3f985dcc0e810b12467e057dd35ef760da8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Mar 2026 23:30:04 +0000 Subject: [PATCH 7/7] Observe CancellationToken in TransactionStatusService: ThrowIfCancellationRequested before each RPC call Nethereum's SendRequestAsync does not accept a CancellationToken, so add explicit cancellationToken.ThrowIfCancellationRequested() before each of the 4 awaited RPC calls (GetTransactionReceipt, GetTransactionByHash x2, GetTransactionCount). Also adds a test verifying OperationCanceledException is thrown for pre-cancelled tokens. Agent-Logs-Url: https://github.com/Tricksfor-Organization/BlockChainTools/sessions/b484f146-89c6-487f-83a5-e2f370307c2c Co-authored-by: Phelecks <47243363+Phelecks@users.noreply.github.com> --- .../Services/TransactionStatusService.cs | 4 ++++ .../TransactionStatusServiceTests.cs | 11 +++++++++++ 2 files changed, 15 insertions(+) diff --git a/src/BlockChainTools/Services/TransactionStatusService.cs b/src/BlockChainTools/Services/TransactionStatusService.cs index 5940f0c..00207e3 100644 --- a/src/BlockChainTools/Services/TransactionStatusService.cs +++ b/src/BlockChainTools/Services/TransactionStatusService.cs @@ -10,6 +10,7 @@ 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 + cancellationToken.ThrowIfCancellationRequested(); TransactionReceipt? receipt = await web3.Eth.Transactions.GetTransactionReceipt.SendRequestAsync(transactionHash); if (receipt is not null) @@ -17,6 +18,7 @@ public async Task GetTransactionStateAsync(Web3 web3, stri var succeeded = receipt.Status?.Value == 1; // Fetch the transaction to populate fields not included in the receipt (Nonce, Value, Gas) + cancellationToken.ThrowIfCancellationRequested(); var confirmedTx = await web3.Eth.Transactions.GetTransactionByHash.SendRequestAsync(transactionHash); return new TransactionStateInfo @@ -33,6 +35,7 @@ public async Task GetTransactionStateAsync(Web3 web3, stri } // 2. No receipt — check if the node still knows about the transaction + cancellationToken.ThrowIfCancellationRequested(); var transaction = await web3.Eth.Transactions.GetTransactionByHash.SendRequestAsync(transactionHash); if (transaction is not null) @@ -44,6 +47,7 @@ public async Task GetTransactionStateAsync(Web3 web3, stri if (senderAddress is not null && txNonce is not null) { // Get the confirmed (mined) nonce for the sender + cancellationToken.ThrowIfCancellationRequested(); var confirmedNonce = await web3.Eth.Transactions.GetTransactionCount.SendRequestAsync( senderAddress, BlockParameter.CreateLatest()); diff --git a/tests/BlockChainTools.Tests/TransactionStatusServiceTests.cs b/tests/BlockChainTools.Tests/TransactionStatusServiceTests.cs index 779d6a9..c106fee 100644 --- a/tests/BlockChainTools.Tests/TransactionStatusServiceTests.cs +++ b/tests/BlockChainTools.Tests/TransactionStatusServiceTests.cs @@ -195,4 +195,15 @@ public async Task GetTransactionStateAsync_ConfirmedSuccess_TransactionDataUnava Assert.That(result.Value, Is.Null); Assert.That(result.Gas, Is.Null); } + + [Test] + public void GetTransactionStateAsync_CancelledToken_ThrowsOperationCanceledException() + { + using var cts = new CancellationTokenSource(); + cts.Cancel(); + var web3 = BuildWeb3(); + + Assert.ThrowsAsync( + () => _service.GetTransactionStateAsync(web3, "0xany", cts.Token)); + } }