Show / Hide Table of Contents

Class ConsensusException

Exception thrown when the gateway node accepted a transaction but its outcome could not be confirmed: the receipt was still pending when the retries ran out, the node stayed too busy to return one, or the node reported the transaction expired or had no receipt for it (ReceiptNotFound).

Inheritance
object
Exception
ConsensusException
Implements
ISerializable
Inherited Members
Exception.GetBaseException()
Exception.GetType()
Exception.ToString()
Exception.Data
Exception.HelpLink
Exception.HResult
Exception.InnerException
Exception.Message
Exception.Source
Exception.StackTrace
Exception.TargetSite
object.Equals(object)
object.Equals(object, object)
object.GetHashCode()
object.ReferenceEquals(object, object)
Namespace: Hiero
Assembly: Hiero.dll
Syntax
public sealed class ConsensusException : Exception, ISerializable
Remarks

The outcome is unknown: the transaction may have executed. Do not resubmit it. A resubmission is a new transaction with a new TransactionId, so if the first one executed, the value moves twice. Do not report it as failed either. Treat it as pending and settle it on a mirror node by TransactionId, as in the example below.

The transaction can no longer execute once the mirror node's latest consensus time is past its valid start plus the TransactionDuration it was sent with, plus a margin (60 seconds in the example) for mirror replicas that lag one another. Read that clock before looking the transaction up, so the lookup sees at least everything the mirror held at that time. Until then, "not found" on the mirror means "not yet", not "never". Judge it by the mirror's clock, never the local one.

The same holds for a PrecheckException whose status describes the node or the network rather than the transaction, and for any other exception raised once the transaction may have been sent, such as a cancellation.

Seen often on a healthy network, this exception usually means the retry window is shorter than the transaction's validity: see RetryCount.

Examples

Classify the ending of a transfer whose transaction id was chosen up front:

// Choose the transaction id first and store it with the payment: it is
// the idempotency key, the mirror lookup and the expiry clock.
var txId = client.CreateNewTransactionId();
try
{
    var receipt = await client.TransferAsync(sender, receiver, amount, ctx => ctx.TransactionId = txId);
    // Executed. Only a receipt with Status == Success is a known success.
}
catch (TransactionException)
{
    // Reached consensus and failed: nothing moved. Its Status says why.
}
catch (PrecheckException ex) when (ex.Status is ResponseCode.InvalidSignature
    or ResponseCode.InsufficientPayerBalance
    or ResponseCode.InvalidAccountId
    or ResponseCode.TokenNotAssociatedToAccount)
{
    // Refused for something wrong with the transaction itself: nothing ran.
}
catch (Exception ex) when (ex is not ArgumentException)
{
    // Every other ending -- ConsensusException, any other precheck code,
    // a transport failure, cancellation -- means the outcome is UNKNOWN:
    // it may have executed. Never resend it and never report it as
    // failed. Report "pending" and settle txId with ResolveOutcomeAsync.
}

Settle an unknown outcome on a mirror node:

// Settles a transaction whose outcome is unknown, on a mirror node. Returns
// Success if it executed; the failure code if it reached consensus and
// failed; TransactionExpired if it never executed and now never can; or
// null while it may still execute, so ask again later. validDuration is the
// TransactionDuration it was sent with (180 seconds by default).
public static async Task<ResponseCode?> ResolveOutcomeAsync(
    MirrorRestClient mirror,
    TransactionId txId,
    TimeSpan validDuration,
    CancellationToken cancellationToken = default)
{
    // Read the mirror's clock FIRST (never this machine's): the lookup
    // that follows then sees at least everything the mirror held at
    // that time, so absence below is judged against it.
    var mirrorNow = await mirror.GetLatestConsensusTimestampAsync(cancellationToken);
    // The records of the transaction itself: not its child records
    // (Nonce > 0), not the execution of a schedule it created, and not the
    // inner transactions of a batch.
    var records = (await mirror.GetTransactionGroupAsync(txId, cancellationToken))
        .Where(t => t.Nonce == 0 && !t.IsScheduled && t.TransactionId == txId)
        .ToArray();
    // A resend can leave a DUPLICATE_TRANSACTION record beside the one that
    // executed, so a success anywhere wins.
    if (records.Any(t => t.Status == ResponseCode.Success))
    {
        return ResponseCode.Success;
    }
    var failure = records.FirstOrDefault(t => t.Status != ResponseCode.DuplicateTransaction);
    if (failure is not null)
    {
        return failure.Status;
    }
    // Nothing decisive recorded. The transaction can no longer execute
    // once that clock is past its valid start plus its duration; the
    // 60-second margin covers mirror replicas that lag one another.
    var validStart = new ConsensusTimeStamp(txId.ValidStartSeconds, txId.ValidStartNanos);
    if (mirrorNow - validStart > (decimal)(validDuration + TimeSpan.FromSeconds(60)).TotalSeconds)
    {
        return ResponseCode.TransactionExpired;
    }
    return null;
}

Properties

| Edit this page View Source

Status

The last ResponseCode the node returned for the transaction. It is not a verdict: the outcome is unknown.

Declaration
public ResponseCode Status { get; }
Property Value
Type Description
ResponseCode
| Edit this page View Source

TransactionId

The transaction ID generated by the library (or client code) identifying the request: the id to settle the outcome by on a mirror node.

Declaration
public TransactionId TransactionId { get; }
Property Value
Type Description
TransactionId

Implements

ISerializable
  • Edit this page
  • View Source
In this article
Back to top .NET Client Library for Hiero Network and Hedera Hashgraph