Skip to content
45 changes: 42 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
32 changes: 31 additions & 1 deletion src/BlockChainTools/DataTransferObjects/TransactionStateInfo.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Numerics;
using Nethereum.RPC.Eth.DTOs;

namespace BlockChainTools.DataTransferObjects;

Expand All @@ -17,6 +18,12 @@ public class TransactionStateInfo
/// </summary>
public required string TransactionHash { get; init; }

/// <summary>
/// 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.
/// </summary>
public TransactionReceipt? Receipt { get; init; }

/// <summary>
/// The receipt status value (1 = success, 0 = revert) when a receipt exists; otherwise null.
/// </summary>
Expand All @@ -29,11 +36,34 @@ public class TransactionStateInfo

/// <summary>
/// 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.
/// </summary>
public BigInteger? Nonce { get; init; }

/// <summary>
/// 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 <see cref="State"/> is <see cref="TransactionState.Replaced"/>;
/// null for all other states.
/// </summary>
public BigInteger? ConfirmedNonce { get; init; }

/// <summary>
/// The amount of native currency (in wei) sent with the transaction.
/// Populated whenever the transaction data is available from the node:
/// <see cref="TransactionState.ConfirmedSuccess"/>, <see cref="TransactionState.ConfirmedReverted"/>,
/// <see cref="TransactionState.Pending"/>, and <see cref="TransactionState.Replaced"/>.
/// Null for <see cref="TransactionState.StalePending"/> (node no longer holds the transaction).
/// This property is not included in the transaction receipt and is sourced from the transaction object.
/// </summary>
public BigInteger? Value { get; init; }

/// <summary>
/// The gas limit provided by the sender.
/// Populated whenever the transaction data is available from the node:
/// <see cref="TransactionState.ConfirmedSuccess"/>, <see cref="TransactionState.ConfirmedReverted"/>,
/// <see cref="TransactionState.Pending"/>, and <see cref="TransactionState.Replaced"/>.
/// Null for <see cref="TransactionState.StalePending"/> (node no longer holds the transaction).
/// This property is not included in the transaction receipt and is sourced from the transaction object.
/// </summary>
public BigInteger? Gas { get; init; }
}
29 changes: 17 additions & 12 deletions src/BlockChainTools/Services/TransactionStatusService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,32 @@ public class TransactionStatusService : ITransactionStatusService
public async Task<TransactionStateInfo> 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)
{
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);

Comment thread
Phelecks marked this conversation as resolved.
Resolved
return new TransactionStateInfo
{
State = succeeded ? TransactionState.ConfirmedSuccess : TransactionState.ConfirmedReverted,
TransactionHash = transactionHash,
Receipt = receipt,
ReceiptStatus = receipt.Status?.Value,
BlockNumber = receipt.BlockNumber?.Value,
Nonce = null,
ConfirmedNonce = null
Nonce = confirmedTx?.Nonce?.Value,
Value = confirmedTx?.Value?.Value,
Gas = confirmedTx?.Gas?.Value
};
}

// 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)
Expand All @@ -38,6 +47,7 @@ public async Task<TransactionStateInfo> 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());

Expand All @@ -48,9 +58,9 @@ public async Task<TransactionStateInfo> GetTransactionStateAsync(Web3 web3, stri
{
State = TransactionState.Replaced,
TransactionHash = transactionHash,
ReceiptStatus = null,
BlockNumber = null,
Nonce = txNonce,
Value = transaction.Value?.Value,
Gas = transaction.Gas?.Value,
ConfirmedNonce = confirmedNonce.Value
};
}
Expand All @@ -60,22 +70,17 @@ public async Task<TransactionStateInfo> GetTransactionStateAsync(Web3 web3, stri
{
State = TransactionState.Pending,
TransactionHash = transactionHash,
ReceiptStatus = null,
BlockNumber = null,
Nonce = txNonce,
ConfirmedNonce = null
Value = transaction.Value?.Value,
Gas = transaction.Gas?.Value
};
}

// 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
TransactionHash = transactionHash
};
}
}
Loading
Loading