nautechsystems/nautilus_trader · error · anyhow::Error
Finalized transaction {} emitted {} Swap logs; expected exac
Error message
Finalized transaction {} emitted {} Swap logs; expected exactly one What it means
Thrown by emit_finalized_swap_fill (crates/adapters/blockchain/src/execution/client.rs:2206) when validating the receipt of a finalized swap transaction. The client filters the receipt logs for the Uniswap V3 Swap event (first topic == keccak256("Swap(address,address,int256,int256,uint160,uint128,int24)")) and requires exactly one match, so a fill maps one-to-one onto a single pool swap. Any other count (zero or multiple) aborts fill emission; the execution transaction record stays owned so reconciliation can retry.
Source
Thrown at crates/adapters/blockchain/src/execution/client.rs:2220
.mark_execution_event_emitted(intent_id, "fill")
.await
}
async fn emit_finalized_swap_fill(
plan: &SwapPlan,
included: &IncludedTransaction,
executor: &TransactionExecutor,
emitter: &ExecutionEventEmitter,
) -> anyhow::Result<()> {
let signature =
keccak256("Swap(address,address,int256,int256,uint160,uint128,int24)").to_string();
let swap_logs = included
.receipt
.logs
.iter()
.filter(|log| log.topics.first().is_some_and(|topic| topic == &signature))
.collect::<Vec<_>>();
anyhow::ensure!(
swap_logs.len() == 1,
"Finalized transaction {} emitted {} Swap logs; expected exactly one",
included.tx_hash,
swap_logs.len()
);
let log = swap_logs[0];
let address = Address::from_str(&log.address)
.with_context(|| format!("Invalid finalized Swap log address {}", log.address))?;
anyhow::ensure!(
address == plan.pool_address,
"Finalized Swap log came from pool {address}, expected {}",
plan.pool_address
);
let dex = crate::exchanges::get_dex_extended(plan.pool.chain.name, &plan.pool.dex.name)
.ok_or_else(|| {
anyhow::anyhow!(
"No RPC Swap decoder for {}:{}",View on GitHub (pinned to 2114cf6f76)
Solutions
- Look up the transaction hash on a block explorer and count logs whose topic0 is the V3 Swap signature; this tells you whether the count is 0 or >1.
- If >1: change the order routing so the executor submits a single-pool exact-input swap with no intermediate hops.
- If 0: verify the pool really is a Uniswap V3-compatible pool and that the pool's dex/chain registration points at the V3 Swap decoder.
- Confirm no unrelated swap shares the transaction (batching, aggregator settlement in the same tx).
- Leave the record for reconciliation after fixing routing; do not hand-edit the transaction record.
Example fix
// before: route through router with multi-hop path let path = [token_in, weth, token_out]; // emits 2 Swap logs // after: exact single-pool swap the executor validates let pool = plan.pool_address; // one V3 pool, exactly one Swap log
Defensive patterns
Strategy: validation
Validate before calling
fn count_v3_swap_logs(receipt: &RpcTransactionReceipt) -> usize {
let sig = keccak256("Swap(address,address,int256,int256,uint160,uint128,int24)").to_string();
receipt.logs.iter().filter(|l| l.topics.first().is_some_and(|t| t == &sig)).count()
}
// before triggering the finalized-fill path:
if count_v3_swap_logs(&receipt) != 1 {
log::warn!("tx {} has {} V3 Swap logs; routing must be single-hop", tx_hash, n);
} Type guard
fn is_single_v3_swap(receipt: &RpcTransactionReceipt) -> bool {
count_v3_swap_logs(receipt) == 1
} Try / catch
Match the anyhow error on substring "expected exactly one"; keep the execution transaction record owned, alert on routing misconfiguration, and let reconciliation retry after the routing is fixed.
Prevention
- Configure swaps as exact single-pool (single-hop) routes; forbid multi-hop paths in strategy config.
- Register pools under the dex variant whose Swap event signature the decoder expects.
- Integration-test the submit-to-finalize path against a forked chain so routing regressions surface before live capital.
When it happens
Trigger: The submitted swap routed through multiple V3 pools (multi-hop router path), the same transaction contained another V3 swap (aggregator/batched path), or the swap happened on a different protocol version whose event signature does not match (Uniswap V2 Pair Swap, V4 PoolManager Swap) yielding zero matches. Also produced by tests replaying synthetic receipts (see finalized_swap_without_log_rejects_and_releases_ownership).
Common situations: Pool configured with a dex name that emits a non-V3 Swap signature; strategy submitting a route with intermediate hops instead of an exact single-pool swap; MEV bundles or manual pre-signing that adds extra swaps into the same transaction.
Related errors
- Finalized execution transaction {tx_hash} no longer has a re
- Finalized Swap input {base_amount} does not match the persis
- Execution intent {} uses unsupported schema version {}
- Execution intent {} has unknown purpose {}
- Active execution intent {} has no nonce
AI-assisted analysis of nautechsystems/nautilus_trader@2114cf6f76 (2026-08-21).
Data as JSON: /api/errors/f08a9f3a3bd34f11.
Report an issue: GitHub.