nautechsystems/nautilus_trader · error · anyhow::Error

Execution intent is , not prepared for signing

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.

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.

Appendix: 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)