{"record":{"id":"a2ab219c713043b1","repo":"nautechsystems/nautilus_trader","slug":"replacement-hash-transaction-hash-conflicts-with","errorCode":null,"errorMessage":"Replacement hash {transaction_hash} conflicts with another intent","messagePattern":"Replacement hash (.+?) conflicts with another intent","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/adapters/blockchain/src/cache/database.rs","lineNumber":3839,"sourceCode":"                intent_id, chain_id, transaction_hash, status, current\n            ) VALUES ($1, $2, $3, 'replaced', TRUE)\n            ON CONFLICT (chain_id, transaction_hash) DO UPDATE\n            SET current = TRUE, updated_at = NOW()\n            WHERE execution_transaction_hash.intent_id = EXCLUDED.intent_id\n            RETURNING\n                id, intent_id, chain_id, transaction_hash, raw_transaction, status,\n                block_number, block_hash, receipt_success, gas_used,\n                effective_gas_price, current\n            \",\n        )\n        .bind(intent_id)\n        .bind(chain_id_db)\n        .bind(transaction_hash)\n        .fetch_optional(&mut *transaction)\n        .await\n        .map_err(|e| anyhow::anyhow!(\"Failed to persist replacement hash {transaction_hash}: {e}\"))?\n        .ok_or_else(|| {\n            anyhow::anyhow!(\"Replacement hash {transaction_hash} conflicts with another intent\")\n        })?;\n\n        sqlx::query(\n            \"UPDATE execution_intent SET status = 'replaced', updated_at = NOW() WHERE id = $1\",\n        )\n        .bind(intent_id)\n        .execute(&mut *transaction)\n        .await\n        .map_err(|e| anyhow::anyhow!(\"Failed to mark execution intent replaced: {e}\"))?;\n        sqlx::query(\n            \"\n            INSERT INTO execution_transaction_transition (\n                intent_id, transaction_hash_id, transition_key, from_status, to_status\n            ) VALUES ($1, $2, $3, $4, 'replaced')\n            ON CONFLICT (intent_id, transition_key) DO NOTHING\n            \",\n        )\n        .bind(intent_id)","sourceCodeStart":3821,"sourceCodeEnd":3857,"githubUrl":"https://github.com/nautechsystems/nautilus_trader/blob/2114cf6f761429e0adb5ca9596fcd7b895b16011/crates/adapters/blockchain/src/cache/database.rs#L3821-L3857","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before: discover cross-intent ownership only via the error\nlet row = db.add_execution_replacement_hash(intent_id, chain_id, &hash).await?;\n\n// after: pre-check hash ownership on the chain\nlet owner = sqlx::query_scalar::<_, i64>(\n    \"SELECT intent_id FROM execution_transaction_hash WHERE chain_id = $1 AND transaction_hash = $2\",\n)\n.bind(chain_id as i32).bind(&hash).fetch_optional(&pool).await?;\nmatch owner {\n    Some(id) if id != intent_id => anyhow::bail!(\"hash {hash} already owned by intent {id}\"),\n    _ => { let row = db.add_execution_replacement_hash(intent_id, chain_id, &hash).await?; }\n}","handlingStrategy":"validation","validationCode":"// pre-check ownership of the (chain_id, transaction_hash) pair\nlet owner = sqlx::query_scalar::<_, i64>(\n    \"SELECT intent_id FROM execution_transaction_hash WHERE chain_id = $1 AND transaction_hash = $2\",\n)\n.bind(i32::try_from(chain_id)?)\n.bind(&hash)\n.fetch_optional(&pool)\n.await?;\nif matches!(owner, Some(other) if other != intent_id) {\n    anyhow::bail!(\"hash {hash} on chain {chain_id} already owned by intent {other}\");\n}","typeGuard":null,"tryCatchPattern":"match db.add_execution_replacement_hash(intent_id, chain_id, &hash).await {\n    Ok(row) => Ok(row),\n    Err(e) if e.to_string().contains(\"conflicts with another intent\") => {\n        // reconcile duplicate intents / misattribution; never delete the owning row to force it\n        reconcile_and_alert(e)\n    }\n    Err(e) => Err(e),\n}","preventionTips":["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"],"tags":["rust","sqlx","postgres","blockchain","conflict","data-integrity","replacement"],"backgroundTag":"unique-constraint-violation","analyzedSha":"2114cf6f761429e0adb5ca9596fcd7b895b16011","analyzedAt":"2026-08-21T11:28:30.864Z","schemaVersion":2},"datasetVersion":"2026-08-22T04:17:13.399Z"}