{"record":{"id":"e83c817952f015ff","repo":"nautechsystems/nautilus_trader","slug":"persisted-swap-intent-has-no-client-order-id","errorCode":null,"errorMessage":"Persisted swap intent has no client order ID","messagePattern":"Persisted swap intent has no client order ID","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/adapters/blockchain/src/execution/client.rs","lineNumber":822,"sourceCode":"            wallet_balance: Arc::clone(&self.wallet_balance),\n            account_id: self.core.account_id,\n            wallet_address: self.wallet_address,\n            chain_id: self.chain.chain_id,\n            max_fee_per_gas_wei: self.config.max_fee_per_gas_wei,\n            base_fee_buffer_bps: self.config.base_fee_buffer_bps,\n            gas_limit: self.config.gas_limit,\n            gas_buffer_bps: self.config.gas_buffer_bps,\n            receipt_timeout: receipt_timeout(self.transaction_limits.receipt_timeout_secs),\n            receipt_max_polls: receipt_max_polls(self.transaction_limits.receipt_timeout_secs),\n        })\n    }\n\n    fn restore_swap_plan(&self, intent: &ExecutionIntentRow) -> anyhow::Result<SwapPlan> {\n        let client_order_id = ClientOrderId::new_checked(\n            intent\n                .client_order_id\n                .as_deref()\n                .ok_or_else(|| anyhow::anyhow!(\"Persisted swap intent has no client order ID\"))?,\n        )?;\n        let order = self\n            .core\n            .cache()\n            .try_order_owned(&client_order_id)\n            .with_context(|| {\n                format!(\n                    \"Cannot reconcile swap intent {} because order {client_order_id} is not restored\",\n                    intent.id\n                )\n            })?;\n        let instrument_id = InstrumentId::from_str(\n            intent\n                .instrument_id\n                .as_deref()\n                .ok_or_else(|| anyhow::anyhow!(\"Persisted swap intent has no instrument ID\"))?,\n        )?;\n        anyhow::ensure!(","sourceCodeStart":804,"sourceCodeEnd":840,"githubUrl":"https://github.com/nautechsystems/nautilus_trader/blob/2114cf6f761429e0adb5ca9596fcd7b895b16011/crates/adapters/blockchain/src/execution/client.rs#L804-L840","documentation":"During connect(), reconcile_unresolved_execution rebuilds any active swap intent via restore_swap_plan (crates/adapters/blockchain/src/execution/client.rs:822), which requires the persisted intent's client_order_id column to re-associate the on-chain transaction with the Nautilus order that spawned it. A NULL client_order_id on a swap intent is a durable-state integrity violation: the reconciliation cannot look up the order in the restored cache, so it refuses rather than guessing. Swap intents are always written with the originating ClientOrderId, so this state implies an old-schema row, a partial write, or manual database edits.","triggerScenarios":"An execution_intent row with purpose=swap and NULL client_order_id exists while connect() runs reconciliation; rows written by an older writer version before client_order_id capture was mandatory; someone manually inserted or edited intent rows; a torn write left the row half-populated.","commonSituations":"Upgrading a deployment that carries old intent rows forward; DBAs trimming 'unused' columns; restoring a Postgres backup across schema versions.","solutions":["Inspect the row: SELECT id, status, purpose, client_order_id FROM execution_intent WHERE id = <id from error>.","Verify the on-chain outcome of the intent's transaction hash, then resolve the row - mark it recoverable/terminal or delete it - so subsequent connects can proceed.","Ensure the writer deployment is on the current EXECUTION_SCHEMA_VERSION so new intents always persist the client order id.","Never hand-edit intent columns; use the client's own status-transition paths."],"exampleFix":"-- before: poisoned swap intent blocks every connect()\nSELECT client_order_id FROM execution_intent WHERE id = 42;\n-- client_order_id | NULL\n\n-- after: verify on-chain state, then clear the intent for reconciliation\nSELECT transaction_hash FROM execution_transaction_hash WHERE intent_id = 42; -- check on explorer\nUPDATE execution_intent SET status = 'recoverable' WHERE id = 42;","handlingStrategy":"try-catch","validationCode":"-- Pre-flight: find active swap intents with a NULL client_order_id before restarting\nSELECT id, status, client_order_id\nFROM execution_intent\nWHERE status IN ('prepared', 'signed', 'submitted')\n  AND purpose = 'swap'\n  AND client_order_id IS NULL;","typeGuard":"fn is_missing_client_order_id(e: &anyhow::Error) -> bool {\n    e.to_string().contains(\"Persisted swap intent has no client order ID\")\n}","tryCatchPattern":"if let Err(e) = client.connect().await {\n    if is_missing_client_order_id(&e) {\n        // durable-state corruption: halt boot, inspect the intent row, resolve on-chain first\n        log::error!(\"poisoned swap intent blocks reconciliation: {e}\");\n        // operator: verify the intent's tx hashes on-chain, then mark resolved/recoverable\n    }\n    return Err(e);\n}","preventionTips":["Never hand-edit execution_intent rows; use the client's status-transition APIs.","Run the NULL-column pre-flight query above before rolling out schema or version upgrades.","Keep writer and reader deployments on the same EXECUTION_SCHEMA_VERSION."],"tags":["reconciliation","postgres","data-integrity","schema","rust"],"backgroundTag":"missing-required-field","analyzedSha":"2114cf6f761429e0adb5ca9596fcd7b895b16011","analyzedAt":"2026-08-21T11:28:30.864Z","schemaVersion":2},"datasetVersion":"2026-08-22T09:17:25.309Z"}