nautechsystems/nautilus_trader · error · anyhow::Error
Replacement hash {transaction_hash} conflicts with another i
Error message
Replacement hash {transaction_hash} conflicts with another intent What it means
Raised in add_execution_replacement_hash (database.rs:3838-3840) when the upsert's RETURNING clause yields no row. The ON CONFLICT (chain_id, transaction_hash) DO UPDATE is guarded by WHERE execution_transaction_hash.intent_id = EXCLUDED.intent_id: if a row with the same (chain_id, transaction_hash) already exists but belongs to a DIFFERENT intent, the DO UPDATE is suppressed, nothing is returned, and this error fires. It is an intentional cross-intent hash-ownership conflict, not an infrastructure fault.
Source
Thrown at crates/adapters/blockchain/src/cache/database.rs:3839
intent_id, chain_id, transaction_hash, status, current
) VALUES ($1, $2, $3, 'replaced', TRUE)
ON CONFLICT (chain_id, transaction_hash) DO UPDATE
SET current = TRUE, updated_at = NOW()
WHERE execution_transaction_hash.intent_id = EXCLUDED.intent_id
RETURNING
id, intent_id, chain_id, transaction_hash, raw_transaction, status,
block_number, block_hash, receipt_success, gas_used,
effective_gas_price, current
",
)
.bind(intent_id)
.bind(chain_id_db)
.bind(transaction_hash)
.fetch_optional(&mut *transaction)
.await
.map_err(|e| anyhow::anyhow!("Failed to persist replacement hash {transaction_hash}: {e}"))?
.ok_or_else(|| {
anyhow::anyhow!("Replacement hash {transaction_hash} conflicts with another intent")
})?;
sqlx::query(
"UPDATE execution_intent SET status = 'replaced', updated_at = NOW() WHERE id = $1",
)
.bind(intent_id)
.execute(&mut *transaction)
.await
.map_err(|e| anyhow::anyhow!("Failed to mark execution intent replaced: {e}"))?;
sqlx::query(
"
INSERT INTO execution_transaction_transition (
intent_id, transaction_hash_id, transition_key, from_status, to_status
) VALUES ($1, $2, $3, $4, 'replaced')
ON CONFLICT (intent_id, transition_key) DO NOTHING
",
)
.bind(intent_id)View on GitHub (pinned to 2114cf6f76)
Solutions
- Find the owning row: SELECT intent_id, status FROM execution_transaction_hash WHERE chain_id = $1 AND transaction_hash = $2
- If both intents refer to the same logical order, consolidate on the owning intent and drop/complete the duplicate rather than forcing this insert
- If the hash was attributed to the wrong intent, fix the watcher's nonce/hash-to-intent matching before calling again
- Do not remove the conflict guard or delete the owning row to make the insert pass - the invariant one hash = one intent per chain is what the guard protects
Example fix
// before: discover cross-intent ownership only via the error
let row = db.add_execution_replacement_hash(intent_id, chain_id, &hash).await?;
// after: pre-check hash ownership on the chain
let owner = sqlx::query_scalar::<_, i64>(
"SELECT intent_id FROM execution_transaction_hash WHERE chain_id = $1 AND transaction_hash = $2",
)
.bind(chain_id as i32).bind(&hash).fetch_optional(&pool).await?;
match owner {
Some(id) if id != intent_id => anyhow::bail!("hash {hash} already owned by intent {id}"),
_ => { let row = db.add_execution_replacement_hash(intent_id, chain_id, &hash).await?; }
} Defensive patterns
Strategy: validation
Validate before calling
// pre-check ownership of the (chain_id, transaction_hash) pair
let owner = sqlx::query_scalar::<_, i64>(
"SELECT intent_id FROM execution_transaction_hash WHERE chain_id = $1 AND transaction_hash = $2",
)
.bind(i32::try_from(chain_id)?)
.bind(&hash)
.fetch_optional(&pool)
.await?;
if matches!(owner, Some(other) if other != intent_id) {
anyhow::bail!("hash {hash} on chain {chain_id} already owned by intent {other}");
} Try / catch
match db.add_execution_replacement_hash(intent_id, chain_id, &hash).await {
Ok(row) => Ok(row),
Err(e) if e.to_string().contains("conflicts with another intent") => {
// reconcile duplicate intents / misattribution; never delete the owning row to force it
reconcile_and_alert(e)
}
Err(e) => Err(e),
} Prevention
- Deduplicate intent creation for the same order/nonce so two intent rows never share a chain
- Attribute replacement hashes by (wallet, nonce, chain) before recording them
- Treat the one-hash-one-intent invariant as load-bearing: the conflict guard protects the audit trail
- When replaying old events after a rebuild, verify hash ownership first
When it happens
Trigger: The same canonical transaction hash already recorded under another intent on the same chain: duplicate intent submissions that ended up with two intent rows for one nonce, a watcher attributing a replacement hash to the wrong intent_id, or two wallets accidentally sharing a signer so one intent consumed the other's hash.
Common situations: Retrying a submission created a second intent row while the first already persisted the hash; recovery tooling replaying old replacement events against new intents; multi-instance adapters both recording the on-chain replacement against their own local intent ids.
Related errors
- Execution transaction hash {transaction_hash} was not found
- Failed to retire replaced execution hash: {e}
- Failed to persist replacement hash {transaction_hash}: {e}
- Failed to mark execution intent replaced: {e}
- Failed to commit replacement transaction: {e}
AI-assisted analysis of nautechsystems/nautilus_trader@2114cf6f76 (2026-08-21).
Data as JSON: /api/errors/a2ab219c713043b1.
Report an issue: GitHub.