nautechsystems/nautilus_trader · error · anyhow::Error

Execution intent {intent_id} is {current_status}, not prepar

Error message

Execution intent {intent_id} is {current_status}, not prepared for signing

What it means

The execution intent exists but its status is neither 'prepared' nor 'signed', so persisting a signed transaction is an invalid state transition. The library enforces a strict state machine: only prepared (or idempotently re-signed) intents may accept a signed payload.

Source

Thrown at crates/adapters/blockchain/src/cache/database.rs:6660

            )
            .bind(EXECUTION_PAYLOAD_COMPONENT)
            .fetch_one(&mut *transaction)
            .await
            .context("failed to inspect execution payload marker")?;
            anyhow::ensure!(
                !marker,
                "Plaintext signed transaction persistence is disabled after payload activation"
            );
        }
        let current_status = sqlx::query_scalar::<_, String>(
            "SELECT status FROM execution_intent WHERE id = $1 FOR UPDATE",
        )
        .bind(intent_id)
        .fetch_optional(&mut *transaction)
        .await
        .map_err(|e| anyhow::anyhow!("Failed to lock execution intent {intent_id}: {e}"))?
        .ok_or_else(|| anyhow::anyhow!("Execution intent {intent_id} was not found"))?;
        anyhow::ensure!(
            current_status == TransactionStatus::Prepared.as_str()
                || current_status == TransactionStatus::Signed.as_str(),
            "Execution intent {intent_id} is {current_status}, not prepared for signing"
        );

        let row = sqlx::query_as::<_, ExecutionTransactionHashRow>(
            "
            INSERT INTO execution_transaction_hash (
                intent_id, chain_id, transaction_hash, payload_expected,
                raw_transaction, sealed_transaction, status
            )
            SELECT id, chain_id, $3, TRUE, $4, $5, 'signed'
            FROM execution_intent
            WHERE id = $1 AND chain_id = $2 AND nonce IS NOT NULL
            ON CONFLICT (chain_id, transaction_hash) DO UPDATE
            SET transaction_hash = EXCLUDED.transaction_hash
            WHERE execution_transaction_hash.intent_id = EXCLUDED.intent_id
              AND execution_transaction_hash.payload_expected

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Fetch the intent's current status first and skip persistence if it is already past 'signed' (the work is done).
  2. If the status is a terminal/failure state, create a new intent with a fresh nonce instead of re-signing.
  3. Resolve concurrent workers (single-owner queue) so only one actor advances an intent.
  4. If status is stale due to a crashed prior run, reconcile the intent via the observation/recovery path rather than forcing a signed persist.

Example fix

// before
db.add_execution_transaction(intent_id, ...).await?;
// after
let status = db.intent_status(intent_id).await?;
if status != "prepared" && status != "signed" { return Ok(()); /* already advanced */ }
db.add_execution_transaction(intent_id, ...).await?;
Defensive patterns

Strategy: validation

Validate before calling

let status: String = sqlx::query_scalar("SELECT status FROM execution_intent WHERE id=$1").bind(intent_id).fetch_one(&pool).await?;
if !(status == "prepared" || status == "signed") {
    tracing::info!(%intent_id, %status, "intent already advanced; skipping signed persist");
    return Ok(SkipReason::AlreadyAdvanced);
}

Type guard

fn signable_status(status: &str) -> bool { status == "prepared" || status == "signed" }

Try / catch

match db.add_execution_transaction(intent_id, ...).await {
    Err(e) if e.to_string().contains("not prepared for signing") => {
        let status = fetch_status(intent_id).await?;
        info!(%status, "idempotent skip: intent already advanced");
    }
    r => r?,
}

Prevention

When it happens

Trigger: Calling add_execution_transaction on an intent whose status is e.g. 'broadcast', 'confirmed', 'failed', or 'expired'. Typically a duplicate signing attempt after the transaction was already broadcast, a replay of an old signing job, or a concurrent worker that advanced the status between fetch and persist.

Common situations: Re-running a signing batch after a crash where intents already advanced; two consumers processing the same intent queue message; retrying a job that previously succeeded and moved the intent to a later status.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08). Data as JSON: /api/errors/dd1ed01e412100ea. Report an issue: GitHub.