Skip to main content

nautilus_blockchain/execution/
client.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use std::{
17    collections::{HashMap, HashSet},
18    fmt::Debug,
19    ops::RangeInclusive,
20    str::FromStr,
21    sync::Arc,
22    time::{Duration, SystemTime, UNIX_EPOCH},
23};
24
25use alloy::{
26    primitives::{
27        Address, B256, Bytes, I256, U256,
28        aliases::{U24, U160},
29        keccak256,
30    },
31    signers::local::PrivateKeySigner,
32    sol_types::SolCall,
33};
34use anyhow::Context;
35use async_trait::async_trait;
36use nautilus_common::{
37    clients::ExecutionClient,
38    live::runner::get_exec_event_sender,
39    messages::execution::{
40        BatchCancelOrders, CancelAllOrders, CancelOrder, GenerateFillReports,
41        GenerateOrderStatusReport, GenerateOrderStatusReports, GeneratePositionStatusReports,
42        ModifyOrder, QueryAccount, QueryOrder, SubmitOrder, SubmitOrderList,
43    },
44};
45use nautilus_core::{
46    Params, UUID4, UnixNanos, datetime::NANOSECONDS_IN_SECOND, hex, time::get_atomic_clock_realtime,
47};
48use nautilus_live::{
49    ExecutionClientCore, ExecutionEventEmitter,
50    task::{TaskGroup, TaskGroupGuard},
51};
52use nautilus_model::{
53    accounts::AccountAny,
54    defi::{
55        DexType, Pool, PoolIdentifier, SharedChain, Token,
56        data::block::{BLOCK_SCOPED_SNAPSHOT_INDEX, BlockPosition},
57        pool_analysis::quote::SwapQuote,
58        validation::validate_address,
59        wallet::{TokenBalance, WalletBalance},
60    },
61    enums::{CurrencyType, LiquiditySide, OmsType, OrderSide, OrderStatus, OrderType},
62    events::{OrderCanceled, OrderDeniedReason, OrderEventAny, OrderFilled, OrderRejected},
63    identifiers::{AccountId, ClientId, ClientOrderId, InstrumentId, TradeId, Venue, VenueOrderId},
64    orders::{Order, OrderAny},
65    reports::{ExecutionMassStatus, FillReport, OrderStatusReport, PositionStatusReport},
66    types::{
67        AccountBalance, Currency, MarginBalance, Money, Price, Quantity, fixed::FIXED_PRECISION,
68    },
69};
70use parking_lot::Mutex;
71use zeroize::Zeroizing;
72
73use crate::{
74    cache::{
75        BlockchainCache,
76        database::{
77            BlockchainCacheDatabase, ExecutionFinalityTransition, ExecutionNonceAssignment,
78            ExecutionPayloadCheck, ExecutionPayloadLease, ExecutionReplacementScan,
79            ExecutionVerificationBatch, ExecutionVerificationBootstrap,
80            ExecutionVerificationDecision, ExecutionVerificationMigration,
81            ExecutionVerificationMigrationRecord, ExecutionVerificationMigrationSnapshot,
82            ExecutionVerifiedHeader, reservation_failure_proven_not_committed,
83        },
84        rows::{ExecutionIntentInsert, ExecutionIntentRow, ExecutionTransactionHashRow},
85    },
86    config::{
87        BlockchainContractRole, BlockchainDeploymentManifest, BlockchainExecutionClientConfig,
88    },
89    contracts::{
90        erc20::{ERC20, Erc20Contract},
91        uniswap_v3_quote::UniswapV3Quote,
92        uniswap_v3_swap::{UniswapV3Factory, UniswapV3RouterState, UniswapV3SwapRouter},
93        weth::WETH9,
94    },
95    execution::{
96        preflight::{
97            BlockchainPreflightReport, ContractCodeCheck, PoolPreflightCheck, TokenPreflightCheck,
98        },
99        sealing::{
100            PayloadKeySet, PayloadPolicy, authenticate_payload, authenticate_payload_identity,
101            authenticate_retained_payload, payload_context, payload_context_identity,
102            persisted_call_fields, retained_payload_requires_policy,
103        },
104        transaction::{
105            TransactionPurpose, TransactionStatus, build_eip1559_transaction, compute_max_fee,
106            decode_signed_transaction, derive_fees, derive_gas_limit, sign_eip1559_transaction,
107        },
108    },
109    rpc::{
110        error::BroadcastError,
111        http::{BlockchainHttpRpcClient, EXECUTION_RPC_TIMEOUT_SECS},
112        log as rpc_log,
113        types::{RpcCallType, RpcTransaction, RpcTransactionReceipt},
114        verification::{
115            VerificationCoordinator, VerificationOutcome, Verified, VerifiedBlockHeader,
116            VerifiedCallTrace, VerifiedSimulation,
117        },
118    },
119};
120
121const RECEIPT_POLL_INTERVAL: Duration = Duration::from_secs(1);
122const MAX_PAYLOAD_OPERATION_BATCH_SIZE: usize = 1_000;
123const BPS_DENOMINATOR: u32 = 10_000;
124/// Denial reason for order-list submissions, which have no on-chain execution route.
125const ORDER_LIST_UNSUPPORTED: &str =
126    "Order lists are not supported; submit each order individually";
127/// Rejection reason for order modifications, which immutable on-chain swaps cannot support.
128const ORDER_MODIFY_UNSUPPORTED: &str = "Order modification is not supported";
129/// Rejection reason for order cancellations, which immutable on-chain swaps cannot support.
130const ORDER_CANCEL_UNSUPPORTED: &str = "Order cancellation is not supported";
131
132/// Error for venue report probes that cannot answer without implying absence.
133const VENUE_EXECUTION_REPORTS_UNSUPPORTED: &str =
134    "Venue execution reports are not supported on the blockchain execution client";
135const MAX_REPLACEMENT_SCAN_BLOCKS: u64 = 4_096;
136
137/// Execution client for blockchain interactions including balance tracking and order execution.
138#[derive(Debug)]
139pub struct BlockchainExecutionClient {
140    core: ExecutionClientCore,
141    emitter: ExecutionEventEmitter,
142    cache: BlockchainCache,
143    config: BlockchainExecutionClientConfig,
144    chain: SharedChain,
145    wallet_address: Address,
146    signer: Option<Arc<PrivateKeySigner>>,
147    payload_keys: Option<Arc<PayloadKeySet>>,
148    router_addresses: Vec<Address>,
149    transaction_limits: TransactionLimits,
150    weth_address: Address,
151    in_flight: Arc<Mutex<Option<InFlightSlot>>>,
152    wallet_balance: Arc<Mutex<WalletBalance>>,
153    erc20_contract: Erc20Contract,
154    http_rpc_client: Arc<BlockchainHttpRpcClient>,
155    verification: VerificationCoordinator,
156    pending_tasks: TaskGroup,
157}
158
159impl BlockchainExecutionClient {
160    /// Creates a new [`BlockchainExecutionClient`] instance for the specified configuration.
161    ///
162    /// # Errors
163    ///
164    /// Returns an error if transaction limits are missing or invalid, independent verification is
165    /// missing or conflicts with the configured chain, the deployment manifest or provider
166    /// topology is invalid, a configured address or token pair is invalid, the router allowlist is
167    /// empty, or the slippage bounds are inconsistent or not below 100%.
168    pub fn new(
169        core_client: ExecutionClientCore,
170        config: BlockchainExecutionClientConfig,
171    ) -> anyhow::Result<Self> {
172        let transaction_limits = Self::transaction_limits(&config)?;
173        let chain = Arc::new(config.chain.clone());
174        let cache = BlockchainCache::new(Arc::clone(&chain));
175        let http_rpc_client = Arc::new(BlockchainHttpRpcClient::new(
176            config.http_rpc_url.clone().into_inner(),
177            config.rpc_requests_per_second,
178            None,
179        ));
180        let verification_config = config.verification.as_ref().ok_or_else(|| {
181            anyhow::anyhow!("Independent Blockchain execution verification is required")
182        })?;
183        anyhow::ensure!(
184            verification_config.chain_anchor.chain_id == config.chain.chain_id,
185            "Verification chain anchor ID does not match the configured chain"
186        );
187        anyhow::ensure!(
188            verification_config.chain_anchor.chain_name == config.chain.name.to_string(),
189            "Verification chain anchor name does not match the configured chain"
190        );
191        let verification = VerificationCoordinator::new(
192            Arc::clone(&http_rpc_client),
193            config.http_rpc_url.expose_secret(),
194            verification_config,
195            config.rpc_requests_per_second,
196        )?;
197        let wallet_address = validate_address(config.wallet_address.as_str())?;
198        let erc20_contract = Erc20Contract::new_with_timeout(
199            Arc::clone(&http_rpc_client),
200            Some(EXECUTION_RPC_TIMEOUT_SECS),
201            true,
202        );
203
204        let router_addresses = config
205            .router_addresses
206            .iter()
207            .map(|address| validate_address(address.as_str()))
208            .collect::<anyhow::Result<Vec<_>>>()?;
209        if router_addresses.is_empty() {
210            anyhow::bail!("`router_addresses` must contain at least one router address");
211        }
212        let weth_address = validate_address(config.weth_address.as_str())?;
213        Self::validate_manifest_contracts(&config, &router_addresses, weth_address)?;
214
215        let mut token_universe = HashSet::new();
216
217        if let Some(specified_tokens) = &config.tokens {
218            for token in specified_tokens {
219                let token_address = validate_address(token.as_str())?;
220                token_universe.insert(token_address);
221            }
222        }
223        let wallet_balance = WalletBalance::new(token_universe);
224        let emitter = ExecutionEventEmitter::new(
225            get_atomic_clock_realtime(),
226            core_client.trader_id,
227            core_client.account_id,
228            core_client.account_type,
229            core_client.base_currency,
230        );
231
232        let pending_tasks = TaskGroup::new();
233
234        Ok(Self {
235            core: core_client,
236            emitter,
237            wallet_balance: Arc::new(Mutex::new(wallet_balance)),
238            chain,
239            cache,
240            config,
241            signer: None,
242            payload_keys: None,
243            router_addresses,
244            transaction_limits,
245            weth_address,
246            in_flight: Arc::new(Mutex::new(None)),
247            erc20_contract,
248            http_rpc_client,
249            verification,
250            wallet_address,
251            pending_tasks,
252        })
253    }
254
255    fn transaction_limits(
256        config: &BlockchainExecutionClientConfig,
257    ) -> anyhow::Result<TransactionLimits> {
258        let (
259            Some(allowed_token_pairs),
260            Some(slippage_bps),
261            Some(max_slippage_bps),
262            Some(max_order_amount),
263            Some(deadline_seconds),
264            Some(max_quote_age_blocks),
265            Some(receipt_timeout_secs),
266        ) = (
267            &config.allowed_token_pairs,
268            config.slippage_bps,
269            config.max_slippage_bps,
270            config.max_order_amount,
271            config.deadline_seconds,
272            config.max_quote_age_blocks,
273            config.receipt_timeout_secs,
274        )
275        else {
276            anyhow::bail!(
277                "Blockchain execution transaction limits are required: allowed_token_pairs, slippage_bps, max_slippage_bps, max_order_amount, deadline_seconds, max_quote_age_blocks, receipt_timeout_secs"
278            );
279        };
280
281        let mut parsed_pairs = HashSet::with_capacity(allowed_token_pairs.len());
282        for (token_in, token_out) in allowed_token_pairs {
283            parsed_pairs.insert((
284                validate_address(token_in.as_str())?,
285                validate_address(token_out.as_str())?,
286            ));
287        }
288
289        let quote_spend_limits = config.quote_spend_limits.as_deref().unwrap_or_default();
290        let mut parsed_quote_spend_limits = HashMap::with_capacity(quote_spend_limits.len());
291        for limit in quote_spend_limits {
292            let token_in = validate_address(limit.token_in.as_str())?;
293            let token_out = validate_address(limit.token_out.as_str())?;
294            let spend_token = validate_address(limit.spend_token.as_str())?;
295
296            if !parsed_pairs.contains(&(token_in, token_out)) {
297                anyhow::bail!(
298                    "Quote spend limit pair {token_in} -> {token_out} is not in the `allowed_token_pairs` allowlist"
299                );
300            }
301
302            if spend_token != token_in {
303                anyhow::bail!(
304                    "Quote spend limit for {token_in} -> {token_out} is denominated in {spend_token}; `spend_token` must match `token_in`"
305                );
306            }
307
308            if limit.max_amount.is_empty()
309                || !limit.max_amount.bytes().all(|byte| byte.is_ascii_digit())
310            {
311                anyhow::bail!(
312                    "Quote spend limit `max_amount` '{}' must be a base-10 unsigned integer string",
313                    limit.max_amount
314                );
315            }
316            let max_amount = U256::from_str(&limit.max_amount).map_err(|_| {
317                anyhow::anyhow!(
318                    "Quote spend limit `max_amount` '{}' exceeds the U256 range",
319                    limit.max_amount
320                )
321            })?;
322            let ceiling = QuoteSpendCeiling {
323                spend_token,
324                spend_token_decimals: limit.spend_token_decimals,
325                max_amount,
326            };
327
328            if parsed_quote_spend_limits
329                .insert((token_in, token_out), ceiling)
330                .is_some()
331            {
332                anyhow::bail!(
333                    "Duplicate quote spend limit for token pair {token_in} -> {token_out}"
334                );
335            }
336        }
337
338        if slippage_bps > max_slippage_bps {
339            anyhow::bail!(
340                "`slippage_bps` {slippage_bps} exceeds `max_slippage_bps` {max_slippage_bps}"
341            );
342        }
343
344        if max_slippage_bps >= BPS_DENOMINATOR {
345            anyhow::bail!("`max_slippage_bps` {max_slippage_bps} must be below {BPS_DENOMINATOR}");
346        }
347
348        if !(1..=4_095).contains(&max_quote_age_blocks) {
349            anyhow::bail!("`max_quote_age_blocks` must be in 1..=4095");
350        }
351
352        Ok(TransactionLimits {
353            allowed_token_pairs: parsed_pairs,
354            quote_spend_limits: parsed_quote_spend_limits,
355            slippage_bps,
356            max_slippage_bps,
357            max_order_amount,
358            deadline_seconds,
359            max_quote_age_blocks,
360            receipt_timeout_secs,
361        })
362    }
363
364    fn validate_manifest_contracts(
365        config: &BlockchainExecutionClientConfig,
366        routers: &[Address],
367        weth: Address,
368    ) -> anyhow::Result<()> {
369        let verification = config.verification.as_ref().ok_or_else(|| {
370            anyhow::anyhow!("Independent Blockchain execution verification is required")
371        })?;
372        let manifest = &verification.deployment_manifest;
373        let role_addresses = |role| {
374            manifest
375                .contracts
376                .iter()
377                .filter(|contract| contract.role == role)
378                .map(|contract| {
379                    Address::from_str(&contract.address)
380                        .map_err(|_| anyhow::anyhow!("Deployment manifest address is invalid"))
381                })
382                .collect::<anyhow::Result<HashSet<_>>>()
383        };
384        let singleton = |role, description: &str| {
385            let addresses = role_addresses(role)?;
386            anyhow::ensure!(
387                addresses.len() == 1,
388                "Deployment manifest must contain exactly one {description} contract"
389            );
390            Ok(*addresses.iter().next().expect("singleton role address"))
391        };
392
393        let configured_routers = routers.iter().copied().collect::<HashSet<_>>();
394        anyhow::ensure!(
395            role_addresses(BlockchainContractRole::Router)? == configured_routers,
396            "Deployment manifest router set does not match `router_addresses`"
397        );
398        anyhow::ensure!(
399            singleton(BlockchainContractRole::WrappedNative, "wrapped native")? == weth,
400            "Deployment manifest wrapped native contract does not match `weth_address`"
401        );
402        let factory = singleton(BlockchainContractRole::Factory, "factory")?;
403        let registered_factory =
404            crate::exchanges::get_dex_extended(config.chain.name, &DexType::UniswapV3)
405                .map(|dex| dex.factory)
406                .ok_or_else(|| {
407                    anyhow::anyhow!(
408                        "No registered Uniswap V3 deployment for chain {}",
409                        config.chain.name
410                    )
411                })?;
412        anyhow::ensure!(
413            factory == registered_factory,
414            "Deployment manifest factory does not match the registered Uniswap V3 factory"
415        );
416        let quote_contract = singleton(BlockchainContractRole::Quote, "quote")?;
417
418        let mut token_decimals = HashMap::new();
419
420        for token in &manifest.tokens {
421            let address = Address::from_str(&token.address)
422                .map_err(|_| anyhow::anyhow!("Deployment manifest token address is invalid"))?;
423            anyhow::ensure!(
424                token_decimals.insert(address, token.decimals).is_none(),
425                "Deployment manifest contains a duplicate token identity"
426            );
427        }
428        anyhow::ensure!(
429            token_decimals.contains_key(&weth),
430            "Deployment manifest has no wrapped native token identity"
431        );
432
433        if let Some(tokens) = &config.tokens {
434            for token in tokens {
435                let address = validate_address(token)?;
436                anyhow::ensure!(
437                    token_decimals.contains_key(&address),
438                    "Configured token {address} has no deployment manifest identity"
439                );
440            }
441        }
442
443        for (token_in, token_out) in config.allowed_token_pairs.as_deref().unwrap_or_default() {
444            let token_in = validate_address(token_in)?;
445            let token_out = validate_address(token_out)?;
446            anyhow::ensure!(
447                token_in != token_out
448                    && token_decimals.contains_key(&token_in)
449                    && token_decimals.contains_key(&token_out),
450                "Allowed token pair {token_in} -> {token_out} is not fully pinned by the deployment manifest"
451            );
452        }
453
454        for limit in config.quote_spend_limits.as_deref().unwrap_or_default() {
455            let spend_token = validate_address(&limit.spend_token)?;
456            anyhow::ensure!(
457                token_decimals.get(&spend_token) == Some(&limit.spend_token_decimals),
458                "Quote spend limit decimals do not match the deployment manifest"
459            );
460        }
461
462        let pool_contracts = role_addresses(BlockchainContractRole::Pool)?;
463
464        for pool in &manifest.pools {
465            let pool_address = Address::from_str(&pool.address)
466                .map_err(|_| anyhow::anyhow!("Deployment manifest pool address is invalid"))?;
467            let pool_factory = Address::from_str(&pool.factory)
468                .map_err(|_| anyhow::anyhow!("Deployment manifest pool factory is invalid"))?;
469            let pool_quote = Address::from_str(&pool.quote_contract).map_err(|_| {
470                anyhow::anyhow!("Deployment manifest pool quote contract is invalid")
471            })?;
472            anyhow::ensure!(
473                pool_contracts.contains(&pool_address)
474                    && pool_factory == factory
475                    && pool_quote == quote_contract,
476                "Deployment manifest pool does not use the pinned pool, factory, and quote identities"
477            );
478        }
479        Ok(())
480    }
481
482    async fn fetch_native_currency_balance(&self) -> anyhow::Result<Money> {
483        let balance_u256 = self
484            .http_rpc_client
485            .get_balance_with_timeout(&self.wallet_address, None, Some(EXECUTION_RPC_TIMEOUT_SECS))
486            .await?;
487
488        let native_currency = self.chain.native_currency();
489
490        Money::from_u256(balance_u256, native_currency).map_err(Into::into)
491    }
492
493    async fn fetch_token_balance(
494        &mut self,
495        token_address: &Address,
496    ) -> anyhow::Result<TokenBalance> {
497        let token = if let Some(token) = self.cache.get_token(token_address) {
498            token.to_owned()
499        } else {
500            let token_info = self.erc20_contract.fetch_token_info(token_address).await?;
501            let token = Token::new(
502                Arc::clone(&self.chain),
503                *token_address,
504                token_info.name,
505                token_info.symbol,
506                token_info.decimals,
507            );
508            self.cache.add_token(token.clone()).await?;
509            token
510        };
511
512        let amount = self
513            .erc20_contract
514            .balance_of(token_address, &self.wallet_address)
515            .await?;
516        let token_balance = TokenBalance::new(amount, token);
517
518        // TODO: Use price oracle here and cache, to get the latest price then convert to USD
519        // then use token_balance.set_amount_usd(amount_usd) to set the amount_usd value.
520
521        Ok(token_balance)
522    }
523
524    /// Refreshes and publishes all native currency and tracked ERC-20 balances.
525    async fn refresh_wallet_balances(&mut self) -> anyhow::Result<()> {
526        let (wallet_balance, balances) = self.fetch_wallet_balances().await?;
527        self.generate_account_state(
528            balances,
529            vec![],
530            true,
531            get_atomic_clock_realtime().get_time_ns(),
532            None,
533        )?;
534        *self.wallet_balance.lock() = wallet_balance;
535        Ok(())
536    }
537
538    async fn fetch_wallet_balances(
539        &mut self,
540    ) -> anyhow::Result<(WalletBalance, Vec<AccountBalance>)> {
541        let native_currency_balance = self.fetch_native_currency_balance().await?;
542        let token_universe = self.wallet_balance.lock().token_universe.clone();
543        let mut token_addresses = token_universe.iter().copied().collect::<Vec<_>>();
544        token_addresses.sort_unstable();
545
546        let mut token_balances = Vec::with_capacity(token_addresses.len());
547        for token_address in token_addresses {
548            let token_balance = self
549                .fetch_token_balance(&token_address)
550                .await
551                .with_context(|| format!("failed to fetch token balance for {token_address}"))?;
552            token_balances.push(token_balance);
553        }
554
555        let mut wallet_balance = WalletBalance::new(token_universe);
556        let balances = wallet_balance.replace_balances(native_currency_balance, token_balances)?;
557        log::debug!(
558            "Refreshed wallet balance with {} account balances",
559            balances.len()
560        );
561        Ok((wallet_balance, balances))
562    }
563
564    /// Runs a read-only execution preflight for the pool selected by `instrument_id`.
565    ///
566    /// Verifies the RPC chain ID against configuration, deployed bytecode at the router, pool,
567    /// and token addresses, wallet native and token balances, exact router allowance of the
568    /// pool's base (input) token, and current fee conditions. Changes no state.
569    ///
570    /// # Errors
571    ///
572    /// Returns an error if the instrument ID cannot be resolved to a cached Uniswap V3 pool
573    /// on the client's chain, or if any RPC call fails. On-chain readiness failures are
574    /// reported in the returned [`BlockchainPreflightReport`], not as errors.
575    pub async fn preflight(
576        &self,
577        instrument_id: &InstrumentId,
578    ) -> anyhow::Result<BlockchainPreflightReport> {
579        let pool = self.resolve_pool(instrument_id)?;
580        let base_token = pool.get_base_token().clone();
581        let quote_token = pool.get_quote_token().clone();
582
583        let actual_chain_id = self.http_rpc_client.chain_id().await?;
584
585        let pool_code = self.http_rpc_client.get_code(&pool.address).await?;
586        let pool_check = PoolPreflightCheck {
587            instrument_id: pool.instrument_id,
588            address: pool.address,
589            has_deployed_code: !pool_code.is_empty(),
590            fee: pool.fee,
591            base_token: base_token.address,
592            quote_token: quote_token.address,
593        };
594
595        let mut routers = Vec::with_capacity(self.router_addresses.len());
596        for router in &self.router_addresses {
597            let code = self.http_rpc_client.get_code(router).await?;
598            routers.push(ContractCodeCheck {
599                address: *router,
600                has_deployed_code: !code.is_empty(),
601            });
602        }
603
604        let mut tokens = Vec::with_capacity(2);
605
606        for token in [&base_token, &quote_token] {
607            let code = self.http_rpc_client.get_code(&token.address).await?;
608            let wallet_balance = self
609                .erc20_contract
610                .balance_of(&token.address, &self.wallet_address)
611                .await?;
612
613            let mut router_allowances = Vec::new();
614            if token.address == base_token.address {
615                router_allowances.reserve(self.router_addresses.len());
616                for router in &self.router_addresses {
617                    let amount = self
618                        .erc20_contract
619                        .allowance(&token.address, &self.wallet_address, router)
620                        .await?;
621                    router_allowances.push((*router, amount));
622                }
623            }
624
625            tokens.push(TokenPreflightCheck {
626                address: token.address,
627                symbol: token.symbol.clone(),
628                has_deployed_code: !code.is_empty(),
629                wallet_balance,
630                router_allowances,
631            });
632        }
633
634        let native_balance_wei = self
635            .http_rpc_client
636            .get_balance_with_timeout(&self.wallet_address, None, Some(EXECUTION_RPC_TIMEOUT_SECS))
637            .await?;
638
639        let latest_block = self.http_rpc_client.latest_block().await?;
640        let base_fee_per_gas_wei = latest_block.base_fee_per_gas.ok_or_else(|| {
641            anyhow::anyhow!("Latest block {} has no base fee", latest_block.number)
642        })?;
643        let max_priority_fee_per_gas_wei = self.http_rpc_client.max_priority_fee_per_gas().await?;
644        let derived_max_fee_per_gas_wei = compute_max_fee(
645            base_fee_per_gas_wei,
646            max_priority_fee_per_gas_wei,
647            self.config.base_fee_buffer_bps,
648        )?;
649
650        Ok(BlockchainPreflightReport::new(
651            u64::from(self.chain.chain_id),
652            actual_chain_id,
653            pool_check,
654            routers,
655            tokens,
656            native_balance_wei,
657            base_fee_per_gas_wei,
658            max_priority_fee_per_gas_wei,
659            derived_max_fee_per_gas_wei,
660            u128::from(self.config.max_fee_per_gas_wei),
661        ))
662    }
663
664    /// Wraps native currency into the wrapped native token via a WETH `deposit()`
665    /// transaction carrying `amount_wei` of native value.
666    ///
667    /// This is an explicit operator operation; it never runs inside `submit_order`.
668    ///
669    /// # Errors
670    ///
671    /// Returns an error if the amount is zero, the WETH target is not a deployed ERC-20 contract,
672    /// the client is not connected, another transaction is in flight, no durable store is
673    /// configured, the WETH balance does not increase by `amount_wei`, or any RPC, policy, signing,
674    /// persistence, or broadcast step fails. A persistence failure after signing, or a failed
675    /// postcondition after finality, leaves the in-flight slot occupied.
676    pub async fn wrap(&mut self, amount_wei: U256) -> anyhow::Result<B256> {
677        if amount_wei.is_zero() {
678            anyhow::bail!("Wrap amount must be positive");
679        }
680
681        self.ensure_transaction_ready(TransactionPurpose::Wrap)?;
682
683        let calldata = WETH9::depositCall {}.abi_encode();
684        let executor = self.transaction_executor()?;
685        let included = executor
686            .transact(
687                self.weth_address,
688                amount_wei,
689                Bytes::from(calldata),
690                TransactionPurpose::Wrap,
691                None,
692                TransactionAuthorization::Wrap {
693                    weth: self.weth_address,
694                },
695            )
696            .await?;
697        let postconditions =
698            verify_wrap_balance_increase(&executor, &self.weth_address, amount_wei, &included)
699                .await?;
700        executor
701            .commit_verified_finality(&included, TransactionStatus::Finalized, &postconditions)
702            .await?;
703        executor
704            .database
705            .mark_execution_event_emitted(included.intent_id, "terminal")
706            .await?;
707        executor.release_slot();
708
709        Ok(included.tx_hash)
710    }
711
712    /// Approves an allowlisted SwapRouter to spend `amount` of `token` via an ERC-20
713    /// `approve` transaction. Zero always revokes the allowance. For nonzero requests,
714    /// `unlimited_approval` changes the target to `U256::MAX`.
715    ///
716    /// This is an explicit operator operation; it never runs inside `submit_order`.
717    ///
718    /// # Errors
719    ///
720    /// Returns an error if the router or token fails policy and deployment checks, a nonzero
721    /// allowance was not cleared first, approval simulation returns false or malformed data, the
722    /// client is not connected, another transaction is in flight, no durable store is configured,
723    /// the resulting allowance differs from the target, or any RPC, signing, persistence, or
724    /// broadcast step fails. A persistence failure after signing, or a failed postcondition after
725    /// finality, leaves the in-flight slot occupied.
726    pub async fn approve(
727        &mut self,
728        token: Address,
729        amount: U256,
730        router: Address,
731    ) -> anyhow::Result<B256> {
732        if !self.router_addresses.contains(&router) {
733            anyhow::bail!("Router {router} is not in the configured `router_addresses` allowlist");
734        }
735
736        if !amount.is_zero()
737            && !self
738                .transaction_limits
739                .allowed_token_pairs
740                .iter()
741                .any(|(token_in, _)| *token_in == token)
742        {
743            anyhow::bail!(
744                "Token {token} is not an input token in the configured `allowed_token_pairs`"
745            );
746        }
747
748        self.ensure_transaction_ready(TransactionPurpose::Approve)?;
749
750        let approval_amount = if amount.is_zero() {
751            U256::ZERO
752        } else if self.config.unlimited_approval {
753            U256::MAX
754        } else {
755            amount
756        };
757        let calldata = ERC20::approveCall {
758            spender: router,
759            amount: approval_amount,
760        }
761        .abi_encode();
762
763        let executor = self.transaction_executor()?;
764        let included = executor
765            .transact(
766                token,
767                U256::ZERO,
768                Bytes::from(calldata),
769                TransactionPurpose::Approve,
770                None,
771                TransactionAuthorization::Approve {
772                    token,
773                    router,
774                    amount: approval_amount,
775                },
776            )
777            .await?;
778        let postconditions =
779            verify_approve_allowance(&executor, &token, &router, approval_amount, &included)
780                .await?;
781        executor
782            .commit_verified_finality(&included, TransactionStatus::Finalized, &postconditions)
783            .await?;
784        executor
785            .database
786            .mark_execution_event_emitted(included.intent_id, "terminal")
787            .await?;
788        executor.release_slot();
789
790        Ok(included.tx_hash)
791    }
792
793    fn uniswap_v3_factory(&self) -> anyhow::Result<Address> {
794        crate::exchanges::get_dex_extended(self.chain.name, &DexType::UniswapV3)
795            .map(|dex| dex.factory)
796            .ok_or_else(|| {
797                anyhow::anyhow!(
798                    "No registered Uniswap V3 deployment for chain {}",
799                    self.chain.name
800                )
801            })
802    }
803
804    fn resolve_pool(&self, instrument_id: &InstrumentId) -> anyhow::Result<Pool> {
805        let (blockchain, dex_type) = instrument_id.venue.parse_dex()?;
806        if blockchain != self.chain.name {
807            anyhow::bail!(
808                "Pool venue chain {blockchain} does not match the client chain {}",
809                self.chain.name
810            );
811        }
812
813        if dex_type != DexType::UniswapV3 {
814            anyhow::bail!("Unsupported DEX type {dex_type}; only UniswapV3 is supported");
815        }
816
817        let pool_identifier = PoolIdentifier::new_checked(instrument_id.symbol.as_str())?;
818        if !pool_identifier.is_address() {
819            anyhow::bail!(
820                "Pool identifier {pool_identifier} is a pool ID; only address identifiers are supported"
821            );
822        }
823
824        let pool = self
825            .core
826            .cache()
827            .pool(instrument_id)
828            .cloned()
829            .ok_or_else(|| {
830                anyhow::anyhow!(
831                    "Unknown pool {instrument_id}; not found in the shared engine cache"
832                )
833            })?;
834
835        if pool.token0.get_token_priority() == pool.token1.get_token_priority() {
836            anyhow::bail!(
837                "Pool {instrument_id} tokens share a token priority; base and quote orientation is ambiguous"
838            );
839        }
840
841        Ok(pool)
842    }
843
844    /// Authenticates every persisted signed transaction in this execution database.
845    ///
846    /// Run this while the execution client is disconnected. The check takes a stable table lock,
847    /// reads in bounded batches, and returns counts, deployment identity, key IDs, and database
848    /// roles with direct ownership or `SELECT` grants.
849    ///
850    /// # Errors
851    ///
852    /// Returns an error if storage state is inconsistent, keys are missing, or a payload cannot be
853    /// opened and authenticated against its durable intent.
854    pub async fn check_payload_storage(
855        &self,
856        batch_size: usize,
857    ) -> anyhow::Result<PayloadStorageCheck> {
858        let batch_size = validate_payload_operation_batch_size(batch_size)?;
859        let database = self.payload_operation_database().await?;
860        let keys = self.load_payload_keys()?;
861        database
862            .check_execution_payload_storage(keys.as_ref(), None, batch_size)
863            .await
864            .map(Into::into)
865    }
866
867    /// Activates or resumes protected signed-transaction storage for this execution database.
868    ///
869    /// Run this while the execution client is disconnected. A full payload check must succeed
870    /// before a later execution connection is permitted.
871    ///
872    /// # Errors
873    ///
874    /// Returns an error if the client is connected, an active key or deployment identity is
875    /// unavailable, or any stored payload cannot be migrated and authenticated.
876    pub async fn protect_payload_storage(&self) -> anyhow::Result<()> {
877        let database = self.payload_operation_database().await?;
878        database.ensure_execution_transaction_schema().await?;
879        let keys = self
880            .load_payload_keys()?
881            .ok_or_else(|| anyhow::anyhow!("Payload protection requires an active payload key"))?;
882        database.ensure_execution_payload_storage(&keys).await
883    }
884
885    /// Rewraps all protected payloads in this execution database with the configured active key.
886    ///
887    /// The prior active key must remain configured as a retired key until this method and a
888    /// subsequent full check both succeed.
889    ///
890    /// # Errors
891    ///
892    /// Returns an error if the client is connected, storage is not protected, required keys are
893    /// unavailable, or any bounded rewrap batch fails authentication.
894    pub async fn rewrap_payload_storage(&self, batch_size: usize) -> anyhow::Result<()> {
895        let batch_size = validate_payload_operation_batch_size(batch_size)?;
896        let database = self.payload_operation_database().await?;
897        let keys = self
898            .load_payload_keys()?
899            .ok_or_else(|| anyhow::anyhow!("Payload rewrap requires an active payload key"))?;
900        database
901            .rewrap_execution_payload_storage(&keys, batch_size)
902            .await
903    }
904
905    /// Restores authenticated plaintext payloads and removes protection from this database.
906    ///
907    /// This incident-only operation is resumable. Keep the complete key set configured until it
908    /// succeeds and the unprotected database passes a full payload check.
909    ///
910    /// # Errors
911    ///
912    /// Returns an error if the client is connected, storage is not protected, required keys are
913    /// unavailable, or any bounded rollback batch fails authentication.
914    pub async fn rollback_payload_storage(&self, batch_size: usize) -> anyhow::Result<()> {
915        let batch_size = validate_payload_operation_batch_size(batch_size)?;
916        let database = self.payload_operation_database().await?;
917        let keys = self
918            .load_payload_keys()?
919            .ok_or_else(|| anyhow::anyhow!("Payload rollback requires an active payload key"))?;
920        database
921            .rollback_execution_payload_storage(&keys, batch_size)
922            .await
923    }
924
925    async fn payload_operation_database(&self) -> anyhow::Result<BlockchainCacheDatabase> {
926        anyhow::ensure!(
927            !self.core.is_connected(),
928            "Disconnect the execution client before payload storage operations"
929        );
930
931        if let Some(database) = &self.cache.database {
932            return Ok(database.clone());
933        }
934        let options = self
935            .config
936            .postgres_cache_database_config
937            .as_ref()
938            .ok_or_else(|| anyhow::anyhow!("No Postgres cache database is configured"))?;
939        BlockchainCacheDatabase::connect(options.clone().into())
940            .await
941            .context("failed to connect to the execution database")
942    }
943
944    fn load_payload_keys(&self) -> anyhow::Result<Option<PayloadKeySet>> {
945        PayloadKeySet::load(
946            self.config.payload_key_env.as_deref(),
947            &self.config.payload_key_retired_env,
948            self.config.payload_deployment_id.as_deref(),
949        )
950    }
951
952    fn payload_policy(&self) -> PayloadPolicy {
953        PayloadPolicy {
954            chain_id: self.chain.chain_id,
955            signer: self.wallet_address,
956            gas_limit: self.config.gas_limit,
957            max_fee_per_gas: self.config.max_fee_per_gas_wei,
958        }
959    }
960
961    fn transaction_executor(&self) -> anyhow::Result<TransactionExecutor> {
962        let database = self.cache.database.clone().ok_or_else(|| {
963            anyhow::anyhow!("No durable store configured; refusing to submit a transaction")
964        })?;
965        let signer = self
966            .signer
967            .clone()
968            .ok_or_else(|| anyhow::anyhow!("Signer not initialized; connect the client first"))?;
969        let payload_keys = self.payload_keys.clone().ok_or_else(|| {
970            anyhow::anyhow!("Protected payload keys are not initialized; connect the client first")
971        })?;
972        let verification_config = self
973            .config
974            .verification
975            .as_ref()
976            .expect("verification config validated at construction");
977        let identities = std::iter::once(&verification_config.authoritative)
978            .chain(
979                verification_config
980                    .verifiers
981                    .iter()
982                    .map(|provider| &provider.identity),
983            )
984            .collect::<Vec<_>>();
985
986        Ok(TransactionExecutor {
987            http_rpc_client: Arc::clone(&self.http_rpc_client),
988            verification: self.verification.clone(),
989            manifest_version: verification_config.manifest_version.clone(),
990            manifest_digest: verification_config.manifest_digest.clone(),
991            deployment_manifest: Arc::new(verification_config.deployment_manifest.clone()),
992            provider_ids: identities
993                .iter()
994                .map(|identity| identity.provider_id.clone())
995                .collect(),
996            operator_ids: identities
997                .iter()
998                .map(|identity| identity.operator_id.clone())
999                .collect(),
1000            failure_domain_ids: identities
1001                .iter()
1002                .flat_map(|identity| identity.failure_domain_ids.iter().cloned())
1003                .collect(),
1004            database,
1005            signer,
1006            payload_keys,
1007            in_flight: Arc::clone(&self.in_flight),
1008            wallet_balance: Arc::clone(&self.wallet_balance),
1009            account_id: self.core.account_id,
1010            wallet_address: self.wallet_address,
1011            chain_id: self.chain.chain_id,
1012            max_fee_per_gas_wei: self.config.max_fee_per_gas_wei,
1013            base_fee_buffer_bps: self.config.base_fee_buffer_bps,
1014            gas_limit: self.config.gas_limit,
1015            gas_buffer_bps: self.config.gas_buffer_bps,
1016            receipt_timeout: receipt_timeout(self.transaction_limits.receipt_timeout_secs),
1017            receipt_max_polls: receipt_max_polls(self.transaction_limits.receipt_timeout_secs),
1018        })
1019    }
1020
1021    fn restore_swap_plan(&self, intent: &ExecutionIntentRow) -> anyhow::Result<SwapPlan> {
1022        let client_order_id = ClientOrderId::new_checked(
1023            intent
1024                .client_order_id
1025                .as_deref()
1026                .ok_or_else(|| anyhow::anyhow!("Persisted swap intent has no client order ID"))?,
1027        )?;
1028        let order = self
1029            .core
1030            .cache()
1031            .try_order_owned(&client_order_id)
1032            .with_context(|| {
1033                format!(
1034                    "Cannot reconcile swap intent {} because order {client_order_id} is not restored",
1035                    intent.id
1036                )
1037            })?;
1038        let instrument_id = InstrumentId::from_str(
1039            intent
1040                .instrument_id
1041                .as_deref()
1042                .ok_or_else(|| anyhow::anyhow!("Persisted swap intent has no instrument ID"))?,
1043        )?;
1044        anyhow::ensure!(
1045            order.instrument_id() == instrument_id,
1046            "Persisted swap instrument {instrument_id} does not match restored order instrument {}",
1047            order.instrument_id()
1048        );
1049        anyhow::ensure!(
1050            intent.trader_id.as_deref() == Some(order.trader_id().as_str()),
1051            "Persisted swap trader does not match restored order"
1052        );
1053        anyhow::ensure!(
1054            intent.strategy_id.as_deref() == Some(order.strategy_id().as_str()),
1055            "Persisted swap strategy does not match restored order"
1056        );
1057        anyhow::ensure!(
1058            intent.account_id.as_deref() == Some(self.core.account_id.as_str()),
1059            "Persisted swap account does not match execution client account"
1060        );
1061
1062        let pool = self.resolve_pool(&instrument_id)?;
1063        let pool_address = Address::from_str(
1064            intent
1065                .pool_address
1066                .as_deref()
1067                .ok_or_else(|| anyhow::anyhow!("Persisted swap intent has no pool address"))?,
1068        )?;
1069        anyhow::ensure!(
1070            pool.address == pool_address,
1071            "Persisted pool {pool_address} does not match restored pool {}",
1072            pool.address
1073        );
1074        let amount_in = U256::from_str(
1075            intent
1076                .amount_in
1077                .as_deref()
1078                .ok_or_else(|| anyhow::anyhow!("Persisted swap intent has no input amount"))?,
1079        )?;
1080        let fee = U24::try_from(
1081            pool.fee
1082                .ok_or_else(|| anyhow::anyhow!("Restored pool {instrument_id} has no fee"))?,
1083        )?;
1084        let quote_token = pool.get_quote_token();
1085        let quote_currency = Currency::new_checked(
1086            &quote_token.symbol,
1087            quote_token.decimals,
1088            0,
1089            &quote_token.name,
1090            CurrencyType::Crypto,
1091        )?;
1092        let (token_in, token_out) = swap_token_pair(
1093            order.order_side(),
1094            pool.get_base_token().address,
1095            quote_token.address,
1096        )?;
1097        let factory = self.uniswap_v3_factory()?;
1098        anyhow::ensure!(
1099            pool.dex.factory == factory,
1100            "Restored pool {instrument_id} references factory {}, expected registered factory {factory}",
1101            pool.dex.factory
1102        );
1103
1104        Ok(SwapPlan {
1105            order,
1106            quote_currency,
1107            pool,
1108            instrument_id,
1109            pool_address,
1110            router: Address::from_str(&intent.transaction_to)?,
1111            factory,
1112            weth: self.weth_address,
1113            token_in,
1114            token_out,
1115            fee,
1116            amount_in,
1117            min_amount_out: U256::ZERO,
1118            slippage_bps: 0,
1119            quote_spend_ceiling: None,
1120            profiler_position: None,
1121        })
1122    }
1123
1124    async fn reconcile_unresolved_execution(&self) -> anyhow::Result<()> {
1125        let database = self.cache.database.clone().ok_or_else(|| {
1126            anyhow::anyhow!("No durable store configured for execution reconciliation")
1127        })?;
1128        let _payload_lease = database
1129            .acquire_execution_payload_lease(self.payload_keys.as_deref().ok_or_else(|| {
1130                anyhow::anyhow!("Protected payload keys are required for execution recovery")
1131            })?)
1132            .await?;
1133        let wallet_address = self.wallet_address.to_string();
1134        anyhow::ensure!(
1135            !database
1136                .has_recoverable_signed_execution(self.chain.chain_id, &wallet_address)
1137                .await?,
1138            "A recoverable execution for wallet {} retains signed transaction bytes; refusing to reuse its nonce without explicit recovery",
1139            self.wallet_address
1140        );
1141        let Some(intent) = database
1142            .get_active_execution_intent(self.chain.chain_id, &wallet_address)
1143            .await?
1144        else {
1145            return Ok(());
1146        };
1147        anyhow::ensure!(
1148            intent.schema_version == crate::execution::transaction::EXECUTION_SCHEMA_VERSION,
1149            "Execution intent {} uses unsupported schema version {}",
1150            intent.id,
1151            intent.schema_version
1152        );
1153
1154        if intent.status == "prepared" {
1155            database
1156                .mark_execution_intent_recoverable(intent.id)
1157                .await?;
1158            release_preparing_slot(&self.in_flight);
1159            return Ok(());
1160        }
1161
1162        let purpose = TransactionPurpose::parse(&intent.purpose).ok_or_else(|| {
1163            anyhow::anyhow!(
1164                "Execution intent {} has unknown purpose {}",
1165                intent.id,
1166                intent.purpose
1167            )
1168        })?;
1169        let nonce = intent
1170            .nonce
1171            .ok_or_else(|| anyhow::anyhow!("Active execution intent {} has no nonce", intent.id))?;
1172        *self.in_flight.lock() = Some(InFlightSlot::Recovering(RecoveryTransaction {
1173            intent_id: intent.id,
1174            nonce,
1175            purpose,
1176        }));
1177        let hashes = database.get_execution_transaction_hashes(intent.id).await?;
1178        let current = current_execution_hash(intent.id, &hashes)?;
1179        let tx_hash = B256::from_str(&current.transaction_hash).with_context(|| {
1180            format!(
1181                "Execution intent {} has invalid transaction hash {}",
1182                intent.id, current.transaction_hash
1183            )
1184        })?;
1185        let policy = PayloadPolicy {
1186            chain_id: self.chain.chain_id,
1187            signer: self.wallet_address,
1188            gas_limit: self.config.gas_limit,
1189            max_fee_per_gas: self.config.max_fee_per_gas_wei,
1190        };
1191        let mut authenticated_payloads = HashMap::new();
1192        let mut current_payload = None;
1193
1194        for hash in &hashes {
1195            anyhow::ensure!(
1196                hash.intent_id == intent.id,
1197                "Persisted transaction row references intent {}, expected {}",
1198                hash.intent_id,
1199                intent.id
1200            );
1201            anyhow::ensure!(
1202                hash.chain_id == self.chain.chain_id,
1203                "Persisted transaction row chain ID {} does not match configured chain ID {}",
1204                hash.chain_id,
1205                self.chain.chain_id
1206            );
1207
1208            if !hash.payload_expected {
1209                anyhow::ensure!(
1210                    hash.raw_transaction.is_none() && hash.sealed_transaction.is_none(),
1211                    "Replacement transaction {} unexpectedly retains signed bytes",
1212                    hash.transaction_hash
1213                );
1214                continue;
1215            }
1216            let raw_transaction = open_execution_payload(
1217                self.payload_keys
1218                    .as_deref()
1219                    .expect("payload keys checked above"),
1220                policy,
1221                &intent,
1222                hash,
1223                "recovery",
1224            )
1225            .map_err(|e| {
1226                anyhow::anyhow!(
1227                    "Execution intent {} signed transaction {} failed authentication: {e}",
1228                    intent.id,
1229                    hash.transaction_hash
1230                )
1231            })?;
1232            let authenticated_hash = B256::from_str(&hash.transaction_hash).with_context(|| {
1233                format!(
1234                    "Execution intent {} has invalid transaction hash {}",
1235                    intent.id, hash.transaction_hash
1236                )
1237            })?;
1238            anyhow::ensure!(
1239                authenticated_payloads
1240                    .insert(authenticated_hash, raw_transaction.clone())
1241                    .is_none(),
1242                "Execution intent {} has duplicate authenticated transaction hash {}",
1243                intent.id,
1244                hash.transaction_hash
1245            );
1246
1247            if hash.id == current.id {
1248                current_payload = Some(raw_transaction);
1249            }
1250        }
1251        anyhow::ensure!(
1252            !authenticated_payloads.is_empty(),
1253            "Execution intent {} has no persisted signed transaction bytes",
1254            intent.id
1255        );
1256
1257        if intent.status == "broadcast" {
1258            anyhow::ensure!(
1259                current_payload.is_some(),
1260                "Broadcast execution intent {} has no persisted signed transaction bytes",
1261                intent.id
1262            );
1263        }
1264
1265        if intent.status == "signed" {
1266            anyhow::bail!(
1267                "Execution intent {} has a signed transaction {} that was not authorized for broadcast; its nonce remains reserved pending explicit recovery",
1268                intent.id,
1269                tx_hash
1270            );
1271        }
1272
1273        *self.in_flight.lock() = Some(InFlightSlot::AwaitingFinality(InFlightTransaction {
1274            intent_id: intent.id,
1275            nonce,
1276            tx_hash,
1277            purpose,
1278        }));
1279
1280        let plan = if purpose == TransactionPurpose::Swap {
1281            Some(self.restore_swap_plan(&intent)?)
1282        } else {
1283            None
1284        };
1285
1286        if let Some(plan) = &plan
1287            && !intent.acknowledgement_emitted
1288        {
1289            if plan.order.ts_submitted().is_none() {
1290                self.emitter.emit_order_submitted(&plan.order);
1291            }
1292            database
1293                .mark_execution_event_emitted(intent.id, "acknowledgement")
1294                .await?;
1295        }
1296
1297        let executor = self.transaction_executor()?;
1298        let mut prepared = PreparedTransaction {
1299            intent_id: intent.id,
1300            created_block: intent.created_block,
1301            nonce,
1302            tx_hash,
1303            raw_tx: current_payload.unwrap_or_default(),
1304            payload_lease: None,
1305        };
1306
1307        let finality_already_committed = matches!(intent.status.as_str(), "finalized" | "reverted");
1308        if !finality_already_committed {
1309            match executor
1310                .authorize_rebroadcast(&prepared, &intent, purpose)
1311                .await?
1312            {
1313                ReconciliationAuthorization::Rebroadcast => {
1314                    match executor.broadcast(&prepared).await? {
1315                        BroadcastOutcome::Accepted => {}
1316                        BroadcastOutcome::Ambiguous(message) => log::warn!("{message}"),
1317                    }
1318                }
1319                ReconciliationAuthorization::Retain => {
1320                    log::warn!(
1321                        "Rebroadcast of transaction {} was suppressed by verified reconciliation state",
1322                        prepared.tx_hash
1323                    );
1324                }
1325                ReconciliationAuthorization::ScanReplacement(head) => {
1326                    let Some((replacement_hash, replacement_payload)) = executor
1327                        .scan_canonical_replacement(&intent, nonce, head, &authenticated_payloads)
1328                        .await?
1329                    else {
1330                        log::warn!(
1331                            "Canonical replacement scan for intent {} reached its bounded verified window",
1332                            intent.id
1333                        );
1334                        return Ok(());
1335                    };
1336                    prepared.tx_hash = replacement_hash;
1337                    prepared.raw_tx = replacement_payload;
1338                    *self.in_flight.lock() =
1339                        Some(InFlightSlot::AwaitingFinality(InFlightTransaction {
1340                            intent_id: intent.id,
1341                            nonce,
1342                            tx_hash: replacement_hash,
1343                            purpose,
1344                        }));
1345                }
1346            }
1347        }
1348        let outcome = if finality_already_committed {
1349            let receipt = verified_value(
1350                executor.verification.verify_receipt(&tx_hash).await,
1351                "persisted terminal receipt",
1352            )?;
1353            let finality = executor
1354                .receipt_is_stably_finalized(&receipt)
1355                .await?
1356                .ok_or_else(|| {
1357                    anyhow::anyhow!(
1358                        "Persisted terminal transaction {tx_hash} is not stable at the finalized boundary"
1359                    )
1360                })?;
1361            let included = IncludedTransaction {
1362                intent_id: intent.id,
1363                nonce,
1364                tx_hash,
1365                block_number: receipt.block_number,
1366                receipt,
1367                finality,
1368            };
1369
1370            if intent.status == "finalized" {
1371                InclusionOutcome::Finalized(included)
1372            } else {
1373                InclusionOutcome::Reverted(included)
1374            }
1375        } else {
1376            executor.await_finality(&prepared).await?
1377        };
1378
1379        match outcome {
1380            InclusionOutcome::Finalized(mut included) => {
1381                let trace_purpose = match (&plan, purpose) {
1382                    (Some(plan), TransactionPurpose::Swap) => match plan.order.order_side() {
1383                        OrderSide::Sell => "swap_sell",
1384                        OrderSide::Buy => "swap_buy",
1385                    },
1386                    (None, TransactionPurpose::Wrap) => "wrap",
1387                    (None, TransactionPurpose::Approve) => "approve",
1388                    _ => anyhow::bail!("Restored transaction purpose is inconsistent"),
1389                };
1390                included.finality.decisions.extend(
1391                    verify_finalized_transaction(
1392                        &included,
1393                        &intent,
1394                        nonce,
1395                        &prepared.raw_tx,
1396                        &executor,
1397                        trace_purpose,
1398                    )
1399                    .await?,
1400                );
1401
1402                if let Some(plan) = plan {
1403                    let fill = validate_finalized_swap_fill(&plan, &included)?;
1404                    let wallet =
1405                        load_verified_wallet_after_fill(&plan, &included, &executor).await?;
1406                    if !finality_already_committed {
1407                        executor
1408                            .commit_verified_finality(
1409                                &included,
1410                                TransactionStatus::Finalized,
1411                                &wallet.decisions,
1412                            )
1413                            .await?;
1414                    }
1415                    complete_finalized_swap(
1416                        &plan,
1417                        intent.id,
1418                        included.tx_hash,
1419                        fill,
1420                        wallet,
1421                        &executor,
1422                        &self.emitter,
1423                    )
1424                    .await?;
1425                    executor.release_slot();
1426                } else {
1427                    let postconditions = self
1428                        .verify_recovered_operator_transaction(
1429                            &intent, purpose, &included, &executor,
1430                        )
1431                        .await?;
1432
1433                    if !finality_already_committed {
1434                        executor
1435                            .commit_verified_finality(
1436                                &included,
1437                                TransactionStatus::Finalized,
1438                                &postconditions,
1439                            )
1440                            .await?;
1441                    }
1442                    database
1443                        .mark_execution_event_emitted(intent.id, "terminal")
1444                        .await?;
1445                    executor.release_slot();
1446                }
1447            }
1448            InclusionOutcome::Reverted(mut included) => {
1449                let trace_purpose = match (&plan, purpose) {
1450                    (Some(plan), TransactionPurpose::Swap) => match plan.order.order_side() {
1451                        OrderSide::Sell => "swap_sell",
1452                        OrderSide::Buy => "swap_buy",
1453                    },
1454                    (None, TransactionPurpose::Wrap) => "wrap",
1455                    (None, TransactionPurpose::Approve) => "approve",
1456                    _ => anyhow::bail!("Restored transaction purpose is inconsistent"),
1457                };
1458                included.finality.decisions.extend(
1459                    verify_finalized_transaction(
1460                        &included,
1461                        &intent,
1462                        nonce,
1463                        &prepared.raw_tx,
1464                        &executor,
1465                        trace_purpose,
1466                    )
1467                    .await?,
1468                );
1469
1470                if !finality_already_committed {
1471                    executor
1472                        .commit_verified_finality(&included, TransactionStatus::Reverted, &[])
1473                        .await?;
1474                }
1475
1476                if let Some(plan) = plan
1477                    && plan.order.status() != OrderStatus::Rejected
1478                {
1479                    send_reverted_order(&self.emitter, &plan.order, &included)?;
1480                }
1481                database
1482                    .mark_execution_event_emitted(intent.id, "terminal")
1483                    .await?;
1484                executor.release_slot();
1485            }
1486            InclusionOutcome::Pending(message) => log::warn!("{message}"),
1487        }
1488        Ok(())
1489    }
1490
1491    /// Revalidates a restored wrap or approve against the finalized signer-nonce transaction
1492    /// and reruns the live postcondition before the restored intent may report success.
1493    ///
1494    /// Fails closed: any identity mismatch or unproven postcondition returns an error, which
1495    /// keeps the in-flight signer slot occupied, leaves the intent active, and fails connect.
1496    async fn verify_recovered_operator_transaction(
1497        &self,
1498        intent: &ExecutionIntentRow,
1499        purpose: TransactionPurpose,
1500        included: &IncludedTransaction,
1501        executor: &TransactionExecutor,
1502    ) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
1503        let (to, input, value) = persisted_call_fields(intent)?;
1504
1505        match purpose {
1506            TransactionPurpose::Wrap => {
1507                verify_wrap_balance_increase(executor, &to, value, included).await
1508            }
1509            TransactionPurpose::Approve => {
1510                let call = ERC20::approveCall::abi_decode(&input)
1511                    .with_context(|| "persisted approve calldata is invalid")?;
1512                verify_approve_allowance(executor, &to, &call.spender, call.amount, included).await
1513            }
1514            TransactionPurpose::Swap => {
1515                unreachable!("swap intents restore a swap plan")
1516            }
1517        }
1518    }
1519
1520    fn ensure_transaction_ready(&self, purpose: TransactionPurpose) -> anyhow::Result<()> {
1521        if !self.core.is_connected() {
1522            anyhow::bail!("Blockchain execution client is not connected");
1523        }
1524
1525        {
1526            let slot = self.in_flight.lock();
1527            if let Some(in_flight) = *slot {
1528                return Err(in_flight_limit_error(&in_flight));
1529            }
1530        }
1531
1532        if !self.cache.has_database() {
1533            anyhow::bail!(
1534                "No durable store configured; refusing to submit a {} transaction",
1535                purpose.as_str()
1536            );
1537        }
1538        Ok(())
1539    }
1540
1541    /// Validates a submit-order command against the configured policy and builds the swap plan.
1542    ///
1543    /// Local checks run first: pool resolution, order semantics, allowlists, amount and
1544    /// slippage limits, and the quote derived from the live pool profiler. Infrastructure
1545    /// readiness (connection, in-flight slot, durable store, signer) follows. Chain state is
1546    /// verified in the spawned task before signing.
1547    fn prepare_swap(&self, cmd: &SubmitOrder, order: &OrderAny) -> anyhow::Result<SwapPlan> {
1548        let instrument_id = order.instrument_id();
1549        let pool = self.resolve_pool(&instrument_id)?;
1550
1551        if order.order_type() != OrderType::Market {
1552            anyhow::bail!(
1553                "Unsupported order type {}; only Market is supported",
1554                order.order_type()
1555            );
1556        }
1557
1558        if !matches!(order.order_side(), OrderSide::Buy | OrderSide::Sell) {
1559            anyhow::bail!(
1560                "Unsupported order side {}; only Buy and Sell are supported",
1561                order.order_side()
1562            );
1563        }
1564
1565        if order.is_quote_quantity() {
1566            anyhow::bail!(
1567                "Quote-denominated quantities are not supported; quantity must be denominated in the base token"
1568            );
1569        }
1570
1571        let fee = pool
1572            .fee
1573            .ok_or_else(|| anyhow::anyhow!("Pool {instrument_id} has no fee tier"))?;
1574        let fee = U24::try_from(fee)
1575            .map_err(|_| anyhow::anyhow!("Pool {instrument_id} fee {fee} exceeds uint24"))?;
1576
1577        let base_token = pool.get_base_token();
1578        let quote_token = pool.get_quote_token();
1579        let quote_currency = Currency::new_checked(
1580            &quote_token.symbol,
1581            quote_token.decimals,
1582            0,
1583            &quote_token.name,
1584            CurrencyType::Crypto,
1585        )?;
1586        let (token_in, token_out) =
1587            swap_token_pair(order.order_side(), base_token.address, quote_token.address)?;
1588
1589        if !self
1590            .transaction_limits
1591            .allowed_token_pairs
1592            .contains(&(token_in, token_out))
1593        {
1594            anyhow::bail!(
1595                "Token pair {token_in} -> {token_out} is not in the `allowed_token_pairs` allowlist"
1596            );
1597        }
1598
1599        let base_amount = quantity_to_raw_amount(order.quantity(), base_token.decimals)?;
1600        if base_amount > U256::from(self.transaction_limits.max_order_amount) {
1601            anyhow::bail!(
1602                "Order amount {base_amount} exceeds the configured `max_order_amount` {}",
1603                self.transaction_limits.max_order_amount
1604            );
1605        }
1606
1607        let slippage_bps = match cmd
1608            .params
1609            .as_ref()
1610            .and_then(|params| params.get_u64("slippage_bps"))
1611        {
1612            Some(value) => u32::try_from(value).map_err(|_| {
1613                anyhow::anyhow!("slippage_bps parameter {value} exceeds the u32 range")
1614            })?,
1615            None => self.transaction_limits.slippage_bps,
1616        };
1617
1618        if slippage_bps > self.transaction_limits.max_slippage_bps {
1619            anyhow::bail!(
1620                "Slippage {slippage_bps} bps exceeds the configured `max_slippage_bps` {}",
1621                self.transaction_limits.max_slippage_bps
1622            );
1623        }
1624
1625        let quote_spend_ceiling = if order.order_side() == OrderSide::Buy {
1626            let ceiling = self
1627                .transaction_limits
1628                .quote_spend_limits
1629                .get(&(token_in, token_out))
1630                .ok_or_else(|| {
1631                    anyhow::anyhow!(
1632                        "No `quote_spend_limits` entry for BUY token pair {token_in} -> {token_out}"
1633                    )
1634                })?;
1635            anyhow::ensure!(
1636                ceiling.spend_token == quote_token.address,
1637                "Quote spend limit for {token_in} -> {token_out} is denominated in {}, expected quote token {}",
1638                ceiling.spend_token,
1639                quote_token.address
1640            );
1641            anyhow::ensure!(
1642                ceiling.spend_token_decimals == quote_token.decimals,
1643                "Quote spend limit for token {} uses {} decimals, expected pool quote-token decimals {}",
1644                ceiling.spend_token,
1645                ceiling.spend_token_decimals,
1646                quote_token.decimals
1647            );
1648            Some(ceiling)
1649        } else {
1650            None
1651        };
1652
1653        let profiler = self
1654            .core
1655            .cache()
1656            .pool_profiler(&instrument_id)
1657            .cloned()
1658            .ok_or_else(|| {
1659                anyhow::anyhow!(
1660                    "No pool profiler for {instrument_id}; an active data subscription is required to quote the swap"
1661                )
1662            })?;
1663
1664        if !profiler.is_initialized {
1665            anyhow::bail!("Pool profiler for {instrument_id} is not initialized");
1666        }
1667        let profiler_position = profiler.last_processed_event.clone().ok_or_else(|| {
1668            anyhow::anyhow!("Pool profiler for {instrument_id} has processed no events")
1669        })?;
1670
1671        let zero_for_one = token_in == pool.token0.address;
1672        let (amount_in, quoted_amount_out) = match order.order_side() {
1673            OrderSide::Sell => {
1674                let quote = profiler
1675                    .swap_exact_in(base_amount, zero_for_one, None)
1676                    .map_err(|e| anyhow::anyhow!("Swap quote failed for {instrument_id}: {e}"))?;
1677                let amount_filled = if zero_for_one {
1678                    quote.amount0
1679                } else {
1680                    quote.amount1
1681                };
1682
1683                if amount_filled != I256::from(base_amount) {
1684                    anyhow::bail!(
1685                        "Local quote for {instrument_id} filled {amount_filled} of the {base_amount} order amount; pool liquidity cannot fill the order"
1686                    );
1687                }
1688                (base_amount, exact_output_amount(&quote, zero_for_one)?)
1689            }
1690            OrderSide::Buy => {
1691                let quote = profiler
1692                    .swap_exact_out(base_amount, zero_for_one, None)
1693                    .map_err(|e| anyhow::anyhow!("Swap quote failed for {instrument_id}: {e}"))?;
1694                let amount_in = quote.get_input_amount();
1695                if amount_in.is_zero() {
1696                    anyhow::bail!("Local quote for {instrument_id} produced a zero quote input");
1697                }
1698                let ceiling = quote_spend_ceiling.ok_or_else(|| {
1699                    anyhow::anyhow!(
1700                        "No `quote_spend_limits` entry for BUY token pair {token_in} -> {token_out}"
1701                    )
1702                })?;
1703
1704                if amount_in > ceiling.max_amount {
1705                    anyhow::bail!(
1706                        "BUY quote amount {amount_in} exceeds the configured `quote_spend_limits` maximum {} for {token_in} -> {token_out}",
1707                        ceiling.max_amount
1708                    );
1709                }
1710                (amount_in, base_amount)
1711            }
1712        };
1713        let min_amount_out = derive_min_amount_out(quoted_amount_out, slippage_bps)?;
1714
1715        self.ensure_transaction_ready(TransactionPurpose::Swap)?;
1716
1717        if self.signer.is_none() {
1718            anyhow::bail!("Signer not initialized; connect the client first");
1719        }
1720
1721        let pool_address = pool.address;
1722        let factory = self.uniswap_v3_factory()?;
1723        anyhow::ensure!(
1724            pool.dex.factory == factory,
1725            "Pool {instrument_id} references factory {}, expected registered factory {factory}",
1726            pool.dex.factory
1727        );
1728
1729        Ok(SwapPlan {
1730            order: order.clone(),
1731            pool,
1732            quote_currency,
1733            instrument_id,
1734            pool_address,
1735            router: self.router_addresses[0],
1736            factory,
1737            weth: self.weth_address,
1738            token_in,
1739            token_out,
1740            fee,
1741            amount_in,
1742            min_amount_out,
1743            slippage_bps,
1744            quote_spend_ceiling: quote_spend_ceiling.copied(),
1745            profiler_position: Some(profiler_position),
1746        })
1747    }
1748}
1749
1750/// Result of authenticating every persisted signed transaction in one execution database.
1751#[derive(Debug, Clone, PartialEq, Eq)]
1752pub struct PayloadStorageCheck {
1753    /// Whether payload protection is active.
1754    pub protected: bool,
1755    /// Durable deployment identity when protection is active.
1756    pub deployment_id: Option<String>,
1757    /// Rows which still contain plaintext signed transaction bytes.
1758    pub plaintext_rows: u64,
1759    /// Signed transaction rows which require a payload.
1760    pub original_rows: u64,
1761    /// Canonical replacement rows whose original bytes are unavailable.
1762    pub replacement_rows: u64,
1763    /// Payload rows successfully opened and authenticated.
1764    pub authenticated_rows: u64,
1765    /// Key IDs referenced by protected payloads.
1766    pub key_ids: Vec<String>,
1767    /// Database roles with direct table ownership or `SELECT` grants.
1768    pub read_roles: Vec<String>,
1769}
1770
1771impl From<ExecutionPayloadCheck> for PayloadStorageCheck {
1772    fn from(value: ExecutionPayloadCheck) -> Self {
1773        Self {
1774            protected: value.protected,
1775            deployment_id: value.deployment_id,
1776            plaintext_rows: value.plaintext_rows,
1777            original_rows: value.original_rows,
1778            replacement_rows: value.replacement_rows,
1779            authenticated_rows: value.authenticated_rows,
1780            key_ids: value.key_ids,
1781            read_roles: value.read_roles,
1782        }
1783    }
1784}
1785
1786// A broadcast transaction awaiting finality, occupying the single in-flight slot.
1787#[derive(Debug, Clone, Copy)]
1788struct InFlightTransaction {
1789    intent_id: i64,
1790    nonce: u64,
1791    tx_hash: B256,
1792    purpose: TransactionPurpose,
1793}
1794
1795#[derive(Debug, Clone, Copy)]
1796struct RecoveryTransaction {
1797    intent_id: i64,
1798    nonce: u64,
1799    purpose: TransactionPurpose,
1800}
1801
1802/// The single in-flight transaction slot.
1803///
1804/// The slot is claimed before any preparation RPC call so the `pending` nonce read stays
1805/// authoritative: a second transaction is rejected before it can sign. A claim is released
1806/// only when preparation fails before signing; from persistence onward the slot is never
1807/// released on failure, because the database may have committed before its acknowledgement
1808/// was lost.
1809#[derive(Debug, Clone, Copy)]
1810enum InFlightSlot {
1811    /// Claimed before preparation; no signed transaction exists yet.
1812    Preparing(TransactionPurpose),
1813    /// Restored durable ownership retained while persisted transaction data is authenticated.
1814    Recovering(RecoveryTransaction),
1815    /// Signed, persisted, and awaiting finality.
1816    AwaitingFinality(InFlightTransaction),
1817}
1818
1819#[derive(Debug, Clone)]
1820struct IncludedTransaction {
1821    intent_id: i64,
1822    nonce: u64,
1823    tx_hash: B256,
1824    block_number: u64,
1825    receipt: RpcTransactionReceipt,
1826    finality: StableFinality,
1827}
1828
1829#[derive(Debug, Clone)]
1830struct StableFinality {
1831    decisions: Vec<ExecutionVerificationDecision>,
1832    inclusion_header: ExecutionVerifiedHeader,
1833    finalized_headers: Vec<ExecutionVerifiedHeader>,
1834}
1835
1836#[derive(Debug, Clone, Copy)]
1837enum TransactionAuthorization {
1838    Wrap {
1839        weth: Address,
1840    },
1841    Approve {
1842        token: Address,
1843        router: Address,
1844        amount: U256,
1845    },
1846}
1847
1848/// The single-in-flight limit error naming the transaction currently occupying the slot.
1849fn in_flight_limit_error(slot: &InFlightSlot) -> anyhow::Error {
1850    match slot {
1851        InFlightSlot::Preparing(purpose) => anyhow::anyhow!(
1852            "A {} transaction is being prepared; at most one transaction can be in flight",
1853            purpose.as_str()
1854        ),
1855        InFlightSlot::Recovering(recovery) => anyhow::anyhow!(
1856            "Execution intent {} ({}, nonce {}) retains signer ownership pending recovery; at most one transaction can be in flight",
1857            recovery.intent_id,
1858            recovery.purpose.as_str(),
1859            recovery.nonce
1860        ),
1861        InFlightSlot::AwaitingFinality(in_flight) => anyhow::anyhow!(
1862            "Transaction {} (intent {}, {}, nonce {}) is still awaiting finality; at most one transaction can be in flight",
1863            in_flight.tx_hash,
1864            in_flight.intent_id,
1865            in_flight.purpose.as_str(),
1866            in_flight.nonce
1867        ),
1868    }
1869}
1870
1871/// Releases a pre-signature slot claim when the slot is still in the preparing state.
1872///
1873/// Aborted or failed preparation can leave a claim behind; because no signed transaction
1874/// exists for a preparing slot, releasing it cannot strand a broadcastable signature.
1875fn release_preparing_slot(in_flight: &Mutex<Option<InFlightSlot>>) {
1876    let mut slot = in_flight.lock();
1877    if matches!(*slot, Some(InFlightSlot::Preparing(_))) {
1878        *slot = None;
1879    }
1880}
1881
1882fn release_preparing_if_reservation_not_committed(
1883    in_flight: &Mutex<Option<InFlightSlot>>,
1884    error: &anyhow::Error,
1885) {
1886    if reservation_failure_proven_not_committed(error) {
1887        release_preparing_slot(in_flight);
1888    }
1889}
1890
1891#[derive(Debug)]
1892struct TransactionLimits {
1893    allowed_token_pairs: HashSet<(Address, Address)>,
1894    quote_spend_limits: HashMap<(Address, Address), QuoteSpendCeiling>,
1895    slippage_bps: u32,
1896    max_slippage_bps: u32,
1897    max_order_amount: u64,
1898    deadline_seconds: u64,
1899    max_quote_age_blocks: u64,
1900    receipt_timeout_secs: u64,
1901}
1902
1903#[derive(Debug, Clone, Copy)]
1904struct QuoteSpendCeiling {
1905    spend_token: Address,
1906    spend_token_decimals: u8,
1907    max_amount: U256,
1908}
1909
1910/// A locally signed EIP-1559 transaction ready for persist-before-broadcast.
1911struct PreparedTransaction {
1912    intent_id: i64,
1913    created_block: u64,
1914    nonce: u64,
1915    tx_hash: B256,
1916    raw_tx: Vec<u8>,
1917    payload_lease: Option<ExecutionPayloadLease>,
1918}
1919
1920impl Debug for PreparedTransaction {
1921    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1922        f.debug_struct(stringify!(PreparedTransaction))
1923            .field("intent_id", &self.intent_id)
1924            .field("created_block", &self.created_block)
1925            .field("nonce", &self.nonce)
1926            .field("tx_hash", &self.tx_hash)
1927            .field("raw_tx", &"<redacted>")
1928            .field(
1929                "payload_lease",
1930                &self.payload_lease.as_ref().map(|_| "held"),
1931            )
1932            .finish()
1933    }
1934}
1935
1936/// The classified outcome of a broadcast attempt.
1937#[derive(Debug)]
1938enum BroadcastOutcome {
1939    /// The node accepted the transaction (or reported it already known).
1940    Accepted,
1941    /// Acceptance is uncertain; the persisted record stays active and the slot occupied.
1942    Ambiguous(String),
1943}
1944
1945/// The classified outcome of awaiting transaction finality.
1946#[derive(Debug)]
1947enum InclusionOutcome {
1948    /// Canonical successful receipt observed through a stable finalized boundary.
1949    Finalized(IncludedTransaction),
1950    /// Canonical failed receipt observed through a stable finalized boundary.
1951    Reverted(IncludedTransaction),
1952    /// No receipt arrived within the poll budget or observation failed; the record stays
1953    /// pending and the in-flight slot occupied.
1954    Pending(String),
1955}
1956
1957enum ReconciliationAuthorization {
1958    Rebroadcast,
1959    Retain,
1960    ScanReplacement(VerifiedBlockHeader),
1961}
1962
1963fn verified_value<T>(outcome: VerificationOutcome<T>, context: &str) -> anyhow::Result<T> {
1964    required_verification(outcome, context).map(|verified| verified.value)
1965}
1966
1967fn required_verification<T>(
1968    outcome: VerificationOutcome<T>,
1969    context: &str,
1970) -> anyhow::Result<Verified<T>> {
1971    match outcome {
1972        VerificationOutcome::Verified(verified) => Ok(verified),
1973        VerificationOutcome::Disagreement(_) => {
1974            anyhow::bail!("{context} verification disagreed")
1975        }
1976        VerificationOutcome::Unavailable(_) => {
1977            anyhow::bail!("{context} verification is unavailable")
1978        }
1979        VerificationOutcome::Retryable(_) => {
1980            anyhow::bail!("{context} verification is retryable")
1981        }
1982        VerificationOutcome::LocallyInvalid(_) => {
1983            anyhow::bail!("{context} verification is locally invalid")
1984        }
1985    }
1986}
1987
1988fn validate_transaction_authorization(
1989    authorization: Option<&TransactionAuthorization>,
1990    to: Address,
1991    value: U256,
1992    input: &[u8],
1993) -> anyhow::Result<()> {
1994    match authorization {
1995        None => Ok(()),
1996        Some(TransactionAuthorization::Wrap { weth }) => {
1997            anyhow::ensure!(
1998                to == *weth && !value.is_zero() && input == WETH9::depositCall::SELECTOR,
1999                "Wrap authorization does not match the transaction call"
2000            );
2001            Ok(())
2002        }
2003        Some(TransactionAuthorization::Approve {
2004            token,
2005            router,
2006            amount,
2007        }) => {
2008            let expected = ERC20::approveCall {
2009                spender: *router,
2010                amount: *amount,
2011            }
2012            .abi_encode();
2013            anyhow::ensure!(
2014                to == *token && value.is_zero() && input == expected,
2015                "Approve authorization does not match the transaction call"
2016            );
2017            Ok(())
2018        }
2019    }
2020}
2021
2022fn verification_decision<T>(
2023    verified: &Verified<T>,
2024    height_start: Option<u64>,
2025    height_end: Option<u64>,
2026) -> ExecutionVerificationDecision {
2027    ExecutionVerificationDecision {
2028        read_class: verified.read.as_str(),
2029        height_start,
2030        height_end,
2031        normalized_value_digest: verified.normalized_value_digest.to_string(),
2032    }
2033}
2034
2035fn parse_verified_header(header: &ExecutionVerifiedHeader) -> anyhow::Result<VerifiedBlockHeader> {
2036    Ok(VerifiedBlockHeader {
2037        number: header.number,
2038        hash: B256::from_str(&header.hash).context("Durable finalized header hash is invalid")?,
2039        parent_hash: B256::from_str(&header.parent_hash)
2040            .context("Durable finalized parent hash is invalid")?,
2041        timestamp: header.timestamp,
2042        base_fee_per_gas: header.base_fee_per_gas,
2043    })
2044}
2045
2046fn durable_verified_header(header: &VerifiedBlockHeader) -> ExecutionVerifiedHeader {
2047    ExecutionVerifiedHeader {
2048        number: header.number,
2049        hash: header.hash.to_string(),
2050        parent_hash: header.parent_hash.to_string(),
2051        timestamp: header.timestamp,
2052        base_fee_per_gas: header.base_fee_per_gas,
2053    }
2054}
2055
2056/// Shared execution context driving the EIP-1559 transaction pipeline.
2057///
2058/// Wrap, approve, and swap submissions share one implementation so the chain, fee, gas,
2059/// signing, persistence, and broadcast policy cannot drift between operator transactions
2060/// and order flow. Carries only `Send` state so order submission can run on a spawned task
2061/// after the synchronous trait method returns.
2062#[derive(Debug, Clone)]
2063struct TransactionExecutor {
2064    http_rpc_client: Arc<BlockchainHttpRpcClient>,
2065    verification: VerificationCoordinator,
2066    manifest_version: String,
2067    manifest_digest: String,
2068    deployment_manifest: Arc<BlockchainDeploymentManifest>,
2069    provider_ids: Vec<String>,
2070    operator_ids: Vec<String>,
2071    failure_domain_ids: Vec<String>,
2072    database: BlockchainCacheDatabase,
2073    signer: Arc<PrivateKeySigner>,
2074    payload_keys: Arc<PayloadKeySet>,
2075    in_flight: Arc<Mutex<Option<InFlightSlot>>>,
2076    wallet_balance: Arc<Mutex<WalletBalance>>,
2077    account_id: AccountId,
2078    wallet_address: Address,
2079    chain_id: u32,
2080    max_fee_per_gas_wei: u64,
2081    base_fee_buffer_bps: u32,
2082    gas_limit: u64,
2083    gas_buffer_bps: u32,
2084    receipt_timeout: Duration,
2085    receipt_max_polls: u32,
2086}
2087
2088impl TransactionExecutor {
2089    /// Builds, signs, persists, and broadcasts an EIP-1559 transaction, then awaits finality.
2090    ///
2091    /// Order of operations: claim the in-flight slot, chain ID verification, nonce and fee and
2092    /// gas policy checks, local signing, persist-before-broadcast, broadcast, inclusion
2093    /// observation. The persisted record guarantees a signed transaction is never forgotten.
2094    async fn transact(
2095        &self,
2096        to: Address,
2097        value: U256,
2098        input: Bytes,
2099        purpose: TransactionPurpose,
2100        client_order_id: Option<ClientOrderId>,
2101        authorization: TransactionAuthorization,
2102    ) -> anyhow::Result<IncludedTransaction> {
2103        self.claim_slot(purpose)?;
2104        let now_unix_secs = current_unix_secs()?;
2105        let decision_header = match required_verification(
2106            self.verification
2107                .verify_decision_header(now_unix_secs)
2108                .await,
2109            "operator decision header",
2110        ) {
2111            Ok(header) => header,
2112            Err(e) => {
2113                release_preparing_slot(&self.in_flight);
2114                return Err(e);
2115            }
2116        };
2117        let created_block = decision_header.value.number;
2118        let intent = ExecutionIntentInsert {
2119            chain_id: self.chain_id,
2120            wallet_address: self.wallet_address.to_string(),
2121            purpose: purpose.as_str().to_string(),
2122            client_order_id: client_order_id.map(|id| id.to_string()),
2123            trader_id: None,
2124            strategy_id: None,
2125            account_id: None,
2126            instrument_id: None,
2127            pool_address: None,
2128            transaction_to: to.to_string(),
2129            transaction_input: hex::encode_prefixed(&input),
2130            transaction_value: value.to_string(),
2131            amount_in: None,
2132            created_block,
2133        };
2134        let intent = match self.database.reserve_execution_intent(&intent).await {
2135            Ok(intent) => intent,
2136            Err(e) => {
2137                release_preparing_if_reservation_not_committed(&self.in_flight, &e);
2138                return Err(e);
2139            }
2140        };
2141        let prepared = match self
2142            .prepare_and_sign(
2143                intent.id,
2144                intent.created_block,
2145                to,
2146                value,
2147                input,
2148                &authorization,
2149                decision_header,
2150            )
2151            .await
2152        {
2153            Ok(prepared) => prepared,
2154            Err(e) => {
2155                if self
2156                    .database
2157                    .mark_execution_intent_recoverable(intent.id)
2158                    .await
2159                    .is_ok()
2160                {
2161                    release_preparing_slot(&self.in_flight);
2162                }
2163                return Err(e);
2164            }
2165        };
2166        self.fill_and_persist(&prepared, purpose).await?;
2167
2168        match self.broadcast(&prepared).await? {
2169            BroadcastOutcome::Accepted => {}
2170            BroadcastOutcome::Ambiguous(message) => log::warn!("{message}"),
2171        }
2172
2173        match self.await_finality(&prepared).await? {
2174            InclusionOutcome::Finalized(mut included) => {
2175                included.finality.decisions.extend(
2176                    verify_finalized_transaction(
2177                        &included,
2178                        &intent,
2179                        prepared.nonce,
2180                        &prepared.raw_tx,
2181                        self,
2182                        purpose.as_str(),
2183                    )
2184                    .await?,
2185                );
2186                Ok(included)
2187            }
2188            InclusionOutcome::Reverted(mut included) => {
2189                included.finality.decisions.extend(
2190                    verify_finalized_transaction(
2191                        &included,
2192                        &intent,
2193                        prepared.nonce,
2194                        &prepared.raw_tx,
2195                        self,
2196                        purpose.as_str(),
2197                    )
2198                    .await?,
2199                );
2200                self.commit_verified_finality(&included, TransactionStatus::Reverted, &[])
2201                    .await?;
2202                self.database
2203                    .mark_execution_event_emitted(prepared.intent_id, "terminal")
2204                    .await?;
2205                self.release_slot();
2206                anyhow::bail!("Transaction {} reverted on-chain", included.tx_hash)
2207            }
2208            InclusionOutcome::Pending(message) => anyhow::bail!(message),
2209        }
2210    }
2211
2212    /// Claims the single in-flight slot before any preparation RPC call, so the `pending`
2213    /// nonce read stays authoritative: a second transaction is rejected before it can sign.
2214    fn claim_slot(&self, purpose: TransactionPurpose) -> anyhow::Result<()> {
2215        let mut slot = self.in_flight.lock();
2216        if let Some(in_flight) = *slot {
2217            return Err(in_flight_limit_error(&in_flight));
2218        }
2219        *slot = Some(InFlightSlot::Preparing(purpose));
2220        Ok(())
2221    }
2222
2223    /// Runs the read-only pre-signing pipeline: chain ID verification, nonce selection, fee
2224    /// and gas policy checks, transaction building, and local signing.
2225    #[expect(
2226        clippy::too_many_arguments,
2227        reason = "Security-critical transaction fields stay explicit at the signing boundary"
2228    )]
2229    async fn prepare_and_sign(
2230        &self,
2231        intent_id: i64,
2232        created_block: u64,
2233        to: Address,
2234        value: U256,
2235        input: Bytes,
2236        authorization: &TransactionAuthorization,
2237        decision_header: Verified<VerifiedBlockHeader>,
2238    ) -> anyhow::Result<PreparedTransaction> {
2239        self.prepare_and_sign_with_anchors(
2240            intent_id,
2241            created_block,
2242            to,
2243            value,
2244            input,
2245            None,
2246            Some(authorization),
2247            Some(decision_header),
2248        )
2249        .await
2250    }
2251
2252    async fn prepare_and_sign_swap(
2253        &self,
2254        intent_id: i64,
2255        created_block: u64,
2256        to: Address,
2257        value: U256,
2258        input: Bytes,
2259        anchors: &SwapQuoteAnchors,
2260    ) -> anyhow::Result<PreparedTransaction> {
2261        self.prepare_and_sign_with_anchors(
2262            intent_id,
2263            created_block,
2264            to,
2265            value,
2266            input,
2267            Some(anchors),
2268            None,
2269            None,
2270        )
2271        .await
2272    }
2273
2274    #[expect(
2275        clippy::too_many_arguments,
2276        reason = "Security-critical transaction fields and verification anchors stay explicit"
2277    )]
2278    async fn prepare_and_sign_with_anchors(
2279        &self,
2280        intent_id: i64,
2281        created_block: u64,
2282        to: Address,
2283        value: U256,
2284        input: Bytes,
2285        swap_anchors: Option<&SwapQuoteAnchors>,
2286        authorization: Option<&TransactionAuthorization>,
2287        decision_header: Option<Verified<VerifiedBlockHeader>>,
2288    ) -> anyhow::Result<PreparedTransaction> {
2289        let expected_chain_id = u64::from(self.chain_id);
2290        let chain_id_verification = required_verification(
2291            self.verification.verify_chain_id().await,
2292            "pre-sign chain ID",
2293        )?;
2294        let actual_chain_id = chain_id_verification.value;
2295        anyhow::ensure!(
2296            actual_chain_id == expected_chain_id,
2297            "Verified chain ID does not match the transaction chain"
2298        );
2299        let decision_header_verification = if let Some(anchors) = swap_anchors {
2300            required_verification(
2301                self.verification.verify_block(anchors.state.number).await,
2302                "pre-sign swap decision header reread",
2303            )?
2304        } else {
2305            match decision_header {
2306                Some(verified) => verified,
2307                None => {
2308                    let now_unix_secs = current_unix_secs()?;
2309                    required_verification(
2310                        self.verification
2311                            .verify_decision_header(now_unix_secs)
2312                            .await,
2313                        "pre-sign decision header",
2314                    )?
2315                }
2316            }
2317        };
2318        let decision_header = decision_header_verification.value;
2319
2320        if let Some(anchors) = swap_anchors {
2321            anyhow::ensure!(
2322                decision_header == anchors.state,
2323                "Verified swap decision header changed before signing"
2324            );
2325        }
2326        let decision_ancestry = self.verify_decision_ancestry(decision_header).await?;
2327        validate_transaction_authorization(authorization, to, value, &input)?;
2328        let deployment_verification = required_verification(
2329            self.verification
2330                .verify_deployment_manifest(&self.deployment_manifest, decision_header.number)
2331                .await,
2332            "pre-sign deployment manifest",
2333        )?;
2334        let authorization_decisions = match authorization {
2335            Some(authorization) => {
2336                self.verify_transaction_authorization(authorization, decision_header.number)
2337                    .await?
2338            }
2339            None => Vec::new(),
2340        };
2341        let base_fee_per_gas_wei = decision_header.base_fee_per_gas.ok_or_else(|| {
2342            anyhow::anyhow!(
2343                "Verified decision block {} has no base fee",
2344                decision_header.number
2345            )
2346        })?;
2347        let priority_fee_verification = required_verification(
2348            self.verification.verify_priority_fee().await,
2349            "pre-sign priority fee",
2350        )?;
2351        let priority_fee_per_gas_wei = priority_fee_verification.value;
2352        let (max_fee_per_gas, max_priority_fee_per_gas) = derive_fees(
2353            base_fee_per_gas_wei,
2354            priority_fee_per_gas_wei,
2355            self.base_fee_buffer_bps,
2356            u128::from(self.max_fee_per_gas_wei),
2357        )?;
2358        let gas_estimate_verification = required_verification(
2359            self.verification
2360                .verify_gas_estimate(
2361                    &self.wallet_address,
2362                    &to,
2363                    value,
2364                    &input,
2365                    decision_header.number,
2366                )
2367                .await,
2368            "pre-sign gas estimate",
2369        )?;
2370        let gas_estimate = gas_estimate_verification.value;
2371        let gas_limit = derive_gas_limit(gas_estimate, self.gas_buffer_bps, self.gas_limit)?;
2372        let max_gas_cost = U256::from(gas_limit)
2373            .checked_mul(U256::from(max_fee_per_gas))
2374            .ok_or_else(|| anyhow::anyhow!("Maximum gas cost overflow"))?;
2375        let max_transaction_cost = value
2376            .checked_add(max_gas_cost)
2377            .ok_or_else(|| anyhow::anyhow!("Maximum transaction cost overflow"))?;
2378        let native_balance_verification = required_verification(
2379            self.verification
2380                .verify_balance(&self.wallet_address, decision_header.number)
2381                .await,
2382            "pre-sign native balance",
2383        )?;
2384        let native_balance = native_balance_verification.value;
2385
2386        if native_balance < max_transaction_cost {
2387            anyhow::bail!(
2388                "Native currency balance {native_balance} wei is below maximum transaction cost {max_transaction_cost} wei"
2389            );
2390        }
2391        let decision_height = Some(decision_header.number);
2392        let mut decisions = vec![
2393            verification_decision(&chain_id_verification, None, None),
2394            verification_decision(
2395                &decision_header_verification,
2396                decision_height,
2397                decision_height,
2398            ),
2399            verification_decision(&deployment_verification, decision_height, decision_height),
2400            verification_decision(&priority_fee_verification, None, None),
2401            verification_decision(&gas_estimate_verification, decision_height, decision_height),
2402            verification_decision(
2403                &native_balance_verification,
2404                decision_height,
2405                decision_height,
2406            ),
2407        ];
2408        decisions.extend(decision_ancestry);
2409        decisions.extend(authorization_decisions);
2410        if let Some(anchors) = swap_anchors {
2411            decisions.extend(self.verify_swap_anchors_before_sign(anchors).await?);
2412            decisions.extend(anchors.precondition_decisions.iter().cloned());
2413        } else {
2414            decisions.extend(self.verify_pre_sign_header_fence(decision_header).await?);
2415        }
2416        let canonical_nonce_verification = required_verification(
2417            self.verification
2418                .verify_transaction_count(&self.wallet_address, decision_header.number)
2419                .await,
2420            "pre-sign canonical nonce reread",
2421        )?;
2422        let pending_nonce_verification = required_verification(
2423            self.verification
2424                .verify_pending_transaction_count(&self.wallet_address)
2425                .await,
2426            "pre-sign pending nonce reread",
2427        )?;
2428        anyhow::ensure!(
2429            pending_nonce_verification.value == canonical_nonce_verification.value,
2430            "Pending nonce does not match the verified canonical nonce"
2431        );
2432        let nonce = canonical_nonce_verification.value;
2433        decisions.push(verification_decision(
2434            &canonical_nonce_verification,
2435            decision_height,
2436            decision_height,
2437        ));
2438        decisions.push(verification_decision(
2439            &pending_nonce_verification,
2440            None,
2441            None,
2442        ));
2443        let tx = build_eip1559_transaction(
2444            expected_chain_id,
2445            nonce,
2446            gas_limit,
2447            max_fee_per_gas,
2448            max_priority_fee_per_gas,
2449            to,
2450            value,
2451            input,
2452        );
2453        let wallet_address = self.wallet_address.to_string();
2454        self.database
2455            .assign_execution_intent_nonce_verified(&ExecutionNonceAssignment {
2456                intent_id,
2457                chain_id: self.chain_id,
2458                wallet_address: &wallet_address,
2459                nonce,
2460                manifest_version: &self.manifest_version,
2461                manifest_digest: &self.manifest_digest,
2462                provider_ids: &self.provider_ids,
2463                operator_ids: &self.operator_ids,
2464                failure_domain_ids: &self.failure_domain_ids,
2465                decisions: &decisions,
2466            })
2467            .await?;
2468        let payload_lease = self
2469            .database
2470            .acquire_execution_payload_lease(&self.payload_keys)
2471            .await?;
2472        let (tx_hash, raw_tx) = sign_eip1559_transaction(tx, &self.signer).await?;
2473
2474        Ok(PreparedTransaction {
2475            intent_id,
2476            created_block,
2477            nonce,
2478            tx_hash,
2479            raw_tx,
2480            payload_lease: Some(payload_lease),
2481        })
2482    }
2483
2484    async fn verify_pre_sign_header_fence(
2485        &self,
2486        target: VerifiedBlockHeader,
2487    ) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
2488        let checkpoint = required_verification(
2489            self.verification.verify_checkpoint().await,
2490            "pre-sign checkpoint reread",
2491        )?;
2492        let header = required_verification(
2493            self.verification.verify_block(target.number).await,
2494            "pre-sign decision header reread",
2495        )?;
2496        anyhow::ensure!(
2497            header.value == target,
2498            "Decision header changed before signing"
2499        );
2500        Ok(vec![
2501            verification_decision(
2502                &checkpoint,
2503                Some(checkpoint.value.number),
2504                Some(checkpoint.value.number),
2505            ),
2506            verification_decision(&header, Some(target.number), Some(target.number)),
2507        ])
2508    }
2509
2510    async fn verify_decision_ancestry(
2511        &self,
2512        target: VerifiedBlockHeader,
2513    ) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
2514        let checkpoint = required_verification(
2515            self.verification.verify_checkpoint().await,
2516            "pre-sign checkpoint",
2517        )?;
2518        anyhow::ensure!(
2519            checkpoint.value.number <= target.number,
2520            "Pre-sign decision header precedes the trusted checkpoint"
2521        );
2522        let mut decisions = vec![verification_decision(
2523            &checkpoint,
2524            Some(checkpoint.value.number),
2525            Some(checkpoint.value.number),
2526        )];
2527        let wallet_address = self.wallet_address.to_string();
2528        let position = self
2529            .database
2530            .load_execution_verification_position(
2531                self.chain_id,
2532                &wallet_address,
2533                &self.manifest_version,
2534                &self.manifest_digest,
2535            )
2536            .await?
2537            .ok_or_else(|| anyhow::anyhow!("Execution verification ledger is not initialized"))?;
2538        let durable_tip = parse_verified_header(&position.finalized_tip)?;
2539        anyhow::ensure!(
2540            durable_tip.number >= checkpoint.value.number && durable_tip.number <= target.number,
2541            "Pre-sign decision header does not extend the durable finalized header tip"
2542        );
2543        let durable_tip_verification = required_verification(
2544            self.verification.verify_block(durable_tip.number).await,
2545            "pre-sign durable finalized tip",
2546        )?;
2547        anyhow::ensure!(
2548            durable_tip_verification.value == durable_tip,
2549            "Durable finalized header tip conflicts with independent sources"
2550        );
2551
2552        if durable_tip != checkpoint.value {
2553            decisions.push(verification_decision(
2554                &durable_tip_verification,
2555                Some(durable_tip.number),
2556                Some(durable_tip.number),
2557            ));
2558        }
2559        let mut cursor = durable_tip;
2560        while cursor.number < target.number {
2561            let end = cursor.number.saturating_add(4_096).min(target.number);
2562            let start = cursor.number.saturating_add(1);
2563            let ancestry = required_verification(
2564                self.verification.verify_header_window(cursor, end).await,
2565                "pre-sign decision ancestry",
2566            )?;
2567            cursor = *ancestry
2568                .value
2569                .last()
2570                .expect("nonempty decision ancestry advances the cursor");
2571            decisions.push(verification_decision(&ancestry, Some(start), Some(end)));
2572        }
2573        anyhow::ensure!(
2574            cursor == target,
2575            "Pre-sign decision header conflicts with its trusted ancestry"
2576        );
2577        Ok(decisions)
2578    }
2579
2580    async fn verify_transaction_authorization(
2581        &self,
2582        authorization: &TransactionAuthorization,
2583        block: u64,
2584    ) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
2585        match *authorization {
2586            TransactionAuthorization::Wrap { weth } => {
2587                let call = ERC20::balanceOfCall {
2588                    account: self.wallet_address,
2589                }
2590                .abi_encode();
2591                let balance = required_verification(
2592                    self.verification
2593                        .verify_decoded_call(None, &weth, U256::ZERO, &call, block, |result| {
2594                            ERC20::balanceOfCall::abi_decode_returns(result).map_err(Into::into)
2595                        })
2596                        .await,
2597                    "pre-sign wrapped token probe",
2598                )?;
2599                Ok(vec![verification_decision(
2600                    &balance,
2601                    Some(block),
2602                    Some(block),
2603                )])
2604            }
2605            TransactionAuthorization::Approve {
2606                token,
2607                router,
2608                amount,
2609            } => {
2610                let allowance_call = ERC20::allowanceCall {
2611                    owner: self.wallet_address,
2612                    spender: router,
2613                }
2614                .abi_encode();
2615                let allowance = required_verification(
2616                    self.verification
2617                        .verify_decoded_call(
2618                            None,
2619                            &token,
2620                            U256::ZERO,
2621                            &allowance_call,
2622                            block,
2623                            |result| {
2624                                ERC20::allowanceCall::abi_decode_returns(result).map_err(Into::into)
2625                            },
2626                        )
2627                        .await,
2628                    "pre-sign router allowance",
2629                )?;
2630                anyhow::ensure!(
2631                    allowance.value.is_zero() || amount.is_zero(),
2632                    "Router allowance for token {token} is already {}; approve zero before setting a new nonzero allowance",
2633                    allowance.value
2634                );
2635
2636                let approve_call = ERC20::approveCall {
2637                    spender: router,
2638                    amount,
2639                }
2640                .abi_encode();
2641                let simulation = required_verification(
2642                    self.verification
2643                        .verify_decoded_simulation(
2644                            &self.wallet_address,
2645                            &token,
2646                            U256::ZERO,
2647                            &approve_call,
2648                            block,
2649                            |result| {
2650                                if result.is_empty() {
2651                                    Ok(true)
2652                                } else {
2653                                    ERC20::approveCall::abi_decode_returns_validate(result)
2654                                        .map_err(Into::into)
2655                                }
2656                            },
2657                        )
2658                        .await,
2659                    "pre-sign approval simulation",
2660                )?;
2661
2662                match &simulation.value {
2663                    VerifiedSimulation::Succeeded(true) => {}
2664                    VerifiedSimulation::Succeeded(false) => {
2665                        anyhow::bail!("ERC-20 approve returned false for token {token}")
2666                    }
2667                    VerifiedSimulation::Denied => {
2668                        anyhow::bail!("ERC-20 approve simulation reverted for token {token}")
2669                    }
2670                }
2671                Ok(vec![
2672                    verification_decision(&allowance, Some(block), Some(block)),
2673                    verification_decision(&simulation, Some(block), Some(block)),
2674                ])
2675            }
2676        }
2677    }
2678
2679    async fn verify_swap_anchors_before_sign(
2680        &self,
2681        anchors: &SwapQuoteAnchors,
2682    ) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
2683        let checkpoint = required_verification(
2684            self.verification.verify_checkpoint().await,
2685            "pre-sign checkpoint reread",
2686        )?;
2687        let watermark = required_verification(
2688            self.verification
2689                .verify_block(anchors.watermark.number)
2690                .await,
2691            "pre-sign profiler watermark reread",
2692        )?;
2693        anyhow::ensure!(
2694            watermark.value == anchors.watermark,
2695            "Pool state header changed before signing"
2696        );
2697        let ancestry = required_verification(
2698            self.verification
2699                .verify_header_window(watermark.value, anchors.state.number)
2700                .await,
2701            "pre-sign profiler ancestry reread",
2702        )?;
2703
2704        if let Some(last) = ancestry.value.last() {
2705            anyhow::ensure!(
2706                *last == anchors.state,
2707                "Swap decision header changed in the pre-sign ancestry reread"
2708            );
2709        } else {
2710            anyhow::ensure!(
2711                watermark.value == anchors.state,
2712                "Empty pre-sign ancestry does not end at the swap decision header"
2713            );
2714        }
2715        let quote = match anchors.quote_kind {
2716            SwapQuoteKind::ExactInput(amount_in) => required_verification(
2717                self.verification
2718                    .verify_quote_exact_input_single(
2719                        &anchors.quote_contract,
2720                        anchors.token_in,
2721                        anchors.token_out,
2722                        amount_in,
2723                        anchors.fee,
2724                        anchors.state.number,
2725                    )
2726                    .await,
2727                "pre-sign exact-input quote reread",
2728            )?,
2729            SwapQuoteKind::ExactOutput(amount_out) => required_verification(
2730                self.verification
2731                    .verify_quote_exact_output_single(
2732                        &anchors.quote_contract,
2733                        anchors.token_in,
2734                        anchors.token_out,
2735                        amount_out,
2736                        anchors.fee,
2737                        anchors.state.number,
2738                    )
2739                    .await,
2740                "pre-sign exact-output quote reread",
2741            )?,
2742        };
2743        anyhow::ensure!(
2744            quote.value == anchors.quote,
2745            "Independent swap quote changed before signing"
2746        );
2747        Ok(vec![
2748            verification_decision(
2749                &checkpoint,
2750                Some(checkpoint.value.number),
2751                Some(checkpoint.value.number),
2752            ),
2753            verification_decision(
2754                &watermark,
2755                Some(anchors.watermark.number),
2756                Some(anchors.watermark.number),
2757            ),
2758            verification_decision(
2759                &ancestry,
2760                Some(anchors.watermark.number),
2761                Some(anchors.state.number),
2762            ),
2763            verification_decision(
2764                &quote,
2765                Some(anchors.state.number),
2766                Some(anchors.state.number),
2767            ),
2768        ])
2769    }
2770
2771    /// Fills the claimed slot with the signed transaction and persists the pending record
2772    /// before broadcast.
2773    ///
2774    /// The slot is filled before the cancellable write: PostgreSQL may commit before this
2775    /// future resumes, so cancellation cannot safely restore the pre-transaction state. A
2776    /// persistence failure leaves the slot occupied because commit acknowledgement is
2777    /// ambiguous.
2778    async fn fill_and_persist(
2779        &self,
2780        prepared: &PreparedTransaction,
2781        purpose: TransactionPurpose,
2782    ) -> anyhow::Result<()> {
2783        {
2784            let mut slot = self.in_flight.lock();
2785            *slot = Some(InFlightSlot::AwaitingFinality(InFlightTransaction {
2786                intent_id: prepared.intent_id,
2787                nonce: prepared.nonce,
2788                tx_hash: prepared.tx_hash,
2789                purpose,
2790            }));
2791        }
2792
2793        let tx_hash = prepared.tx_hash;
2794        let transaction_hash = tx_hash.to_string();
2795        let policy = self.payload_policy();
2796        let intent = self
2797            .database
2798            .get_execution_intent(prepared.intent_id)
2799            .await?;
2800        authenticate_payload_identity(
2801            &prepared.raw_tx,
2802            &intent,
2803            &transaction_hash,
2804            self.chain_id,
2805            policy,
2806        )
2807        .with_context(|| format!("Newly signed transaction {tx_hash} failed authentication"))?;
2808
2809        self.database
2810            .reserve_execution_payload_seal(&self.payload_keys)
2811            .await?;
2812        let context = payload_context_identity(
2813            &intent,
2814            &transaction_hash,
2815            self.chain_id,
2816            self.payload_keys.deployment_id(),
2817        )?;
2818        let envelope = self.payload_keys.seal(&prepared.raw_tx, &context)?;
2819        let row = self
2820            .database
2821            .add_execution_transaction_envelope(
2822                prepared.intent_id,
2823                self.chain_id,
2824                &transaction_hash,
2825                &envelope,
2826            )
2827            .await
2828        .map_err(|e| {
2829            anyhow::anyhow!(
2830                "Failed to persist transaction {tx_hash}: {e}; the in-flight slot stays occupied"
2831            )
2832        })?;
2833        let stored = open_execution_payload(
2834            &self.payload_keys,
2835            policy,
2836            &intent,
2837            &row,
2838            "initial persistence",
2839        )?;
2840        anyhow::ensure!(
2841            stored == prepared.raw_tx,
2842            "Persisted transaction {tx_hash} does not match the signed bytes"
2843        );
2844        Ok(())
2845    }
2846
2847    fn payload_policy(&self) -> PayloadPolicy {
2848        PayloadPolicy {
2849            chain_id: self.chain_id,
2850            signer: self.wallet_address,
2851            gas_limit: self.gas_limit,
2852            max_fee_per_gas: self.max_fee_per_gas_wei,
2853        }
2854    }
2855
2856    async fn authorize_rebroadcast(
2857        &self,
2858        prepared: &PreparedTransaction,
2859        intent: &ExecutionIntentRow,
2860        purpose: TransactionPurpose,
2861    ) -> anyhow::Result<ReconciliationAuthorization> {
2862        let now_unix_secs = current_unix_secs()?;
2863        let decision_header = required_verification(
2864            self.verification
2865                .verify_decision_header(now_unix_secs)
2866                .await,
2867            "rebroadcast decision header",
2868        )?;
2869        let block = decision_header.value.number;
2870        let mut decisions = vec![verification_decision(
2871            &decision_header,
2872            Some(block),
2873            Some(block),
2874        )];
2875        decisions.extend(self.verify_decision_ancestry(decision_header.value).await?);
2876        let deployment = required_verification(
2877            self.verification
2878                .verify_deployment_manifest(&self.deployment_manifest, block)
2879                .await,
2880            "rebroadcast deployment manifest",
2881        )?;
2882        decisions.push(verification_decision(&deployment, Some(block), Some(block)));
2883        let canonical_nonce = required_verification(
2884            self.verification
2885                .verify_transaction_count(&self.wallet_address, block)
2886                .await,
2887            "rebroadcast canonical nonce",
2888        )?;
2889        decisions.push(verification_decision(
2890            &canonical_nonce,
2891            Some(block),
2892            Some(block),
2893        ));
2894        let pending_nonce = required_verification(
2895            self.verification
2896                .verify_reconciliation_pending_transaction_count(
2897                    &self.wallet_address,
2898                    prepared.nonce,
2899                )
2900                .await,
2901            "rebroadcast pending nonce",
2902        )?;
2903        decisions.push(verification_decision(&pending_nonce, None, None));
2904        let receipt_absence = required_verification(
2905            self.verification
2906                .verify_receipt_absence(&prepared.tx_hash)
2907                .await,
2908            "rebroadcast receipt absence",
2909        )?;
2910        decisions.push(verification_decision(&receipt_absence, None, None));
2911
2912        let next_nonce = prepared
2913            .nonce
2914            .checked_add(1)
2915            .ok_or_else(|| anyhow::anyhow!("Owned signer nonce overflow"))?;
2916        anyhow::ensure!(
2917            (prepared.nonce..=next_nonce).contains(&pending_nonce.value),
2918            "Pending nonce {} is outside the owned reconciliation range {}..={next_nonce}",
2919            pending_nonce.value,
2920            prepared.nonce
2921        );
2922        anyhow::ensure!(
2923            (prepared.nonce..=next_nonce).contains(&canonical_nonce.value),
2924            "Canonical nonce {} is outside the owned reconciliation range {}..={next_nonce}",
2925            canonical_nonce.value,
2926            prepared.nonce
2927        );
2928
2929        if !receipt_absence.value {
2930            self.persist_rebroadcast_decisions(intent.id, prepared.nonce, &decisions)
2931                .await?;
2932            return Ok(ReconciliationAuthorization::Retain);
2933        }
2934
2935        if canonical_nonce.value == next_nonce {
2936            self.persist_rebroadcast_decisions(intent.id, prepared.nonce, &decisions)
2937                .await?;
2938            return Ok(ReconciliationAuthorization::ScanReplacement(
2939                decision_header.value,
2940            ));
2941        }
2942
2943        let (to, input, value) = persisted_call_fields(intent)?;
2944        let authorized = match purpose {
2945            TransactionPurpose::Wrap => {
2946                let simulation = required_verification(
2947                    self.verification
2948                        .verify_decoded_simulation(
2949                            &self.wallet_address,
2950                            &to,
2951                            value,
2952                            &input,
2953                            block,
2954                            |result| Ok(result.is_empty()),
2955                        )
2956                        .await,
2957                    "rebroadcast wrap simulation",
2958                )?;
2959                let authorized = matches!(&simulation.value, VerifiedSimulation::Succeeded(true));
2960                decisions.push(verification_decision(&simulation, Some(block), Some(block)));
2961                authorized
2962            }
2963            TransactionPurpose::Approve => {
2964                let simulation = required_verification(
2965                    self.verification
2966                        .verify_decoded_simulation(
2967                            &self.wallet_address,
2968                            &to,
2969                            value,
2970                            &input,
2971                            block,
2972                            |result| {
2973                                if result.is_empty() {
2974                                    Ok(true)
2975                                } else {
2976                                    ERC20::approveCall::abi_decode_returns_validate(result)
2977                                        .map_err(Into::into)
2978                                }
2979                            },
2980                        )
2981                        .await,
2982                    "rebroadcast approve simulation",
2983                )?;
2984                let authorized = matches!(&simulation.value, VerifiedSimulation::Succeeded(true));
2985                decisions.push(verification_decision(&simulation, Some(block), Some(block)));
2986                authorized
2987            }
2988            TransactionPurpose::Swap => {
2989                let call = UniswapV3SwapRouter::exactInputSingleCall::abi_decode(&input)
2990                    .with_context(|| "persisted swap calldata is invalid")?;
2991
2992                if U256::from(decision_header.value.timestamp) > call.params.deadline {
2993                    false
2994                } else {
2995                    let simulation = required_verification(
2996                        self.verification
2997                            .verify_decoded_simulation(
2998                                &self.wallet_address,
2999                                &to,
3000                                value,
3001                                &input,
3002                                block,
3003                                |result| {
3004                                    UniswapV3SwapRouter::exactInputSingleCall::abi_decode_returns(
3005                                        result,
3006                                    )
3007                                    .map_err(Into::into)
3008                                },
3009                            )
3010                            .await,
3011                        "rebroadcast swap simulation",
3012                    )?;
3013                    let authorized = match &simulation.value {
3014                        VerifiedSimulation::Succeeded(amount_out) => {
3015                            *amount_out >= call.params.amountOutMinimum
3016                        }
3017                        VerifiedSimulation::Denied => false,
3018                    };
3019                    decisions.push(verification_decision(&simulation, Some(block), Some(block)));
3020                    authorized
3021                }
3022            }
3023        };
3024        self.persist_rebroadcast_decisions(intent.id, prepared.nonce, &decisions)
3025            .await?;
3026        Ok(if authorized {
3027            ReconciliationAuthorization::Rebroadcast
3028        } else {
3029            ReconciliationAuthorization::Retain
3030        })
3031    }
3032
3033    async fn scan_canonical_replacement(
3034        &self,
3035        intent: &ExecutionIntentRow,
3036        nonce: u64,
3037        head: VerifiedBlockHeader,
3038        authenticated_payloads: &HashMap<B256, Vec<u8>>,
3039    ) -> anyhow::Result<Option<(B256, Vec<u8>)>> {
3040        let wallet_address = self.wallet_address.to_string();
3041        let cursor = self
3042            .database
3043            .load_execution_replacement_cursor(
3044                intent.id,
3045                self.chain_id,
3046                &wallet_address,
3047                nonce,
3048                &self.manifest_digest,
3049            )
3050            .await?;
3051        let start = cursor.as_ref().map_or(intent.created_block, |header| {
3052            header.number.saturating_add(1)
3053        });
3054        anyhow::ensure!(
3055            start <= head.number,
3056            "Canonical nonce advanced without an authenticated signer transaction in the scanned canonical range"
3057        );
3058        let scan_range = replacement_scan_range(start, head.number)?;
3059        let end = *scan_range.end();
3060        let mut decisions = Vec::new();
3061        let mut blocks = Vec::new();
3062
3063        if let Some(cursor) = cursor.as_ref() {
3064            let parent = parse_verified_header(cursor)?;
3065            let window = required_verification(
3066                self.verification
3067                    .verify_replacement_window(parent, end)
3068                    .await,
3069                "canonical replacement window",
3070            )?;
3071            decisions.push(verification_decision(&window, Some(start), Some(end)));
3072            blocks = window.value;
3073        } else {
3074            let start_header = required_verification(
3075                self.verification.verify_block(start).await,
3076                "canonical replacement start header",
3077            )?;
3078            decisions.push(verification_decision(
3079                &start_header,
3080                Some(start),
3081                Some(start),
3082            ));
3083            let start_block = required_verification(
3084                self.verification.verify_replacement_block(start).await,
3085                "canonical replacement start block",
3086            )?;
3087            anyhow::ensure!(
3088                VerifiedBlockHeader::from(start_block.value.clone()) == start_header.value,
3089                "Replacement block conflicts with its canonical header"
3090            );
3091            decisions.push(verification_decision(
3092                &start_block,
3093                Some(start),
3094                Some(start),
3095            ));
3096            blocks.push(start_block.value);
3097
3098            if end > start {
3099                let window = required_verification(
3100                    self.verification
3101                        .verify_replacement_window(start_header.value, end)
3102                        .await,
3103                    "canonical replacement window",
3104                )?;
3105                decisions.push(verification_decision(&window, Some(start + 1), Some(end)));
3106                blocks.extend(window.value);
3107            }
3108        }
3109
3110        let scanned_tip = blocks
3111            .last()
3112            .map(|block| VerifiedBlockHeader::from(block.clone()))
3113            .ok_or_else(|| anyhow::anyhow!("Verified replacement scan returned no blocks"))?;
3114        if end == head.number {
3115            anyhow::ensure!(
3116                scanned_tip == head,
3117                "Replacement scan tip conflicts with the verified canonical head"
3118            );
3119        }
3120        let mut candidates = blocks
3121            .iter()
3122            .flat_map(|block| block.transactions.iter())
3123            .filter(|transaction| {
3124                transaction.from == self.wallet_address && transaction.nonce == nonce
3125            });
3126        let candidate = candidates.next();
3127        anyhow::ensure!(
3128            candidates.next().is_none(),
3129            "Canonical replacement scan found duplicate signer-nonce transactions"
3130        );
3131
3132        let finalized_cursor = self
3133            .database
3134            .load_execution_verified_header(
3135                self.chain_id,
3136                &wallet_address,
3137                end,
3138                &self.manifest_digest,
3139            )
3140            .await?;
3141
3142        if let Some(cursor) = finalized_cursor.as_ref() {
3143            anyhow::ensure!(
3144                parse_verified_header(cursor)? == scanned_tip,
3145                "Replacement scan conflicts with the durable finalized header ledger"
3146            );
3147        }
3148
3149        let mut mismatch = None;
3150        let matched = candidate.and_then(|transaction| {
3151            let Some(raw_transaction) = authenticated_payloads.get(&transaction.hash).cloned()
3152            else {
3153                mismatch = Some(anyhow::anyhow!(
3154                    "Canonical signer-nonce transaction {} has no authenticated retained payload",
3155                    transaction.hash
3156                ));
3157                return None;
3158            };
3159
3160            if let Err(e) = validate_rpc_transaction_matches_payload(transaction, &raw_transaction)
3161            {
3162                mismatch = Some(e.context(format!(
3163                    "Canonical signer-nonce transaction {} failed authenticated payload validation",
3164                    transaction.hash
3165                )));
3166                return None;
3167            }
3168            Some((transaction.hash, raw_transaction))
3169        });
3170        let matched_hash = matched.as_ref().map(|(hash, _)| hash.to_string());
3171        self.database
3172            .record_execution_replacement_scan(&ExecutionReplacementScan {
3173                intent_id: intent.id,
3174                chain_id: self.chain_id,
3175                wallet_address: &wallet_address,
3176                nonce,
3177                finalized_cursor: finalized_cursor.as_ref(),
3178                matched_transaction_hash: matched_hash.as_deref(),
3179                manifest_version: &self.manifest_version,
3180                manifest_digest: &self.manifest_digest,
3181                provider_ids: &self.provider_ids,
3182                operator_ids: &self.operator_ids,
3183                failure_domain_ids: &self.failure_domain_ids,
3184                decisions: &decisions,
3185            })
3186            .await?;
3187
3188        if let Some(e) = mismatch {
3189            return Err(e);
3190        }
3191
3192        if matched.is_none() && end == head.number {
3193            anyhow::bail!(
3194                "Canonical nonce advanced without an authenticated signer transaction in the canonical range"
3195            );
3196        }
3197        Ok(matched)
3198    }
3199
3200    async fn persist_rebroadcast_decisions(
3201        &self,
3202        intent_id: i64,
3203        nonce: u64,
3204        decisions: &[ExecutionVerificationDecision],
3205    ) -> anyhow::Result<()> {
3206        let wallet_address = self.wallet_address.to_string();
3207        self.database
3208            .record_execution_verification_batch(&ExecutionVerificationBatch {
3209                intent_id,
3210                chain_id: self.chain_id,
3211                wallet_address: &wallet_address,
3212                nonce,
3213                decision_class: "rebroadcast",
3214                manifest_version: &self.manifest_version,
3215                manifest_digest: &self.manifest_digest,
3216                provider_ids: &self.provider_ids,
3217                operator_ids: &self.operator_ids,
3218                failure_domain_ids: &self.failure_domain_ids,
3219                decisions,
3220            })
3221            .await
3222    }
3223
3224    async fn broadcast(&self, prepared: &PreparedTransaction) -> anyhow::Result<BroadcastOutcome> {
3225        let tx_hash = prepared.tx_hash;
3226
3227        self.database
3228            .record_execution_status(
3229                prepared.intent_id,
3230                &tx_hash.to_string(),
3231                TransactionStatus::Broadcast,
3232                None,
3233                None,
3234                None,
3235                None,
3236                None,
3237            )
3238            .await
3239            .map_err(|e| {
3240                anyhow::anyhow!(
3241                    "Failed to persist broadcast attempt for transaction {tx_hash}: {e}; the in-flight slot stays occupied"
3242                )
3243            })?;
3244
3245        match self
3246            .http_rpc_client
3247            .send_raw_transaction(&prepared.raw_tx, &tx_hash)
3248            .await
3249        {
3250            Ok(broadcast_hash) => {
3251                if broadcast_hash != tx_hash {
3252                    // The node acknowledged a different hash: acceptance of the signed
3253                    // transaction is unverified, so reconcile through the persisted record
3254                    // rather than poll a hash that cannot match the chain
3255                    return Ok(BroadcastOutcome::Ambiguous(format!(
3256                        "Broadcast of transaction {tx_hash} returned a differing hash {broadcast_hash}; the persisted record reconciles instead of rebroadcasting"
3257                    )));
3258                }
3259                Ok(BroadcastOutcome::Accepted)
3260            }
3261            Err(BroadcastError::TimeoutAfterSend) => Ok(BroadcastOutcome::Ambiguous(format!(
3262                "Broadcast of transaction {tx_hash} timed out after send; the persisted record reconciles instead of rebroadcasting"
3263            ))),
3264            Err(BroadcastError::Failed(message)) => {
3265                // Transport or response failure: acceptance is ambiguous (the failure may also
3266                // predate dispatch; see BroadcastError::Failed), so occupy the slot and
3267                // reconcile rather than risk a rebroadcast
3268                Ok(BroadcastOutcome::Ambiguous(format!(
3269                    "Broadcast of transaction {tx_hash} failed ambiguously ({message}); the persisted record reconciles instead of rebroadcasting"
3270                )))
3271            }
3272            Err(error @ BroadcastError::Rejected { .. }) => {
3273                Ok(BroadcastOutcome::Ambiguous(format!(
3274                    "Broadcast of transaction {tx_hash} was rejected ({error}); the signed hash remains occupied until canonical nonce reconciliation"
3275                )))
3276            }
3277        }
3278    }
3279
3280    /// Polls until the receipt is canonical at a stable finalized boundary.
3281    async fn await_finality(
3282        &self,
3283        prepared: &PreparedTransaction,
3284    ) -> anyhow::Result<InclusionOutcome> {
3285        let tx_hash = prepared.tx_hash;
3286        let deadline = tokio::time::Instant::now() + self.receipt_timeout;
3287
3288        for attempt in 0..self.receipt_max_polls {
3289            if tokio::time::Instant::now() >= deadline {
3290                break;
3291            }
3292
3293            if attempt > 0 {
3294                tokio::time::sleep(RECEIPT_POLL_INTERVAL).await;
3295            }
3296
3297            let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
3298            let receipt_result =
3299                match tokio::time::timeout(remaining, self.verification.verify_receipt(&tx_hash))
3300                    .await
3301                {
3302                    Ok(result) => result,
3303                    Err(_) => break,
3304                };
3305
3306            match receipt_result {
3307                VerificationOutcome::Verified(verified_receipt) => {
3308                    let receipt = &verified_receipt.value;
3309                    let canonical_verification = required_verification(
3310                        self.verification.verify_block(receipt.block_number).await,
3311                        "receipt inclusion header",
3312                    )?;
3313                    let canonical = canonical_verification.value;
3314
3315                    if canonical.hash != receipt.block_hash {
3316                        continue;
3317                    }
3318
3319                    if let Some(mut finality) = self.receipt_is_stably_finalized(receipt).await? {
3320                        finality.decisions.insert(
3321                            0,
3322                            verification_decision(
3323                                &canonical_verification,
3324                                Some(receipt.block_number),
3325                                Some(receipt.block_number),
3326                            ),
3327                        );
3328                        finality.decisions.insert(
3329                            0,
3330                            verification_decision(
3331                                &verified_receipt,
3332                                Some(receipt.block_number),
3333                                Some(receipt.block_number),
3334                            ),
3335                        );
3336                        let included = IncludedTransaction {
3337                            intent_id: prepared.intent_id,
3338                            nonce: prepared.nonce,
3339                            tx_hash,
3340                            block_number: receipt.block_number,
3341                            receipt: receipt.clone(),
3342                            finality,
3343                        };
3344                        return if receipt.status {
3345                            Ok(InclusionOutcome::Finalized(included))
3346                        } else {
3347                            Ok(InclusionOutcome::Reverted(included))
3348                        };
3349                    }
3350                }
3351                VerificationOutcome::Retryable(_) => {
3352                    continue;
3353                }
3354                VerificationOutcome::Disagreement(_) => {
3355                    return Ok(InclusionOutcome::Pending(format!(
3356                        "Receipt verification disagreed for transaction {tx_hash}; the intent stays occupied for reconciliation"
3357                    )));
3358                }
3359                VerificationOutcome::Unavailable(_) => {
3360                    log::warn!(
3361                        "Finality poll {}/{} for transaction {tx_hash} was unavailable",
3362                        attempt + 1,
3363                        self.receipt_max_polls
3364                    );
3365                }
3366                VerificationOutcome::LocallyInvalid(_) => {
3367                    anyhow::bail!(
3368                        "Receipt verification is locally invalid for transaction {tx_hash}"
3369                    );
3370                }
3371            }
3372        }
3373
3374        self.database
3375            .record_execution_status(
3376                prepared.intent_id,
3377                &tx_hash.to_string(),
3378                TransactionStatus::Dropped,
3379                None,
3380                None,
3381                None,
3382                None,
3383                None,
3384            )
3385            .await?;
3386        Ok(InclusionOutcome::Pending(format!(
3387            "Timed out awaiting finality of transaction {tx_hash}; the intent stays occupied for reconciliation"
3388        )))
3389    }
3390
3391    async fn receipt_is_stably_finalized(
3392        &self,
3393        receipt: &RpcTransactionReceipt,
3394    ) -> anyhow::Result<Option<StableFinality>> {
3395        let finalized_verification = match self.verification.verify_finalized_header().await {
3396            VerificationOutcome::Verified(verified) => verified,
3397            VerificationOutcome::Retryable(_) | VerificationOutcome::Unavailable(_) => {
3398                return Ok(None);
3399            }
3400            VerificationOutcome::Disagreement(_) => {
3401                anyhow::bail!("Finalized header verification disagreed")
3402            }
3403            VerificationOutcome::LocallyInvalid(_) => {
3404                anyhow::bail!("Finalized header verification is locally invalid")
3405            }
3406        };
3407        let finalized = finalized_verification.value;
3408        if finalized.number < receipt.block_number {
3409            return Ok(None);
3410        }
3411
3412        let checkpoint_verification = required_verification(
3413            self.verification.verify_checkpoint().await,
3414            "finality checkpoint reread",
3415        )?;
3416        let checkpoint = checkpoint_verification.value;
3417        let mut decisions = vec![verification_decision(
3418            &checkpoint_verification,
3419            Some(checkpoint.number),
3420            Some(checkpoint.number),
3421        )];
3422        let position = self
3423            .database
3424            .load_execution_verification_position(
3425                self.chain_id,
3426                &self.wallet_address.to_string(),
3427                &self.manifest_version,
3428                &self.manifest_digest,
3429            )
3430            .await?
3431            .ok_or_else(|| anyhow::anyhow!("Execution verification ledger is not initialized"))?;
3432        let durable_tip = parse_verified_header(&position.finalized_tip)?;
3433        anyhow::ensure!(
3434            durable_tip.number >= checkpoint.number,
3435            "Durable finalized header tip precedes the trusted checkpoint"
3436        );
3437        let mut finalized_headers = vec![durable_tip];
3438        let durable_tip_verification = required_verification(
3439            self.verification.verify_block(durable_tip.number).await,
3440            "finality durable header tip",
3441        )?;
3442        anyhow::ensure!(
3443            durable_tip_verification.value == durable_tip,
3444            "Durable finalized header tip conflicts with independent sources"
3445        );
3446        decisions.push(verification_decision(
3447            &durable_tip_verification,
3448            Some(durable_tip.number),
3449            Some(durable_tip.number),
3450        ));
3451        anyhow::ensure!(
3452            finalized.number >= durable_tip.number,
3453            "Verified finalized height regressed below the durable finalized header tip"
3454        );
3455        let mut ancestry_cursor = durable_tip;
3456        while ancestry_cursor.number < finalized.number {
3457            let end = ancestry_cursor
3458                .number
3459                .saturating_add(4_096)
3460                .min(finalized.number);
3461            let start = ancestry_cursor.number.saturating_add(1);
3462            let ancestry_verification = required_verification(
3463                self.verification
3464                    .verify_header_window(ancestry_cursor, end)
3465                    .await,
3466                "finality ancestry",
3467            )?;
3468            let ancestry = &ancestry_verification.value;
3469            ancestry_cursor = *ancestry
3470                .last()
3471                .expect("nonempty finality ancestry advances the cursor");
3472            decisions.push(verification_decision(
3473                &ancestry_verification,
3474                Some(start),
3475                Some(end),
3476            ));
3477            finalized_headers.extend(ancestry.iter().copied());
3478        }
3479        anyhow::ensure!(
3480            ancestry_cursor == finalized,
3481            "Finalized header conflicts with its verified ancestry"
3482        );
3483
3484        let canonical_again_verification = required_verification(
3485            self.verification.verify_block(receipt.block_number).await,
3486            "finality inclusion header reread",
3487        )?;
3488        let canonical_again = canonical_again_verification.value;
3489        let finalized_again_verification = required_verification(
3490            self.verification.verify_block(finalized.number).await,
3491            "finalized header reread",
3492        )?;
3493        let finalized_again = finalized_again_verification.value;
3494        anyhow::ensure!(
3495            canonical_again.hash == receipt.block_hash && finalized_again == finalized,
3496            "Finality verification disagreed with the receipt or finalized header"
3497        );
3498        decisions.extend([
3499            verification_decision(
3500                &finalized_verification,
3501                Some(finalized.number),
3502                Some(finalized.number),
3503            ),
3504            verification_decision(
3505                &canonical_again_verification,
3506                Some(receipt.block_number),
3507                Some(receipt.block_number),
3508            ),
3509            verification_decision(
3510                &finalized_again_verification,
3511                Some(finalized.number),
3512                Some(finalized.number),
3513            ),
3514        ]);
3515        Ok(Some(StableFinality {
3516            decisions,
3517            inclusion_header: durable_verified_header(&canonical_again),
3518            finalized_headers: finalized_headers
3519                .iter()
3520                .map(durable_verified_header)
3521                .collect(),
3522        }))
3523    }
3524
3525    async fn commit_verified_finality(
3526        &self,
3527        included: &IncludedTransaction,
3528        status: TransactionStatus,
3529        verified_postconditions: &[ExecutionVerificationDecision],
3530    ) -> anyhow::Result<()> {
3531        anyhow::ensure!(
3532            matches!(
3533                status,
3534                TransactionStatus::Finalized | TransactionStatus::Reverted
3535            ) && included.receipt.status == (status == TransactionStatus::Finalized),
3536            "Finality status conflicts with the verified transaction receipt"
3537        );
3538        let mut decisions = included.finality.decisions.clone();
3539        decisions.extend_from_slice(verified_postconditions);
3540        let wallet_address = self.wallet_address.to_string();
3541        let transaction_hash = included.tx_hash.to_string();
3542        let block_hash = included.receipt.block_hash.to_string();
3543        let effective_gas_price = included.receipt.effective_gas_price.to_string();
3544        self.database
3545            .record_execution_finality_verified(&ExecutionFinalityTransition {
3546                intent_id: included.intent_id,
3547                chain_id: self.chain_id,
3548                wallet_address: &wallet_address,
3549                nonce: included.nonce,
3550                transaction_hash: &transaction_hash,
3551                status,
3552                block_number: included.block_number,
3553                block_hash: &block_hash,
3554                receipt_success: included.receipt.status,
3555                gas_used: included.receipt.gas_used,
3556                effective_gas_price: &effective_gas_price,
3557                manifest_version: &self.manifest_version,
3558                manifest_digest: &self.manifest_digest,
3559                provider_ids: &self.provider_ids,
3560                operator_ids: &self.operator_ids,
3561                failure_domain_ids: &self.failure_domain_ids,
3562                decisions: &decisions,
3563                finalized_headers: &included.finality.finalized_headers,
3564            })
3565            .await
3566    }
3567
3568    fn release_slot(&self) {
3569        *self.in_flight.lock() = None;
3570    }
3571}
3572
3573fn replacement_scan_range(from_block: u64, head_block: u64) -> anyhow::Result<RangeInclusive<u64>> {
3574    anyhow::ensure!(
3575        head_block >= from_block,
3576        "Canonical head {head_block} is behind execution creation block {from_block}"
3577    );
3578    let max_end = from_block.saturating_add(MAX_REPLACEMENT_SCAN_BLOCKS - 1);
3579    Ok(from_block..=head_block.min(max_end))
3580}
3581
3582fn current_unix_secs() -> anyhow::Result<u64> {
3583    SystemTime::now()
3584        .duration_since(UNIX_EPOCH)
3585        .map_err(|_| anyhow::anyhow!("Trusted host clock precedes the Unix epoch"))
3586        .map(|duration| duration.as_secs())
3587}
3588
3589fn validate_payload_operation_batch_size(batch_size: usize) -> anyhow::Result<i64> {
3590    anyhow::ensure!(
3591        (1..=MAX_PAYLOAD_OPERATION_BATCH_SIZE).contains(&batch_size),
3592        "Payload operation batch size must be between 1 and {MAX_PAYLOAD_OPERATION_BATCH_SIZE}"
3593    );
3594    Ok(i64::try_from(batch_size).expect("bounded payload batch size fits i64"))
3595}
3596
3597fn current_execution_hash(
3598    intent_id: i64,
3599    hashes: &[ExecutionTransactionHashRow],
3600) -> anyhow::Result<&ExecutionTransactionHashRow> {
3601    let mut current = hashes.iter().filter(|row| row.current);
3602    let row = current
3603        .next()
3604        .ok_or_else(|| anyhow::anyhow!("Execution intent {intent_id} has no current hash"))?;
3605    anyhow::ensure!(
3606        current.next().is_none(),
3607        "Execution intent {intent_id} has more than one current hash"
3608    );
3609    Ok(row)
3610}
3611
3612fn open_execution_payload(
3613    keys: &PayloadKeySet,
3614    policy: PayloadPolicy,
3615    intent: &ExecutionIntentRow,
3616    hash: &ExecutionTransactionHashRow,
3617    reason: &str,
3618) -> anyhow::Result<Vec<u8>> {
3619    anyhow::ensure!(
3620        hash.payload_expected,
3621        "Execution transaction {} has no signed payload",
3622        hash.transaction_hash
3623    );
3624    anyhow::ensure!(
3625        hash.raw_transaction.is_none(),
3626        "Protected execution transaction {} contains plaintext",
3627        hash.transaction_hash
3628    );
3629    let envelope = hash.sealed_transaction.as_deref().ok_or_else(|| {
3630        anyhow::anyhow!(
3631            "Protected execution transaction {} has no sealed payload",
3632            hash.transaction_hash
3633        )
3634    })?;
3635    let context = payload_context(intent, hash, keys.deployment_id())?;
3636    let raw_transaction = keys.unseal(envelope, &context)?;
3637    log::info!(
3638        "Unsealed execution payload for intent {} transaction {} during {reason}",
3639        intent.id,
3640        hash.transaction_hash
3641    );
3642    authenticate_retained_payload(&raw_transaction, intent, hash, keys.deployment_id())?;
3643    if retained_payload_requires_policy(intent, hash, policy)? {
3644        authenticate_payload(&raw_transaction, intent, hash, policy, keys.deployment_id())
3645            .with_context(|| {
3646                format!(
3647                    "execution intent {} transaction {} violates current execution policy",
3648                    intent.id, hash.transaction_hash
3649                )
3650            })?;
3651    }
3652    Ok(raw_transaction)
3653}
3654
3655fn receipt_max_polls(receipt_timeout_secs: u64) -> u32 {
3656    u32::try_from(receipt_timeout_secs.max(1)).unwrap_or(u32::MAX)
3657}
3658
3659fn receipt_timeout(receipt_timeout_secs: u64) -> Duration {
3660    Duration::from_secs(receipt_timeout_secs.clamp(1, u64::from(u32::MAX)))
3661}
3662
3663/// A fully validated swap ready for asynchronous dispatch.
3664#[derive(Debug)]
3665struct SwapPlan {
3666    order: OrderAny,
3667    pool: Pool,
3668    quote_currency: Currency,
3669    instrument_id: InstrumentId,
3670    pool_address: Address,
3671    router: Address,
3672    factory: Address,
3673    weth: Address,
3674    token_in: Address,
3675    token_out: Address,
3676    fee: U24,
3677    amount_in: U256,
3678    min_amount_out: U256,
3679    slippage_bps: u32,
3680    quote_spend_ceiling: Option<QuoteSpendCeiling>,
3681    profiler_position: Option<BlockPosition>,
3682}
3683
3684#[derive(Debug, Clone)]
3685struct SwapQuoteAnchors {
3686    watermark: VerifiedBlockHeader,
3687    state: VerifiedBlockHeader,
3688    quote_contract: Address,
3689    token_in: Address,
3690    token_out: Address,
3691    fee: U24,
3692    quote_kind: SwapQuoteKind,
3693    quote: UniswapV3Quote,
3694    precondition_decisions: Vec<ExecutionVerificationDecision>,
3695}
3696
3697#[derive(Debug, Clone, Copy)]
3698enum SwapQuoteKind {
3699    ExactInput(U256),
3700    ExactOutput(U256),
3701}
3702
3703/// Executes a validated swap plan: read-only pre-trade checks, signing, persistence,
3704/// broadcast, and finality observation.
3705///
3706/// Emits only events justified by known transaction state: `OrderDenied` before the
3707/// transaction exists, `OrderSubmitted` after broadcast acceptance, and `OrderRejected` on
3708/// a definitive node rejection or an on-chain revert. No fill is emitted at broadcast or
3709/// first inclusion; fills arrive with finality reconciliation. Ambiguous outcomes keep the
3710/// order submitted untouched while the persisted record reconciles. The client retains
3711/// unresolved submissions, so an exhausted in-flight check leaves the order submitted.
3712async fn execute_swap(
3713    mut plan: SwapPlan,
3714    executor: TransactionExecutor,
3715    emitter: ExecutionEventEmitter,
3716    max_quote_age_blocks: u64,
3717    deadline_seconds: u64,
3718) -> anyhow::Result<()> {
3719    let order = &plan.order;
3720
3721    if let Err(e) = executor.claim_slot(TransactionPurpose::Swap) {
3722        emitter.emit_order_denied(order, &e.to_string());
3723        return Ok(());
3724    }
3725
3726    let Some(profiler_position) = plan.profiler_position.as_ref() else {
3727        release_preparing_slot(&executor.in_flight);
3728        emitter.emit_order_denied(order, "Pool profiler has no quote provenance");
3729        return Ok(());
3730    };
3731    let mut swap_anchors = match validate_swap_quote(
3732        profiler_position,
3733        &plan,
3734        max_quote_age_blocks,
3735        &executor.verification,
3736        &executor.deployment_manifest,
3737    )
3738    .await
3739    {
3740        Ok(anchors) => anchors,
3741        Err(e) => {
3742            release_preparing_slot(&executor.in_flight);
3743            emitter.emit_order_denied(order, &e.to_string());
3744            return Ok(());
3745        }
3746    };
3747    let (amount_in, min_amount_out) = match verified_swap_amounts(&plan, swap_anchors.quote) {
3748        Ok(amounts) => amounts,
3749        Err(e) => {
3750            release_preparing_slot(&executor.in_flight);
3751            emitter.emit_order_denied(order, &e.to_string());
3752            return Ok(());
3753        }
3754    };
3755    plan.amount_in = amount_in;
3756    plan.min_amount_out = min_amount_out;
3757    let deadline = match swap_anchors.state.timestamp.checked_add(deadline_seconds) {
3758        Some(deadline) => deadline,
3759        None => {
3760            release_preparing_slot(&executor.in_flight);
3761            emitter.emit_order_denied(
3762                order,
3763                &format!(
3764                    "Swap deadline overflow: anchor timestamp {} plus `deadline_seconds` {deadline_seconds} exceeds u64",
3765                    swap_anchors.state.timestamp
3766                ),
3767            );
3768            return Ok(());
3769        }
3770    };
3771
3772    swap_anchors.precondition_decisions =
3773        match check_swap_preconditions(&plan, swap_anchors.state.number, &executor).await {
3774            Ok(decisions) => decisions,
3775            Err(e) => {
3776                release_preparing_slot(&executor.in_flight);
3777                emitter.emit_order_denied(order, &e.to_string());
3778                return Ok(());
3779            }
3780        };
3781
3782    let calldata = UniswapV3SwapRouter::exactInputSingleCall {
3783        params: UniswapV3SwapRouter::ExactInputSingleParams {
3784            tokenIn: plan.token_in,
3785            tokenOut: plan.token_out,
3786            fee: plan.fee,
3787            recipient: executor.wallet_address,
3788            deadline: U256::from(deadline),
3789            amountIn: plan.amount_in,
3790            amountOutMinimum: plan.min_amount_out,
3791            sqrtPriceLimitX96: U160::ZERO,
3792        },
3793    }
3794    .abi_encode();
3795
3796    let calldata = Bytes::from(calldata);
3797    let intent = ExecutionIntentInsert {
3798        chain_id: executor.chain_id,
3799        wallet_address: executor.wallet_address.to_string(),
3800        purpose: TransactionPurpose::Swap.as_str().to_string(),
3801        client_order_id: Some(order.client_order_id().to_string()),
3802        trader_id: Some(order.trader_id().to_string()),
3803        strategy_id: Some(order.strategy_id().to_string()),
3804        account_id: Some(executor.account_id.to_string()),
3805        instrument_id: Some(plan.instrument_id.to_string()),
3806        pool_address: Some(plan.pool_address.to_string()),
3807        transaction_to: plan.router.to_string(),
3808        transaction_input: hex::encode_prefixed(&calldata),
3809        transaction_value: U256::ZERO.to_string(),
3810        amount_in: Some(plan.amount_in.to_string()),
3811        created_block: swap_anchors.state.number,
3812    };
3813    let intent = match executor.database.reserve_execution_intent(&intent).await {
3814        Ok(intent) => intent,
3815        Err(e) => {
3816            release_preparing_if_reservation_not_committed(&executor.in_flight, &e);
3817            emitter.emit_order_denied(order, &e.to_string());
3818            return Ok(());
3819        }
3820    };
3821    let prepared = match executor
3822        .prepare_and_sign_swap(
3823            intent.id,
3824            intent.created_block,
3825            plan.router,
3826            U256::ZERO,
3827            calldata,
3828            &swap_anchors,
3829        )
3830        .await
3831    {
3832        Ok(prepared) => prepared,
3833        Err(e) => {
3834            if executor
3835                .database
3836                .mark_execution_intent_recoverable(intent.id)
3837                .await
3838                .is_ok()
3839            {
3840                release_preparing_slot(&executor.in_flight);
3841            }
3842            emitter.emit_order_denied(order, &e.to_string());
3843            return Ok(());
3844        }
3845    };
3846
3847    if let Err(e) = executor
3848        .fill_and_persist(&prepared, TransactionPurpose::Swap)
3849        .await
3850    {
3851        emitter.emit_order_denied(order, &e.to_string());
3852        return Ok(());
3853    }
3854
3855    let broadcast = executor.broadcast(&prepared).await?;
3856    emitter.emit_order_submitted(order);
3857    executor
3858        .database
3859        .mark_execution_event_emitted(intent.id, "acknowledgement")
3860        .await?;
3861
3862    if let BroadcastOutcome::Ambiguous(message) = broadcast {
3863        log::warn!("{message}");
3864    }
3865
3866    let trace_purpose = match plan.order.order_side() {
3867        OrderSide::Sell => "swap_sell",
3868        OrderSide::Buy => "swap_buy",
3869    };
3870
3871    match executor.await_finality(&prepared).await? {
3872        InclusionOutcome::Finalized(mut included) => {
3873            included.finality.decisions.extend(
3874                verify_finalized_transaction(
3875                    &included,
3876                    &intent,
3877                    prepared.nonce,
3878                    &prepared.raw_tx,
3879                    &executor,
3880                    trace_purpose,
3881                )
3882                .await?,
3883            );
3884            let fill = validate_finalized_swap_fill(&plan, &included)?;
3885            let wallet = load_verified_wallet_after_fill(&plan, &included, &executor).await?;
3886            executor
3887                .commit_verified_finality(
3888                    &included,
3889                    TransactionStatus::Finalized,
3890                    &wallet.decisions,
3891                )
3892                .await?;
3893            complete_finalized_swap(
3894                &plan,
3895                intent.id,
3896                included.tx_hash,
3897                fill,
3898                wallet,
3899                &executor,
3900                &emitter,
3901            )
3902            .await?;
3903            executor.release_slot();
3904            Ok(())
3905        }
3906        InclusionOutcome::Reverted(mut included) => {
3907            included.finality.decisions.extend(
3908                verify_finalized_transaction(
3909                    &included,
3910                    &intent,
3911                    prepared.nonce,
3912                    &prepared.raw_tx,
3913                    &executor,
3914                    trace_purpose,
3915                )
3916                .await?,
3917            );
3918            executor
3919                .commit_verified_finality(&included, TransactionStatus::Reverted, &[])
3920                .await?;
3921            send_reverted_order(&emitter, order, &included)?;
3922            executor
3923                .database
3924                .mark_execution_event_emitted(intent.id, "terminal")
3925                .await?;
3926            executor.release_slot();
3927            Ok(())
3928        }
3929        InclusionOutcome::Pending(message) => anyhow::bail!(message),
3930    }
3931}
3932
3933async fn validate_swap_quote(
3934    position: &BlockPosition,
3935    plan: &SwapPlan,
3936    max_age_blocks: u64,
3937    verification: &VerificationCoordinator,
3938    manifest: &BlockchainDeploymentManifest,
3939) -> anyhow::Result<SwapQuoteAnchors> {
3940    let block_hash = position.block_hash.as_deref().ok_or_else(|| {
3941        anyhow::anyhow!(
3942            "Pool state at block {} has no ingestion-time block hash; refresh the profiler before execution",
3943            position.number
3944        )
3945    })?;
3946    let expected_block_hash = B256::from_str(block_hash)
3947        .with_context(|| format!("Invalid profiler block hash {block_hash}"))?;
3948    let now_unix_secs = current_unix_secs()?;
3949    let head = verified_value(
3950        verification.verify_decision_header(now_unix_secs).await,
3951        "swap decision header",
3952    )?;
3953    validate_quote_age(position.number, head.number, max_age_blocks)?;
3954    let canonical_block = verified_value(
3955        verification.verify_block(position.number).await,
3956        "profiler watermark header",
3957    )?;
3958    anyhow::ensure!(
3959        canonical_block.hash == expected_block_hash,
3960        "Pool state block {} changed from {} to {}; refresh the profiler before execution",
3961        position.number,
3962        expected_block_hash,
3963        canonical_block.hash
3964    );
3965
3966    let snapshot_transaction = position.transaction_index == BLOCK_SCOPED_SNAPSHOT_INDEX;
3967    let snapshot_log = position.log_index == BLOCK_SCOPED_SNAPSHOT_INDEX;
3968    anyhow::ensure!(
3969        snapshot_transaction == snapshot_log,
3970        "Pool state at block {} has an invalid partial snapshot watermark",
3971        position.number
3972    );
3973
3974    if snapshot_transaction {
3975        let snapshot_hash = B256::from_str(&position.transaction_hash)
3976            .with_context(|| "Invalid block-scoped snapshot hash")?;
3977        anyhow::ensure!(
3978            snapshot_hash == expected_block_hash,
3979            "Block-scoped snapshot hash {snapshot_hash} does not match ingestion hash {expected_block_hash}"
3980        );
3981    } else {
3982        validate_profiler_event_verified(
3983            position,
3984            expected_block_hash,
3985            plan.pool_address,
3986            &plan.pool,
3987            verification,
3988        )
3989        .await?;
3990    }
3991
3992    let ancestry = verified_value(
3993        verification
3994            .verify_header_window(canonical_block, head.number)
3995            .await,
3996        "profiler-to-decision ancestry",
3997    )?;
3998
3999    if let Some(last) = ancestry.last() {
4000        anyhow::ensure!(
4001            *last == head,
4002            "Swap decision header conflicts with profiler ancestry"
4003        );
4004    } else {
4005        anyhow::ensure!(
4006            canonical_block == head,
4007            "Empty profiler ancestry does not end at the decision header"
4008        );
4009    }
4010    verified_value(
4011        verification
4012            .verify_deployment_manifest(manifest, head.number)
4013            .await,
4014        "swap deployment manifest",
4015    )?;
4016    let quote_contract = validate_manifest_pool(plan, manifest)?;
4017    let quote_kind = match plan.order.order_side() {
4018        OrderSide::Sell => SwapQuoteKind::ExactInput(quantity_to_raw_amount(
4019            plan.order.quantity(),
4020            plan.pool.get_base_token().decimals,
4021        )?),
4022        OrderSide::Buy => SwapQuoteKind::ExactOutput(quantity_to_raw_amount(
4023            plan.order.quantity(),
4024            plan.pool.get_base_token().decimals,
4025        )?),
4026    };
4027    let quote = verified_value(
4028        verify_swap_quote(verification, quote_contract, plan, quote_kind, head.number).await,
4029        "independent swap quote",
4030    )?;
4031
4032    Ok(SwapQuoteAnchors {
4033        watermark: canonical_block,
4034        state: head,
4035        quote_contract,
4036        token_in: plan.token_in,
4037        token_out: plan.token_out,
4038        fee: plan.fee,
4039        quote_kind,
4040        quote,
4041        precondition_decisions: Vec::new(),
4042    })
4043}
4044
4045async fn verify_swap_quote(
4046    verification: &VerificationCoordinator,
4047    quote_contract: Address,
4048    plan: &SwapPlan,
4049    kind: SwapQuoteKind,
4050    block: u64,
4051) -> VerificationOutcome<UniswapV3Quote> {
4052    match kind {
4053        SwapQuoteKind::ExactInput(amount_in) => {
4054            verification
4055                .verify_quote_exact_input_single(
4056                    &quote_contract,
4057                    plan.token_in,
4058                    plan.token_out,
4059                    amount_in,
4060                    plan.fee,
4061                    block,
4062                )
4063                .await
4064        }
4065        SwapQuoteKind::ExactOutput(amount_out) => {
4066            verification
4067                .verify_quote_exact_output_single(
4068                    &quote_contract,
4069                    plan.token_in,
4070                    plan.token_out,
4071                    amount_out,
4072                    plan.fee,
4073                    block,
4074                )
4075                .await
4076        }
4077    }
4078}
4079
4080fn verified_swap_amounts(plan: &SwapPlan, quote: UniswapV3Quote) -> anyhow::Result<(U256, U256)> {
4081    anyhow::ensure!(
4082        !quote.amount.is_zero(),
4083        "Independent swap quote returned zero"
4084    );
4085    let base_amount =
4086        quantity_to_raw_amount(plan.order.quantity(), plan.pool.get_base_token().decimals)?;
4087    let slippage_bps = plan.slippage_bps;
4088    match plan.order.order_side() {
4089        OrderSide::Sell => Ok((
4090            base_amount,
4091            derive_min_amount_out(quote.amount, slippage_bps)?,
4092        )),
4093        OrderSide::Buy => {
4094            let ceiling = plan.quote_spend_ceiling.ok_or_else(|| {
4095                anyhow::anyhow!(
4096                    "No quote spend ceiling for BUY token pair {} -> {}",
4097                    plan.token_in,
4098                    plan.token_out
4099                )
4100            })?;
4101            anyhow::ensure!(
4102                quote.amount <= ceiling.max_amount,
4103                "BUY quote amount {} exceeds the configured quote-spend maximum {} for {} -> {}",
4104                quote.amount,
4105                ceiling.max_amount,
4106                plan.token_in,
4107                plan.token_out
4108            );
4109            Ok((
4110                quote.amount,
4111                derive_min_amount_out(base_amount, slippage_bps)?,
4112            ))
4113        }
4114    }
4115}
4116
4117fn validate_manifest_pool(
4118    plan: &SwapPlan,
4119    manifest: &BlockchainDeploymentManifest,
4120) -> anyhow::Result<Address> {
4121    let matching = manifest
4122        .pools
4123        .iter()
4124        .filter(|pool| Address::from_str(&pool.address).ok() == Some(plan.pool_address))
4125        .collect::<Vec<_>>();
4126    anyhow::ensure!(
4127        matching.len() == 1,
4128        "Pool {} does not have exactly one deployment manifest definition",
4129        plan.pool_address
4130    );
4131    let pool = matching[0];
4132    let token0 = Address::from_str(&pool.token0)?;
4133    let token1 = Address::from_str(&pool.token1)?;
4134    let factory = Address::from_str(&pool.factory)?;
4135    let quote_contract = Address::from_str(&pool.quote_contract)?;
4136    anyhow::ensure!(
4137        token0 == plan.pool.token0.address
4138            && token1 == plan.pool.token1.address
4139            && pool.fee == plan.pool.fee.expect("validated pool fee")
4140            && factory == plan.factory,
4141        "Cached pool {} does not match its deployment manifest identity",
4142        plan.pool_address
4143    );
4144
4145    for token in [&plan.pool.token0, &plan.pool.token1] {
4146        let identities = manifest
4147            .tokens
4148            .iter()
4149            .filter(|identity| Address::from_str(&identity.address).ok() == Some(token.address))
4150            .collect::<Vec<_>>();
4151        anyhow::ensure!(
4152            identities.len() == 1,
4153            "Token {} does not have exactly one deployment manifest identity",
4154            token.address
4155        );
4156        let identity = identities[0];
4157        anyhow::ensure!(
4158            identity.name == token.name
4159                && identity.symbol == token.symbol
4160                && identity.decimals == token.decimals,
4161            "Cached token {} does not match its deployment manifest identity",
4162            token.address
4163        );
4164        let expected_role = if token.address == plan.pool.get_base_token().address {
4165            "base"
4166        } else {
4167            "quote"
4168        };
4169        anyhow::ensure!(
4170            matches!(identity.asset_role.as_str(), "both") || identity.asset_role == expected_role,
4171            "Token {} is not permitted as the pool {expected_role} asset",
4172            token.address
4173        );
4174    }
4175    Ok(quote_contract)
4176}
4177
4178async fn validate_profiler_event_verified(
4179    position: &BlockPosition,
4180    expected_block_hash: B256,
4181    pool_address: Address,
4182    pool: &Pool,
4183    verification: &VerificationCoordinator,
4184) -> anyhow::Result<()> {
4185    let transaction_hash = B256::from_str(&position.transaction_hash).with_context(|| {
4186        format!(
4187            "Invalid profiler transaction hash {}",
4188            position.transaction_hash
4189        )
4190    })?;
4191    let receipt = verified_value(
4192        verification.verify_receipt(&transaction_hash).await,
4193        "profiler watermark receipt",
4194    )?;
4195    anyhow::ensure!(
4196        receipt.status,
4197        "Profiler transaction did not execute successfully"
4198    );
4199    anyhow::ensure!(
4200        receipt.transaction_hash == transaction_hash,
4201        "Profiler receipt transaction hash does not match its ingestion watermark"
4202    );
4203    anyhow::ensure!(
4204        receipt.block_number == position.number
4205            && receipt.block_hash == expected_block_hash
4206            && receipt.transaction_index == u64::from(position.transaction_index),
4207        "Profiler receipt position does not match its ingestion watermark"
4208    );
4209    let matching_logs = receipt
4210        .logs
4211        .iter()
4212        .filter(|log| rpc_log::extract_log_index(log).ok() == Some(position.log_index))
4213        .collect::<Vec<_>>();
4214    anyhow::ensure!(
4215        matching_logs.len() == 1,
4216        "Profiler receipt contains {} logs at global index {}; expected exactly one",
4217        matching_logs.len(),
4218        position.log_index
4219    );
4220    let log = matching_logs[0];
4221    let log_transaction_hash = B256::from_str(&rpc_log::extract_transaction_hash(log)?)
4222        .with_context(|| "Invalid profiler log transaction hash")?;
4223    let log_block_hash = log
4224        .block_hash
4225        .as_deref()
4226        .ok_or_else(|| anyhow::anyhow!("Profiler log has no block hash"))?;
4227    anyhow::ensure!(
4228        !log.removed
4229            && log_transaction_hash == transaction_hash
4230            && rpc_log::extract_block_number(log)? == position.number
4231            && rpc_log::extract_transaction_index(log)? == position.transaction_index
4232            && B256::from_str(log_block_hash)? == expected_block_hash,
4233        "Profiler log position does not match its ingestion watermark"
4234    );
4235    anyhow::ensure!(
4236        rpc_log::extract_address(log)? == pool_address,
4237        "Profiler watermark log did not come from expected pool {pool_address}"
4238    );
4239    let signature = log
4240        .topics
4241        .first()
4242        .ok_or_else(|| anyhow::anyhow!("Profiler watermark log has no event signature"))?;
4243    let supported =
4244        profiler_event_signatures(pool).any(|expected| expected.eq_ignore_ascii_case(signature));
4245    anyhow::ensure!(
4246        supported,
4247        "Profiler watermark log has an unsupported event signature"
4248    );
4249    Ok(())
4250}
4251
4252fn profiler_event_signatures(pool: &Pool) -> impl Iterator<Item = &str> {
4253    [
4254        Some(pool.dex.swap_created_event.as_ref()),
4255        Some(pool.dex.mint_created_event.as_ref()),
4256        Some(pool.dex.burn_created_event.as_ref()),
4257        Some(pool.dex.collect_created_event.as_ref()),
4258        pool.dex.flash_created_event.as_deref(),
4259        pool.dex.fee_protocol_update_event.as_deref(),
4260        pool.dex.fee_protocol_collect_event.as_deref(),
4261    ]
4262    .into_iter()
4263    .flatten()
4264}
4265
4266fn validate_quote_age(
4267    profiler_block: u64,
4268    latest_block: u64,
4269    max_age_blocks: u64,
4270) -> anyhow::Result<()> {
4271    anyhow::ensure!(
4272        profiler_block <= latest_block,
4273        "Pool state at block {profiler_block} is ahead of the latest block {latest_block}; the execution RPC endpoint lags the data feed"
4274    );
4275    let quote_age = latest_block - profiler_block;
4276    anyhow::ensure!(
4277        quote_age <= max_age_blocks,
4278        "Stale quote: pool state at block {profiler_block}, latest block {latest_block}, exceeds `max_quote_age_blocks` {max_age_blocks}"
4279    );
4280    Ok(())
4281}
4282
4283fn validate_rpc_transaction_matches_payload(
4284    transaction: &RpcTransaction,
4285    raw_transaction: &[u8],
4286) -> anyhow::Result<()> {
4287    let signed = decode_signed_transaction(raw_transaction)?;
4288    anyhow::ensure!(
4289        transaction.hash == signed.hash
4290            && transaction.from == signed.signer
4291            && transaction.nonce == signed.nonce
4292            && transaction.chain_id == Some(signed.chain_id)
4293            && transaction.transaction_type == Some(2)
4294            && transaction.to == Some(signed.to)
4295            && transaction.input == signed.input
4296            && transaction.value == signed.value
4297            && transaction.gas == Some(signed.gas_limit)
4298            && transaction.max_fee_per_gas == Some(U256::from(signed.max_fee_per_gas))
4299            && transaction.max_priority_fee_per_gas
4300                == Some(U256::from(signed.max_priority_fee_per_gas)),
4301        "Verified transaction fields differ from the authenticated signed payload"
4302    );
4303    Ok(())
4304}
4305
4306async fn verify_finalized_transaction(
4307    included: &IncludedTransaction,
4308    intent: &ExecutionIntentRow,
4309    nonce: u64,
4310    raw_transaction: &[u8],
4311    executor: &TransactionExecutor,
4312    trace_purpose: &str,
4313) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
4314    verify_finalized_transaction_identity(
4315        included,
4316        intent,
4317        nonce,
4318        raw_transaction,
4319        &executor.verification,
4320        executor.wallet_address,
4321        executor.chain_id,
4322        &executor.deployment_manifest,
4323        trace_purpose,
4324    )
4325    .await
4326}
4327
4328#[expect(clippy::too_many_arguments)]
4329async fn verify_finalized_transaction_identity(
4330    included: &IncludedTransaction,
4331    intent: &ExecutionIntentRow,
4332    nonce: u64,
4333    raw_transaction: &[u8],
4334    verification: &VerificationCoordinator,
4335    wallet_address: Address,
4336    chain_id: u32,
4337    deployment_manifest: &BlockchainDeploymentManifest,
4338    trace_purpose: &str,
4339) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
4340    let transaction_verification = required_verification(
4341        verification.verify_transaction(&included.tx_hash).await,
4342        "finalized transaction",
4343    )?;
4344    let transaction = &transaction_verification.value;
4345    let signed = decode_signed_transaction(raw_transaction)?;
4346    let (expected_to, expected_input, expected_value) = persisted_call_fields(intent)?;
4347    anyhow::ensure!(
4348        included.receipt.transaction_hash == included.tx_hash
4349            && signed.hash == included.tx_hash
4350            && signed.signer == wallet_address
4351            && signed.chain_id == u64::from(chain_id)
4352            && signed.nonce == nonce
4353            && signed.to == expected_to
4354            && signed.input == expected_input
4355            && signed.value == expected_value,
4356        "Finalized transaction does not match the authenticated signed payload and persisted intent"
4357    );
4358    validate_rpc_transaction_matches_payload(transaction, raw_transaction)
4359        .context("finalized transaction identity mismatch")?;
4360
4361    let trace_verification = required_verification(
4362        verification.verify_call_trace(&included.tx_hash).await,
4363        "finalized call trace",
4364    )?;
4365    validate_call_trace(
4366        &trace_verification.value,
4367        &signed,
4368        included.receipt.status,
4369        trace_purpose,
4370        deployment_manifest,
4371    )?;
4372    let deployment_verification = required_verification(
4373        verification
4374            .verify_deployment_manifest(deployment_manifest, included.block_number)
4375            .await,
4376        "inclusion deployment manifest",
4377    )?;
4378
4379    Ok(vec![
4380        verification_decision(
4381            &transaction_verification,
4382            Some(included.block_number),
4383            Some(included.block_number),
4384        ),
4385        verification_decision(
4386            &trace_verification,
4387            Some(included.block_number),
4388            Some(included.block_number),
4389        ),
4390        verification_decision(
4391            &deployment_verification,
4392            Some(included.block_number),
4393            Some(included.block_number),
4394        ),
4395    ])
4396}
4397
4398fn validate_call_trace(
4399    trace: &VerifiedCallTrace,
4400    signed: &crate::execution::transaction::DecodedSignedTransaction,
4401    receipt_success: bool,
4402    purpose: &str,
4403    manifest: &BlockchainDeploymentManifest,
4404) -> anyhow::Result<()> {
4405    anyhow::ensure!(
4406        trace.call_type == RpcCallType::Call
4407            && trace.from == signed.signer
4408            && trace.to == Some(signed.to)
4409            && trace.value == signed.value
4410            && trace.input_digest == keccak256(&signed.input)
4411            && trace.success == receipt_success,
4412        "Verified call-trace root differs from the authenticated transaction"
4413    );
4414    validate_internal_calls(&trace.calls, signed.to, purpose, manifest)
4415}
4416
4417fn validate_internal_calls(
4418    calls: &[VerifiedCallTrace],
4419    caller_context: Address,
4420    purpose: &str,
4421    manifest: &BlockchainDeploymentManifest,
4422) -> anyhow::Result<()> {
4423    for call in calls {
4424        anyhow::ensure!(
4425            call.from == caller_context,
4426            "Verified call trace child has an invalid caller context"
4427        );
4428        let target = call.to.ok_or_else(|| {
4429            anyhow::anyhow!("Verified call trace contains an operation without a target")
4430        })?;
4431        let call_type = match call.call_type {
4432            RpcCallType::Call => "call",
4433            RpcCallType::Callcode => "callcode",
4434            RpcCallType::Delegatecall => "delegatecall",
4435            RpcCallType::Staticcall => "staticcall",
4436            RpcCallType::Create | RpcCallType::Create2 | RpcCallType::Selfdestruct => {
4437                anyhow::bail!("Verified call trace contains a forbidden state-changing operation")
4438            }
4439        };
4440        let permitted = manifest.call_edges.iter().any(|edge| {
4441            edge.purpose == purpose
4442                && edge.call_type.eq_ignore_ascii_case(call_type)
4443                && Address::from_str(&edge.caller).ok() == Some(call.from)
4444                && Address::from_str(&edge.target).ok() == Some(target)
4445        });
4446        anyhow::ensure!(
4447            permitted,
4448            "Verified call trace contains an unreviewed {call_type} edge {} -> {target} for {purpose}",
4449            call.from
4450        );
4451        let child_context = match call.call_type {
4452            RpcCallType::Call | RpcCallType::Staticcall => target,
4453            RpcCallType::Callcode | RpcCallType::Delegatecall => caller_context,
4454            RpcCallType::Create | RpcCallType::Create2 | RpcCallType::Selfdestruct => {
4455                unreachable!("forbidden operations return before child traversal")
4456            }
4457        };
4458        validate_internal_calls(&call.calls, child_context, purpose, manifest)?;
4459    }
4460    Ok(())
4461}
4462
4463async fn verify_wrap_balance_increase(
4464    executor: &TransactionExecutor,
4465    weth_address: &Address,
4466    amount_wei: U256,
4467    included: &IncludedTransaction,
4468) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
4469    let previous_block = included.block_number.checked_sub(1).ok_or_else(|| {
4470        anyhow::anyhow!(
4471            "Included wrap transaction {} has invalid block number 0",
4472            included.tx_hash
4473        )
4474    })?;
4475    let call = ERC20::balanceOfCall {
4476        account: executor.wallet_address,
4477    }
4478    .abi_encode();
4479    let balance_before = required_verification(
4480        executor
4481            .verification
4482            .verify_decoded_call(
4483                None,
4484                weth_address,
4485                U256::ZERO,
4486                &call,
4487                previous_block,
4488                |result| ERC20::balanceOfCall::abi_decode_returns(result).map_err(Into::into),
4489            )
4490            .await,
4491        "wrapped balance before finality",
4492    )
4493    .with_context(|| {
4494        format!(
4495            "failed to verify WETH balance before included transaction {} at block {previous_block}",
4496            included.tx_hash
4497        )
4498    })?;
4499    let balance_after = required_verification(
4500        executor
4501            .verification
4502            .verify_decoded_call(
4503                None,
4504                weth_address,
4505                U256::ZERO,
4506                &call,
4507                included.block_number,
4508                |result| ERC20::balanceOfCall::abi_decode_returns(result).map_err(Into::into),
4509            )
4510            .await,
4511        "wrapped balance after finality",
4512    )
4513    .with_context(|| {
4514        format!(
4515            "failed to verify WETH balance after included transaction {} at block {}",
4516            included.tx_hash, included.block_number
4517        )
4518    })?;
4519    let expected_balance = balance_before
4520        .value
4521        .checked_add(amount_wei)
4522        .ok_or_else(|| {
4523            anyhow::anyhow!(
4524                "WETH balance overflow for included transaction {} at block {}",
4525                included.tx_hash,
4526                included.block_number
4527            )
4528        })?;
4529    anyhow::ensure!(
4530        balance_after.value == expected_balance,
4531        "WETH balance after transaction {} did not increase by {amount_wei}: expected {expected_balance}, was {}",
4532        included.tx_hash,
4533        balance_after.value
4534    );
4535
4536    Ok(vec![
4537        verification_decision(&balance_before, Some(previous_block), Some(previous_block)),
4538        verification_decision(
4539            &balance_after,
4540            Some(included.block_number),
4541            Some(included.block_number),
4542        ),
4543    ])
4544}
4545
4546async fn verify_approve_allowance(
4547    executor: &TransactionExecutor,
4548    token: &Address,
4549    router: &Address,
4550    amount: U256,
4551    included: &IncludedTransaction,
4552) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
4553    let call = ERC20::allowanceCall {
4554        owner: executor.wallet_address,
4555        spender: *router,
4556    }
4557    .abi_encode();
4558    let allowance = required_verification(
4559        executor
4560            .verification
4561            .verify_decoded_call(
4562                None,
4563                token,
4564                U256::ZERO,
4565                &call,
4566                included.block_number,
4567                |result| ERC20::allowanceCall::abi_decode_returns(result).map_err(Into::into),
4568            )
4569            .await,
4570        "router allowance after finality",
4571    )
4572    .with_context(|| {
4573        format!(
4574            "failed to verify router allowance after included transaction {} at block {}",
4575            included.tx_hash, included.block_number
4576        )
4577    })?;
4578    anyhow::ensure!(
4579        allowance.value == amount,
4580        "Router allowance after transaction {} does not equal the requested amount {amount}: was {}",
4581        included.tx_hash,
4582        allowance.value
4583    );
4584
4585    Ok(vec![verification_decision(
4586        &allowance,
4587        Some(included.block_number),
4588        Some(included.block_number),
4589    )])
4590}
4591
4592async fn complete_finalized_swap(
4593    plan: &SwapPlan,
4594    intent_id: i64,
4595    tx_hash: B256,
4596    fill: Option<FinalizedSwapFill>,
4597    wallet: VerifiedWalletRefresh,
4598    executor: &TransactionExecutor,
4599    emitter: &ExecutionEventEmitter,
4600) -> anyhow::Result<()> {
4601    if let Some(fill) = fill {
4602        let filled = OrderFilled::new(
4603            emitter.trader_id(),
4604            plan.order.strategy_id(),
4605            plan.order.instrument_id(),
4606            plan.order.client_order_id(),
4607            fill.venue_order_id,
4608            emitter.account_id(),
4609            fill.trade_id,
4610            plan.order.order_side(),
4611            plan.order.order_type(),
4612            fill.last_qty,
4613            fill.last_px,
4614            plan.quote_currency,
4615            LiquiditySide::Taker,
4616            execution_event_id(tx_hash, b"fill"),
4617            fill.ts_event,
4618            fill.ts_event,
4619            false,
4620            None,
4621            Some(fill.commission),
4622            None,
4623        );
4624        emitter.try_send_order_event(OrderEventAny::Filled(filled))?;
4625
4626        if fill.last_qty < plan.order.quantity() {
4627            let canceled = OrderCanceled::new(
4628                emitter.trader_id(),
4629                plan.order.strategy_id(),
4630                plan.order.instrument_id(),
4631                plan.order.client_order_id(),
4632                execution_event_id(tx_hash, b"partial_cancel"),
4633                fill.ts_event,
4634                fill.ts_event,
4635                false,
4636                Some(fill.venue_order_id),
4637                Some(emitter.account_id()),
4638                None,
4639            );
4640            emitter.try_send_order_event(OrderEventAny::Canceled(canceled))?;
4641        }
4642    }
4643
4644    *executor.wallet_balance.lock() = wallet.wallet_balance;
4645    emitter.try_emit_account_state(
4646        wallet.balances,
4647        vec![],
4648        true,
4649        get_atomic_clock_realtime().get_time_ns(),
4650        None,
4651    )?;
4652    executor
4653        .database
4654        .mark_execution_event_emitted(intent_id, "fill")
4655        .await
4656}
4657
4658fn send_reverted_order(
4659    emitter: &ExecutionEventEmitter,
4660    order: &OrderAny,
4661    included: &IncludedTransaction,
4662) -> anyhow::Result<()> {
4663    let ts_event = finalized_inclusion_time(included)?;
4664    let rejected = OrderRejected::new(
4665        emitter.trader_id(),
4666        order.strategy_id(),
4667        order.instrument_id(),
4668        order.client_order_id(),
4669        emitter.account_id(),
4670        format!("Transaction {} reverted on-chain", included.tx_hash).into(),
4671        execution_event_id(included.tx_hash, b"reverted"),
4672        ts_event,
4673        ts_event,
4674        false,
4675        false,
4676    );
4677    emitter.try_send_order_event(OrderEventAny::Rejected(rejected))
4678}
4679
4680fn finalized_inclusion_time(included: &IncludedTransaction) -> anyhow::Result<UnixNanos> {
4681    let inclusion = &included.finality.inclusion_header;
4682    anyhow::ensure!(
4683        inclusion.number == included.block_number
4684            && inclusion.hash == included.receipt.block_hash.to_string(),
4685        "Verified inclusion header does not match the finalized receipt"
4686    );
4687    let timestamp = inclusion.timestamp;
4688    let nanos = timestamp
4689        .checked_mul(NANOSECONDS_IN_SECOND)
4690        .ok_or_else(|| anyhow::anyhow!("Verified inclusion timestamp exceeds nanoseconds"))?;
4691    Ok(UnixNanos::from(nanos))
4692}
4693
4694fn execution_event_id(tx_hash: B256, event: &[u8]) -> UUID4 {
4695    let mut identity = Vec::with_capacity(tx_hash.len() + event.len());
4696    identity.extend_from_slice(tx_hash.as_slice());
4697    identity.extend_from_slice(event);
4698    let digest = keccak256(identity);
4699    let mut bytes = [0u8; 16];
4700    bytes.copy_from_slice(&digest[..16]);
4701    UUID4::from_bytes(bytes)
4702}
4703
4704struct FinalizedSwapFill {
4705    venue_order_id: VenueOrderId,
4706    trade_id: TradeId,
4707    last_qty: Quantity,
4708    last_px: Price,
4709    commission: Money,
4710    ts_event: UnixNanos,
4711}
4712
4713struct VerifiedWalletRefresh {
4714    wallet_balance: WalletBalance,
4715    balances: Vec<AccountBalance>,
4716    decisions: Vec<ExecutionVerificationDecision>,
4717}
4718
4719fn validate_finalized_swap_fill(
4720    plan: &SwapPlan,
4721    included: &IncludedTransaction,
4722) -> anyhow::Result<Option<FinalizedSwapFill>> {
4723    let signature =
4724        keccak256("Swap(address,address,int256,int256,uint160,uint128,int24)").to_string();
4725    let swap_logs = included
4726        .receipt
4727        .logs
4728        .iter()
4729        .filter(|log| {
4730            !log.removed
4731                && log.topics.first().is_some_and(|topic| topic == &signature)
4732                && Address::from_str(&log.address).ok() == Some(plan.pool_address)
4733        })
4734        .collect::<Vec<_>>();
4735    anyhow::ensure!(
4736        swap_logs.len() == 1,
4737        "Finalized transaction {} emitted {} Swap logs from expected pool {}; expected exactly one",
4738        included.tx_hash,
4739        swap_logs.len(),
4740        plan.pool_address
4741    );
4742    let log = swap_logs[0];
4743    let log_transaction_hash = B256::from_str(&rpc_log::extract_transaction_hash(log)?)
4744        .with_context(|| "Invalid finalized Swap log transaction hash")?;
4745    let log_block_hash = log
4746        .block_hash
4747        .as_deref()
4748        .ok_or_else(|| anyhow::anyhow!("Finalized Swap log has no block hash"))?;
4749    anyhow::ensure!(
4750        log_transaction_hash == included.tx_hash
4751            && rpc_log::extract_block_number(log)? == included.block_number
4752            && u64::from(rpc_log::extract_transaction_index(log)?)
4753                == included.receipt.transaction_index
4754            && B256::from_str(log_block_hash)
4755                .with_context(|| "Invalid finalized Swap log block hash")?
4756                == included.receipt.block_hash,
4757        "Finalized Swap log position does not match transaction {}",
4758        included.tx_hash
4759    );
4760
4761    let dex = crate::exchanges::get_dex_extended(plan.pool.chain.name, &plan.pool.dex.name)
4762        .ok_or_else(|| {
4763            anyhow::anyhow!(
4764                "No RPC Swap decoder for {}:{}",
4765                plan.pool.chain.name,
4766                plan.pool.dex.name
4767            )
4768        })?;
4769    let event = dex.parse_swap_event_rpc(log)?;
4770    let (base_amount, quote_amount) =
4771        if plan.pool.get_base_token().address == plan.pool.token0.address {
4772            (event.amount0, event.amount1)
4773        } else {
4774            (event.amount1, event.amount0)
4775        };
4776    let last_qty = match plan.order.order_side() {
4777        OrderSide::Sell => {
4778            anyhow::ensure!(
4779                base_amount.is_positive() && base_amount.unsigned_abs() == plan.amount_in,
4780                "Finalized Swap input {base_amount} does not match the persisted amount {}",
4781                plan.amount_in
4782            );
4783            plan.order.quantity()
4784        }
4785        OrderSide::Buy => {
4786            anyhow::ensure!(
4787                quote_amount.is_positive() && quote_amount.unsigned_abs() == plan.amount_in,
4788                "Finalized Swap input {quote_amount} does not match the persisted amount {}",
4789                plan.amount_in
4790            );
4791            anyhow::ensure!(
4792                base_amount.is_negative(),
4793                "Finalized Swap base amount {base_amount} is not a BUY output"
4794            );
4795            raw_amount_to_quantity(
4796                base_amount.unsigned_abs(),
4797                plan.pool.get_base_token().decimals,
4798            )?
4799        }
4800    };
4801
4802    let block = &included.finality.inclusion_header;
4803    anyhow::ensure!(
4804        block.number == included.block_number
4805            && block.hash == included.receipt.block_hash.to_string(),
4806        "Verified inclusion header {} does not match receipt hash {}",
4807        included.block_number,
4808        included.receipt.block_hash
4809    );
4810    let timestamp_ns = block
4811        .timestamp
4812        .checked_mul(NANOSECONDS_IN_SECOND)
4813        .ok_or_else(|| anyhow::anyhow!("Finalized block timestamp overflows nanoseconds"))?;
4814    let mut swap = event.to_pool_swap(
4815        Arc::clone(&plan.pool.chain),
4816        plan.instrument_id,
4817        plan.pool.pool_identifier,
4818        UnixNanos::from(timestamp_ns),
4819    );
4820    swap.calculate_trade_info(&plan.pool.token0, &plan.pool.token1, None)?;
4821    let trade = swap
4822        .trade_info
4823        .as_ref()
4824        .ok_or_else(|| anyhow::anyhow!("Finalized Swap has no calculated trade information"))?;
4825    anyhow::ensure!(
4826        trade.order_side == plan.order.order_side(),
4827        "Finalized Swap side {} does not match {} order",
4828        trade.order_side,
4829        plan.order.order_side()
4830    );
4831    let gas_cost = included
4832        .receipt
4833        .effective_gas_price
4834        .checked_mul(U256::from(included.receipt.gas_used))
4835        .ok_or_else(|| anyhow::anyhow!("Finalized transaction gas commission overflow"))?;
4836    let commission = Money::from_u256(gas_cost, plan.pool.chain.native_currency())?;
4837    let trade_digest = keccak256(format!("{}:{}", included.tx_hash, swap.log_index));
4838    let trade_digest = trade_digest.to_string();
4839    let trade_id = TradeId::new_checked(&trade_digest[2..38])?;
4840
4841    if plan
4842        .order
4843        .trade_ids()
4844        .iter()
4845        .any(|existing| **existing == trade_id)
4846    {
4847        return Ok(None);
4848    }
4849
4850    let venue_order_id = VenueOrderId::new_checked(included.tx_hash.to_string())?;
4851    let ts_event = UnixNanos::from(timestamp_ns);
4852    let last_px = match plan.order.order_side() {
4853        OrderSide::Buy => fill_price_from_quote(last_qty, plan.amount_in, plan.quote_currency)?,
4854        _ => trade.execution_price,
4855    };
4856    Ok(Some(FinalizedSwapFill {
4857        venue_order_id,
4858        trade_id,
4859        last_qty,
4860        last_px,
4861        commission,
4862        ts_event,
4863    }))
4864}
4865
4866async fn load_verified_wallet_after_fill(
4867    plan: &SwapPlan,
4868    included: &IncludedTransaction,
4869    executor: &TransactionExecutor,
4870) -> anyhow::Result<VerifiedWalletRefresh> {
4871    let mut token_universe = executor.wallet_balance.lock().token_universe.clone();
4872    token_universe.insert(plan.pool.token0.address);
4873    token_universe.insert(plan.pool.token1.address);
4874
4875    let native_amount = required_verification(
4876        executor
4877            .verification
4878            .verify_balance(&executor.wallet_address, included.block_number)
4879            .await,
4880        "finalized native balance",
4881    )?;
4882    let native_balance = Money::from_u256(native_amount.value, plan.pool.chain.native_currency())?;
4883    let mut decisions = vec![verification_decision(
4884        &native_amount,
4885        Some(included.block_number),
4886        Some(included.block_number),
4887    )];
4888    let mut token_addresses = token_universe.iter().copied().collect::<Vec<_>>();
4889    token_addresses.sort_unstable();
4890    let mut token_balances = Vec::with_capacity(token_addresses.len());
4891    for address in token_addresses {
4892        let identities = executor
4893            .deployment_manifest
4894            .tokens
4895            .iter()
4896            .filter(|identity| Address::from_str(&identity.address).ok() == Some(address))
4897            .collect::<Vec<_>>();
4898        anyhow::ensure!(
4899            identities.len() == 1,
4900            "Wallet token {address} does not have exactly one deployment manifest identity"
4901        );
4902        let identity = identities[0];
4903        let token = Token::new(
4904            Arc::clone(&plan.pool.chain),
4905            address,
4906            identity.name.clone(),
4907            identity.symbol.clone(),
4908            identity.decimals,
4909        );
4910        let call = ERC20::balanceOfCall {
4911            account: executor.wallet_address,
4912        }
4913        .abi_encode();
4914        let amount = required_verification(
4915            executor
4916                .verification
4917                .verify_decoded_call(
4918                    None,
4919                    &address,
4920                    U256::ZERO,
4921                    &call,
4922                    included.block_number,
4923                    |result| ERC20::balanceOfCall::abi_decode_returns(result).map_err(Into::into),
4924                )
4925                .await,
4926            "finalized token balance",
4927        )?;
4928        decisions.push(verification_decision(
4929            &amount,
4930            Some(included.block_number),
4931            Some(included.block_number),
4932        ));
4933        token_balances.push(TokenBalance::new(amount.value, token));
4934    }
4935
4936    let mut wallet_balance = WalletBalance::new(token_universe);
4937    let balances = wallet_balance.replace_balances(native_balance, token_balances)?;
4938    Ok(VerifiedWalletRefresh {
4939        wallet_balance,
4940        balances,
4941        decisions,
4942    })
4943}
4944
4945async fn verify_connect_capabilities(
4946    verification: &VerificationCoordinator,
4947    manifest: &BlockchainDeploymentManifest,
4948    wallet: Address,
4949    weth: Address,
4950    block: u64,
4951) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
4952    let contract = manifest
4953        .contracts
4954        .first()
4955        .ok_or_else(|| anyhow::anyhow!("Deployment manifest has no capability probe target"))?;
4956    let contract_address = Address::from_str(&contract.address)
4957        .with_context(|| "Deployment manifest capability probe target is invalid")?;
4958    let storage = required_verification(
4959        verification
4960            .verify_storage(&contract_address, &B256::ZERO, block)
4961            .await,
4962        "Blockchain explicit-height storage capability",
4963    )?;
4964
4965    let balance_call = ERC20::balanceOfCall { account: wallet }.abi_encode();
4966    let gas = required_verification(
4967        verification
4968            .verify_gas_estimate(&wallet, &weth, U256::ZERO, &balance_call, block)
4969            .await,
4970        "Blockchain explicit-height gas capability",
4971    )?;
4972
4973    let mut decisions = vec![
4974        verification_decision(&storage, Some(block), Some(block)),
4975        verification_decision(&gas, Some(block), Some(block)),
4976    ];
4977
4978    for pool in &manifest.pools {
4979        let quote_contract = Address::from_str(&pool.quote_contract)
4980            .with_context(|| "Deployment manifest quote capability target is invalid")?;
4981        let token_in = Address::from_str(&pool.token0)
4982            .with_context(|| "Deployment manifest quote input token is invalid")?;
4983        let token_out = Address::from_str(&pool.token1)
4984            .with_context(|| "Deployment manifest quote output token is invalid")?;
4985        let fee = U24::try_from(pool.fee)
4986            .map_err(|_| anyhow::anyhow!("Deployment manifest pool fee is invalid"))?;
4987        let quote = required_verification(
4988            verification
4989                .verify_quote_exact_input_single(
4990                    &quote_contract,
4991                    token_in,
4992                    token_out,
4993                    U256::from(1u64),
4994                    fee,
4995                    block,
4996                )
4997                .await,
4998            "Blockchain explicit-height quote capability",
4999        )?;
5000        decisions.push(verification_decision(&quote, Some(block), Some(block)));
5001    }
5002
5003    let trace = required_verification(
5004        verification.verify_call_trace_capability().await,
5005        "Blockchain call trace capability",
5006    )?;
5007    decisions.push(verification_decision(&trace, None, None));
5008    Ok(decisions)
5009}
5010
5011/// Runs the read-only pre-trade checks for a swap: deployed bytecode at the pool, router,
5012/// and token addresses, and an operator-prepared router allowance and input-token balance
5013/// covering the amount. The shared signing pipeline checks the exact maximum native cost.
5014/// Never wraps or approves.
5015async fn check_swap_preconditions(
5016    plan: &SwapPlan,
5017    block: u64,
5018    executor: &TransactionExecutor,
5019) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
5020    let mut decisions = Vec::new();
5021    let factory_call = UniswapV3RouterState::factoryCall.abi_encode();
5022    let router_factory = required_verification(
5023        executor
5024            .verification
5025            .verify_decoded_call(
5026                None,
5027                &plan.router,
5028                U256::ZERO,
5029                &factory_call,
5030                block,
5031                |result| {
5032                    UniswapV3RouterState::factoryCall::abi_decode_returns(result)
5033                        .map_err(Into::into)
5034                },
5035            )
5036            .await,
5037        "swap router factory",
5038    )?;
5039    anyhow::ensure!(
5040        router_factory.value == plan.factory,
5041        "Router {} reports an unexpected factory",
5042        plan.router,
5043    );
5044    decisions.push(verification_decision(
5045        &router_factory,
5046        Some(block),
5047        Some(block),
5048    ));
5049
5050    let weth_call = UniswapV3RouterState::WETH9Call.abi_encode();
5051    let router_weth = required_verification(
5052        executor
5053            .verification
5054            .verify_decoded_call(
5055                None,
5056                &plan.router,
5057                U256::ZERO,
5058                &weth_call,
5059                block,
5060                |result| {
5061                    UniswapV3RouterState::WETH9Call::abi_decode_returns(result).map_err(Into::into)
5062                },
5063            )
5064            .await,
5065        "swap router wrapped native",
5066    )?;
5067    anyhow::ensure!(
5068        router_weth.value == plan.weth,
5069        "Router {} reports an unexpected wrapped native contract",
5070        plan.router,
5071    );
5072    decisions.push(verification_decision(
5073        &router_weth,
5074        Some(block),
5075        Some(block),
5076    ));
5077
5078    let pool_call = UniswapV3Factory::getPoolCall {
5079        tokenA: plan.token_in,
5080        tokenB: plan.token_out,
5081        fee: plan.fee,
5082    }
5083    .abi_encode();
5084    let registered_pool = required_verification(
5085        executor
5086            .verification
5087            .verify_decoded_call(
5088                None,
5089                &plan.factory,
5090                U256::ZERO,
5091                &pool_call,
5092                block,
5093                |result| {
5094                    UniswapV3Factory::getPoolCall::abi_decode_returns(result).map_err(Into::into)
5095                },
5096            )
5097            .await,
5098        "swap factory pool",
5099    )?;
5100    anyhow::ensure!(
5101        registered_pool.value == plan.pool_address,
5102        "Factory resolves an unexpected pool for the swap token pair and fee"
5103    );
5104    decisions.push(verification_decision(
5105        &registered_pool,
5106        Some(block),
5107        Some(block),
5108    ));
5109
5110    for token in [&plan.pool.token0, &plan.pool.token1] {
5111        let decimals_call = ERC20::decimalsCall.abi_encode();
5112        let decimals = required_verification(
5113            executor
5114                .verification
5115                .verify_decoded_call(
5116                    None,
5117                    &token.address,
5118                    U256::ZERO,
5119                    &decimals_call,
5120                    block,
5121                    |result| ERC20::decimalsCall::abi_decode_returns(result).map_err(Into::into),
5122                )
5123                .await,
5124            "swap token decimals",
5125        )?;
5126        anyhow::ensure!(
5127            decimals.value == token.decimals,
5128            "Token {} reports unexpected decimals",
5129            token.address,
5130        );
5131        decisions.push(verification_decision(&decimals, Some(block), Some(block)));
5132    }
5133
5134    let allowance_call = ERC20::allowanceCall {
5135        owner: executor.wallet_address,
5136        spender: plan.router,
5137    }
5138    .abi_encode();
5139    let allowance = required_verification(
5140        executor
5141            .verification
5142            .verify_decoded_call(
5143                None,
5144                &plan.token_in,
5145                U256::ZERO,
5146                &allowance_call,
5147                block,
5148                |result| ERC20::allowanceCall::abi_decode_returns(result).map_err(Into::into),
5149            )
5150            .await,
5151        "swap input allowance",
5152    )?;
5153
5154    if allowance.value < plan.amount_in {
5155        anyhow::bail!(
5156            "Router allowance {} is below the swap amount {} for input token {}; approve the router explicitly before submitting",
5157            allowance.value,
5158            plan.amount_in,
5159            plan.token_in
5160        );
5161    }
5162    decisions.push(verification_decision(&allowance, Some(block), Some(block)));
5163
5164    let balance_call = ERC20::balanceOfCall {
5165        account: executor.wallet_address,
5166    }
5167    .abi_encode();
5168    let balance = required_verification(
5169        executor
5170            .verification
5171            .verify_decoded_call(
5172                None,
5173                &plan.token_in,
5174                U256::ZERO,
5175                &balance_call,
5176                block,
5177                |result| ERC20::balanceOfCall::abi_decode_returns(result).map_err(Into::into),
5178            )
5179            .await,
5180        "swap input balance",
5181    )?;
5182
5183    if balance.value < plan.amount_in {
5184        anyhow::bail!(
5185            "Input token {} balance {} is below the swap amount {}",
5186            plan.token_in,
5187            balance.value,
5188            plan.amount_in
5189        );
5190    }
5191    decisions.push(verification_decision(&balance, Some(block), Some(block)));
5192
5193    Ok(decisions)
5194}
5195
5196/// Converts a base-denominated order quantity into raw token units with exact integer
5197/// scaling by the base token decimals.
5198///
5199/// `Quantity::raw` is scaled to the greater of its declared precision and `FIXED_PRECISION`;
5200/// token amounts are scaled to the token's decimals. Quantities not exactly representable in
5201/// token units are rejected rather than rounded.
5202fn quantity_to_raw_amount(quantity: Quantity, decimals: u8) -> anyhow::Result<U256> {
5203    if quantity.is_zero() {
5204        anyhow::bail!("Order quantity must be positive");
5205    }
5206
5207    let raw = U256::from(quantity.raw());
5208    let raw_precision = quantity.precision.max(FIXED_PRECISION);
5209    if decimals >= raw_precision {
5210        let scale = U256::from(10u64)
5211            .checked_pow(U256::from(decimals - raw_precision))
5212            .ok_or_else(|| anyhow::anyhow!("Order amount scaling overflow"))?;
5213        raw.checked_mul(scale).ok_or_else(|| {
5214            anyhow::anyhow!("Order amount overflow scaling quantity to raw token units")
5215        })
5216    } else {
5217        let divisor = U256::from(10u64)
5218            .checked_pow(U256::from(raw_precision - decimals))
5219            .ok_or_else(|| anyhow::anyhow!("Order amount scaling overflow"))?;
5220        if !(raw % divisor).is_zero() {
5221            anyhow::bail!(
5222                "Order quantity {quantity} is not exactly representable in {decimals} base token decimals"
5223            );
5224        }
5225        Ok(raw / divisor)
5226    }
5227}
5228
5229fn swap_token_pair(
5230    side: OrderSide,
5231    base: Address,
5232    quote: Address,
5233) -> anyhow::Result<(Address, Address)> {
5234    match side {
5235        OrderSide::Sell => Ok((base, quote)),
5236        OrderSide::Buy => Ok((quote, base)),
5237    }
5238}
5239
5240fn fill_price_from_quote(
5241    last_qty: Quantity,
5242    quote_amount: U256,
5243    quote_currency: Currency,
5244) -> anyhow::Result<Price> {
5245    let quote = Money::from_u256(quote_amount, quote_currency)?;
5246    Price::from_decimal_dp(quote.as_decimal() / last_qty.as_decimal(), FIXED_PRECISION)
5247        .map_err(anyhow::Error::from)
5248}
5249
5250fn raw_amount_to_quantity(amount: U256, decimals: u8) -> anyhow::Result<Quantity> {
5251    if amount.is_zero() {
5252        anyhow::bail!("Executed amount must be positive");
5253    }
5254    let quantity = if decimals >= FIXED_PRECISION {
5255        let scale = U256::from(10u64)
5256            .checked_pow(U256::from(decimals - FIXED_PRECISION))
5257            .ok_or_else(|| anyhow::anyhow!("Executed amount scaling overflow"))?;
5258        Quantity::from_u256(amount / scale, FIXED_PRECISION).map_err(anyhow::Error::from)?
5259    } else {
5260        Quantity::from_u256(amount, decimals).map_err(anyhow::Error::from)?
5261    };
5262
5263    if quantity.is_zero() {
5264        anyhow::bail!(
5265            "Executed amount {amount} is below representable quantity precision {FIXED_PRECISION}"
5266        );
5267    }
5268    Ok(quantity)
5269}
5270
5271fn exact_output_amount(quote: &SwapQuote, zero_for_one: bool) -> anyhow::Result<U256> {
5272    let amount = if zero_for_one {
5273        quote.amount1
5274    } else {
5275        quote.amount0
5276    };
5277
5278    if !amount.is_negative() {
5279        anyhow::bail!("Swap quote output amount {amount} is not a positive output");
5280    }
5281    Ok(amount.unsigned_abs())
5282}
5283
5284/// Derives the minimum output accepted for the swap: the quoted output reduced by
5285/// `slippage_bps`, in exact integer arithmetic. Rejects a zero minimum, which would leave
5286/// the swap without slippage protection.
5287fn derive_min_amount_out(quoted_amount_out: U256, slippage_bps: u32) -> anyhow::Result<U256> {
5288    if slippage_bps >= BPS_DENOMINATOR {
5289        anyhow::bail!("Slippage {slippage_bps} bps must be below {BPS_DENOMINATOR}");
5290    }
5291    let min_amount_out = quoted_amount_out
5292        .checked_mul(U256::from(BPS_DENOMINATOR - slippage_bps))
5293        .and_then(|scaled| scaled.checked_div(U256::from(BPS_DENOMINATOR)))
5294        .ok_or_else(|| anyhow::anyhow!("Minimum output derivation overflow"))?;
5295    if min_amount_out.is_zero() {
5296        anyhow::bail!(
5297            "Derived minimum output is zero for quoted output {quoted_amount_out} at {slippage_bps} bps slippage"
5298        );
5299    }
5300    Ok(min_amount_out)
5301}
5302
5303impl BlockchainExecutionClient {
5304    async fn build_execution_verification_migration(
5305        &self,
5306        snapshot: ExecutionVerificationMigrationSnapshot,
5307        finalized: VerifiedBlockHeader,
5308        finalized_headers: &[VerifiedBlockHeader],
5309        nonce_verification: &Verified<u64>,
5310    ) -> anyhow::Result<ExecutionVerificationMigration> {
5311        let next_canonical_nonce = nonce_verification.value;
5312        let mut hashes_by_intent: HashMap<i64, Vec<&ExecutionTransactionHashRow>> = HashMap::new();
5313        for hash in &snapshot.hashes {
5314            hashes_by_intent
5315                .entry(hash.intent_id)
5316                .or_default()
5317                .push(hash);
5318        }
5319        let active_count = snapshot
5320            .intents
5321            .iter()
5322            .filter(|intent| intent.active)
5323            .count();
5324        anyhow::ensure!(
5325            active_count <= 1,
5326            "Retained execution history has multiple active signer owners"
5327        );
5328
5329        let mut nonce_owners = HashMap::new();
5330        let mut records = Vec::with_capacity(snapshot.intents.len());
5331        let finalized_headers = finalized_headers
5332            .iter()
5333            .map(durable_verified_header)
5334            .collect::<Vec<_>>();
5335
5336        for intent in &snapshot.intents {
5337            anyhow::ensure!(
5338                intent.chain_id == self.chain.chain_id
5339                    && intent.wallet_address == self.config.wallet_address,
5340                "Retained execution intent belongs to another signer"
5341            );
5342            let purpose = TransactionPurpose::parse(&intent.purpose).ok_or_else(|| {
5343                anyhow::anyhow!("Retained execution intent has an unsupported purpose")
5344            })?;
5345            let hashes = hashes_by_intent
5346                .get(&intent.id)
5347                .map(Vec::as_slice)
5348                .unwrap_or_default();
5349            let current = hashes
5350                .iter()
5351                .copied()
5352                .filter(|hash| hash.current)
5353                .collect::<Vec<_>>();
5354            anyhow::ensure!(
5355                current.len() <= 1,
5356                "Retained execution intent {} has multiple current hashes",
5357                intent.id
5358            );
5359            let current = current.first().copied();
5360            let mut authenticated = HashMap::new();
5361
5362            for hash in hashes {
5363                if hash.payload_expected {
5364                    let raw = open_execution_payload(
5365                        self.payload_keys
5366                            .as_deref()
5367                            .expect("Postgres execution requires payload keys"),
5368                        self.payload_policy(),
5369                        intent,
5370                        hash,
5371                        "verification migration",
5372                    )?;
5373                    authenticated.insert(hash.id, raw);
5374                } else {
5375                    anyhow::ensure!(
5376                        hash.raw_transaction.is_none() && hash.sealed_transaction.is_none(),
5377                        "Unowned replacement hash retains signed bytes"
5378                    );
5379                }
5380            }
5381
5382            if let Some(nonce) = intent.nonce {
5383                anyhow::ensure!(
5384                    nonce_owners.insert(nonce, intent.id).is_none(),
5385                    "Retained execution history has duplicate signer nonce ownership"
5386                );
5387            }
5388
5389            let base_decision = verification_decision(
5390                nonce_verification,
5391                Some(finalized.number),
5392                Some(finalized.number),
5393            );
5394
5395            if !intent.active {
5396                if matches!(intent.status.as_str(), "finalized" | "reverted") {
5397                    let expected_marker =
5398                        if purpose == TransactionPurpose::Swap && intent.status == "finalized" {
5399                            intent.fill_emitted
5400                        } else {
5401                            intent.terminal_emitted
5402                        };
5403                    anyhow::ensure!(
5404                        expected_marker,
5405                        "Released terminal intent {} has no durable event marker",
5406                        intent.id
5407                    );
5408                } else {
5409                    anyhow::ensure!(
5410                        matches!(intent.status.as_str(), "dropped" | "recoverable")
5411                            && authenticated.is_empty(),
5412                        "Released nonterminal intent {} retains signed ownership",
5413                        intent.id
5414                    );
5415                    records.push(ExecutionVerificationMigrationRecord {
5416                        intent_id: intent.id,
5417                        nonce: intent.nonce,
5418                        transaction_hash: None,
5419                        terminal_status: None,
5420                        block_number: None,
5421                        block_hash: None,
5422                        receipt_success: None,
5423                        gas_used: None,
5424                        effective_gas_price: None,
5425                        recover_prepared: false,
5426                        decisions: vec![base_decision],
5427                    });
5428                    continue;
5429                }
5430            }
5431
5432            if intent.active && intent.nonce.is_none() {
5433                anyhow::ensure!(
5434                    intent.status == "prepared" && hashes.is_empty(),
5435                    "Unassigned active intent {} is not an unsigned preparation",
5436                    intent.id
5437                );
5438                records.push(ExecutionVerificationMigrationRecord {
5439                    intent_id: intent.id,
5440                    nonce: None,
5441                    transaction_hash: None,
5442                    terminal_status: None,
5443                    block_number: None,
5444                    block_hash: None,
5445                    receipt_success: None,
5446                    gas_used: None,
5447                    effective_gas_price: None,
5448                    recover_prepared: true,
5449                    decisions: vec![base_decision],
5450                });
5451                continue;
5452            }
5453
5454            let nonce = intent.nonce.ok_or_else(|| {
5455                anyhow::anyhow!("Retained signed intent {} has no nonce", intent.id)
5456            })?;
5457            let current = current.ok_or_else(|| {
5458                anyhow::anyhow!("Retained signed intent {} has no current hash", intent.id)
5459            })?;
5460            let raw_transaction = authenticated.get(&current.id).ok_or_else(|| {
5461                anyhow::anyhow!(
5462                    "Retained signed intent {} has no authenticated current payload",
5463                    intent.id
5464                )
5465            })?;
5466
5467            if intent.active && nonce == next_canonical_nonce {
5468                anyhow::ensure!(
5469                    !matches!(intent.status.as_str(), "finalized" | "reverted"),
5470                    "Active terminal intent conflicts with the canonical nonce ledger"
5471                );
5472                records.push(ExecutionVerificationMigrationRecord {
5473                    intent_id: intent.id,
5474                    nonce: Some(nonce),
5475                    transaction_hash: Some(current.transaction_hash.clone()),
5476                    terminal_status: None,
5477                    block_number: None,
5478                    block_hash: None,
5479                    receipt_success: None,
5480                    gas_used: None,
5481                    effective_gas_price: None,
5482                    recover_prepared: false,
5483                    decisions: vec![base_decision],
5484                });
5485                continue;
5486            }
5487            anyhow::ensure!(
5488                nonce < next_canonical_nonce,
5489                "Retained active nonce {nonce} is above canonical nonce {next_canonical_nonce}"
5490            );
5491
5492            let receipt_verification = required_verification(
5493                self.verification
5494                    .verify_receipt(&B256::from_str(&current.transaction_hash).with_context(
5495                        || {
5496                            format!(
5497                                "Retained transaction hash {} is invalid",
5498                                current.transaction_hash
5499                            )
5500                        },
5501                    )?)
5502                    .await,
5503                "migration receipt",
5504            )?;
5505            let receipt = receipt_verification.value.clone();
5506            anyhow::ensure!(
5507                receipt.block_number <= finalized.number,
5508                "Retained terminal receipt is above the verified finalized boundary"
5509            );
5510            let inclusion_verification = required_verification(
5511                self.verification.verify_block(receipt.block_number).await,
5512                "migration inclusion header",
5513            )?;
5514            anyhow::ensure!(
5515                inclusion_verification.value.hash == receipt.block_hash
5516                    && finalized_headers.iter().any(|header| {
5517                        header.number == receipt.block_number
5518                            && header.hash == receipt.block_hash.to_string()
5519                    }),
5520                "Retained terminal receipt is not on the verified finalized ancestry"
5521            );
5522            let tx_hash = B256::from_str(&current.transaction_hash)
5523                .context("Retained transaction hash is invalid")?;
5524            let included = IncludedTransaction {
5525                intent_id: intent.id,
5526                nonce,
5527                tx_hash,
5528                block_number: receipt.block_number,
5529                receipt: receipt.clone(),
5530                finality: StableFinality {
5531                    decisions: Vec::new(),
5532                    inclusion_header: durable_verified_header(&inclusion_verification.value),
5533                    finalized_headers: finalized_headers.clone(),
5534                },
5535            };
5536            let trace_purpose = match purpose {
5537                TransactionPurpose::Wrap => "wrap",
5538                TransactionPurpose::Approve => "approve",
5539                TransactionPurpose::Swap => {
5540                    match self.restore_swap_plan(intent)?.order.order_side() {
5541                        OrderSide::Sell => "swap_sell",
5542                        OrderSide::Buy => "swap_buy",
5543                    }
5544                }
5545            };
5546            let mut decisions = vec![
5547                verification_decision(
5548                    &receipt_verification,
5549                    Some(receipt.block_number),
5550                    Some(receipt.block_number),
5551                ),
5552                verification_decision(
5553                    &inclusion_verification,
5554                    Some(receipt.block_number),
5555                    Some(receipt.block_number),
5556                ),
5557            ];
5558            decisions.extend(
5559                verify_finalized_transaction_identity(
5560                    &included,
5561                    intent,
5562                    nonce,
5563                    raw_transaction,
5564                    &self.verification,
5565                    self.wallet_address,
5566                    self.chain.chain_id,
5567                    &self
5568                        .config
5569                        .verification
5570                        .as_ref()
5571                        .expect("verification config validated")
5572                        .deployment_manifest,
5573                    trace_purpose,
5574                )
5575                .await?,
5576            );
5577            let terminal_status = if receipt.status {
5578                TransactionStatus::Finalized
5579            } else {
5580                TransactionStatus::Reverted
5581            };
5582
5583            if !intent.active {
5584                anyhow::ensure!(
5585                    intent.status == terminal_status.as_str(),
5586                    "Released terminal intent status conflicts with its verified receipt"
5587                );
5588            }
5589            records.push(ExecutionVerificationMigrationRecord {
5590                intent_id: intent.id,
5591                nonce: Some(nonce),
5592                transaction_hash: Some(current.transaction_hash.clone()),
5593                terminal_status: Some(terminal_status),
5594                block_number: Some(receipt.block_number),
5595                block_hash: Some(receipt.block_hash.to_string()),
5596                receipt_success: Some(receipt.status),
5597                gas_used: Some(receipt.gas_used),
5598                effective_gas_price: Some(receipt.effective_gas_price.to_string()),
5599                recover_prepared: false,
5600                decisions,
5601            });
5602        }
5603
5604        Ok(ExecutionVerificationMigration { snapshot, records })
5605    }
5606}
5607
5608#[async_trait(?Send)]
5609impl ExecutionClient for BlockchainExecutionClient {
5610    fn is_connected(&self) -> bool {
5611        self.core.is_connected()
5612    }
5613
5614    fn client_id(&self) -> ClientId {
5615        self.core.client_id
5616    }
5617
5618    fn account_id(&self) -> AccountId {
5619        self.core.account_id
5620    }
5621
5622    fn venue(&self) -> Venue {
5623        self.core.venue
5624    }
5625
5626    fn handles_order_venue(&self, venue: Venue) -> bool {
5627        venue.parse_dex().is_ok_and(|(blockchain, dex_type)| {
5628            blockchain == self.chain.name && dex_type == DexType::UniswapV3
5629        })
5630    }
5631
5632    fn oms_type(&self) -> OmsType {
5633        self.core.oms_type
5634    }
5635
5636    fn get_account(&self) -> Option<AccountAny> {
5637        self.core.cache().account_owned(&self.core.account_id)
5638    }
5639
5640    fn generate_account_state(
5641        &self,
5642        balances: Vec<AccountBalance>,
5643        margins: Vec<MarginBalance>,
5644        reported: bool,
5645        ts_event: UnixNanos,
5646        info: Option<Params>,
5647    ) -> anyhow::Result<()> {
5648        self.emitter
5649            .try_emit_account_state(balances, margins, reported, ts_event, info)
5650    }
5651
5652    fn start(&mut self) -> anyhow::Result<()> {
5653        if self.core.is_started() {
5654            return Ok(());
5655        }
5656
5657        self.emitter.set_sender(get_exec_event_sender());
5658        self.core.set_started();
5659        log::info!(
5660            "Started: client_id={}, account_id={}",
5661            self.core.client_id,
5662            self.core.account_id
5663        );
5664        Ok(())
5665    }
5666
5667    fn stop(&mut self) -> anyhow::Result<()> {
5668        if self.core.is_stopped() {
5669            return Ok(());
5670        }
5671
5672        self.pending_tasks.begin_shutdown();
5673        self.signer = None;
5674        self.core.set_stopped();
5675        self.core.set_disconnected();
5676        log::info!("Stopped: client_id={}", self.core.client_id);
5677        Ok(())
5678    }
5679
5680    fn submit_order(&self, cmd: SubmitOrder) -> anyhow::Result<()> {
5681        let order = self.core.cache().try_order_owned(&cmd.client_order_id)?;
5682
5683        if order.is_closed() {
5684            log::warn!("Cannot submit closed order {}", order.client_order_id());
5685            return Ok(());
5686        }
5687
5688        if let Err(reason) = validate_order(&order) {
5689            self.emitter.emit_order_denied(&order, &reason.to_string());
5690            return Ok(());
5691        }
5692
5693        if !self.pending_tasks.is_open() {
5694            self.emitter
5695                .emit_order_denied(&order, "Blockchain execution client is shutting down");
5696            return Ok(());
5697        }
5698
5699        let plan = match self.prepare_swap(&cmd, &order) {
5700            Ok(plan) => plan,
5701            Err(e) => {
5702                self.emitter.emit_order_denied(&order, &e.to_string());
5703                return Ok(());
5704            }
5705        };
5706
5707        let executor = match self.transaction_executor() {
5708            Ok(executor) => executor,
5709            Err(e) => {
5710                self.emitter.emit_order_denied(&order, &e.to_string());
5711                return Ok(());
5712            }
5713        };
5714
5715        let emitter = self.emitter.clone();
5716        let max_quote_age_blocks = self.transaction_limits.max_quote_age_blocks;
5717        let deadline_seconds = self.transaction_limits.deadline_seconds;
5718        let client_order_id = order.client_order_id();
5719
5720        let future = async move {
5721            if let Err(e) = execute_swap(
5722                plan,
5723                executor,
5724                emitter,
5725                max_quote_age_blocks,
5726                deadline_seconds,
5727            )
5728            .await
5729            {
5730                log::warn!("Swap execution for order {client_order_id} failed: {e:?}");
5731            }
5732        };
5733
5734        if let Err(e) = self.pending_tasks.spawn(future) {
5735            release_preparing_slot(&self.in_flight);
5736            log::warn!("Skipping blockchain swap after shutdown began: {e}");
5737        }
5738
5739        Ok(())
5740    }
5741
5742    fn submit_order_list(&self, cmd: SubmitOrderList) -> anyhow::Result<()> {
5743        let orders = self
5744            .core
5745            .cache()
5746            .orders_for_ids(&cmd.order_list.client_order_ids, &cmd);
5747
5748        for order in &orders {
5749            if order.is_closed() {
5750                log::warn!("Cannot submit closed order {}", order.client_order_id());
5751                continue;
5752            }
5753
5754            self.emitter
5755                .emit_order_denied(order, ORDER_LIST_UNSUPPORTED);
5756        }
5757
5758        Ok(())
5759    }
5760
5761    fn modify_order(&self, cmd: ModifyOrder) -> anyhow::Result<()> {
5762        let Ok(order) = self.core.cache().try_order_owned(&cmd.client_order_id) else {
5763            log::warn!("Cannot modify unknown order {}", cmd.client_order_id);
5764            return Ok(());
5765        };
5766
5767        self.emitter.emit_order_modify_rejected(
5768            &order,
5769            cmd.venue_order_id,
5770            ORDER_MODIFY_UNSUPPORTED,
5771            get_atomic_clock_realtime().get_time_ns(),
5772        );
5773        Ok(())
5774    }
5775
5776    fn cancel_order(&self, cmd: CancelOrder) -> anyhow::Result<()> {
5777        let Ok(order) = self.core.cache().try_order_owned(&cmd.client_order_id) else {
5778            log::warn!("Cannot cancel unknown order {}", cmd.client_order_id);
5779            return Ok(());
5780        };
5781
5782        self.emitter.emit_order_cancel_rejected(
5783            &order,
5784            cmd.venue_order_id,
5785            ORDER_CANCEL_UNSUPPORTED,
5786            get_atomic_clock_realtime().get_time_ns(),
5787        );
5788        Ok(())
5789    }
5790
5791    fn cancel_all_orders(&self, cmd: CancelAllOrders) -> anyhow::Result<()> {
5792        log::warn!(
5793            "Cancel-all for {} is not supported on the blockchain execution client",
5794            cmd.instrument_id
5795        );
5796        Ok(())
5797    }
5798
5799    fn batch_cancel_orders(&self, cmd: BatchCancelOrders) -> anyhow::Result<()> {
5800        for cancel in cmd.cancels {
5801            self.cancel_order(cancel)?;
5802        }
5803        Ok(())
5804    }
5805
5806    fn query_account(&self, cmd: QueryAccount) -> anyhow::Result<()> {
5807        anyhow::ensure!(
5808            cmd.account_id == self.core.account_id,
5809            "Query account ID {} does not match client account ID {}",
5810            cmd.account_id,
5811            self.core.account_id
5812        );
5813        anyhow::ensure!(self.core.is_started(), "Execution client is not started");
5814
5815        let balances = self.wallet_balance.lock().as_account_balances()?;
5816        self.generate_account_state(
5817            balances,
5818            vec![],
5819            true,
5820            get_atomic_clock_realtime().get_time_ns(),
5821            None,
5822        )
5823    }
5824
5825    fn query_order(&self, cmd: QueryOrder) -> anyhow::Result<()> {
5826        log::debug!(
5827            "Order queries are not supported on the blockchain execution client; cannot query {}",
5828            cmd.client_order_id
5829        );
5830        Ok(())
5831    }
5832
5833    fn retain_unresolved_submissions(&self) -> bool {
5834        true
5835    }
5836
5837    async fn connect(&mut self) -> anyhow::Result<()> {
5838        if self.core.is_connected() {
5839            log::warn!("Blockchain execution client already connected");
5840            return Ok(());
5841        }
5842
5843        log::info!(
5844            "Connecting to blockchain execution client on chain {}",
5845            self.chain.name
5846        );
5847
5848        if !self.pending_tasks.is_open() || !self.pending_tasks.is_empty() {
5849            self.pending_tasks.begin_shutdown();
5850            self.pending_tasks
5851                .finish_shutdown(Duration::from_secs(5), Duration::from_secs(2))
5852                .await
5853                .map_err(|e| anyhow::anyhow!("Failed to terminate blockchain submissions: {e}"))?;
5854            self.signer = None;
5855            self.pending_tasks
5856                .start_generation()
5857                .map_err(|e| anyhow::anyhow!("Failed to start blockchain task generation: {e}"))?;
5858        }
5859        release_preparing_slot(&self.in_flight);
5860
5861        let setup_guard = TaskGroupGuard::new(&[&self.pending_tasks], || {});
5862
5863        let payload_keys = PayloadKeySet::load(
5864            self.config.payload_key_env.as_deref(),
5865            &self.config.payload_key_retired_env,
5866            self.config.payload_deployment_id.as_deref(),
5867        )?
5868        .map(Arc::new);
5869
5870        if self.cache.database.is_some() || self.config.postgres_cache_database_config.is_some() {
5871            let keys = payload_keys.as_deref().ok_or_else(|| {
5872                anyhow::anyhow!(
5873                    "Postgres execution requires an active payload key and deployment identity"
5874                )
5875            })?;
5876
5877            if self.cache.database.is_none() {
5878                let pg_options = self
5879                    .config
5880                    .postgres_cache_database_config
5881                    .as_ref()
5882                    .expect("Postgres configuration checked above");
5883                let database = crate::cache::database::BlockchainCacheDatabase::connect(
5884                    pg_options.clone().into(),
5885                )
5886                .await
5887                .map_err(|e| {
5888                    anyhow::anyhow!("Failed to connect to the Postgres cache database: {e}")
5889                })?;
5890                self.cache.database = Some(database);
5891            }
5892            self.cache
5893                .database
5894                .as_ref()
5895                .expect("database was attached")
5896                .require_execution_payload_storage_ready(keys)
5897                .await?;
5898            self.cache.initialize_chain().await;
5899            self.cache.ensure_execution_transaction_schema().await?;
5900            let check = self
5901                .cache
5902                .database
5903                .as_ref()
5904                .expect("database was attached")
5905                .check_execution_payload_storage(
5906                    Some(keys),
5907                    Some(PayloadPolicy {
5908                        chain_id: self.chain.chain_id,
5909                        signer: self.wallet_address,
5910                        gas_limit: self.config.gas_limit,
5911                        max_fee_per_gas: self.config.max_fee_per_gas_wei,
5912                    }),
5913                    100,
5914                )
5915                .await?;
5916            anyhow::ensure!(
5917                check.protected,
5918                "Postgres execution requires protected payload storage"
5919            );
5920        } else {
5921            log::warn!(
5922                "No Postgres cache database configured; transactions will be refused (no durable store)"
5923            );
5924        }
5925        self.payload_keys = payload_keys;
5926
5927        let verification = self
5928            .config
5929            .verification
5930            .as_ref()
5931            .expect("verification config validated at construction");
5932        let position = if let Some(database) = self.cache.database.as_ref() {
5933            database
5934                .load_execution_verification_position(
5935                    self.chain.chain_id,
5936                    &self.config.wallet_address,
5937                    &verification.manifest_version,
5938                    &verification.manifest_digest,
5939                )
5940                .await?
5941        } else {
5942            None
5943        };
5944        let migration_snapshot = if position.is_none() {
5945            if let Some(database) = self.cache.database.as_ref() {
5946                let snapshot = database
5947                    .load_execution_verification_migration_snapshot(
5948                        self.chain.chain_id,
5949                        &self.config.wallet_address,
5950                    )
5951                    .await?;
5952
5953                if snapshot.intents.is_empty() {
5954                    None
5955                } else {
5956                    Some(snapshot)
5957                }
5958            } else {
5959                None
5960            }
5961        } else {
5962            None
5963        };
5964
5965        let chain_id_verification = required_verification(
5966            self.verification.verify_chain_id().await,
5967            "Blockchain chain ID",
5968        )?;
5969        let checkpoint_verification = required_verification(
5970            self.verification.verify_checkpoint().await,
5971            "Blockchain checkpoint",
5972        )?;
5973        let checkpoint = checkpoint_verification.value;
5974        let finalized_verification = required_verification(
5975            self.verification.verify_finalized_header().await,
5976            "Blockchain finalized header",
5977        )?;
5978        let finalized = finalized_verification.value;
5979        let mut connect_decisions = vec![
5980            verification_decision(&chain_id_verification, None, None),
5981            verification_decision(
5982                &checkpoint_verification,
5983                Some(checkpoint.number),
5984                Some(checkpoint.number),
5985            ),
5986            verification_decision(
5987                &finalized_verification,
5988                Some(finalized.number),
5989                Some(finalized.number),
5990            ),
5991        ];
5992        let mut finalized_headers = if let Some(position) = position.as_ref() {
5993            let durable_tip = parse_verified_header(&position.finalized_tip)?;
5994            anyhow::ensure!(
5995                durable_tip.number >= checkpoint.number,
5996                "Durable finalized header tip precedes the trusted checkpoint"
5997            );
5998            let durable_tip_verification = required_verification(
5999                self.verification.verify_block(durable_tip.number).await,
6000                "Blockchain durable finalized tip",
6001            )?;
6002            anyhow::ensure!(
6003                durable_tip_verification.value == durable_tip,
6004                "Durable finalized header tip conflicts with independent sources"
6005            );
6006            connect_decisions.push(verification_decision(
6007                &durable_tip_verification,
6008                Some(durable_tip.number),
6009                Some(durable_tip.number),
6010            ));
6011            vec![durable_tip]
6012        } else {
6013            vec![checkpoint]
6014        };
6015        let mut ancestry_cursor = *finalized_headers
6016            .last()
6017            .expect("finalized header ledger is nonempty");
6018        anyhow::ensure!(
6019            finalized.number >= ancestry_cursor.number,
6020            "Verified finalized height regressed below the durable finalized header tip"
6021        );
6022
6023        while ancestry_cursor.number < finalized.number {
6024            let end = ancestry_cursor
6025                .number
6026                .saturating_add(4_096)
6027                .min(finalized.number);
6028            let start = ancestry_cursor.number.saturating_add(1);
6029            let headers_verification = required_verification(
6030                self.verification
6031                    .verify_header_window(ancestry_cursor, end)
6032                    .await,
6033                "Blockchain finalized ancestry",
6034            )?;
6035            let headers = &headers_verification.value;
6036            ancestry_cursor = *headers
6037                .last()
6038                .expect("nonempty ancestry window advances the cursor");
6039            finalized_headers.extend(headers.iter().copied());
6040            connect_decisions.push(verification_decision(
6041                &headers_verification,
6042                Some(start),
6043                Some(end),
6044            ));
6045        }
6046        anyhow::ensure!(
6047            ancestry_cursor == finalized,
6048            "Verified finalized header conflicts with its ancestry window"
6049        );
6050        let nonce_verification = required_verification(
6051            self.verification
6052                .verify_transaction_count(&self.wallet_address, finalized.number)
6053                .await,
6054            "Blockchain finalized transaction count",
6055        )?;
6056        let observed_canonical_nonce = nonce_verification.value;
6057        let next_canonical_nonce = position
6058            .as_ref()
6059            .map_or(observed_canonical_nonce, |position| {
6060                position.next_canonical_nonce
6061            });
6062
6063        if let Some(position) = position.as_ref() {
6064            log::debug!(
6065                "Resumed execution verification ledger at nonce revision {} with observed finalized nonce {}",
6066                position.revision,
6067                observed_canonical_nonce,
6068            );
6069        }
6070        connect_decisions.push(verification_decision(
6071            &nonce_verification,
6072            Some(finalized.number),
6073            Some(finalized.number),
6074        ));
6075        let deployment_verification = required_verification(
6076            self.verification
6077                .verify_deployment_manifest(&verification.deployment_manifest, finalized.number)
6078                .await,
6079            "Blockchain deployment manifest",
6080        )?;
6081        connect_decisions.push(verification_decision(
6082            &deployment_verification,
6083            Some(finalized.number),
6084            Some(finalized.number),
6085        ));
6086        connect_decisions.extend(
6087            verify_connect_capabilities(
6088                &self.verification,
6089                &verification.deployment_manifest,
6090                self.wallet_address,
6091                self.weth_address,
6092                finalized.number,
6093            )
6094            .await?,
6095        );
6096        let migration = if let Some(snapshot) = migration_snapshot {
6097            Some(
6098                self.build_execution_verification_migration(
6099                    snapshot,
6100                    finalized,
6101                    &finalized_headers,
6102                    &nonce_verification,
6103                )
6104                .await?,
6105            )
6106        } else {
6107            None
6108        };
6109
6110        if let Some(database) = self.cache.database.as_ref() {
6111            let identities = std::iter::once(&verification.authoritative)
6112                .chain(
6113                    verification
6114                        .verifiers
6115                        .iter()
6116                        .map(|provider| &provider.identity),
6117                )
6118                .collect::<Vec<_>>();
6119            let provider_ids = identities
6120                .iter()
6121                .map(|identity| identity.provider_id.clone())
6122                .collect::<Vec<_>>();
6123            let operator_ids = identities
6124                .iter()
6125                .map(|identity| identity.operator_id.clone())
6126                .collect::<Vec<_>>();
6127            let failure_domain_ids = identities
6128                .iter()
6129                .flat_map(|identity| identity.failure_domain_ids.iter().cloned())
6130                .collect::<Vec<_>>();
6131            let finalized_headers = finalized_headers
6132                .iter()
6133                .map(durable_verified_header)
6134                .collect::<Vec<_>>();
6135            database
6136                .ensure_execution_verification_schema(&ExecutionVerificationBootstrap {
6137                    chain_id: self.chain.chain_id,
6138                    wallet_address: &self.config.wallet_address,
6139                    manifest_version: &verification.manifest_version,
6140                    manifest_digest: &verification.manifest_digest,
6141                    checkpoint_number: checkpoint.number,
6142                    checkpoint_hash: &checkpoint.hash.to_string(),
6143                    checkpoint_parent_hash: &checkpoint.parent_hash.to_string(),
6144                    checkpoint_timestamp: checkpoint.timestamp,
6145                    checkpoint_base_fee_per_gas: checkpoint.base_fee_per_gas,
6146                    finalized_headers: &finalized_headers,
6147                    next_canonical_nonce,
6148                    observed_canonical_nonce,
6149                    provider_ids: &provider_ids,
6150                    operator_ids: &operator_ids,
6151                    failure_domain_ids: &failure_domain_ids,
6152                    decisions: &connect_decisions,
6153                    migration: migration.as_ref(),
6154                })
6155                .await?;
6156        }
6157
6158        let payload_connect_lease = if let (Some(database), Some(keys)) =
6159            (self.cache.database.as_ref(), self.payload_keys.as_deref())
6160        {
6161            Some(
6162                database
6163                    .require_execution_payload_storage(keys, self.payload_policy(), 100)
6164                    .await?,
6165            )
6166        } else {
6167            None
6168        };
6169
6170        // Load the signer key from the configured environment variable; the key is never
6171        // logged, serialized, or stored in configuration
6172        let private_key = Zeroizing::new(
6173            std::env::var(&self.config.signer_private_key_env).map_err(|_| {
6174                anyhow::anyhow!(
6175                    "Signer private key environment variable '{}' is not set",
6176                    self.config.signer_private_key_env
6177                )
6178            })?,
6179        );
6180        let encoded_key = private_key.trim();
6181        let encoded_key = encoded_key.strip_prefix("0x").unwrap_or(encoded_key);
6182        let key_bytes = Zeroizing::new(hex::decode_array::<32>(encoded_key).map_err(|_| {
6183            anyhow::anyhow!(
6184                "Signer private key in '{}' is not a valid hex private key",
6185                self.config.signer_private_key_env
6186            )
6187        })?);
6188        let signer = PrivateKeySigner::from_slice(&key_bytes[..]).map_err(|_| {
6189            anyhow::anyhow!(
6190                "Signer private key in '{}' is not a valid secp256k1 private key",
6191                self.config.signer_private_key_env
6192            )
6193        })?;
6194
6195        if signer.address() != self.wallet_address {
6196            anyhow::bail!(
6197                "Signer address {} derived from '{}' does not match configured wallet address {}",
6198                signer.address(),
6199                self.config.signer_private_key_env,
6200                self.wallet_address
6201            );
6202        }
6203
6204        self.signer = Some(Arc::new(signer));
6205        drop(payload_connect_lease);
6206
6207        if self.cache.has_database()
6208            && let Err(e) = self.reconcile_unresolved_execution().await
6209        {
6210            self.signer = None;
6211            return Err(e);
6212        }
6213
6214        if let Err(e) = self.refresh_wallet_balances().await {
6215            self.signer = None;
6216            return Err(e);
6217        }
6218        self.core.set_connected();
6219        setup_guard.disarm();
6220        log::info!(
6221            "Blockchain execution client connected on chain {}",
6222            self.chain.name
6223        );
6224        Ok(())
6225    }
6226
6227    async fn disconnect(&mut self) -> anyhow::Result<()> {
6228        self.pending_tasks.begin_shutdown();
6229        let tasks_result = self
6230            .pending_tasks
6231            .finish_shutdown(Duration::from_secs(5), Duration::from_secs(2))
6232            .await;
6233        self.signer = None;
6234        self.core.set_disconnected();
6235        tasks_result
6236            .map(|_| ())
6237            .map_err(|e| anyhow::anyhow!("Failed to terminate blockchain submissions: {e}"))?;
6238        Ok(())
6239    }
6240
6241    async fn generate_order_status_report(
6242        &self,
6243        _cmd: &GenerateOrderStatusReport,
6244    ) -> anyhow::Result<Option<OrderStatusReport>> {
6245        anyhow::bail!("{VENUE_EXECUTION_REPORTS_UNSUPPORTED}");
6246    }
6247
6248    async fn generate_order_status_reports(
6249        &self,
6250        _cmd: &GenerateOrderStatusReports,
6251    ) -> anyhow::Result<Vec<OrderStatusReport>> {
6252        anyhow::bail!("{VENUE_EXECUTION_REPORTS_UNSUPPORTED}");
6253    }
6254
6255    async fn generate_fill_reports(
6256        &self,
6257        _cmd: GenerateFillReports,
6258    ) -> anyhow::Result<Vec<FillReport>> {
6259        anyhow::bail!("{VENUE_EXECUTION_REPORTS_UNSUPPORTED}");
6260    }
6261
6262    async fn generate_position_status_reports(
6263        &self,
6264        _cmd: &GeneratePositionStatusReports,
6265    ) -> anyhow::Result<Vec<PositionStatusReport>> {
6266        anyhow::bail!("{VENUE_EXECUTION_REPORTS_UNSUPPORTED}");
6267    }
6268
6269    async fn generate_mass_status(
6270        &self,
6271        _lookback_mins: Option<u64>,
6272    ) -> anyhow::Result<Option<ExecutionMassStatus>> {
6273        // Venue mass status is unsupported; durable intent reconciliation at connect
6274        // covers restart recovery, and Ok(None) keeps LiveNode startup safe
6275        log::info!(
6276            "Mass status is not supported on the blockchain execution client; skipping venue reconciliation"
6277        );
6278        Ok(None)
6279    }
6280}
6281
6282fn validate_order(order: &impl Order) -> Result<(), OrderDeniedReason> {
6283    if order.is_reduce_only() {
6284        return Err(OrderDeniedReason::UnsupportedReduceOnly);
6285    }
6286
6287    Ok(())
6288}
6289
6290#[cfg(test)]
6291mod tests {
6292    use std::{
6293        cell::RefCell,
6294        rc::Rc,
6295        sync::atomic::{AtomicU64, Ordering},
6296    };
6297
6298    use alloy::{
6299        primitives::{address, aliases::I24},
6300        sol_types::SolValue,
6301    };
6302    use nautilus_common::{
6303        cache::Cache,
6304        clock::{Clock, VirtualClock},
6305        live::runner::replace_exec_event_sender,
6306        messages::{ExecutionEvent, execution::TradingCommand},
6307        testing::wait_until_async,
6308    };
6309    use nautilus_core::UUID4;
6310    use nautilus_infrastructure::sql::pg::{PostgresConnectOptions, get_postgres_connect_options};
6311    use nautilus_live::{
6312        execution::{
6313            config::ExecutionManagerConfig,
6314            submission::{SubmissionRecoveryExhausted, SubmissionRecoverySource},
6315        },
6316        manager::ExecutionManager,
6317    };
6318    use nautilus_model::{
6319        defi::{
6320            PoolProfiler,
6321            chain::chains,
6322            data::block::BlockPosition,
6323            pool_analysis::{
6324                position::PoolPosition,
6325                snapshot::{PoolAnalytics, PoolSnapshot, PoolState},
6326            },
6327            tick_map::{tick::PoolTick, tick_math::get_tick_at_sqrt_ratio},
6328        },
6329        enums::AccountType,
6330        events::OrderEventAny,
6331        identifiers::{OrderListId, StrategyId, TraderId},
6332        orders::{OrderList, OrderTestBuilder},
6333        types::Price,
6334    };
6335    use rstest::rstest;
6336    use sqlx::postgres::{PgAdvisoryLock, PgAdvisoryLockKey, PgPoolOptions};
6337
6338    use super::*;
6339    use crate::{
6340        cache::database::tests::connect_test_database,
6341        config::{
6342            BlockchainCallEdgeManifest, BlockchainChainAnchorConfig, BlockchainContractManifest,
6343            BlockchainContractProbe, BlockchainContractRole, BlockchainDeploymentManifest,
6344            BlockchainPoolManifest, BlockchainProviderIdentity, BlockchainTokenManifest,
6345            BlockchainVerificationConfig, BlockchainVerificationProviderConfig, QuoteSpendLimit,
6346        },
6347        constants::BLOCKCHAIN_VENUE,
6348        exchanges::arbitrum::UNISWAP_V3,
6349        rpc::http::{
6350            EXECUTION_RPC_TIMEOUT_SECS,
6351            tests::mock::{MockRpcState, start_mock_rpc_server},
6352        },
6353    };
6354
6355    /// Polls for the receipt of a broadcast transaction until it exists or the poll bound
6356    /// is exhausted. A `null` receipt result is a legitimate pending response.
6357    async fn poll_for_receipt(
6358        http_rpc_client: &BlockchainHttpRpcClient,
6359        tx_hash: &B256,
6360        max_polls: u32,
6361        interval: Duration,
6362    ) -> anyhow::Result<Option<RpcTransactionReceipt>> {
6363        let mut last_error = None;
6364        let mut observed_pending = false;
6365
6366        for attempt in 0..max_polls {
6367            if attempt > 0 {
6368                tokio::time::sleep(interval).await;
6369            }
6370
6371            match http_rpc_client.get_transaction_receipt(tx_hash).await {
6372                Ok(Some(receipt)) => return Ok(Some(receipt)),
6373                Ok(None) => observed_pending = true,
6374                Err(e) => {
6375                    log::warn!(
6376                        "Receipt poll {}/{} for transaction {tx_hash} failed: {e}",
6377                        attempt + 1,
6378                        max_polls
6379                    );
6380                    last_error = Some(e);
6381                }
6382            }
6383        }
6384
6385        if !observed_pending && let Some(e) = last_error {
6386            return Err(e);
6387        }
6388
6389        Ok(None)
6390    }
6391
6392    const CHAIN_ID_ARBITRUM: &str =
6393        include_str!("../../test_data/execution/rpc_eth_chain_id_arbitrum.json");
6394    const CHAIN_ID_ETHEREUM: &str =
6395        include_str!("../../test_data/execution/rpc_eth_chain_id_ethereum.json");
6396    const GET_CODE_DEPLOYED: &str =
6397        include_str!("../../test_data/execution/rpc_eth_get_code_deployed.json");
6398    const GET_CODE_EMPTY: &str =
6399        include_str!("../../test_data/execution/rpc_eth_get_code_empty.json");
6400    const GET_BALANCE: &str = include_str!("../../test_data/execution/rpc_eth_get_balance.json");
6401    const GET_BALANCE_ZERO: &str =
6402        include_str!("../../test_data/execution/rpc_eth_get_balance_zero.json");
6403    const GET_BALANCE_INSUFFICIENT: &str =
6404        include_str!("../../test_data/execution/rpc_eth_get_balance_insufficient.json");
6405    const CALL_BALANCE: &str = include_str!("../../test_data/execution/rpc_eth_call_balance.json");
6406    const CALL_BALANCE_AFTER_WRAP: &str =
6407        include_str!("../../test_data/execution/rpc_eth_call_balance_after_wrap.json");
6408    const CALL_BALANCE_WETH: &str =
6409        include_str!("../../test_data/execution/rpc_eth_call_balance_weth.json");
6410    const CALL_BALANCE_USDC: &str =
6411        include_str!("../../test_data/execution/rpc_eth_call_balance_usdc.json");
6412    const CALL_BALANCE_WETH_UPDATED: &str =
6413        include_str!("../../test_data/execution/rpc_eth_call_balance_weth_updated.json");
6414    const CALL_BALANCE_USDC_UPDATED: &str =
6415        include_str!("../../test_data/execution/rpc_eth_call_balance_usdc_updated.json");
6416    const CALL_BOOL_TRUE: &str =
6417        include_str!("../../test_data/execution/rpc_eth_call_bool_true.json");
6418    const CALL_EMPTY: &str = include_str!("../../test_data/execution/rpc_eth_call_empty.json");
6419    const CALL_ZERO: &str = include_str!("../../test_data/execution/rpc_eth_call_zero.json");
6420    const CALL_ALLOWANCE: &str =
6421        include_str!("../../test_data/execution/rpc_eth_call_allowance.json");
6422    const CALL_ALLOWANCE_1000: &str = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0x00000000000000000000000000000000000000000000000000000000000003e8\"}";
6423    const CALL_ALLOWANCE_MAX: &str = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\"}";
6424    const CALL_FACTORY: &str = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0x0000000000000000000000001f98431c8ad98523631ae4a59f267346ea31f984\"}";
6425    const CALL_WETH: &str = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0x00000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab1\"}";
6426    const CALL_USDC: &str = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0x000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e5831\"}";
6427    const CALL_FEE_500: &str = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0x00000000000000000000000000000000000000000000000000000000000001f4\"}";
6428    const CALL_POOL: &str = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0x000000000000000000000000c6962004f452be9203591991d15f6b388e09e8d0\"}";
6429    const CALL_REVERTED: &str =
6430        r#"{"jsonrpc":"2.0","id":1,"error":{"code":3,"message":"execution reverted"}}"#;
6431    const STORAGE_ZERO: &str = r#"{"jsonrpc":"2.0","id":1,"result":"0x0000000000000000000000000000000000000000000000000000000000000000"}"#;
6432    const TRACE_UNKNOWN_TRANSACTION: &str =
6433        r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32000,"message":"transaction not found"}}"#;
6434    const CALL_DECIMALS_18: &str = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0x0000000000000000000000000000000000000000000000000000000000000012\"}";
6435    const CALL_DECIMALS_6: &str = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0x0000000000000000000000000000000000000000000000000000000000000006\"}";
6436    const TRANSACTION_COUNT: &str =
6437        include_str!("../../test_data/execution/rpc_eth_get_transaction_count.json");
6438    const TRANSACTION_COUNT_NEXT: &str =
6439        include_str!("../../test_data/execution/rpc_eth_get_transaction_count_next.json");
6440    const ESTIMATE_GAS: &str = include_str!("../../test_data/execution/rpc_eth_estimate_gas.json");
6441    const MAX_PRIORITY_FEE: &str =
6442        include_str!("../../test_data/execution/rpc_eth_max_priority_fee_per_gas.json");
6443    const BLOCK_BY_NUMBER: &str =
6444        include_str!("../../test_data/execution/rpc_eth_get_block_by_number.json");
6445    const BLOCK_CANONICAL: &str =
6446        include_str!("../../test_data/execution/rpc_eth_get_block_canonical.json");
6447    const BLOCK_FINALIZED: &str =
6448        include_str!("../../test_data/execution/rpc_eth_get_block_finalized.json");
6449    const RECEIPT_SUCCESS: &str =
6450        include_str!("../../test_data/execution/rpc_eth_get_transaction_receipt_success.json");
6451    const RECEIPT_REVERTED: &str =
6452        include_str!("../../test_data/execution/rpc_eth_get_transaction_receipt_reverted.json");
6453    const RECEIPT_NULL: &str =
6454        include_str!("../../test_data/execution/rpc_eth_get_transaction_receipt_null.json");
6455    const SEND_RAW_TRANSACTION: &str =
6456        include_str!("../../test_data/execution/rpc_eth_send_raw_transaction.json");
6457    const SEND_RAW_TRANSACTION_REJECTED: &str =
6458        include_str!("../../test_data/execution/rpc_eth_send_raw_transaction_rejected.json");
6459    const SEND_RAW_TRANSACTION_NONCE_TOO_LOW: &str =
6460        include_str!("../../test_data/execution/rpc_eth_send_raw_transaction_nonce_too_low.json");
6461    const RPC_METHOD_NOT_FOUND: &str =
6462        include_str!("../../test_data/execution/rpc_error_method_not_found.json");
6463
6464    const WALLET: &str = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266";
6465    const ROUTER: &str = "0xE592427A0AEce92De3Edee1F18E0157C05861564";
6466    const WETH: &str = "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1";
6467    const USDC: &str = "0xaf88d065e77c8cC2239327C5EDb3A432268e5831";
6468    const TEST_TIMEOUT: Duration = Duration::from_secs(10);
6469
6470    const WETH_ADDRESS: Address = address!("82aF49447D8a07e3bd95BD0d56f35241523fBab1");
6471    const USDC_ADDRESS: Address = address!("af88d065e77c8cC2239327C5EDb3A432268e5831");
6472    const ROUTER_ADDRESS: Address = address!("E592427A0AEce92De3Edee1F18E0157C05861564");
6473
6474    // Anvil development key for WALLET (public, test-only)
6475    const TEST_PRIVATE_KEY: &str =
6476        "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
6477
6478    const BALANCE_OF_SELECTOR: &str = "0x70a08231";
6479    const ALLOWANCE_SELECTOR: &str = "0xdd62ed3e";
6480    const POOL_TOKEN0_SELECTOR: &str = "0x0dfe1681";
6481    const POOL_TOKEN1_SELECTOR: &str = "0xd21220a7";
6482    const POOL_FEE_SELECTOR: &str = "0xddca3f43";
6483    const DECIMALS_SELECTOR: &str = "0x313ce567";
6484    const FACTORY_SELECTOR: &str = "0xc45a0155";
6485    const WETH9_SELECTOR: &str = "0x4aa4a4fc";
6486    const GET_POOL_SELECTOR: &str = "0x1698ee82";
6487    const QUOTE_EXACT_INPUT_SELECTOR: &str = "0xc6a5026a";
6488    const QUOTE_EXACT_OUTPUT_SELECTOR: &str = "0xbd21704a";
6489
6490    fn test_pool() -> Pool {
6491        let chain = Arc::new(chains::ARBITRUM.clone());
6492        let dex = Arc::clone(&UNISWAP_V3.dex);
6493        let weth = Token::new(
6494            Arc::clone(&chain),
6495            address!("82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
6496            "Wrapped Ether".to_string(),
6497            "WETH".to_string(),
6498            18,
6499        );
6500        let usdc = Token::new(
6501            Arc::clone(&chain),
6502            address!("af88d065e77c8cC2239327C5EDb3A432268e5831"),
6503            "USD Coin".to_string(),
6504            "USDC".to_string(),
6505            6,
6506        );
6507
6508        Pool::new(
6509            chain,
6510            dex,
6511            address!("C6962004f452bE9203591991D15f6b388e09E8D0"),
6512            PoolIdentifier::from_address(address!("C6962004f452bE9203591991D15f6b388e09E8D0")),
6513            55_000_000,
6514            weth,
6515            usdc,
6516            Some(500),
6517            Some(10),
6518            UnixNanos::default(),
6519        )
6520    }
6521
6522    fn test_config(http_rpc_url: String) -> BlockchainExecutionClientConfig {
6523        test_config_with_signer_env(http_rpc_url, "BLOCKCHAIN_TEST_PRIVATE_KEY")
6524    }
6525
6526    fn test_config_with_signer_env(
6527        http_rpc_url: String,
6528        signer_env: &str,
6529    ) -> BlockchainExecutionClientConfig {
6530        let verifier_separator = if http_rpc_url.contains('?') { '&' } else { '?' };
6531        let code_hash = keccak256(
6532            hex::decode("6080604052348015600e575f5ffd5b5060").expect("valid test bytecode"),
6533        )
6534        .to_string();
6535        let result = |response: &str| {
6536            serde_json::from_str::<serde_json::Value>(response).unwrap()["result"]
6537                .as_str()
6538                .unwrap()
6539                .to_string()
6540        };
6541        let probe = |call_data: &str, expected_output: String| BlockchainContractProbe {
6542            call_data: call_data.to_string(),
6543            expected_output,
6544        };
6545        let contract = |address: &str, role| {
6546            let probes = match role {
6547                BlockchainContractRole::Router => vec![
6548                    probe(FACTORY_SELECTOR, result(CALL_FACTORY)),
6549                    probe(WETH9_SELECTOR, result(CALL_WETH)),
6550                ],
6551                BlockchainContractRole::Factory => vec![probe(
6552                    &hex::encode_prefixed(
6553                        UniswapV3Factory::getPoolCall {
6554                            tokenA: WETH_ADDRESS,
6555                            tokenB: USDC_ADDRESS,
6556                            fee: U24::try_from(500u32).unwrap(),
6557                        }
6558                        .abi_encode(),
6559                    ),
6560                    result(CALL_POOL),
6561                )],
6562                BlockchainContractRole::WrappedNative => {
6563                    vec![probe(DECIMALS_SELECTOR, result(CALL_DECIMALS_18))]
6564                }
6565                BlockchainContractRole::Quote => {
6566                    vec![probe(FACTORY_SELECTOR, result(CALL_FACTORY))]
6567                }
6568                BlockchainContractRole::Token => {
6569                    vec![probe(DECIMALS_SELECTOR, result(CALL_DECIMALS_6))]
6570                }
6571                BlockchainContractRole::Pool => vec![
6572                    probe(POOL_TOKEN0_SELECTOR, result(CALL_WETH)),
6573                    probe(POOL_TOKEN1_SELECTOR, result(CALL_USDC)),
6574                    probe(POOL_FEE_SELECTOR, result(CALL_FEE_500)),
6575                ],
6576                BlockchainContractRole::Implementation => Vec::new(),
6577            };
6578            BlockchainContractManifest {
6579                address: address.to_string(),
6580                role,
6581                runtime_code_hash: code_hash.clone(),
6582                proxy: None,
6583                probes,
6584            }
6585        };
6586        let deployment_manifest = BlockchainDeploymentManifest {
6587            version: "test-v1".to_string(),
6588            chain_id: chains::ARBITRUM.chain_id,
6589            chain_name: chains::ARBITRUM.name.to_string(),
6590            contracts: vec![
6591                contract(ROUTER, BlockchainContractRole::Router),
6592                contract(
6593                    "0x1F98431c8aD98523631AE4a59f267346ea31F984",
6594                    BlockchainContractRole::Factory,
6595                ),
6596                contract(WETH, BlockchainContractRole::WrappedNative),
6597                contract(
6598                    "0x61fFE014bA17989E743c5F6cB21bF9697530B21e",
6599                    BlockchainContractRole::Quote,
6600                ),
6601                contract(USDC, BlockchainContractRole::Token),
6602                contract(
6603                    "0xC6962004f452bE9203591991D15f6b388e09E8D0",
6604                    BlockchainContractRole::Pool,
6605                ),
6606            ],
6607            tokens: vec![
6608                BlockchainTokenManifest {
6609                    address: WETH.to_string(),
6610                    name: "Wrapped Ether".to_string(),
6611                    symbol: "WETH".to_string(),
6612                    decimals: 18,
6613                    asset_role: "both".to_string(),
6614                },
6615                BlockchainTokenManifest {
6616                    address: USDC.to_string(),
6617                    name: "USD Coin".to_string(),
6618                    symbol: "USDC".to_string(),
6619                    decimals: 6,
6620                    asset_role: "both".to_string(),
6621                },
6622            ],
6623            pools: vec![BlockchainPoolManifest {
6624                address: "0xC6962004f452bE9203591991D15f6b388e09E8D0".to_string(),
6625                token0: WETH.to_string(),
6626                token1: USDC.to_string(),
6627                fee: 500,
6628                factory: "0x1F98431c8aD98523631AE4a59f267346ea31F984".to_string(),
6629                quote_contract: "0x61fFE014bA17989E743c5F6cB21bF9697530B21e".to_string(),
6630            }],
6631            call_edges: ["swap_sell", "swap_buy"]
6632                .into_iter()
6633                .map(|purpose| BlockchainCallEdgeManifest {
6634                    purpose: purpose.to_string(),
6635                    caller: ROUTER.to_string(),
6636                    target: "0xC6962004f452bE9203591991D15f6b388e09E8D0".to_string(),
6637                    call_type: "call".to_string(),
6638                })
6639                .collect(),
6640        };
6641        let manifest_digest =
6642            keccak256(serde_json::to_vec(&deployment_manifest).unwrap()).to_string();
6643        let verification = BlockchainVerificationConfig {
6644            authoritative: BlockchainProviderIdentity {
6645                provider_id: "authoritative".to_string(),
6646                operator_id: "operator-a".to_string(),
6647                failure_domain_ids: vec!["domain-a".to_string()],
6648            },
6649            verifiers: vec![
6650                BlockchainVerificationProviderConfig {
6651                    identity: BlockchainProviderIdentity {
6652                        provider_id: "verifier-a".to_string(),
6653                        operator_id: "operator-b".to_string(),
6654                        failure_domain_ids: vec!["domain-b".to_string()],
6655                    },
6656                    http_rpc_url: format!("{http_rpc_url}{verifier_separator}source=verifier-a")
6657                        .into(),
6658                },
6659                BlockchainVerificationProviderConfig {
6660                    identity: BlockchainProviderIdentity {
6661                        provider_id: "verifier-b".to_string(),
6662                        operator_id: "operator-c".to_string(),
6663                        failure_domain_ids: vec!["domain-c".to_string()],
6664                    },
6665                    http_rpc_url: format!("{http_rpc_url}{verifier_separator}source=verifier-b")
6666                        .into(),
6667                },
6668            ],
6669            chain_anchor: BlockchainChainAnchorConfig {
6670                chain_id: chains::ARBITRUM.chain_id,
6671                chain_name: chains::ARBITRUM.name.to_string(),
6672                checkpoint_height: 30_346_560,
6673                checkpoint_hash:
6674                    "0x1111111111111111111111111111111111111111111111111111111111111111".to_string(),
6675                checkpoint_timestamp: 1_761_888_800,
6676                max_head_skew_blocks: 3,
6677                max_head_age_secs: u64::MAX,
6678                max_future_drift_secs: u64::MAX,
6679            },
6680            manifest_version: "test-v1".to_string(),
6681            manifest_digest,
6682            deployment_manifest,
6683        };
6684        BlockchainExecutionClientConfig::builder()
6685            .client_id(AccountId::from("BLOCKCHAIN-001"))
6686            .chain(chains::ARBITRUM.clone())
6687            .wallet_address(WALLET.to_string())
6688            .http_rpc_url(http_rpc_url.into())
6689            .verification(verification)
6690            .signer_private_key_env(signer_env.to_string())
6691            .router_addresses(vec![ROUTER.to_string()])
6692            .weth_address(WETH.to_string())
6693            .max_fee_per_gas_wei(1_000_000_000)
6694            .base_fee_buffer_bps(2_000)
6695            .gas_limit(1_000_000)
6696            .gas_buffer_bps(2_000)
6697            .allowed_token_pairs(vec![(WETH.to_string(), USDC.to_string())])
6698            .slippage_bps(50)
6699            .max_slippage_bps(200)
6700            .max_order_amount(1_000_000_000_000_000_000)
6701            .deadline_seconds(300)
6702            .max_quote_age_blocks(100)
6703            .receipt_timeout_secs(1)
6704            .build()
6705    }
6706
6707    fn buy_test_config(http_rpc_url: String) -> BlockchainExecutionClientConfig {
6708        let max_amount = expected_buy_amount_in().to_string();
6709        let mut config = test_config(http_rpc_url);
6710        config.allowed_token_pairs = Some(vec![
6711            (WETH.to_string(), USDC.to_string()),
6712            (USDC.to_string(), WETH.to_string()),
6713        ]);
6714        config.quote_spend_limits = Some(vec![quote_spend_limit(USDC, WETH, 6, &max_amount)]);
6715        config
6716    }
6717
6718    fn refresh_test_manifest_digest(config: &mut BlockchainExecutionClientConfig) {
6719        let verification = config.verification.as_mut().unwrap();
6720        verification.manifest_digest =
6721            keccak256(serde_json::to_vec(&verification.deployment_manifest).unwrap()).to_string();
6722    }
6723
6724    fn quote_spend_limit(
6725        token_in: &str,
6726        token_out: &str,
6727        spend_token_decimals: u8,
6728        max_amount: &str,
6729    ) -> QuoteSpendLimit {
6730        QuoteSpendLimit::builder()
6731            .token_in(token_in.to_string())
6732            .token_out(token_out.to_string())
6733            .spend_token(token_in.to_string())
6734            .spend_token_decimals(spend_token_decimals)
6735            .max_amount(max_amount.to_string())
6736            .build()
6737    }
6738
6739    fn test_client_from_config(
6740        config: BlockchainExecutionClientConfig,
6741        pool: Pool,
6742    ) -> BlockchainExecutionClient {
6743        test_client_result(config, pool).unwrap()
6744    }
6745
6746    fn test_client_result(
6747        config: BlockchainExecutionClientConfig,
6748        pool: Pool,
6749    ) -> anyhow::Result<BlockchainExecutionClient> {
6750        test_client_and_cache(config, pool).map(|(client, _)| client)
6751    }
6752
6753    fn test_client_and_cache(
6754        config: BlockchainExecutionClientConfig,
6755        pool: Pool,
6756    ) -> anyhow::Result<(BlockchainExecutionClient, Rc<RefCell<Cache>>)> {
6757        let cache = Rc::new(RefCell::new(Cache::default()));
6758        cache.borrow_mut().add_pool(pool).unwrap();
6759        let core = ExecutionClientCore::new(
6760            TraderId::from("TRADER-001"),
6761            ClientId::from("BLOCKCHAIN-001"),
6762            *BLOCKCHAIN_VENUE,
6763            OmsType::Netting,
6764            AccountId::from("BLOCKCHAIN-001"),
6765            AccountType::Wallet,
6766            None,
6767            Rc::clone(&cache),
6768        );
6769
6770        let client = BlockchainExecutionClient::new(core, config)?;
6771        Ok((client, cache))
6772    }
6773
6774    fn test_client(http_rpc_url: String) -> BlockchainExecutionClient {
6775        test_client_from_config(test_config(http_rpc_url), test_pool())
6776    }
6777
6778    async fn client_with_mock_rpc(
6779        state: MockRpcState,
6780    ) -> (BlockchainExecutionClient, MockRpcState) {
6781        let addr = start_mock_rpc_server(state.clone()).await;
6782        (test_client(format!("http://{addr}")), state)
6783    }
6784
6785    async fn client_with_token_mock_rpc(
6786        state: MockRpcState,
6787        signer_env: &str,
6788    ) -> (BlockchainExecutionClient, MockRpcState, Rc<RefCell<Cache>>) {
6789        let state = with_connect_capabilities(state);
6790        let addr = start_mock_rpc_server(state.clone()).await;
6791        let pool = test_pool();
6792        let tokens = [pool.token0.clone(), pool.token1.clone()];
6793        let mut config = test_config_with_signer_env(format!("http://{addr}"), signer_env);
6794        config.tokens = Some(vec![WETH.to_string(), USDC.to_string()]);
6795        let (mut client, cache) = test_client_and_cache(config, pool).unwrap();
6796        for token in tokens {
6797            client.cache.add_token(token).await.unwrap();
6798        }
6799        (client, state, cache)
6800    }
6801
6802    fn execution_rpc_state() -> MockRpcState {
6803        MockRpcState::default()
6804            .with_receipt_hash_from_request()
6805            .with_response("eth_chainId", CHAIN_ID_ARBITRUM)
6806            .with_response("eth_getCode", GET_CODE_DEPLOYED)
6807            .with_response("eth_getBalance", GET_BALANCE)
6808            .with_response("eth_getBlockByNumber", BLOCK_BY_NUMBER)
6809            .with_response("eth_getTransactionCount", TRANSACTION_COUNT)
6810            .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", BLOCK_CANONICAL)
6811            .with_parameter_response("eth_getBlockByNumber", "finalized", BLOCK_FINALIZED)
6812            .with_parameter_response("eth_getBlockByNumber", "0x1cf0d42", BLOCK_FINALIZED)
6813            .with_response("eth_maxPriorityFeePerGas", MAX_PRIORITY_FEE)
6814            .with_call_response(FACTORY_SELECTOR, CALL_FACTORY)
6815            .with_call_response(WETH9_SELECTOR, CALL_WETH)
6816            .with_call_response(GET_POOL_SELECTOR, CALL_POOL)
6817            .with_call_response(POOL_TOKEN0_SELECTOR, CALL_WETH)
6818            .with_call_response(POOL_TOKEN1_SELECTOR, CALL_USDC)
6819            .with_call_response(POOL_FEE_SELECTOR, CALL_FEE_500)
6820            .with_contract_call_response(WETH, DECIMALS_SELECTOR, CALL_DECIMALS_18)
6821            .with_contract_call_response(USDC, DECIMALS_SELECTOR, CALL_DECIMALS_6)
6822            .with_call_response(
6823                QUOTE_EXACT_INPUT_SELECTOR,
6824                &quote_response(expected_sell_quote_amount()),
6825            )
6826            .with_call_response(
6827                QUOTE_EXACT_OUTPUT_SELECTOR,
6828                &quote_response(expected_buy_amount_in()),
6829            )
6830    }
6831
6832    fn ready_rpc_state() -> MockRpcState {
6833        with_connect_capabilities(execution_rpc_state())
6834            .with_call_response(BALANCE_OF_SELECTOR, CALL_BALANCE)
6835            .with_call_response(ALLOWANCE_SELECTOR, CALL_ALLOWANCE)
6836    }
6837
6838    fn with_connect_capabilities(state: MockRpcState) -> MockRpcState {
6839        state
6840            .with_response("eth_getStorageAt", STORAGE_ZERO)
6841            .with_response("eth_estimateGas", ESTIMATE_GAS)
6842            .with_parameter_response(
6843                "debug_traceTransaction",
6844                &B256::ZERO.to_string(),
6845                TRACE_UNKNOWN_TRANSACTION,
6846            )
6847    }
6848
6849    fn signing_rpc_state() -> MockRpcState {
6850        ready_rpc_state()
6851            .with_response("eth_getTransactionCount", TRANSACTION_COUNT)
6852            .with_response("eth_estimateGas", ESTIMATE_GAS)
6853    }
6854
6855    fn broadcast_rpc_state() -> MockRpcState {
6856        execution_rpc_state()
6857            .with_response("eth_getTransactionCount", TRANSACTION_COUNT)
6858            .with_response("eth_estimateGas", ESTIMATE_GAS)
6859            .with_response("eth_getTransactionReceipt", RECEIPT_SUCCESS)
6860            .with_send_raw_transaction_echo()
6861            .with_call_response_sequence(ALLOWANCE_SELECTOR, &[CALL_ZERO, CALL_ALLOWANCE_1000])
6862    }
6863
6864    async fn expected_wrap_tx_hash(value: U256) -> B256 {
6865        expected_tx_hash(
6866            WETH_ADDRESS,
6867            value,
6868            Bytes::from(nautilus_core::hex::decode("d0e30db0").unwrap()),
6869        )
6870        .await
6871    }
6872
6873    async fn expected_approve_tx_hash(amount: U256) -> B256 {
6874        let calldata = ERC20::approveCall {
6875            spender: ROUTER_ADDRESS,
6876            amount,
6877        }
6878        .abi_encode();
6879        expected_tx_hash(WETH_ADDRESS, U256::ZERO, Bytes::from(calldata)).await
6880    }
6881
6882    async fn expected_tx_hash(to: Address, value: U256, input: Bytes) -> B256 {
6883        // The orchestration's policy math is deterministic: nonce 7 from the fixture,
6884        // buffered gas 78000, buffered max fee 130000000, priority fee 10000000
6885        let expected_tx =
6886            build_eip1559_transaction(42161, 7, 78_000, 130_000_000, 10_000_000, to, value, input);
6887        let (expected_hash, _) = sign_eip1559_transaction(
6888            expected_tx,
6889            &PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
6890        )
6891        .await
6892        .unwrap();
6893        expected_hash
6894    }
6895
6896    async fn await_recorded_requests(state: &MockRpcState, method: &str, expected: usize) {
6897        wait_until_async(
6898            || async {
6899                state
6900                    .recorded_requests()
6901                    .iter()
6902                    .filter(|request| request["method"] == method)
6903                    .count()
6904                    >= expected
6905            },
6906            TEST_TIMEOUT,
6907        )
6908        .await;
6909    }
6910
6911    /// The block number served by the `eth_getBlockByNumber` fixture; swap quotes pin their
6912    /// profiler state to it so the freshness check passes.
6913    const FIXTURE_BLOCK: u64 = 30_346_560;
6914    const FIXTURE_BLOCK_PARAM: &str = "0x1cf0d40";
6915    const FIXTURE_BLOCK_HASH: &str =
6916        "0x1111111111111111111111111111111111111111111111111111111111111111";
6917    /// The timestamp served by the `eth_getBlockByNumber` fixture.
6918    const FIXTURE_BLOCK_TIMESTAMP: u64 = 1_761_888_800;
6919    /// Synthetic full-range liquidity for the test pool profiler.
6920    const TEST_LIQUIDITY: u128 = 1_000_000_000_000_000_000_000;
6921
6922    fn test_profiler(pool: &Pool, block_number: u64) -> PoolProfiler {
6923        test_profiler_at_block(pool, block_number, FIXTURE_BLOCK_HASH)
6924    }
6925
6926    fn test_profiler_at_block(pool: &Pool, block_number: u64, block_hash: &str) -> PoolProfiler {
6927        test_profiler_with_range(
6928            pool,
6929            block_number,
6930            block_hash,
6931            U160::from(1u128 << 96),
6932            -887_220,
6933            887_220,
6934            TEST_LIQUIDITY,
6935        )
6936    }
6937
6938    fn test_profiler_with_state(
6939        pool: &Pool,
6940        block_number: u64,
6941        sqrt_price_x96: U160,
6942        liquidity: u128,
6943    ) -> PoolProfiler {
6944        test_profiler_with_range(
6945            pool,
6946            block_number,
6947            FIXTURE_BLOCK_HASH,
6948            sqrt_price_x96,
6949            -887_220,
6950            887_220,
6951            liquidity,
6952        )
6953    }
6954
6955    /// Builds a profiler from a synthetic snapshot with a single position over
6956    /// `[tick_lower, tick_upper]`: exact-input quotes within the range match the
6957    /// constant-product curve, and a tiny test swap never crosses a tick.
6958    fn test_profiler_with_range(
6959        pool: &Pool,
6960        block_number: u64,
6961        block_hash: &str,
6962        sqrt_price_x96: U160,
6963        tick_lower: i32,
6964        tick_upper: i32,
6965        liquidity: u128,
6966    ) -> PoolProfiler {
6967        let snapshot = PoolSnapshot::new(
6968            pool.instrument_id,
6969            PoolState {
6970                current_tick: get_tick_at_sqrt_ratio(sqrt_price_x96),
6971                price_sqrt_ratio_x96: sqrt_price_x96,
6972                liquidity,
6973                protocol_fees_token0: U256::ZERO,
6974                protocol_fees_token1: U256::ZERO,
6975                fee_protocol: 0,
6976                fee_protocol0_basis_points: None,
6977                fee_protocol1_basis_points: None,
6978                fee_growth_global_0: U256::ZERO,
6979                fee_growth_global_1: U256::ZERO,
6980            },
6981            vec![PoolPosition::new(
6982                address!("DeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF"),
6983                tick_lower,
6984                tick_upper,
6985                liquidity as i128,
6986            )],
6987            vec![
6988                PoolTick::new(
6989                    tick_lower,
6990                    liquidity,
6991                    liquidity as i128,
6992                    U256::ZERO,
6993                    U256::ZERO,
6994                    true,
6995                    0,
6996                ),
6997                PoolTick::new(
6998                    tick_upper,
6999                    liquidity,
7000                    -(liquidity as i128),
7001                    U256::ZERO,
7002                    U256::ZERO,
7003                    true,
7004                    0,
7005                ),
7006            ],
7007            PoolAnalytics::default(),
7008            BlockPosition::new(
7009                block_number,
7010                block_hash.to_string(),
7011                BLOCK_SCOPED_SNAPSHOT_INDEX,
7012                BLOCK_SCOPED_SNAPSHOT_INDEX,
7013            )
7014            .with_block_hash(Some(block_hash.to_string())),
7015            UnixNanos::default(),
7016            UnixNanos::default(),
7017        );
7018        let mut profiler = PoolProfiler::new(Arc::new(pool.clone()));
7019        profiler.restore_from_snapshot(snapshot).unwrap();
7020        profiler
7021    }
7022
7023    fn test_market_sell_order(instrument_id: InstrumentId) -> OrderAny {
7024        market_sell_order_with_id(instrument_id, "O-SWAP-001")
7025    }
7026
7027    fn test_market_buy_order(instrument_id: InstrumentId) -> OrderAny {
7028        market_buy_order_with_id(instrument_id, "O-SWAP-BUY-001")
7029    }
7030
7031    fn submit_order_cmd(order: &OrderAny) -> SubmitOrder {
7032        SubmitOrder::new(
7033            TraderId::from("TRADER-001"),
7034            Some(ClientId::from("BLOCKCHAIN-001")),
7035            order.strategy_id(),
7036            order.instrument_id(),
7037            order.client_order_id(),
7038            order.init_event().clone(),
7039            None,
7040            None,
7041            None,
7042            UUID4::new(),
7043            UnixNanos::default(),
7044            None,
7045        )
7046    }
7047
7048    /// Builds a client whose shared cache holds the test pool, a live profiler at
7049    /// `FIXTURE_BLOCK`, and a SELL market order, without a durable store.
7050    fn swap_client_with_cache(
7051        config: BlockchainExecutionClientConfig,
7052    ) -> (BlockchainExecutionClient, Rc<RefCell<Cache>>) {
7053        let cache = Rc::new(RefCell::new(Cache::default()));
7054        let pool = test_pool();
7055        cache.borrow_mut().add_pool(pool.clone()).unwrap();
7056        cache
7057            .borrow_mut()
7058            .add_order(
7059                test_market_sell_order(pool.instrument_id),
7060                None,
7061                None,
7062                false,
7063            )
7064            .unwrap();
7065        cache
7066            .borrow_mut()
7067            .add_pool_profiler(test_profiler_at_block(
7068                &pool,
7069                FIXTURE_BLOCK,
7070                FIXTURE_BLOCK_HASH,
7071            ))
7072            .unwrap();
7073        let core = ExecutionClientCore::new(
7074            TraderId::from("TRADER-001"),
7075            ClientId::from("BLOCKCHAIN-001"),
7076            *BLOCKCHAIN_VENUE,
7077            OmsType::Netting,
7078            AccountId::from("BLOCKCHAIN-001"),
7079            AccountType::Wallet,
7080            None,
7081            Rc::clone(&cache),
7082        );
7083
7084        let client = BlockchainExecutionClient::new(core, config).unwrap();
7085        (client, cache)
7086    }
7087
7088    /// Builds a connected, signer-equipped swap client backed by a fresh Postgres schema.
7089    async fn swap_client_with_database(
7090        test_name: &str,
7091        state: MockRpcState,
7092    ) -> Option<(
7093        sqlx::PgPool,
7094        String,
7095        BlockchainExecutionClient,
7096        MockRpcState,
7097        Rc<RefCell<Cache>>,
7098    )> {
7099        swap_client_with_database_config(test_name, state, test_config).await
7100    }
7101
7102    async fn swap_client_with_buy_database(
7103        test_name: &str,
7104        state: MockRpcState,
7105    ) -> Option<(
7106        sqlx::PgPool,
7107        String,
7108        BlockchainExecutionClient,
7109        MockRpcState,
7110        Rc<RefCell<Cache>>,
7111    )> {
7112        swap_client_with_database_config(test_name, state, buy_test_config).await
7113    }
7114
7115    async fn swap_client_with_database_config<F>(
7116        test_name: &str,
7117        state: MockRpcState,
7118        config: F,
7119    ) -> Option<(
7120        sqlx::PgPool,
7121        String,
7122        BlockchainExecutionClient,
7123        MockRpcState,
7124        Rc<RefCell<Cache>>,
7125    )>
7126    where
7127        F: FnOnce(String) -> BlockchainExecutionClientConfig,
7128    {
7129        let (admin_pool, pg_config) = connect_test_postgres(test_name).await?;
7130        let schema = format!("{test_name}_{}", std::process::id());
7131        setup_execution_schema(&admin_pool, &schema).await;
7132
7133        let db_options: sqlx::postgres::PgConnectOptions = pg_config.into();
7134        let db_options = db_options.options([("search_path", schema.clone())]);
7135        let database = connect_test_database(db_options).await.unwrap();
7136        let addr = start_mock_rpc_server(state.clone()).await;
7137        let (mut client, cache) = swap_client_with_cache(config(format!("http://{addr}")));
7138        client.cache.database = Some(database);
7139        // Mirror the connect-time migration: tests create the pre-submission table shape
7140        client
7141            .cache
7142            .ensure_execution_transaction_schema()
7143            .await
7144            .unwrap();
7145        protect_test_storage(&mut client, &schema).await;
7146        initialize_test_verification_ledger(&client).await;
7147        client.signer = Some(Arc::new(
7148            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
7149        ));
7150        client.core.set_connected();
7151
7152        Some((admin_pool, schema, client, state, cache))
7153    }
7154
7155    async fn initialize_test_verification_ledger(client: &BlockchainExecutionClient) {
7156        initialize_test_verification_ledger_with_headers(
7157            client,
7158            &[ExecutionVerifiedHeader {
7159                number: FIXTURE_BLOCK,
7160                hash: FIXTURE_BLOCK_HASH.to_string(),
7161                parent_hash: "0x0000000000000000000000000000000000000000000000000000000000000001"
7162                    .to_string(),
7163                timestamp: FIXTURE_BLOCK_TIMESTAMP,
7164                base_fee_per_gas: Some(100_000_000),
7165            }],
7166        )
7167        .await;
7168    }
7169
7170    async fn initialize_test_verification_ledger_with_headers(
7171        client: &BlockchainExecutionClient,
7172        finalized_headers: &[ExecutionVerifiedHeader],
7173    ) {
7174        ensure_test_verification_ledger(client, finalized_headers, 7, 7)
7175            .await
7176            .unwrap();
7177    }
7178
7179    async fn ensure_test_verification_ledger(
7180        client: &BlockchainExecutionClient,
7181        finalized_headers: &[ExecutionVerifiedHeader],
7182        next_canonical_nonce: u64,
7183        observed_canonical_nonce: u64,
7184    ) -> anyhow::Result<()> {
7185        let verification = client.config.verification.as_ref().unwrap();
7186        let provider_ids = vec![
7187            "authoritative".to_string(),
7188            "verifier-a".to_string(),
7189            "verifier-b".to_string(),
7190        ];
7191        let operator_ids = vec![
7192            "operator-a".to_string(),
7193            "operator-b".to_string(),
7194            "operator-c".to_string(),
7195        ];
7196        let failure_domain_ids = vec![
7197            "domain-a".to_string(),
7198            "domain-b".to_string(),
7199            "domain-c".to_string(),
7200        ];
7201        let decisions = [ExecutionVerificationDecision {
7202            read_class: "numbered_block",
7203            height_start: Some(FIXTURE_BLOCK),
7204            height_end: Some(FIXTURE_BLOCK),
7205            normalized_value_digest: B256::ZERO.to_string(),
7206        }];
7207        client
7208            .cache
7209            .database
7210            .as_ref()
7211            .unwrap()
7212            .ensure_execution_verification_schema(&ExecutionVerificationBootstrap {
7213                chain_id: 42_161,
7214                wallet_address: WALLET,
7215                manifest_version: &verification.manifest_version,
7216                manifest_digest: &verification.manifest_digest,
7217                checkpoint_number: FIXTURE_BLOCK,
7218                checkpoint_hash: FIXTURE_BLOCK_HASH,
7219                checkpoint_parent_hash:
7220                    "0x0000000000000000000000000000000000000000000000000000000000000001",
7221                checkpoint_timestamp: FIXTURE_BLOCK_TIMESTAMP,
7222                checkpoint_base_fee_per_gas: Some(100_000_000),
7223                finalized_headers,
7224                next_canonical_nonce,
7225                observed_canonical_nonce,
7226                provider_ids: &provider_ids,
7227                operator_ids: &operator_ids,
7228                failure_domain_ids: &failure_domain_ids,
7229                decisions: &decisions,
7230                migration: None,
7231            })
7232            .await
7233    }
7234
7235    async fn initialize_test_verification_migration(
7236        client: &BlockchainExecutionClient,
7237        finalized_headers: &[ExecutionVerifiedHeader],
7238        next_canonical_nonce: u64,
7239        decisions: &[ExecutionVerificationDecision],
7240        migration: &ExecutionVerificationMigration,
7241    ) {
7242        let verification = client.config.verification.as_ref().unwrap();
7243        let provider_ids = vec![
7244            "authoritative".to_string(),
7245            "verifier-a".to_string(),
7246            "verifier-b".to_string(),
7247        ];
7248        let operator_ids = vec![
7249            "operator-a".to_string(),
7250            "operator-b".to_string(),
7251            "operator-c".to_string(),
7252        ];
7253        let failure_domain_ids = vec![
7254            "domain-a".to_string(),
7255            "domain-b".to_string(),
7256            "domain-c".to_string(),
7257        ];
7258        client
7259            .cache
7260            .database
7261            .as_ref()
7262            .unwrap()
7263            .ensure_execution_verification_schema(&ExecutionVerificationBootstrap {
7264                chain_id: 42_161,
7265                wallet_address: WALLET,
7266                manifest_version: &verification.manifest_version,
7267                manifest_digest: &verification.manifest_digest,
7268                checkpoint_number: FIXTURE_BLOCK,
7269                checkpoint_hash: FIXTURE_BLOCK_HASH,
7270                checkpoint_parent_hash:
7271                    "0x0000000000000000000000000000000000000000000000000000000000000001",
7272                checkpoint_timestamp: FIXTURE_BLOCK_TIMESTAMP,
7273                checkpoint_base_fee_per_gas: Some(100_000_000),
7274                finalized_headers,
7275                next_canonical_nonce,
7276                observed_canonical_nonce: next_canonical_nonce,
7277                provider_ids: &provider_ids,
7278                operator_ids: &operator_ids,
7279                failure_domain_ids: &failure_domain_ids,
7280                decisions,
7281                migration: Some(migration),
7282            })
7283            .await
7284            .unwrap();
7285    }
7286
7287    async fn swap_rpc_state() -> MockRpcState {
7288        let min_amount_out = expected_min_amount_out(50);
7289        swap_rpc_state_with_min_amount_out(min_amount_out).await
7290    }
7291
7292    async fn swap_rpc_state_with_min_amount_out(min_amount_out: U256) -> MockRpcState {
7293        let (tx_hash, _) = expected_swap_tx(min_amount_out).await;
7294        finalized_swap_rpc_state(tx_hash, min_amount_out)
7295    }
7296
7297    async fn swap_rpc_state_for_mismatch() -> MockRpcState {
7298        swap_rpc_state()
7299            .await
7300            .with_response("eth_sendRawTransaction", SEND_RAW_TRANSACTION)
7301    }
7302
7303    fn awaiting_in_flight(client: &BlockchainExecutionClient) -> InFlightTransaction {
7304        let slot = *client.in_flight.lock();
7305        let Some(InFlightSlot::AwaitingFinality(in_flight)) = slot else {
7306            panic!("expected an awaiting-finality transaction, was {slot:?}");
7307        };
7308        in_flight
7309    }
7310
7311    fn recovering_in_flight(client: &BlockchainExecutionClient) -> RecoveryTransaction {
7312        let slot = *client.in_flight.lock();
7313        let Some(InFlightSlot::Recovering(recovery)) = slot else {
7314            panic!("expected a recovery transaction, was {slot:?}");
7315        };
7316        recovery
7317    }
7318
7319    async fn execution_intent_markers(
7320        admin_pool: &sqlx::PgPool,
7321        schema: &str,
7322    ) -> Vec<(String, String, bool, bool)> {
7323        sqlx::query_as(sqlx::AssertSqlSafe(format!(
7324            "SELECT purpose, status, terminal_emitted, active \
7325             FROM {schema}.execution_intent ORDER BY id"
7326        )))
7327        .fetch_all(admin_pool)
7328        .await
7329        .unwrap()
7330    }
7331
7332    #[allow(unsafe_code)] // env-var mutation in tests; unique names avoid cross-test races
7333    fn payload_test_keys(
7334        active: [u8; 32],
7335        retired: Vec<[u8; 32]>,
7336        deployment_id: &str,
7337    ) -> PayloadKeySet {
7338        static NEXT_ENV_ID: AtomicU64 = AtomicU64::new(0);
7339
7340        let env_id = NEXT_ENV_ID.fetch_add(1, Ordering::Relaxed);
7341        let active_env = format!("BLOCKCHAIN_TEST_PAYLOAD_KEY_{env_id}_ACTIVE");
7342        let retired_envs = retired
7343            .iter()
7344            .enumerate()
7345            .map(|(index, _)| format!("BLOCKCHAIN_TEST_PAYLOAD_KEY_{env_id}_RETIRED_{index}"))
7346            .collect::<Vec<_>>();
7347        // SAFETY: each invocation uses unique variable names and removes them before returning
7348        unsafe { std::env::set_var(&active_env, hex::encode(active)) };
7349        for (env, key) in retired_envs.iter().zip(&retired) {
7350            // SAFETY: the retired variable name is unique to this invocation
7351            unsafe { std::env::set_var(env, hex::encode(key)) };
7352        }
7353
7354        let keys = PayloadKeySet::load(Some(&active_env), &retired_envs, Some(deployment_id))
7355            .unwrap()
7356            .unwrap();
7357
7358        // SAFETY: no other test or thread uses these unique variable names
7359        unsafe { std::env::remove_var(active_env) };
7360        for env in retired_envs {
7361            // SAFETY: the retired variable name is unique to this invocation
7362            unsafe { std::env::remove_var(env) };
7363        }
7364        keys
7365    }
7366
7367    #[allow(unsafe_code)] // env-var mutation in tests; unique names avoid cross-test races
7368    fn set_test_payload_key(
7369        client: &mut BlockchainExecutionClient,
7370        key: [u8; 32],
7371        deployment_id: &str,
7372    ) -> PayloadKeySet {
7373        static NEXT_ENV_ID: AtomicU64 = AtomicU64::new(0);
7374
7375        let env_id = NEXT_ENV_ID.fetch_add(1, Ordering::Relaxed);
7376        let active_env = format!("BLOCKCHAIN_TEST_EXECUTION_KEY_{env_id}");
7377        // SAFETY: each invocation uses a unique variable name retained for reconnect tests
7378        unsafe { std::env::set_var(&active_env, hex::encode(key)) };
7379        client.config.payload_key_env = Some(active_env);
7380        client.config.payload_deployment_id = Some(deployment_id.to_string());
7381        client.load_payload_keys().unwrap().unwrap()
7382    }
7383
7384    async fn protect_test_storage(client: &mut BlockchainExecutionClient, deployment_id: &str) {
7385        let keys = set_test_payload_key(client, [0xa5; 32], deployment_id);
7386        client
7387            .cache
7388            .database
7389            .as_ref()
7390            .unwrap()
7391            .ensure_execution_payload_storage(&keys)
7392            .await
7393            .unwrap();
7394        client.payload_keys = Some(Arc::new(keys));
7395    }
7396
7397    async fn reserve_test_wrap_intent(database: &BlockchainCacheDatabase) -> ExecutionIntentRow {
7398        reserve_test_wrap_intent_for_wallet(database, WALLET).await
7399    }
7400
7401    async fn reserve_test_wrap_intent_for_wallet(
7402        database: &BlockchainCacheDatabase,
7403        wallet: &str,
7404    ) -> ExecutionIntentRow {
7405        database
7406            .reserve_execution_intent(&ExecutionIntentInsert {
7407                chain_id: 42161,
7408                wallet_address: wallet.to_string(),
7409                purpose: "wrap".to_string(),
7410                client_order_id: None,
7411                trader_id: None,
7412                strategy_id: None,
7413                account_id: None,
7414                instrument_id: None,
7415                pool_address: None,
7416                transaction_to: WETH_ADDRESS.to_string(),
7417                transaction_input: "0xd0e30db0".to_string(),
7418                transaction_value: "1".to_string(),
7419                amount_in: None,
7420                created_block: FIXTURE_BLOCK,
7421            })
7422            .await
7423            .unwrap()
7424    }
7425
7426    async fn persist_test_wrap_broadcast(
7427        database: &BlockchainCacheDatabase,
7428        keys: Option<&PayloadKeySet>,
7429    ) -> (ExecutionIntentRow, B256, Vec<u8>) {
7430        let intent = reserve_test_wrap_intent(database).await;
7431        database
7432            .assign_execution_intent_nonce(intent.id, 7)
7433            .await
7434            .unwrap();
7435        let intent = database.get_execution_intent(intent.id).await.unwrap();
7436        let transaction = build_eip1559_transaction(
7437            42161,
7438            7,
7439            78_000,
7440            130_000_000,
7441            10_000_000,
7442            WETH_ADDRESS,
7443            U256::from(1u64),
7444            Bytes::from(nautilus_core::hex::decode("d0e30db0").unwrap()),
7445        );
7446        let (tx_hash, raw_tx) = sign_eip1559_transaction(
7447            transaction,
7448            &PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
7449        )
7450        .await
7451        .unwrap();
7452        persist_test_payload(database, keys, &intent, tx_hash, &raw_tx).await;
7453        database
7454            .record_execution_status(
7455                intent.id,
7456                &tx_hash.to_string(),
7457                TransactionStatus::Broadcast,
7458                None,
7459                None,
7460                None,
7461                None,
7462                None,
7463            )
7464            .await
7465            .unwrap();
7466        (intent, tx_hash, raw_tx)
7467    }
7468
7469    async fn reserve_test_swap_intent(database: &BlockchainCacheDatabase) -> ExecutionIntentRow {
7470        let pool = test_pool();
7471        let order = test_market_sell_order(pool.instrument_id);
7472        let calldata = expected_swap_calldata(expected_min_amount_out(50));
7473
7474        database
7475            .reserve_execution_intent(&ExecutionIntentInsert {
7476                chain_id: 42161,
7477                wallet_address: WALLET.to_string(),
7478                purpose: "swap".to_string(),
7479                client_order_id: Some(order.client_order_id().to_string()),
7480                trader_id: Some(order.trader_id().to_string()),
7481                strategy_id: Some(order.strategy_id().to_string()),
7482                account_id: Some("BLOCKCHAIN-001".to_string()),
7483                instrument_id: Some(pool.instrument_id.to_string()),
7484                pool_address: Some(pool.address.to_string()),
7485                transaction_to: ROUTER_ADDRESS.to_string(),
7486                transaction_input: hex::encode_prefixed(&calldata),
7487                transaction_value: U256::ZERO.to_string(),
7488                amount_in: Some("1000000000000000".to_string()),
7489                created_block: FIXTURE_BLOCK,
7490            })
7491            .await
7492            .unwrap()
7493    }
7494
7495    async fn persist_invalid_test_swap(
7496        database: &BlockchainCacheDatabase,
7497        keys: Option<&PayloadKeySet>,
7498    ) -> (ExecutionIntentRow, B256) {
7499        let intent = reserve_test_swap_intent(database).await;
7500        database
7501            .assign_execution_intent_nonce(intent.id, 7)
7502            .await
7503            .unwrap();
7504        let intent = database.get_execution_intent(intent.id).await.unwrap();
7505        let (tx_hash, raw_transaction) = expected_swap_tx(expected_min_amount_out(50)).await;
7506        let mut raw_transaction = hex::decode(raw_transaction.strip_prefix("0x").unwrap()).unwrap();
7507        raw_transaction.push(0xff);
7508        persist_test_payload(database, keys, &intent, tx_hash, &raw_transaction).await;
7509        database
7510            .record_execution_status(
7511                intent.id,
7512                &tx_hash.to_string(),
7513                TransactionStatus::Broadcast,
7514                None,
7515                None,
7516                None,
7517                None,
7518                None,
7519            )
7520            .await
7521            .unwrap();
7522
7523        (intent, tx_hash)
7524    }
7525
7526    async fn persist_test_swap_broadcast(
7527        database: &BlockchainCacheDatabase,
7528        keys: Option<&PayloadKeySet>,
7529    ) -> (ExecutionIntentRow, B256, Vec<u8>) {
7530        let intent = reserve_test_swap_intent(database).await;
7531        database
7532            .assign_execution_intent_nonce(intent.id, 7)
7533            .await
7534            .unwrap();
7535        let intent = database.get_execution_intent(intent.id).await.unwrap();
7536        let (tx_hash, raw_transaction) = expected_swap_tx(expected_min_amount_out(50)).await;
7537        let raw_transaction = hex::decode(raw_transaction.strip_prefix("0x").unwrap()).unwrap();
7538        persist_test_payload(database, keys, &intent, tx_hash, &raw_transaction).await;
7539        database
7540            .record_execution_status(
7541                intent.id,
7542                &tx_hash.to_string(),
7543                TransactionStatus::Broadcast,
7544                None,
7545                None,
7546                None,
7547                None,
7548                None,
7549            )
7550            .await
7551            .unwrap();
7552        (intent, tx_hash, raw_transaction)
7553    }
7554
7555    async fn persist_test_payload(
7556        database: &BlockchainCacheDatabase,
7557        keys: Option<&PayloadKeySet>,
7558        intent: &ExecutionIntentRow,
7559        tx_hash: B256,
7560        raw_transaction: &[u8],
7561    ) {
7562        if let Some(keys) = keys {
7563            database.reserve_execution_payload_seal(keys).await.unwrap();
7564            let transaction_hash = tx_hash.to_string();
7565            let context =
7566                payload_context_identity(intent, &transaction_hash, 42_161, keys.deployment_id())
7567                    .unwrap();
7568            let envelope = keys.seal(raw_transaction, &context).unwrap();
7569            database
7570                .add_execution_transaction_envelope(intent.id, 42_161, &transaction_hash, &envelope)
7571                .await
7572                .unwrap();
7573        } else {
7574            database
7575                .add_execution_transaction_hash(
7576                    intent.id,
7577                    42_161,
7578                    &tx_hash.to_string(),
7579                    raw_transaction,
7580                )
7581                .await
7582                .unwrap();
7583        }
7584    }
7585
7586    async fn later_reconnect(
7587        previous: BlockchainExecutionClient,
7588        http_rpc_url: String,
7589    ) -> anyhow::Error {
7590        let database = previous.cache.database.as_ref().unwrap().clone();
7591        let payload_keys = previous.payload_keys.clone();
7592        drop(previous);
7593        let mut next = test_client(http_rpc_url);
7594        next.cache.database = Some(database);
7595        next.payload_keys = payload_keys;
7596        next.signer = Some(Arc::new(
7597            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
7598        ));
7599        next.reconcile_unresolved_execution().await.unwrap_err()
7600    }
7601
7602    fn start_with_events(
7603        client: &mut BlockchainExecutionClient,
7604    ) -> tokio::sync::mpsc::UnboundedReceiver<ExecutionEvent> {
7605        let (sender, receiver) = tokio::sync::mpsc::unbounded_channel();
7606        replace_exec_event_sender(sender);
7607        client.start().unwrap();
7608        receiver
7609    }
7610
7611    async fn await_pending_tasks(client: &BlockchainExecutionClient) {
7612        tokio::time::timeout(TEST_TIMEOUT, async {
7613            while !client.pending_tasks.all_finished() {
7614                tokio::time::sleep(Duration::from_millis(1)).await;
7615            }
7616        })
7617        .await
7618        .unwrap();
7619    }
7620
7621    fn collect_order_events(
7622        receiver: &mut tokio::sync::mpsc::UnboundedReceiver<ExecutionEvent>,
7623    ) -> Vec<OrderEventAny> {
7624        let mut events = Vec::new();
7625
7626        while let Ok(event) = receiver.try_recv() {
7627            if let ExecutionEvent::Order(order_event) = event {
7628                events.push(order_event);
7629            }
7630        }
7631        events
7632    }
7633
7634    fn assert_swap_quarantined_without_terminal_event(events: &[OrderEventAny]) {
7635        assert_eq!(events.len(), 1, "was: {events:?}");
7636        assert!(matches!(&events[0], OrderEventAny::Submitted(_)));
7637    }
7638
7639    fn assert_swap_submitted_and_filled(events: &[OrderEventAny]) {
7640        assert_eq!(events.len(), 2, "was: {events:?}");
7641        assert!(matches!(&events[0], OrderEventAny::Submitted(_)));
7642        assert!(matches!(&events[1], OrderEventAny::Filled(_)));
7643    }
7644
7645    fn expected_swap_calldata(min_amount_out: U256) -> Vec<u8> {
7646        UniswapV3SwapRouter::exactInputSingleCall {
7647            params: UniswapV3SwapRouter::ExactInputSingleParams {
7648                tokenIn: WETH_ADDRESS,
7649                tokenOut: address!("af88d065e77c8cC2239327C5EDb3A432268e5831"),
7650                fee: U24::try_from(500u32).unwrap(),
7651                recipient: address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266"),
7652                deadline: U256::from(FIXTURE_BLOCK_TIMESTAMP + 300),
7653                amountIn: U256::from(1_000_000_000_000_000u64),
7654                amountOutMinimum: min_amount_out,
7655                sqrtPriceLimitX96: U160::ZERO,
7656            },
7657        }
7658        .abi_encode()
7659    }
7660
7661    fn expected_min_amount_out(slippage_bps: u32) -> U256 {
7662        expected_min_amount_out_for(&test_pool(), true, slippage_bps)
7663    }
7664
7665    fn expected_min_amount_out_for(pool: &Pool, zero_for_one: bool, slippage_bps: u32) -> U256 {
7666        let profiler = test_profiler(pool, FIXTURE_BLOCK);
7667        let quote = profiler
7668            .swap_exact_in(U256::from(1_000_000_000_000_000u64), zero_for_one, None)
7669            .unwrap();
7670        let quoted = exact_output_amount(&quote, zero_for_one).unwrap();
7671        derive_min_amount_out(quoted, slippage_bps).unwrap()
7672    }
7673
7674    fn expected_sell_quote_amount() -> U256 {
7675        let profiler = test_profiler(&test_pool(), FIXTURE_BLOCK);
7676        let quote = profiler
7677            .swap_exact_in(U256::from(1_000_000_000_000_000u64), true, None)
7678            .unwrap();
7679        exact_output_amount(&quote, true).unwrap()
7680    }
7681
7682    fn quote_response(amount: U256) -> String {
7683        let result = (amount, U160::from(1u128 << 96), 0u32, U256::from(50_000u64)).abi_encode();
7684        serde_json::json!({
7685            "jsonrpc": "2.0",
7686            "id": 1,
7687            "result": hex::encode_prefixed(result),
7688        })
7689        .to_string()
7690    }
7691
7692    async fn expected_swap_tx(min_amount_out: U256) -> (B256, String) {
7693        // The orchestration's policy math is deterministic: nonce 7 from the fixture,
7694        // buffered gas 78000, buffered max fee 130000000, priority fee 10000000
7695        let expected_tx = build_eip1559_transaction(
7696            42161,
7697            7,
7698            78_000,
7699            130_000_000,
7700            10_000_000,
7701            ROUTER_ADDRESS,
7702            U256::ZERO,
7703            Bytes::from(expected_swap_calldata(min_amount_out)),
7704        );
7705        sign_eip1559_transaction(
7706            expected_tx,
7707            &PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
7708        )
7709        .await
7710        .map(|(hash, raw)| (hash, nautilus_core::hex::encode_prefixed(&raw)))
7711        .unwrap()
7712    }
7713
7714    fn expected_buy_base_amount() -> U256 {
7715        U256::from(1_000_000_000_000_000u64)
7716    }
7717
7718    fn expected_buy_amount_in() -> U256 {
7719        let profiler = test_profiler(&test_pool(), FIXTURE_BLOCK);
7720        profiler
7721            .swap_exact_out(expected_buy_base_amount(), false, None)
7722            .unwrap()
7723            .get_input_amount()
7724    }
7725
7726    fn expected_buy_min_amount_out(slippage_bps: u32) -> U256 {
7727        derive_min_amount_out(expected_buy_base_amount(), slippage_bps).unwrap()
7728    }
7729
7730    fn expected_buy_swap_calldata(min_amount_out: U256, amount_in: U256) -> Vec<u8> {
7731        UniswapV3SwapRouter::exactInputSingleCall {
7732            params: UniswapV3SwapRouter::ExactInputSingleParams {
7733                tokenIn: USDC_ADDRESS,
7734                tokenOut: WETH_ADDRESS,
7735                fee: U24::try_from(500u32).unwrap(),
7736                recipient: address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266"),
7737                deadline: U256::from(FIXTURE_BLOCK_TIMESTAMP + 300),
7738                amountIn: amount_in,
7739                amountOutMinimum: min_amount_out,
7740                sqrtPriceLimitX96: U160::ZERO,
7741            },
7742        }
7743        .abi_encode()
7744    }
7745
7746    async fn expected_buy_swap_tx(min_amount_out: U256, amount_in: U256) -> (B256, String) {
7747        let expected_tx = build_eip1559_transaction(
7748            42161,
7749            7,
7750            78_000,
7751            130_000_000,
7752            10_000_000,
7753            ROUTER_ADDRESS,
7754            U256::ZERO,
7755            Bytes::from(expected_buy_swap_calldata(min_amount_out, amount_in)),
7756        );
7757        sign_eip1559_transaction(
7758            expected_tx,
7759            &PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
7760        )
7761        .await
7762        .map(|(hash, raw)| (hash, nautilus_core::hex::encode_prefixed(&raw)))
7763        .unwrap()
7764    }
7765
7766    fn finalized_buy_swap_receipt(tx_hash: B256, amount_in: U256) -> String {
7767        finalized_buy_swap_receipt_with_base_out(tx_hash, amount_in, expected_buy_base_amount())
7768    }
7769
7770    fn finalized_buy_swap_receipt_with_base_out(
7771        tx_hash: B256,
7772        amount_in: U256,
7773        base_out: U256,
7774    ) -> String {
7775        let data = (
7776            -I256::try_from(u128::try_from(base_out).unwrap()).unwrap(),
7777            I256::try_from(u128::try_from(amount_in).unwrap()).unwrap(),
7778            U160::from(1_u128 << 96),
7779            TEST_LIQUIDITY,
7780            I24::try_from(0).unwrap(),
7781        )
7782            .abi_encode();
7783        serde_json::json!({
7784            "jsonrpc": "2.0",
7785            "id": 1,
7786            "result": {
7787                "transactionHash": tx_hash.to_string(),
7788                "blockHash": "0x2222222222222222222222222222222222222222222222222222222222222222",
7789                "blockNumber": "0x1cf0d41",
7790                "transactionIndex": "0x2",
7791                "gasUsed": "0xc3c0",
7792                "effectiveGasPrice": "0x5f5e100",
7793                "status": "0x1",
7794                "logs": [{
7795                    "removed": false,
7796                    "logIndex": "0x6",
7797                    "transactionIndex": "0x2",
7798                    "transactionHash": tx_hash.to_string(),
7799                    "blockHash": "0x2222222222222222222222222222222222222222222222222222222222222222",
7800                    "blockNumber": "0x1cf0d41",
7801                    "address": test_pool().address.to_string(),
7802                    "data": hex::encode_prefixed(data),
7803                    "topics": [
7804                        "0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67",
7805                        "0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266",
7806                        "0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266"
7807                    ]
7808                }]
7809            }
7810        })
7811        .to_string()
7812    }
7813
7814    fn finalized_buy_swap_block(tx_hash: B256, min_amount_out: U256, amount_in: U256) -> String {
7815        serde_json::json!({
7816            "jsonrpc": "2.0",
7817            "id": 1,
7818            "result": {
7819                "number": "0x1cf0d41",
7820                "hash": "0x2222222222222222222222222222222222222222222222222222222222222222",
7821                "parentHash": FIXTURE_BLOCK_HASH,
7822                "timestamp": "0x69044a21",
7823                "baseFeePerGas": "0x5f5e100",
7824                "transactions": [{
7825                    "hash": tx_hash.to_string(),
7826                    "from": WALLET,
7827                    "nonce": "0x7",
7828                    "chainId": "0xa4b1",
7829                    "type": "0x2",
7830                    "to": ROUTER,
7831                    "input": hex::encode_prefixed(expected_buy_swap_calldata(min_amount_out, amount_in)),
7832                    "value": "0x0",
7833                    "gas": "0x130b0",
7834                    "maxFeePerGas": "0x7bfa480",
7835                    "maxPriorityFeePerGas": "0x989680"
7836                }]
7837            }
7838        })
7839        .to_string()
7840    }
7841
7842    fn finalized_buy_swap_rpc_state(
7843        tx_hash: B256,
7844        min_amount_out: U256,
7845        amount_in: U256,
7846    ) -> MockRpcState {
7847        let receipt = finalized_buy_swap_receipt(tx_hash, amount_in);
7848        let block = finalized_buy_swap_block(tx_hash, min_amount_out, amount_in);
7849        with_finalized_identity(
7850            signing_rpc_state()
7851                .with_response("eth_getTransactionReceipt", &receipt)
7852                .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block)
7853                .with_send_raw_transaction_echo()
7854                .with_call_response(BALANCE_OF_SELECTOR, CALL_BALANCE)
7855                .with_call_response(ALLOWANCE_SELECTOR, CALL_ALLOWANCE),
7856            &block,
7857            &receipt,
7858        )
7859    }
7860
7861    fn finalized_swap_receipt(tx_hash: B256) -> String {
7862        let data = (
7863            I256::try_from(1_000_000_000_000_000_i128).unwrap(),
7864            I256::try_from(-1_000_000_i128).unwrap(),
7865            U160::from(1_u128 << 96),
7866            TEST_LIQUIDITY,
7867            I24::try_from(0).unwrap(),
7868        )
7869            .abi_encode();
7870        serde_json::json!({
7871            "jsonrpc": "2.0",
7872            "id": 1,
7873            "result": {
7874                "transactionHash": tx_hash.to_string(),
7875                "blockHash": "0x2222222222222222222222222222222222222222222222222222222222222222",
7876                "blockNumber": "0x1cf0d41",
7877                "transactionIndex": "0x2",
7878                "gasUsed": "0xc3c0",
7879                "effectiveGasPrice": "0x5f5e100",
7880                "status": "0x1",
7881                "logs": [{
7882                    "removed": false,
7883                    "logIndex": "0x6",
7884                    "transactionIndex": "0x2",
7885                    "transactionHash": tx_hash.to_string(),
7886                    "blockHash": "0x2222222222222222222222222222222222222222222222222222222222222222",
7887                    "blockNumber": "0x1cf0d41",
7888                    "address": test_pool().address.to_string(),
7889                    "data": hex::encode_prefixed(data),
7890                    "topics": [
7891                        "0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67",
7892                        "0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266",
7893                        "0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266"
7894                    ]
7895                }]
7896            }
7897        })
7898        .to_string()
7899    }
7900
7901    fn finalized_swap_receipt_with_unrelated_swap(tx_hash: B256) -> String {
7902        let mut receipt: serde_json::Value =
7903            serde_json::from_str(&finalized_swap_receipt(tx_hash)).unwrap();
7904        let logs = receipt["result"]["logs"].as_array_mut().unwrap();
7905        let mut unrelated = logs[0].clone();
7906        unrelated["address"] = serde_json::json!(ROUTER);
7907        unrelated["logIndex"] = serde_json::json!("0x7");
7908        logs.push(unrelated);
7909        receipt.to_string()
7910    }
7911
7912    fn finalized_swap_block(tx_hash: B256, min_amount_out: U256) -> String {
7913        serde_json::json!({
7914            "jsonrpc": "2.0",
7915            "id": 1,
7916            "result": {
7917                "number": "0x1cf0d41",
7918                "hash": "0x2222222222222222222222222222222222222222222222222222222222222222",
7919                "parentHash": FIXTURE_BLOCK_HASH,
7920                "timestamp": "0x69044a21",
7921                "baseFeePerGas": "0x5f5e100",
7922                "transactions": [{
7923                    "hash": tx_hash.to_string(),
7924                    "from": WALLET,
7925                    "nonce": "0x7",
7926                    "chainId": "0xa4b1",
7927                    "type": "0x2",
7928                    "to": ROUTER,
7929                    "input": hex::encode_prefixed(expected_swap_calldata(min_amount_out)),
7930                    "value": "0x0",
7931                    "gas": "0x130b0",
7932                    "maxFeePerGas": "0x7bfa480",
7933                    "maxPriorityFeePerGas": "0x989680"
7934                }]
7935            }
7936        })
7937        .to_string()
7938    }
7939
7940    fn finalized_swap_rpc_state(tx_hash: B256, min_amount_out: U256) -> MockRpcState {
7941        let receipt = finalized_swap_receipt(tx_hash);
7942        let block = finalized_swap_block(tx_hash, min_amount_out);
7943        with_finalized_identity(
7944            signing_rpc_state()
7945                .with_response("eth_getTransactionReceipt", &receipt)
7946                .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block)
7947                .with_send_raw_transaction_echo()
7948                .with_call_response(BALANCE_OF_SELECTOR, CALL_BALANCE)
7949                .with_call_response(ALLOWANCE_SELECTOR, CALL_ALLOWANCE),
7950            &block,
7951            &receipt,
7952        )
7953    }
7954
7955    fn with_finalized_identity(
7956        state: MockRpcState,
7957        block_response: &str,
7958        receipt_response: &str,
7959    ) -> MockRpcState {
7960        let block: serde_json::Value = serde_json::from_str(block_response).unwrap();
7961        let receipt: serde_json::Value = serde_json::from_str(receipt_response).unwrap();
7962        let transaction = block["result"]["transactions"][0].clone();
7963        let transaction_response = serde_json::json!({
7964            "jsonrpc": "2.0",
7965            "id": 1,
7966            "result": transaction,
7967        })
7968        .to_string();
7969        let success = receipt["result"]["status"] == "0x1";
7970        let mut trace = serde_json::json!({
7971            "type": "CALL",
7972            "from": transaction["from"],
7973            "to": transaction["to"],
7974            "value": transaction["value"],
7975            "gas": transaction["gas"],
7976            "gasUsed": receipt["result"]["gasUsed"],
7977            "input": transaction["input"],
7978            "output": "0x",
7979            "calls": [],
7980        });
7981
7982        if !success {
7983            trace["error"] = serde_json::json!("execution reverted");
7984        }
7985        let trace_response = serde_json::json!({
7986            "jsonrpc": "2.0",
7987            "id": 1,
7988            "result": trace,
7989        })
7990        .to_string();
7991        state
7992            .with_response("eth_getTransactionByHash", &transaction_response)
7993            .with_response("debug_traceTransaction", &trace_response)
7994    }
7995
7996    fn receipt_with_transaction_hash(receipt_response: &str, tx_hash: B256) -> String {
7997        let mut receipt: serde_json::Value = serde_json::from_str(receipt_response).unwrap();
7998        receipt["result"]["transactionHash"] = serde_json::json!(tx_hash.to_string());
7999        receipt.to_string()
8000    }
8001
8002    fn replacement_head_block(tx_hash: B256) -> String {
8003        serde_json::json!({
8004            "jsonrpc": "2.0",
8005            "id": 1,
8006            "result": {
8007                "number": "0x1cf0d40",
8008                "hash": "0x1111111111111111111111111111111111111111111111111111111111111111",
8009                "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000001",
8010                "timestamp": "0x69044a20",
8011                "baseFeePerGas": "0x5f5e100",
8012                "transactions": [{
8013                    "hash": "0x3333333333333333333333333333333333333333333333333333333333333333",
8014                    "from": "0x0000000000000000000000000000000000000001",
8015                    "nonce": "0x1",
8016                    "type": "0x0",
8017                    "to": WETH,
8018                    "input": "0x",
8019                    "value": "0x0",
8020                    "gas": "0x5208"
8021                }, {
8022                    "hash": tx_hash.to_string(),
8023                    "from": WALLET,
8024                    "nonce": "0x7",
8025                    "chainId": "0xa4b1",
8026                    "type": "0x2",
8027                    "to": WETH,
8028                    "input": "0xd0e30db0",
8029                    "value": "0x38d7ea4c68000",
8030                    "gas": "0x130b0",
8031                    "maxFeePerGas": "0x7bfa480",
8032                    "maxPriorityFeePerGas": "0x989680"
8033                }]
8034            }
8035        })
8036        .to_string()
8037    }
8038
8039    fn finalized_wrap_block(tx_hash: B256) -> String {
8040        serde_json::json!({
8041            "jsonrpc": "2.0",
8042            "id": 1,
8043            "result": {
8044                "number": "0x1cf0d41",
8045                "hash": "0x2222222222222222222222222222222222222222222222222222222222222222",
8046                "parentHash": FIXTURE_BLOCK_HASH,
8047                "timestamp": "0x69044a21",
8048                "baseFeePerGas": "0x5f5e100",
8049                "transactions": [{
8050                    "hash": tx_hash.to_string(),
8051                    "from": WALLET,
8052                    "nonce": "0x7",
8053                    "chainId": "0xa4b1",
8054                    "type": "0x2",
8055                    "to": WETH,
8056                    "input": "0xd0e30db0",
8057                    "value": "0x38d7ea4c68000",
8058                    "gas": "0x130b0",
8059                    "maxFeePerGas": "0x7bfa480",
8060                    "maxPriorityFeePerGas": "0x989680"
8061                }]
8062            }
8063        })
8064        .to_string()
8065    }
8066
8067    fn finalized_approve_block(tx_hash: B256, amount: U256) -> String {
8068        let calldata = ERC20::approveCall {
8069            spender: ROUTER_ADDRESS,
8070            amount,
8071        }
8072        .abi_encode();
8073        serde_json::json!({
8074            "jsonrpc": "2.0",
8075            "id": 1,
8076            "result": {
8077                "number": "0x1cf0d41",
8078                "hash": "0x2222222222222222222222222222222222222222222222222222222222222222",
8079                "parentHash": FIXTURE_BLOCK_HASH,
8080                "timestamp": "0x69044a21",
8081                "baseFeePerGas": "0x5f5e100",
8082                "transactions": [{
8083                    "hash": tx_hash.to_string(),
8084                    "from": WALLET,
8085                    "nonce": "0x7",
8086                    "chainId": "0xa4b1",
8087                    "type": "0x2",
8088                    "to": WETH,
8089                    "input": hex::encode_prefixed(calldata),
8090                    "value": "0x0",
8091                    "gas": "0x130b0",
8092                    "maxFeePerGas": "0x7bfa480",
8093                    "maxPriorityFeePerGas": "0x989680"
8094                }]
8095            }
8096        })
8097        .to_string()
8098    }
8099
8100    fn fixture_sell_plan() -> SwapPlan {
8101        let pool = test_pool();
8102        let order = test_market_sell_order(pool.instrument_id);
8103        let quote_token = pool.get_quote_token();
8104        SwapPlan {
8105            order,
8106            quote_currency: Currency::new_checked(
8107                &quote_token.symbol,
8108                quote_token.decimals,
8109                0,
8110                &quote_token.name,
8111                CurrencyType::Crypto,
8112            )
8113            .unwrap(),
8114            instrument_id: pool.instrument_id,
8115            pool_address: pool.address,
8116            router: ROUTER_ADDRESS,
8117            factory: UNISWAP_V3.dex.factory,
8118            weth: WETH_ADDRESS,
8119            token_in: WETH_ADDRESS,
8120            token_out: USDC_ADDRESS,
8121            fee: U24::try_from(500u32).unwrap(),
8122            amount_in: U256::from(1_000_000_000_000_000u64),
8123            min_amount_out: expected_min_amount_out(50),
8124            slippage_bps: 50,
8125            quote_spend_ceiling: None,
8126            profiler_position: Some(profiler_event_position()),
8127            pool,
8128        }
8129    }
8130
8131    fn fixture_block_response(number: u64, hash: B256) -> String {
8132        let parent_hash = if number == FIXTURE_BLOCK {
8133            B256::from_str("0x0000000000000000000000000000000000000000000000000000000000000001")
8134                .unwrap()
8135        } else {
8136            B256::from_str(FIXTURE_BLOCK_HASH).unwrap()
8137        };
8138        serde_json::json!({
8139            "jsonrpc": "2.0",
8140            "id": 1,
8141            "result": {
8142                "number": format!("0x{number:x}"),
8143                "hash": hash.to_string(),
8144                "parentHash": parent_hash.to_string(),
8145                "timestamp": format!("0x{:x}", FIXTURE_BLOCK_TIMESTAMP + number - FIXTURE_BLOCK),
8146                "baseFeePerGas": "0x5f5e100",
8147                "transactions": []
8148            }
8149        })
8150        .to_string()
8151    }
8152
8153    fn profiler_event_receipt(pool_address: Address) -> String {
8154        let transaction_hash = B256::from([0x33; 32]);
8155        serde_json::json!({
8156            "jsonrpc": "2.0",
8157            "id": 1,
8158            "result": {
8159                "transactionHash": transaction_hash.to_string(),
8160                "blockHash": FIXTURE_BLOCK_HASH,
8161                "blockNumber": "0x1cf0d40",
8162                "transactionIndex": "0x2",
8163                "gasUsed": "0xc3c0",
8164                "effectiveGasPrice": "0x5f5e100",
8165                "status": "0x1",
8166                "logs": [{
8167                    "removed": false,
8168                    "logIndex": "0x6",
8169                    "transactionIndex": "0x2",
8170                    "transactionHash": transaction_hash.to_string(),
8171                    "blockHash": FIXTURE_BLOCK_HASH,
8172                    "blockNumber": "0x1cf0d40",
8173                    "address": pool_address.to_string(),
8174                    "data": "0x",
8175                    "topics": [test_pool().dex.swap_created_event.as_ref()]
8176                }]
8177            }
8178        })
8179        .to_string()
8180    }
8181
8182    fn profiler_event_position() -> BlockPosition {
8183        BlockPosition::new(FIXTURE_BLOCK, B256::from([0x33; 32]).to_string(), 2, 6)
8184            .with_block_hash(Some(FIXTURE_BLOCK_HASH.to_string()))
8185    }
8186
8187    #[rstest]
8188    fn quantity_to_raw_amount_scales_by_token_decimals() {
8189        assert_eq!(
8190            quantity_to_raw_amount(Quantity::from("0.001"), 18).unwrap(),
8191            U256::from(1_000_000_000_000_000u64)
8192        );
8193        assert_eq!(
8194            quantity_to_raw_amount(Quantity::from("1.5"), 18).unwrap(),
8195            U256::from(1_500_000_000_000_000_000u128)
8196        );
8197        assert_eq!(
8198            quantity_to_raw_amount(Quantity::from("12.5"), 6).unwrap(),
8199            U256::from(12_500_000u64)
8200        );
8201    }
8202
8203    #[rstest]
8204    fn quantity_to_raw_amount_uses_defi_quantity_precision() {
8205        let amount = U256::from(10_000_000_000_000_000u64);
8206        let quantity = Quantity::from_u256(amount, 18).unwrap();
8207
8208        assert_eq!(quantity_to_raw_amount(quantity, 18).unwrap(), amount);
8209    }
8210
8211    #[rstest]
8212    fn quantity_to_raw_amount_rejects_zero() {
8213        let error = quantity_to_raw_amount(Quantity::from("0.0"), 18).unwrap_err();
8214
8215        assert_eq!(error.to_string(), "Order quantity must be positive");
8216    }
8217
8218    #[rstest]
8219    fn quantity_to_raw_amount_rejects_inexact_token_units() {
8220        let error = quantity_to_raw_amount(Quantity::from("0.0000001"), 6).unwrap_err();
8221
8222        assert!(
8223            error
8224                .to_string()
8225                .contains("is not exactly representable in 6 base token decimals"),
8226            "was: {error}"
8227        );
8228    }
8229
8230    #[rstest]
8231    fn raw_amount_to_quantity_inverts_base_quantity() {
8232        let quantity = Quantity::from("0.001");
8233        let amount = quantity_to_raw_amount(quantity, 18).unwrap();
8234
8235        assert_eq!(raw_amount_to_quantity(amount, 18).unwrap(), quantity);
8236    }
8237
8238    #[rstest]
8239    fn raw_amount_to_quantity_rejects_positive_amount_truncated_to_zero() {
8240        let error = raw_amount_to_quantity(U256::from(99u64), 18).unwrap_err();
8241
8242        assert!(
8243            error
8244                .to_string()
8245                .contains("is below representable quantity precision"),
8246            "was: {error}"
8247        );
8248    }
8249
8250    #[rstest]
8251    fn fill_price_from_quote_recovers_spent_quote_after_quantity_truncation() {
8252        let last_qty = raw_amount_to_quantity(U256::from(1_000_000_403_079_044u64), 18).unwrap();
8253        let quote_amount = U256::from(1_891_348u64);
8254        let quote = Currency::new_checked("USDC", 6, 0, "USD Coin", CurrencyType::Crypto).unwrap();
8255        let last_px = fill_price_from_quote(last_qty, quote_amount, quote).unwrap();
8256
8257        assert_eq!(
8258            Money::from_decimal(last_qty.as_decimal() * last_px.as_decimal(), quote).unwrap(),
8259            Money::from_u256(quote_amount, quote).unwrap()
8260        );
8261    }
8262
8263    #[rstest]
8264    fn swap_token_pair_is_directional() {
8265        assert_eq!(
8266            swap_token_pair(OrderSide::Sell, WETH_ADDRESS, USDC_ADDRESS).unwrap(),
8267            (WETH_ADDRESS, USDC_ADDRESS)
8268        );
8269        assert_eq!(
8270            swap_token_pair(OrderSide::Buy, WETH_ADDRESS, USDC_ADDRESS).unwrap(),
8271            (USDC_ADDRESS, WETH_ADDRESS)
8272        );
8273    }
8274
8275    #[rstest]
8276    fn restore_swap_plan_buy_uses_quote_input() {
8277        let (client, cache) =
8278            swap_client_with_cache(buy_test_config("http://127.0.0.1:1".to_string()));
8279        let order = test_market_buy_order(test_pool().instrument_id);
8280        cache
8281            .borrow_mut()
8282            .add_order(order.clone(), None, None, true)
8283            .unwrap();
8284        let amount_in = U256::from(2_345_678u64);
8285        let intent = ExecutionIntentRow {
8286            id: 7,
8287            schema_version: crate::execution::transaction::EXECUTION_SCHEMA_VERSION,
8288            chain_id: 42161,
8289            wallet_address: WALLET.to_string(),
8290            nonce: Some(7),
8291            purpose: "swap".to_string(),
8292            status: "finalized".to_string(),
8293            client_order_id: Some(order.client_order_id().to_string()),
8294            trader_id: Some(order.trader_id().to_string()),
8295            strategy_id: Some(order.strategy_id().to_string()),
8296            account_id: Some("BLOCKCHAIN-001".to_string()),
8297            instrument_id: Some(order.instrument_id().to_string()),
8298            pool_address: Some(test_pool().address.to_string()),
8299            transaction_to: ROUTER.to_string(),
8300            transaction_input: "0x".to_string(),
8301            transaction_value: "0".to_string(),
8302            amount_in: Some(amount_in.to_string()),
8303            created_block: FIXTURE_BLOCK,
8304            acknowledgement_emitted: true,
8305            fill_emitted: false,
8306            terminal_emitted: false,
8307            active: true,
8308        };
8309
8310        let plan = client.restore_swap_plan(&intent).unwrap();
8311
8312        assert_eq!(plan.token_in, USDC_ADDRESS);
8313        assert_eq!(plan.token_out, WETH_ADDRESS);
8314        assert_eq!(plan.amount_in, amount_in);
8315    }
8316
8317    #[rstest]
8318    #[case(1_000_000, 50, 995_000)]
8319    #[case(1_000_000, 0, 1_000_000)]
8320    #[case(1_000_000, 200, 980_000)]
8321    #[case(10_000, 9_999, 1)]
8322    fn derive_min_amount_out_applies_slippage(
8323        #[case] quoted: u64,
8324        #[case] slippage_bps: u32,
8325        #[case] expected: u64,
8326    ) {
8327        assert_eq!(
8328            derive_min_amount_out(U256::from(quoted), slippage_bps).unwrap(),
8329            U256::from(expected)
8330        );
8331    }
8332
8333    #[rstest]
8334    fn derive_min_amount_out_rejects_zero_result() {
8335        let error = derive_min_amount_out(U256::from(9_999u64), 9_999).unwrap_err();
8336
8337        assert!(
8338            error.to_string().contains("Derived minimum output is zero"),
8339            "was: {error}"
8340        );
8341    }
8342
8343    #[rstest]
8344    fn derive_min_amount_out_rejects_full_slippage() {
8345        let error = derive_min_amount_out(U256::from(1_000_000u64), 10_000).unwrap_err();
8346
8347        assert!(
8348            error.to_string().contains("must be below 10000"),
8349            "was: {error}"
8350        );
8351    }
8352
8353    #[rstest]
8354    fn replacement_scan_range_is_bounded_and_checked() {
8355        assert_eq!(
8356            replacement_scan_range(10, 10).unwrap(),
8357            RangeInclusive::new(10, 10)
8358        );
8359        assert_eq!(
8360            replacement_scan_range(10, 10 + MAX_REPLACEMENT_SCAN_BLOCKS).unwrap(),
8361            RangeInclusive::new(10, 10 + MAX_REPLACEMENT_SCAN_BLOCKS - 1)
8362        );
8363        assert!(
8364            replacement_scan_range(11, 10)
8365                .unwrap_err()
8366                .to_string()
8367                .contains("is behind execution creation block")
8368        );
8369        assert_eq!(
8370            replacement_scan_range(u64::MAX - 1, u64::MAX).unwrap(),
8371            RangeInclusive::new(u64::MAX - 1, u64::MAX)
8372        );
8373    }
8374
8375    #[rstest]
8376    fn terminal_execution_event_ids_are_stable_and_kind_specific() {
8377        let transaction_hash = B256::from([0x42; 32]);
8378
8379        let fill = execution_event_id(transaction_hash, b"fill");
8380        let fill_retry = execution_event_id(transaction_hash, b"fill");
8381        let reverted = execution_event_id(transaction_hash, b"reverted");
8382
8383        assert_eq!(fill, fill_retry);
8384        assert_ne!(fill, reverted);
8385    }
8386
8387    #[rstest]
8388    fn prepared_transaction_debug_redacts_raw_transaction() {
8389        let prepared = PreparedTransaction {
8390            intent_id: 1,
8391            created_block: 2,
8392            nonce: 3,
8393            tx_hash: B256::ZERO,
8394            raw_tx: vec![0xde, 0xad, 0xbe, 0xef],
8395            payload_lease: None,
8396        };
8397
8398        let debug = format!("{prepared:?}");
8399
8400        assert!(debug.contains("raw_tx: \"<redacted>\""));
8401        assert!(!debug.contains("[222, 173, 190, 239]"));
8402    }
8403
8404    #[rstest]
8405    fn call_trace_rejects_unreviewed_internal_edge() {
8406        let signed = crate::execution::transaction::DecodedSignedTransaction {
8407            hash: B256::from([1; 32]),
8408            signer: Address::from_str(WALLET).unwrap(),
8409            chain_id: 42_161,
8410            nonce: 7,
8411            to: ROUTER_ADDRESS,
8412            value: U256::ZERO,
8413            input: Bytes::from(expected_swap_calldata(expected_min_amount_out(50))),
8414            gas_limit: 78_000,
8415            max_fee_per_gas: 130_000_000,
8416            max_priority_fee_per_gas: 10_000_000,
8417        };
8418        let input_digest = keccak256(&signed.input);
8419        let trace = VerifiedCallTrace {
8420            call_type: RpcCallType::Call,
8421            from: signed.signer,
8422            to: Some(signed.to),
8423            value: signed.value,
8424            input_selector: signed
8425                .input
8426                .get(..4)
8427                .map(|selector| selector.try_into().unwrap()),
8428            input_digest,
8429            success: true,
8430            calls: vec![VerifiedCallTrace {
8431                call_type: RpcCallType::Call,
8432                from: ROUTER_ADDRESS,
8433                to: Some(WETH_ADDRESS),
8434                value: U256::ZERO,
8435                input_selector: None,
8436                input_digest: B256::ZERO,
8437                success: true,
8438                calls: Vec::new(),
8439            }],
8440        };
8441        let manifest = &test_config("http://127.0.0.1:1".to_string())
8442            .verification
8443            .unwrap()
8444            .deployment_manifest;
8445
8446        let error = validate_call_trace(&trace, &signed, true, "swap_sell", manifest).unwrap_err();
8447
8448        assert!(
8449            error.to_string().contains("unreviewed call edge"),
8450            "was: {error}"
8451        );
8452    }
8453
8454    #[rstest]
8455    fn call_trace_rejects_unlisted_precompile_target() {
8456        let manifest = test_config("http://127.0.0.1:1".to_string())
8457            .verification
8458            .unwrap()
8459            .deployment_manifest;
8460        let calls = [VerifiedCallTrace {
8461            call_type: RpcCallType::Call,
8462            from: ROUTER_ADDRESS,
8463            to: Some(address!("0000000000000000000000000000000000000007")),
8464            value: U256::ZERO,
8465            input_selector: None,
8466            input_digest: B256::ZERO,
8467            success: true,
8468            calls: Vec::new(),
8469        }];
8470
8471        let error =
8472            validate_internal_calls(&calls, ROUTER_ADDRESS, "swap_sell", &manifest).unwrap_err();
8473
8474        assert_eq!(
8475            error.to_string(),
8476            format!(
8477                "Verified call trace contains an unreviewed call edge {ROUTER_ADDRESS} -> {} for swap_sell",
8478                address!("0000000000000000000000000000000000000007")
8479            )
8480        );
8481    }
8482
8483    #[rstest]
8484    fn call_trace_requires_exact_call_type() {
8485        let manifest = test_config("http://127.0.0.1:1".to_string())
8486            .verification
8487            .unwrap()
8488            .deployment_manifest;
8489        let calls = [VerifiedCallTrace {
8490            call_type: RpcCallType::Staticcall,
8491            from: ROUTER_ADDRESS,
8492            to: Some(test_pool().address),
8493            value: U256::ZERO,
8494            input_selector: None,
8495            input_digest: B256::ZERO,
8496            success: true,
8497            calls: Vec::new(),
8498        }];
8499
8500        let error =
8501            validate_internal_calls(&calls, ROUTER_ADDRESS, "swap_sell", &manifest).unwrap_err();
8502
8503        assert!(
8504            error.to_string().contains("unreviewed staticcall edge"),
8505            "was: {error}"
8506        );
8507    }
8508
8509    #[rstest]
8510    fn call_trace_requires_exact_caller() {
8511        let manifest = test_config("http://127.0.0.1:1".to_string())
8512            .verification
8513            .unwrap()
8514            .deployment_manifest;
8515        let calls = [VerifiedCallTrace {
8516            call_type: RpcCallType::Call,
8517            from: WETH_ADDRESS,
8518            to: Some(test_pool().address),
8519            value: U256::ZERO,
8520            input_selector: None,
8521            input_digest: B256::ZERO,
8522            success: true,
8523            calls: Vec::new(),
8524        }];
8525
8526        let error =
8527            validate_internal_calls(&calls, ROUTER_ADDRESS, "swap_sell", &manifest).unwrap_err();
8528
8529        assert_eq!(
8530            error.to_string(),
8531            "Verified call trace child has an invalid caller context"
8532        );
8533    }
8534
8535    #[rstest]
8536    fn call_trace_rejects_contract_creation() {
8537        let signed = crate::execution::transaction::DecodedSignedTransaction {
8538            hash: B256::from([1; 32]),
8539            signer: Address::from_str(WALLET).unwrap(),
8540            chain_id: 42_161,
8541            nonce: 7,
8542            to: ROUTER_ADDRESS,
8543            value: U256::ZERO,
8544            input: Bytes::from(expected_swap_calldata(expected_min_amount_out(50))),
8545            gas_limit: 78_000,
8546            max_fee_per_gas: 130_000_000,
8547            max_priority_fee_per_gas: 10_000_000,
8548        };
8549        let trace = VerifiedCallTrace {
8550            call_type: RpcCallType::Call,
8551            from: signed.signer,
8552            to: Some(signed.to),
8553            value: signed.value,
8554            input_selector: signed
8555                .input
8556                .get(..4)
8557                .map(|selector| selector.try_into().unwrap()),
8558            input_digest: keccak256(&signed.input),
8559            success: true,
8560            calls: vec![VerifiedCallTrace {
8561                call_type: RpcCallType::Create,
8562                from: ROUTER_ADDRESS,
8563                to: Some(address!("0000000000000000000000000000000000000007")),
8564                value: U256::ZERO,
8565                input_selector: None,
8566                input_digest: B256::ZERO,
8567                success: true,
8568                calls: Vec::new(),
8569            }],
8570        };
8571        let manifest = &test_config("http://127.0.0.1:1".to_string())
8572            .verification
8573            .unwrap()
8574            .deployment_manifest;
8575
8576        let error = validate_call_trace(&trace, &signed, true, "swap_sell", manifest).unwrap_err();
8577
8578        assert!(
8579            error
8580                .to_string()
8581                .contains("forbidden state-changing operation"),
8582            "was: {error}"
8583        );
8584    }
8585
8586    #[tokio::test]
8587    async fn swap_quote_rejects_missing_ingestion_block_hash() {
8588        let (client, state) = client_with_mock_rpc(execution_rpc_state()).await;
8589        let plan = fixture_sell_plan();
8590        let position = BlockPosition::new(
8591            FIXTURE_BLOCK,
8592            FIXTURE_BLOCK_HASH.to_string(),
8593            BLOCK_SCOPED_SNAPSHOT_INDEX,
8594            BLOCK_SCOPED_SNAPSHOT_INDEX,
8595        );
8596
8597        let error = validate_swap_quote(
8598            &position,
8599            &plan,
8600            100,
8601            &client.verification,
8602            &client
8603                .config
8604                .verification
8605                .as_ref()
8606                .unwrap()
8607                .deployment_manifest,
8608        )
8609        .await
8610        .unwrap_err();
8611
8612        assert!(
8613            error.to_string().contains("no ingestion-time block hash"),
8614            "was: {error}"
8615        );
8616        assert!(state.recorded_requests().is_empty());
8617    }
8618
8619    #[tokio::test]
8620    async fn swap_quote_rejects_replaced_ingestion_block() {
8621        let changed = fixture_block_response(FIXTURE_BLOCK, B256::from([0x44; 32]));
8622        let state = execution_rpc_state().with_parameter_response(
8623            "eth_getBlockByNumber",
8624            "0x1cf0d40",
8625            &changed,
8626        );
8627        let (client, _) = client_with_mock_rpc(state).await;
8628        let plan = fixture_sell_plan();
8629        let position = BlockPosition::new(
8630            FIXTURE_BLOCK,
8631            FIXTURE_BLOCK_HASH.to_string(),
8632            BLOCK_SCOPED_SNAPSHOT_INDEX,
8633            BLOCK_SCOPED_SNAPSHOT_INDEX,
8634        )
8635        .with_block_hash(Some(FIXTURE_BLOCK_HASH.to_string()));
8636
8637        let error = validate_swap_quote(
8638            &position,
8639            &plan,
8640            100,
8641            &client.verification,
8642            &client
8643                .config
8644                .verification
8645                .as_ref()
8646                .unwrap()
8647                .deployment_manifest,
8648        )
8649        .await
8650        .unwrap_err();
8651
8652        assert!(error.to_string().contains("changed from"), "was: {error}");
8653    }
8654
8655    #[tokio::test]
8656    async fn swap_quote_accepts_exact_canonical_event_watermark() {
8657        let pool = test_pool();
8658        let receipt = profiler_event_receipt(pool.address);
8659        let state = execution_rpc_state().with_response("eth_getTransactionReceipt", &receipt);
8660        let (client, _) = client_with_mock_rpc(state).await;
8661        let plan = fixture_sell_plan();
8662
8663        let anchors = validate_swap_quote(
8664            &profiler_event_position(),
8665            &plan,
8666            100,
8667            &client.verification,
8668            &client
8669                .config
8670                .verification
8671                .as_ref()
8672                .unwrap()
8673                .deployment_manifest,
8674        )
8675        .await
8676        .unwrap();
8677
8678        assert_eq!(anchors.watermark.number, profiler_event_position().number);
8679        assert_eq!(anchors.state.number, FIXTURE_BLOCK);
8680        assert_eq!(anchors.state.hash, B256::from([0x11; 32]));
8681        assert_eq!(anchors.state.timestamp, FIXTURE_BLOCK_TIMESTAMP);
8682    }
8683
8684    #[tokio::test]
8685    async fn swap_quote_rejects_mismatched_receipt_position() {
8686        let pool = test_pool();
8687        let mut receipt: serde_json::Value =
8688            serde_json::from_str(&profiler_event_receipt(pool.address)).unwrap();
8689        receipt["result"]["transactionIndex"] = serde_json::json!("0x3");
8690        let state =
8691            execution_rpc_state().with_response("eth_getTransactionReceipt", &receipt.to_string());
8692        let (client, _) = client_with_mock_rpc(state).await;
8693        let plan = fixture_sell_plan();
8694
8695        let error = validate_swap_quote(
8696            &profiler_event_position(),
8697            &plan,
8698            100,
8699            &client.verification,
8700            &client
8701                .config
8702                .verification
8703                .as_ref()
8704                .unwrap()
8705                .deployment_manifest,
8706        )
8707        .await
8708        .unwrap_err();
8709
8710        assert_eq!(
8711            error.to_string(),
8712            "Profiler receipt position does not match its ingestion watermark"
8713        );
8714    }
8715
8716    #[tokio::test]
8717    async fn swap_quote_rejects_watermark_from_different_pool() {
8718        let receipt = profiler_event_receipt(ROUTER_ADDRESS);
8719        let state = execution_rpc_state().with_response("eth_getTransactionReceipt", &receipt);
8720        let (client, _) = client_with_mock_rpc(state).await;
8721        let plan = fixture_sell_plan();
8722
8723        let error = validate_swap_quote(
8724            &profiler_event_position(),
8725            &plan,
8726            100,
8727            &client.verification,
8728            &client
8729                .config
8730                .verification
8731                .as_ref()
8732                .unwrap()
8733                .deployment_manifest,
8734        )
8735        .await
8736        .unwrap_err();
8737
8738        assert!(
8739            error
8740                .to_string()
8741                .contains("did not come from expected pool"),
8742            "was: {error}"
8743        );
8744    }
8745
8746    #[rstest]
8747    fn verified_sell_quote_sets_signed_amounts() {
8748        let plan = fixture_sell_plan();
8749        let quote = UniswapV3Quote {
8750            amount: expected_sell_quote_amount(),
8751            sqrt_price_x96_after: U160::from(1u128 << 96),
8752            initialized_ticks_crossed: 0,
8753            gas_estimate: U256::from(50_000u64),
8754        };
8755
8756        let (amount_in, min_amount_out) = verified_swap_amounts(&plan, quote).unwrap();
8757
8758        assert_eq!(amount_in, U256::from(1_000_000_000_000_000u64));
8759        assert_eq!(min_amount_out, expected_min_amount_out(50));
8760    }
8761
8762    #[rstest]
8763    fn exact_output_amount_extracts_negative_leg() {
8764        let pool = test_pool();
8765        let profiler = test_profiler(&pool, FIXTURE_BLOCK);
8766        let quote = profiler
8767            .swap_exact_in(U256::from(1_000_000_000_000_000u64), true, None)
8768            .unwrap();
8769
8770        let amount = exact_output_amount(&quote, true).unwrap();
8771
8772        assert!(amount < U256::from(1_000_000_000_000_000u64));
8773        assert!(amount > U256::from(990_000_000_000_000u64));
8774
8775        let error = exact_output_amount(&quote, false).unwrap_err();
8776        assert!(
8777            error.to_string().contains("is not a positive output"),
8778            "was: {error}"
8779        );
8780    }
8781
8782    #[rstest]
8783    #[case("0xC6962004f452bE9203591991D15f6b388e09E8D0.Arbitrum:UniswapV3", true)]
8784    #[case("0xC6962004f452bE9203591991D15f6b388e09E8D0.Ethereum:UniswapV3", false)]
8785    #[case("0xC6962004f452bE9203591991D15f6b388e09E8D0.Arbitrum:UniswapV4", false)]
8786    #[case("ETHUSDT-PERP.BINANCE", false)]
8787    fn handles_order_venue_matches_chain_and_dex(#[case] instrument: &str, #[case] expected: bool) {
8788        let client = test_client("http://127.0.0.1:1".to_string());
8789        let instrument_id: InstrumentId = instrument.parse().unwrap();
8790
8791        assert_eq!(client.handles_order_venue(instrument_id.venue), expected);
8792    }
8793
8794    #[tokio::test]
8795    async fn submit_order_denies_buy_without_allowlisted_pair() {
8796        let (mut client, cache) =
8797            swap_client_with_cache(test_config("http://127.0.0.1:1".to_string()));
8798        let pool = test_pool();
8799        let order = test_market_buy_order(pool.instrument_id);
8800        cache
8801            .borrow_mut()
8802            .add_order(order.clone(), None, None, true)
8803            .unwrap();
8804        let mut receiver = start_with_events(&mut client);
8805
8806        client.submit_order(submit_order_cmd(&order)).unwrap();
8807
8808        let events = collect_order_events(&mut receiver);
8809        assert_eq!(events.len(), 1);
8810        let OrderEventAny::Denied(denied) = &events[0] else {
8811            panic!("expected OrderDenied, was {:?}", events[0]);
8812        };
8813        assert!(
8814            denied
8815                .reason
8816                .contains("not in the `allowed_token_pairs` allowlist"),
8817            "was: {}",
8818            denied.reason
8819        );
8820        assert!(
8821            denied.reason.contains(&USDC_ADDRESS.to_string()),
8822            "was: {}",
8823            denied.reason
8824        );
8825    }
8826
8827    #[tokio::test]
8828    async fn submit_order_denies_buy_quote_denominated_quantity() {
8829        let (mut client, cache) =
8830            swap_client_with_cache(buy_test_config("http://127.0.0.1:1".to_string()));
8831        let pool = test_pool();
8832        let order = OrderTestBuilder::new(OrderType::Market)
8833            .trader_id(TraderId::from("TRADER-001"))
8834            .strategy_id(StrategyId::from("S-001"))
8835            .instrument_id(pool.instrument_id)
8836            .client_order_id(ClientOrderId::from("O-SWAP-BUY-001"))
8837            .side(OrderSide::Buy)
8838            .quantity(Quantity::from("0.001"))
8839            .quote_quantity(true)
8840            .build();
8841        cache
8842            .borrow_mut()
8843            .add_order(order.clone(), None, None, true)
8844            .unwrap();
8845        let mut receiver = start_with_events(&mut client);
8846
8847        client.submit_order(submit_order_cmd(&order)).unwrap();
8848
8849        let events = collect_order_events(&mut receiver);
8850        assert_eq!(events.len(), 1);
8851        let OrderEventAny::Denied(denied) = &events[0] else {
8852            panic!("expected OrderDenied, was {:?}", events[0]);
8853        };
8854        assert!(
8855            denied.reason.contains("Quote-denominated"),
8856            "was: {}",
8857            denied.reason
8858        );
8859    }
8860
8861    #[tokio::test]
8862    async fn submit_order_denies_buy_amount_above_max_order_amount() {
8863        let mut config = buy_test_config("http://127.0.0.1:1".to_string());
8864        config.max_order_amount = Some(999_999_999_999_999);
8865        let (mut client, cache) = swap_client_with_cache(config);
8866        let order = test_market_buy_order(test_pool().instrument_id);
8867        cache
8868            .borrow_mut()
8869            .add_order(order.clone(), None, None, true)
8870            .unwrap();
8871        let mut receiver = start_with_events(&mut client);
8872
8873        client.submit_order(submit_order_cmd(&order)).unwrap();
8874
8875        let events = collect_order_events(&mut receiver);
8876        assert_eq!(events.len(), 1);
8877        let OrderEventAny::Denied(denied) = &events[0] else {
8878            panic!("expected OrderDenied, was {:?}", events[0]);
8879        };
8880        assert!(
8881            denied
8882                .reason
8883                .contains("exceeds the configured `max_order_amount`"),
8884            "was: {}",
8885            denied.reason
8886        );
8887    }
8888
8889    #[tokio::test]
8890    async fn submit_order_denies_buy_without_quote_spend_limit() {
8891        let mut config = buy_test_config("http://127.0.0.1:1".to_string());
8892        config.quote_spend_limits = None;
8893        let (mut client, cache) = swap_client_with_cache(config);
8894        let order = test_market_buy_order(test_pool().instrument_id);
8895        cache
8896            .borrow_mut()
8897            .add_order(order.clone(), None, None, true)
8898            .unwrap();
8899        let mut receiver = start_with_events(&mut client);
8900
8901        client.submit_order(submit_order_cmd(&order)).unwrap();
8902
8903        let events = collect_order_events(&mut receiver);
8904        assert_eq!(events.len(), 1);
8905        let OrderEventAny::Denied(denied) = &events[0] else {
8906            panic!("expected OrderDenied, was {:?}", events[0]);
8907        };
8908        assert!(
8909            denied
8910                .reason
8911                .contains("No `quote_spend_limits` entry for BUY token pair"),
8912            "was: {}",
8913            denied.reason
8914        );
8915    }
8916
8917    #[tokio::test]
8918    async fn submit_order_uses_pair_specific_quote_spend_limit() {
8919        let mut config = buy_test_config("http://127.0.0.1:1".to_string());
8920        config.quote_spend_limits = Some(vec![quote_spend_limit(
8921            WETH,
8922            USDC,
8923            18,
8924            "1000000000000000000",
8925        )]);
8926        let (mut client, cache) = swap_client_with_cache(config);
8927        let order = test_market_buy_order(test_pool().instrument_id);
8928        cache
8929            .borrow_mut()
8930            .add_order(order.clone(), None, None, true)
8931            .unwrap();
8932        let mut receiver = start_with_events(&mut client);
8933
8934        client.submit_order(submit_order_cmd(&order)).unwrap();
8935
8936        let events = collect_order_events(&mut receiver);
8937        assert_eq!(events.len(), 1);
8938        let OrderEventAny::Denied(denied) = &events[0] else {
8939            panic!("expected OrderDenied, was {:?}", events[0]);
8940        };
8941        assert!(
8942            denied
8943                .reason
8944                .contains("No `quote_spend_limits` entry for BUY token pair"),
8945            "was: {}",
8946            denied.reason
8947        );
8948    }
8949
8950    #[tokio::test]
8951    async fn submit_order_denies_buy_with_quote_spend_precision_mismatch() {
8952        let mut config = buy_test_config("http://127.0.0.1:1".to_string());
8953        config.quote_spend_limits.as_mut().unwrap()[0].spend_token_decimals = 18;
8954        let error = test_client_result(config, test_pool()).unwrap_err();
8955        assert!(
8956            error
8957                .to_string()
8958                .contains("Quote spend limit decimals do not match the deployment manifest"),
8959            "was: {error}"
8960        );
8961    }
8962
8963    #[tokio::test]
8964    async fn submit_order_denies_buy_with_zero_quote_spend_limit() {
8965        let mut config = buy_test_config("http://127.0.0.1:1".to_string());
8966        config.quote_spend_limits.as_mut().unwrap()[0].max_amount = "0".to_string();
8967        let (mut client, cache) = swap_client_with_cache(config);
8968        let order = test_market_buy_order(test_pool().instrument_id);
8969        cache
8970            .borrow_mut()
8971            .add_order(order.clone(), None, None, true)
8972            .unwrap();
8973        let mut receiver = start_with_events(&mut client);
8974
8975        client.submit_order(submit_order_cmd(&order)).unwrap();
8976
8977        let events = collect_order_events(&mut receiver);
8978        assert_eq!(events.len(), 1);
8979        let OrderEventAny::Denied(denied) = &events[0] else {
8980            panic!("expected OrderDenied, was {:?}", events[0]);
8981        };
8982        assert!(
8983            denied
8984                .reason
8985                .contains("exceeds the configured `quote_spend_limits` maximum 0"),
8986            "was: {}",
8987            denied.reason
8988        );
8989    }
8990
8991    #[tokio::test]
8992    async fn submit_order_denies_buy_one_raw_unit_above_quote_spend_limit_before_readiness() {
8993        let amount_in = expected_buy_amount_in();
8994        let mut config = buy_test_config("http://127.0.0.1:1".to_string());
8995        config.quote_spend_limits.as_mut().unwrap()[0].max_amount =
8996            (amount_in - U256::from(1u8)).to_string();
8997        let (mut client, cache) = swap_client_with_cache(config);
8998        let order = test_market_buy_order(test_pool().instrument_id);
8999        cache
9000            .borrow_mut()
9001            .add_order(order.clone(), None, None, true)
9002            .unwrap();
9003        let mut receiver = start_with_events(&mut client);
9004
9005        client.submit_order(submit_order_cmd(&order)).unwrap();
9006
9007        let events = collect_order_events(&mut receiver);
9008        assert_eq!(events.len(), 1);
9009        let OrderEventAny::Denied(denied) = &events[0] else {
9010            panic!("expected OrderDenied, was {:?}", events[0]);
9011        };
9012        assert!(
9013            denied.reason.contains(&format!(
9014                "BUY quote amount {amount_in} exceeds the configured `quote_spend_limits`"
9015            )),
9016            "was: {}",
9017            denied.reason
9018        );
9019        assert!(!client.core.is_connected());
9020        assert!(!client.cache.has_database());
9021        assert!(client.signer.is_none());
9022        assert!(client.pending_tasks.is_empty());
9023    }
9024
9025    #[tokio::test]
9026    async fn submit_order_denies_non_market_order_type() {
9027        let (mut client, cache) =
9028            swap_client_with_cache(test_config("http://127.0.0.1:1".to_string()));
9029        let pool = test_pool();
9030        let order = OrderTestBuilder::new(OrderType::Limit)
9031            .trader_id(TraderId::from("TRADER-001"))
9032            .strategy_id(StrategyId::from("S-001"))
9033            .instrument_id(pool.instrument_id)
9034            .client_order_id(ClientOrderId::from("O-SWAP-001"))
9035            .side(OrderSide::Sell)
9036            .quantity(Quantity::from("0.001"))
9037            .price(Price::from("2000"))
9038            .build();
9039        cache
9040            .borrow_mut()
9041            .add_order(order.clone(), None, None, true)
9042            .unwrap();
9043        let mut receiver = start_with_events(&mut client);
9044
9045        client.submit_order(submit_order_cmd(&order)).unwrap();
9046
9047        let events = collect_order_events(&mut receiver);
9048        assert_eq!(events.len(), 1);
9049        let OrderEventAny::Denied(denied) = &events[0] else {
9050            panic!("expected OrderDenied, was {:?}", events[0]);
9051        };
9052        assert!(
9053            denied.reason.contains("only Market is supported"),
9054            "was: {}",
9055            denied.reason
9056        );
9057    }
9058
9059    #[tokio::test]
9060    async fn submit_order_denies_quote_denominated_quantity() {
9061        let (mut client, cache) =
9062            swap_client_with_cache(test_config("http://127.0.0.1:1".to_string()));
9063        let pool = test_pool();
9064        let order = OrderTestBuilder::new(OrderType::Market)
9065            .trader_id(TraderId::from("TRADER-001"))
9066            .strategy_id(StrategyId::from("S-001"))
9067            .instrument_id(pool.instrument_id)
9068            .client_order_id(ClientOrderId::from("O-SWAP-001"))
9069            .side(OrderSide::Sell)
9070            .quantity(Quantity::from("0.001"))
9071            .quote_quantity(true)
9072            .build();
9073        cache
9074            .borrow_mut()
9075            .add_order(order.clone(), None, None, true)
9076            .unwrap();
9077        let mut receiver = start_with_events(&mut client);
9078
9079        client.submit_order(submit_order_cmd(&order)).unwrap();
9080
9081        let events = collect_order_events(&mut receiver);
9082        assert_eq!(events.len(), 1);
9083        let OrderEventAny::Denied(denied) = &events[0] else {
9084            panic!("expected OrderDenied, was {:?}", events[0]);
9085        };
9086        assert!(
9087            denied.reason.contains("Quote-denominated"),
9088            "was: {}",
9089            denied.reason
9090        );
9091    }
9092
9093    #[tokio::test]
9094    async fn submit_order_denies_unknown_pool() {
9095        let (mut client, cache) =
9096            swap_client_with_cache(test_config("http://127.0.0.1:1".to_string()));
9097        let unknown: InstrumentId = "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45.Arbitrum:UniswapV3"
9098            .parse()
9099            .unwrap();
9100        let order = OrderTestBuilder::new(OrderType::Market)
9101            .trader_id(TraderId::from("TRADER-001"))
9102            .strategy_id(StrategyId::from("S-001"))
9103            .instrument_id(unknown)
9104            .client_order_id(ClientOrderId::from("O-SWAP-001"))
9105            .side(OrderSide::Sell)
9106            .quantity(Quantity::from("0.001"))
9107            .build();
9108        cache
9109            .borrow_mut()
9110            .add_order(order.clone(), None, None, true)
9111            .unwrap();
9112        let mut receiver = start_with_events(&mut client);
9113
9114        client.submit_order(submit_order_cmd(&order)).unwrap();
9115
9116        let events = collect_order_events(&mut receiver);
9117        assert_eq!(events.len(), 1);
9118        let OrderEventAny::Denied(denied) = &events[0] else {
9119            panic!("expected OrderDenied, was {:?}", events[0]);
9120        };
9121        assert!(
9122            denied.reason.contains("Unknown pool"),
9123            "was: {}",
9124            denied.reason
9125        );
9126    }
9127
9128    #[tokio::test]
9129    async fn submit_order_denies_sell_when_only_buy_pair_allowlisted() {
9130        let mut config = test_config("http://127.0.0.1:1".to_string());
9131        config.allowed_token_pairs = Some(vec![(USDC.to_string(), WETH.to_string())]);
9132        let (mut client, _) = swap_client_with_cache(config);
9133        let order = test_market_sell_order(test_pool().instrument_id);
9134        let mut receiver = start_with_events(&mut client);
9135
9136        client.submit_order(submit_order_cmd(&order)).unwrap();
9137
9138        let events = collect_order_events(&mut receiver);
9139        assert_eq!(events.len(), 1);
9140        let OrderEventAny::Denied(denied) = &events[0] else {
9141            panic!("expected OrderDenied, was {:?}", events[0]);
9142        };
9143        assert!(
9144            denied
9145                .reason
9146                .contains("not in the `allowed_token_pairs` allowlist"),
9147            "was: {}",
9148            denied.reason
9149        );
9150        assert!(
9151            denied.reason.contains(&WETH_ADDRESS.to_string()),
9152            "was: {}",
9153            denied.reason
9154        );
9155    }
9156
9157    #[tokio::test]
9158    async fn submit_order_sell_ignores_quote_spend_limits() {
9159        let mut config = test_config("http://127.0.0.1:1".to_string());
9160        config.quote_spend_limits = Some(vec![quote_spend_limit(WETH, USDC, 18, "0")]);
9161        let (mut client, _) = swap_client_with_cache(config);
9162        let order = test_market_sell_order(test_pool().instrument_id);
9163        let mut receiver = start_with_events(&mut client);
9164
9165        client.submit_order(submit_order_cmd(&order)).unwrap();
9166
9167        let events = collect_order_events(&mut receiver);
9168        assert_eq!(events.len(), 1);
9169        let OrderEventAny::Denied(denied) = &events[0] else {
9170            panic!("expected OrderDenied, was {:?}", events[0]);
9171        };
9172        assert!(
9173            denied
9174                .reason
9175                .contains("Blockchain execution client is not connected"),
9176            "was: {}",
9177            denied.reason
9178        );
9179    }
9180
9181    #[tokio::test]
9182    async fn submit_order_denies_token_pair_outside_allowlist() {
9183        let mut config = test_config("http://127.0.0.1:1".to_string());
9184        config.allowed_token_pairs = Some(Vec::new());
9185        let (mut client, _) = swap_client_with_cache(config);
9186        let order = test_market_sell_order(test_pool().instrument_id);
9187        let mut receiver = start_with_events(&mut client);
9188
9189        client.submit_order(submit_order_cmd(&order)).unwrap();
9190
9191        let events = collect_order_events(&mut receiver);
9192        assert_eq!(events.len(), 1);
9193        let OrderEventAny::Denied(denied) = &events[0] else {
9194            panic!("expected OrderDenied, was {:?}", events[0]);
9195        };
9196        assert!(
9197            denied
9198                .reason
9199                .contains("not in the `allowed_token_pairs` allowlist"),
9200            "was: {}",
9201            denied.reason
9202        );
9203    }
9204
9205    #[tokio::test]
9206    async fn submit_order_denies_amount_above_max_order_amount() {
9207        let mut config = test_config("http://127.0.0.1:1".to_string());
9208        config.max_order_amount = Some(999_999_999_999_999); // 0.001 WETH in raw units minus one
9209        let (mut client, _) = swap_client_with_cache(config);
9210        let order = test_market_sell_order(test_pool().instrument_id);
9211        let mut receiver = start_with_events(&mut client);
9212
9213        client.submit_order(submit_order_cmd(&order)).unwrap();
9214
9215        let events = collect_order_events(&mut receiver);
9216        assert_eq!(events.len(), 1);
9217        let OrderEventAny::Denied(denied) = &events[0] else {
9218            panic!("expected OrderDenied, was {:?}", events[0]);
9219        };
9220        assert!(
9221            denied
9222                .reason
9223                .contains("exceeds the configured `max_order_amount`"),
9224            "was: {}",
9225            denied.reason
9226        );
9227    }
9228
9229    #[tokio::test]
9230    async fn submit_order_denies_slippage_param_above_ceiling() {
9231        let (mut client, _) = swap_client_with_cache(test_config("http://127.0.0.1:1".to_string()));
9232        let order = test_market_sell_order(test_pool().instrument_id);
9233        let mut cmd = submit_order_cmd(&order);
9234        cmd.params = Some(serde_json::from_str(r#"{"slippage_bps": 201}"#).unwrap());
9235        let mut receiver = start_with_events(&mut client);
9236
9237        client.submit_order(cmd).unwrap();
9238
9239        let events = collect_order_events(&mut receiver);
9240        assert_eq!(events.len(), 1);
9241        let OrderEventAny::Denied(denied) = &events[0] else {
9242            panic!("expected OrderDenied, was {:?}", events[0]);
9243        };
9244        assert!(
9245            denied
9246                .reason
9247                .contains("exceeds the configured `max_slippage_bps`"),
9248            "was: {}",
9249            denied.reason
9250        );
9251    }
9252
9253    #[tokio::test]
9254    async fn submit_order_denies_pool_without_fee_tier() {
9255        let addr = start_mock_rpc_server(MockRpcState::default()).await;
9256        let mut pool = test_pool();
9257        pool.fee = None;
9258        let cache = Rc::new(RefCell::new(Cache::default()));
9259        cache.borrow_mut().add_pool(pool.clone()).unwrap();
9260        let order = test_market_sell_order(pool.instrument_id);
9261        cache
9262            .borrow_mut()
9263            .add_order(order.clone(), None, None, false)
9264            .unwrap();
9265        let core = ExecutionClientCore::new(
9266            TraderId::from("TRADER-001"),
9267            ClientId::from("BLOCKCHAIN-001"),
9268            *BLOCKCHAIN_VENUE,
9269            OmsType::Netting,
9270            AccountId::from("BLOCKCHAIN-001"),
9271            AccountType::Wallet,
9272            None,
9273            cache,
9274        );
9275        let mut client =
9276            BlockchainExecutionClient::new(core, test_config(format!("http://{addr}"))).unwrap();
9277        let mut receiver = start_with_events(&mut client);
9278
9279        client.submit_order(submit_order_cmd(&order)).unwrap();
9280
9281        let events = collect_order_events(&mut receiver);
9282        assert_eq!(events.len(), 1);
9283        let OrderEventAny::Denied(denied) = &events[0] else {
9284            panic!("expected OrderDenied, was {:?}", events[0]);
9285        };
9286        assert!(
9287            denied.reason.contains("no fee tier"),
9288            "was: {}",
9289            denied.reason
9290        );
9291    }
9292
9293    #[tokio::test]
9294    async fn submit_order_denies_without_live_profiler() {
9295        let addr = start_mock_rpc_server(MockRpcState::default()).await;
9296        let cache = Rc::new(RefCell::new(Cache::default()));
9297        let pool = test_pool();
9298        cache.borrow_mut().add_pool(pool.clone()).unwrap();
9299        let order = test_market_sell_order(pool.instrument_id);
9300        cache
9301            .borrow_mut()
9302            .add_order(order.clone(), None, None, false)
9303            .unwrap();
9304        let core = ExecutionClientCore::new(
9305            TraderId::from("TRADER-001"),
9306            ClientId::from("BLOCKCHAIN-001"),
9307            *BLOCKCHAIN_VENUE,
9308            OmsType::Netting,
9309            AccountId::from("BLOCKCHAIN-001"),
9310            AccountType::Wallet,
9311            None,
9312            cache,
9313        );
9314        let mut client =
9315            BlockchainExecutionClient::new(core, test_config(format!("http://{addr}"))).unwrap();
9316        let mut receiver = start_with_events(&mut client);
9317
9318        client.submit_order(submit_order_cmd(&order)).unwrap();
9319
9320        let events = collect_order_events(&mut receiver);
9321        assert_eq!(events.len(), 1);
9322        let OrderEventAny::Denied(denied) = &events[0] else {
9323            panic!("expected OrderDenied, was {:?}", events[0]);
9324        };
9325        assert!(
9326            denied.reason.contains("No pool profiler"),
9327            "was: {}",
9328            denied.reason
9329        );
9330    }
9331
9332    #[tokio::test]
9333    async fn submit_order_denies_when_not_connected() {
9334        let (mut client, _) = swap_client_with_cache(test_config("http://127.0.0.1:1".to_string()));
9335        let order = test_market_sell_order(test_pool().instrument_id);
9336        let mut receiver = start_with_events(&mut client);
9337
9338        client.submit_order(submit_order_cmd(&order)).unwrap();
9339
9340        let events = collect_order_events(&mut receiver);
9341        assert_eq!(events.len(), 1);
9342        let OrderEventAny::Denied(denied) = &events[0] else {
9343            panic!("expected OrderDenied, was {:?}", events[0]);
9344        };
9345        assert!(
9346            denied.reason.contains("is not connected"),
9347            "was: {}",
9348            denied.reason
9349        );
9350    }
9351
9352    #[tokio::test]
9353    async fn submit_order_denies_without_durable_store() {
9354        let (mut client, _) = swap_client_with_cache(test_config("http://127.0.0.1:1".to_string()));
9355        client.core.set_connected();
9356        let order = test_market_sell_order(test_pool().instrument_id);
9357        let mut receiver = start_with_events(&mut client);
9358
9359        client.submit_order(submit_order_cmd(&order)).unwrap();
9360
9361        let events = collect_order_events(&mut receiver);
9362        assert_eq!(events.len(), 1);
9363        let OrderEventAny::Denied(denied) = &events[0] else {
9364            panic!("expected OrderDenied, was {:?}", events[0]);
9365        };
9366        assert!(
9367            denied.reason.contains("No durable store configured"),
9368            "was: {}",
9369            denied.reason
9370        );
9371    }
9372
9373    #[tokio::test]
9374    async fn submit_order_denies_when_transaction_in_flight() {
9375        let (mut client, _) = swap_client_with_cache(test_config("http://127.0.0.1:1".to_string()));
9376        client.core.set_connected();
9377        *client.in_flight.lock() = Some(InFlightSlot::AwaitingFinality(InFlightTransaction {
9378            intent_id: 1,
9379            nonce: 7,
9380            tx_hash: B256::ZERO,
9381            purpose: TransactionPurpose::Wrap,
9382        }));
9383        let order = test_market_sell_order(test_pool().instrument_id);
9384        let mut receiver = start_with_events(&mut client);
9385
9386        client.submit_order(submit_order_cmd(&order)).unwrap();
9387
9388        let events = collect_order_events(&mut receiver);
9389        assert_eq!(events.len(), 1);
9390        let OrderEventAny::Denied(denied) = &events[0] else {
9391            panic!("expected OrderDenied, was {:?}", events[0]);
9392        };
9393        assert!(
9394            denied.reason.contains("still awaiting finality"),
9395            "was: {}",
9396            denied.reason
9397        );
9398    }
9399
9400    #[tokio::test]
9401    async fn submit_order_denies_without_signer() {
9402        let Some((admin_pool, schema, mut client, _, _)) =
9403            swap_client_with_database("execution_submit_no_signer_test", swap_rpc_state().await)
9404                .await
9405        else {
9406            return;
9407        };
9408        client.signer = None;
9409        let order = test_market_sell_order(test_pool().instrument_id);
9410        let mut receiver = start_with_events(&mut client);
9411
9412        client.submit_order(submit_order_cmd(&order)).unwrap();
9413
9414        let events = collect_order_events(&mut receiver);
9415        assert_eq!(events.len(), 1);
9416        let OrderEventAny::Denied(denied) = &events[0] else {
9417            panic!("expected OrderDenied, was {:?}", events[0]);
9418        };
9419        assert!(
9420            denied.reason.contains("Signer not initialized"),
9421            "was: {}",
9422            denied.reason
9423        );
9424
9425        drop_execution_schema(&admin_pool, &schema).await;
9426    }
9427
9428    #[tokio::test]
9429    async fn submit_order_broadcasts_swap_and_records_client_order_id() {
9430        let Some((admin_pool, schema, mut client, state, _)) =
9431            swap_client_with_database("execution_submit_success_test", swap_rpc_state().await)
9432                .await
9433        else {
9434            return;
9435        };
9436        let order = test_market_sell_order(test_pool().instrument_id);
9437        let mut receiver = start_with_events(&mut client);
9438        let expected_min_out = expected_min_amount_out(50);
9439
9440        client.submit_order(submit_order_cmd(&order)).unwrap();
9441        await_pending_tasks(&client).await;
9442
9443        let events = collect_order_events(&mut receiver);
9444        assert_swap_submitted_and_filled(&events);
9445        let OrderEventAny::Submitted(submitted) = &events[0] else {
9446            panic!("expected OrderSubmitted, was {:?}", events[0]);
9447        };
9448        assert_eq!(submitted.client_order_id, order.client_order_id());
9449
9450        // The recorded broadcast matches the fully signed expected transaction
9451        let (expected_hash, expected_raw) = expected_swap_tx(expected_min_out).await;
9452        let broadcasts: Vec<_> = state
9453            .recorded_requests()
9454            .into_iter()
9455            .filter(|request| request["method"] == "eth_sendRawTransaction")
9456            .collect();
9457        assert_eq!(broadcasts.len(), 1);
9458        assert_eq!(broadcasts[0]["params"][0].as_str().unwrap(), expected_raw);
9459
9460        let record = client
9461            .cache
9462            .get_execution_transaction(42161, &expected_hash.to_string())
9463            .await
9464            .unwrap()
9465            .unwrap();
9466        assert_eq!(record.nonce, 7);
9467        assert_eq!(record.purpose, "swap");
9468        assert_eq!(record.status, "finalized");
9469        assert_eq!(
9470            record.client_order_id.as_deref(),
9471            Some(order.client_order_id().as_str())
9472        );
9473        assert!(client.in_flight.lock().is_none());
9474        let nonce_state = sqlx::query_as::<_, (i64, i64)>(sqlx::AssertSqlSafe(format!(
9475            "SELECT next_canonical_nonce, revision FROM {schema}.execution_verification_nonce"
9476        )))
9477        .fetch_one(&admin_pool)
9478        .await
9479        .unwrap();
9480        let decision_count = sqlx::query_scalar::<_, i64>(sqlx::AssertSqlSafe(format!(
9481            "SELECT COUNT(*) FROM {schema}.execution_verification_decision \
9482             WHERE outcome = 'verified'"
9483        )))
9484        .fetch_one(&admin_pool)
9485        .await
9486        .unwrap();
9487        let connect_decision_count = sqlx::query_scalar::<_, i64>(sqlx::AssertSqlSafe(format!(
9488            "SELECT COUNT(*) FROM {schema}.execution_verification_decision \
9489                 WHERE decision_class = 'connect' AND outcome = 'verified'"
9490        )))
9491        .fetch_one(&admin_pool)
9492        .await
9493        .unwrap();
9494        assert_eq!(nonce_state, (8, 1));
9495        assert_eq!(decision_count, 35);
9496        assert_eq!(connect_decision_count, 1);
9497
9498        drop_execution_schema(&admin_pool, &schema).await;
9499    }
9500
9501    #[tokio::test]
9502    async fn submit_order_pins_every_pre_sign_state_read_to_swap_anchor() {
9503        let Some((admin_pool, schema, mut client, state, _)) =
9504            swap_client_with_database("execution_submit_anchor_reads_test", swap_rpc_state().await)
9505                .await
9506        else {
9507            return;
9508        };
9509        let order = test_market_sell_order(test_pool().instrument_id);
9510        let mut receiver = start_with_events(&mut client);
9511
9512        client.submit_order(submit_order_cmd(&order)).unwrap();
9513        await_pending_tasks(&client).await;
9514
9515        let events = collect_order_events(&mut receiver);
9516        assert_swap_submitted_and_filled(&events);
9517        let requests = state.recorded_requests();
9518        let broadcast_index = requests
9519            .iter()
9520            .position(|request| request["method"] == "eth_sendRawTransaction")
9521            .unwrap();
9522        let pre_broadcast = &requests[..broadcast_index];
9523        let latest_reads: Vec<_> = pre_broadcast
9524            .iter()
9525            .filter(|request| {
9526                request["method"] == "eth_getBlockByNumber" && request["params"][0] == "latest"
9527            })
9528            .collect();
9529        assert_eq!(latest_reads.len(), 3);
9530        assert!(
9531            latest_reads
9532                .iter()
9533                .all(|request| request["params"] == serde_json::json!(["latest", false]))
9534        );
9535
9536        for method in [
9537            "eth_getCode",
9538            "eth_call",
9539            "eth_estimateGas",
9540            "eth_getBalance",
9541        ] {
9542            let pinned: Vec<_> = pre_broadcast
9543                .iter()
9544                .filter(|request| request["method"] == method)
9545                .collect();
9546            assert!(!pinned.is_empty(), "method {method}");
9547            assert_eq!(pinned.len() % 3, 0, "method {method}: {pinned:?}");
9548            assert!(
9549                pinned
9550                    .iter()
9551                    .all(|request| request["params"][1] == FIXTURE_BLOCK_PARAM),
9552                "method {method}: {pinned:?}"
9553            );
9554        }
9555
9556        let numbered_blocks: Vec<_> = pre_broadcast
9557            .iter()
9558            .filter(|request| {
9559                request["method"] == "eth_getBlockByNumber"
9560                    && request["params"][0] == FIXTURE_BLOCK_PARAM
9561            })
9562            .collect();
9563        assert!(!numbered_blocks.is_empty());
9564        assert_eq!(numbered_blocks.len() % 3, 0);
9565        assert!(
9566            numbered_blocks
9567                .iter()
9568                .all(|request| request["params"][1] == false)
9569        );
9570
9571        let chain_ids: Vec<_> = pre_broadcast
9572            .iter()
9573            .filter(|request| request["method"] == "eth_chainId")
9574            .collect();
9575        assert_eq!(chain_ids.len(), 3);
9576        assert!(
9577            chain_ids
9578                .iter()
9579                .all(|request| request["params"] == serde_json::json!([]))
9580        );
9581        let nonces: Vec<_> = pre_broadcast
9582            .iter()
9583            .filter(|request| request["method"] == "eth_getTransactionCount")
9584            .collect();
9585        assert_eq!(nonces.len(), 6);
9586        assert_eq!(
9587            nonces
9588                .iter()
9589                .filter(|request| {
9590                    request["params"] == serde_json::json!([WALLET.to_ascii_lowercase(), "pending"])
9591                })
9592                .count(),
9593            3
9594        );
9595        assert_eq!(
9596            nonces
9597                .iter()
9598                .filter(|request| {
9599                    request["params"]
9600                        == serde_json::json!([WALLET.to_ascii_lowercase(), FIXTURE_BLOCK_PARAM])
9601                })
9602                .count(),
9603            3
9604        );
9605        let priority_fees: Vec<_> = pre_broadcast
9606            .iter()
9607            .filter(|request| request["method"] == "eth_maxPriorityFeePerGas")
9608            .collect();
9609        assert_eq!(priority_fees.len(), 3);
9610        assert!(
9611            priority_fees
9612                .iter()
9613                .all(|request| request["params"] == serde_json::json!([]))
9614        );
9615
9616        let (_, expected_raw) = expected_swap_tx(expected_min_amount_out(50)).await;
9617        assert_eq!(
9618            requests[broadcast_index]["params"][0].as_str().unwrap(),
9619            expected_raw
9620        );
9621        let created_block: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
9622            "SELECT created_block FROM {schema}.execution_intent"
9623        )))
9624        .fetch_one(&admin_pool)
9625        .await
9626        .unwrap();
9627        assert_eq!(created_block, i64::try_from(FIXTURE_BLOCK).unwrap());
9628
9629        drop_execution_schema(&admin_pool, &schema).await;
9630    }
9631
9632    #[tokio::test]
9633    async fn submit_order_denies_changed_swap_anchor_before_signing() {
9634        let canonical = fixture_block_response(FIXTURE_BLOCK, B256::from([0x11; 32]));
9635        let changed = fixture_block_response(FIXTURE_BLOCK, B256::from([0x44; 32]));
9636        let state = swap_rpc_state().await.with_parameter_response_sequence(
9637            "eth_getBlockByNumber",
9638            FIXTURE_BLOCK_PARAM,
9639            &[
9640                &canonical, &canonical, &canonical, &canonical, &canonical, &canonical, &canonical,
9641                &canonical, &canonical, &canonical, &canonical, &canonical, &canonical, &canonical,
9642                &canonical, &changed, &changed, &changed,
9643            ],
9644        );
9645        let Some((admin_pool, schema, mut client, state, _)) =
9646            swap_client_with_database("execution_submit_anchor_change_test", state).await
9647        else {
9648            return;
9649        };
9650        let order = test_market_sell_order(test_pool().instrument_id);
9651        let mut receiver = start_with_events(&mut client);
9652
9653        client.submit_order(submit_order_cmd(&order)).unwrap();
9654        await_pending_tasks(&client).await;
9655
9656        let events = collect_order_events(&mut receiver);
9657        assert_eq!(events.len(), 1, "was: {events:?}");
9658        let OrderEventAny::Denied(denied) = &events[0] else {
9659            panic!("expected OrderDenied, was {:?}", events[0]);
9660        };
9661        assert!(
9662            denied
9663                .reason
9664                .contains("pre-sign checkpoint reread verification disagreed"),
9665            "was: {}",
9666            denied.reason
9667        );
9668        assert_eq!(
9669            execution_intent_markers(&admin_pool, &schema).await,
9670            vec![("swap".into(), "recoverable".into(), false, false)]
9671        );
9672        let signed_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
9673            "SELECT COUNT(*) FROM {schema}.execution_transaction_hash"
9674        )))
9675        .fetch_one(&admin_pool)
9676        .await
9677        .unwrap();
9678        assert_eq!(signed_count, 0);
9679        assert!(
9680            state
9681                .recorded_requests()
9682                .iter()
9683                .all(|request| { request["method"] != "eth_sendRawTransaction" })
9684        );
9685        assert!(client.in_flight.lock().is_none());
9686
9687        drop_execution_schema(&admin_pool, &schema).await;
9688    }
9689
9690    #[tokio::test]
9691    async fn submit_order_keeps_verified_swap_anchor_when_latest_advances() {
9692        let canonical = fixture_block_response(FIXTURE_BLOCK, B256::from([0x11; 32]));
9693        let newer = fixture_block_response(FIXTURE_BLOCK + 1, B256::from([0x44; 32]));
9694        let state = swap_rpc_state().await.with_parameter_response_sequence(
9695            "eth_getBlockByNumber",
9696            "latest",
9697            &[&canonical, &canonical, &canonical, &newer, &newer, &newer],
9698        );
9699        let Some((admin_pool, schema, mut client, state, _)) =
9700            swap_client_with_database("execution_submit_advancing_head_test", state).await
9701        else {
9702            return;
9703        };
9704        let order = test_market_sell_order(test_pool().instrument_id);
9705        let mut receiver = start_with_events(&mut client);
9706
9707        client.submit_order(submit_order_cmd(&order)).unwrap();
9708        await_pending_tasks(&client).await;
9709
9710        let events = collect_order_events(&mut receiver);
9711        assert_swap_submitted_and_filled(&events);
9712        let latest_reads = state
9713            .recorded_requests()
9714            .iter()
9715            .filter(|request| {
9716                request["method"] == "eth_getBlockByNumber" && request["params"][0] == "latest"
9717            })
9718            .count();
9719        assert_eq!(latest_reads, 3);
9720
9721        drop_execution_schema(&admin_pool, &schema).await;
9722    }
9723
9724    #[tokio::test]
9725    async fn submit_order_denies_changed_quote_watermark_before_signing() {
9726        let watermark_number = FIXTURE_BLOCK;
9727        let watermark_hash = B256::from([0x11; 32]);
9728        let canonical = fixture_block_response(watermark_number, watermark_hash);
9729        let changed = fixture_block_response(watermark_number, B256::from([0x66; 32]));
9730        let watermark_param = format!("0x{watermark_number:x}");
9731        let min_amount_out = expected_min_amount_out(50);
9732        let (tx_hash, _) = expected_swap_tx(min_amount_out).await;
9733        let head = finalized_swap_block(tx_hash, min_amount_out);
9734        let state = swap_rpc_state()
9735            .await
9736            .with_response("eth_getBlockByNumber", &head)
9737            .with_parameter_response_sequence(
9738                "eth_getBlockByNumber",
9739                &watermark_param,
9740                &[
9741                    &canonical, &canonical, &canonical, &canonical, &canonical, &canonical,
9742                    &canonical, &canonical, &canonical, &canonical, &canonical, &canonical,
9743                    &changed, &changed, &changed,
9744                ],
9745            );
9746        let Some((admin_pool, schema, mut client, state, cache)) =
9747            swap_client_with_database("execution_submit_watermark_change_test", state).await
9748        else {
9749            return;
9750        };
9751        let pool = test_pool();
9752        cache
9753            .borrow_mut()
9754            .add_pool_profiler(test_profiler_at_block(
9755                &pool,
9756                watermark_number,
9757                &watermark_hash.to_string(),
9758            ))
9759            .unwrap();
9760        let order = test_market_sell_order(pool.instrument_id);
9761        let mut receiver = start_with_events(&mut client);
9762
9763        client.submit_order(submit_order_cmd(&order)).unwrap();
9764        await_pending_tasks(&client).await;
9765
9766        let events = collect_order_events(&mut receiver);
9767        assert_eq!(events.len(), 1, "was: {events:?}");
9768        let OrderEventAny::Denied(denied) = &events[0] else {
9769            panic!("expected OrderDenied, was {:?}", events[0]);
9770        };
9771        assert!(
9772            denied.reason.contains("changed before signing"),
9773            "was: {}",
9774            denied.reason
9775        );
9776        assert_eq!(
9777            execution_intent_markers(&admin_pool, &schema).await,
9778            vec![("swap".into(), "recoverable".into(), false, false)]
9779        );
9780        let signed_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
9781            "SELECT COUNT(*) FROM {schema}.execution_transaction_hash"
9782        )))
9783        .fetch_one(&admin_pool)
9784        .await
9785        .unwrap();
9786        assert_eq!(signed_count, 0);
9787        assert!(
9788            state
9789                .recorded_requests()
9790                .iter()
9791                .all(|request| { request["method"] != "eth_sendRawTransaction" })
9792        );
9793        assert!(client.in_flight.lock().is_none());
9794
9795        drop_execution_schema(&admin_pool, &schema).await;
9796    }
9797
9798    #[tokio::test]
9799    async fn changed_swap_anchor_retains_ownership_when_recovery_commit_fails() {
9800        let canonical = fixture_block_response(FIXTURE_BLOCK, B256::from([0x11; 32]));
9801        let changed = fixture_block_response(FIXTURE_BLOCK, B256::from([0x44; 32]));
9802        let state = swap_rpc_state().await.with_parameter_response_sequence(
9803            "eth_getBlockByNumber",
9804            FIXTURE_BLOCK_PARAM,
9805            &[
9806                &canonical, &canonical, &canonical, &canonical, &canonical, &canonical, &canonical,
9807                &canonical, &canonical, &canonical, &canonical, &canonical, &canonical, &canonical,
9808                &canonical, &changed, &changed, &changed,
9809            ],
9810        );
9811        let Some((admin_pool, schema, mut client, state, _)) =
9812            swap_client_with_database("execution_submit_anchor_recovery_fail_test", state).await
9813        else {
9814            return;
9815        };
9816        install_recoverable_commit_rejection(&admin_pool, &schema).await;
9817        let order = test_market_sell_order(test_pool().instrument_id);
9818        let mut receiver = start_with_events(&mut client);
9819
9820        client.submit_order(submit_order_cmd(&order)).unwrap();
9821        await_pending_tasks(&client).await;
9822
9823        let events = collect_order_events(&mut receiver);
9824        assert_eq!(events.len(), 1, "was: {events:?}");
9825        let OrderEventAny::Denied(denied) = &events[0] else {
9826            panic!("expected OrderDenied, was {:?}", events[0]);
9827        };
9828        assert!(
9829            denied
9830                .reason
9831                .contains("pre-sign checkpoint reread verification disagreed"),
9832            "was: {}",
9833            denied.reason
9834        );
9835        assert_eq!(
9836            execution_intent_markers(&admin_pool, &schema).await,
9837            vec![("swap".into(), "prepared".into(), false, true)]
9838        );
9839        let signed_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
9840            "SELECT COUNT(*) FROM {schema}.execution_transaction_hash"
9841        )))
9842        .fetch_one(&admin_pool)
9843        .await
9844        .unwrap();
9845        assert_eq!(signed_count, 0);
9846        assert!(
9847            state
9848                .recorded_requests()
9849                .iter()
9850                .all(|request| { request["method"] != "eth_sendRawTransaction" })
9851        );
9852        assert!(matches!(
9853            *client.in_flight.lock(),
9854            Some(InFlightSlot::Preparing(TransactionPurpose::Swap))
9855        ));
9856
9857        drop_execution_schema(&admin_pool, &schema).await;
9858    }
9859
9860    #[rstest]
9861    #[case::filled("execution_inflight_finality_fill_test", false)]
9862    #[case::reverted("execution_inflight_finality_revert_test", true)]
9863    #[tokio::test]
9864    async fn swap_awaiting_finality_survives_inflight_exhaustion(
9865        #[case] test_name: &str,
9866        #[case] reverted: bool,
9867    ) {
9868        let min_amount_out = expected_min_amount_out(50);
9869        let (expected_hash, _) = expected_swap_tx(min_amount_out).await;
9870
9871        let state = if reverted {
9872            let block = finalized_swap_block(expected_hash, min_amount_out);
9873            let receipt = receipt_with_transaction_hash(RECEIPT_REVERTED, expected_hash);
9874            with_finalized_identity(
9875                swap_rpc_state()
9876                    .await
9877                    .with_response("eth_getTransactionReceipt", &receipt),
9878                &block,
9879                &receipt,
9880            )
9881        } else {
9882            finalized_swap_rpc_state(expected_hash, min_amount_out)
9883        };
9884
9885        let Some((admin_pool, schema, mut client, _state, cache)) =
9886            swap_client_with_database(test_name, state).await
9887        else {
9888            return;
9889        };
9890
9891        let order = test_market_sell_order(test_pool().instrument_id);
9892        let client_order_id = order.client_order_id();
9893        let mut receiver = start_with_events(&mut client);
9894
9895        let plan = client
9896            .prepare_swap(&submit_order_cmd(&order), &order)
9897            .unwrap();
9898        execute_swap(
9899            plan,
9900            client.transaction_executor().unwrap(),
9901            client.emitter.clone(),
9902            client.transaction_limits.max_quote_age_blocks,
9903            client.transaction_limits.deadline_seconds,
9904        )
9905        .await
9906        .unwrap();
9907        let swap_events = collect_order_events(&mut receiver);
9908        assert_eq!(swap_events.len(), 2, "was: {swap_events:?}");
9909        assert!(matches!(&swap_events[0], OrderEventAny::Submitted(_)));
9910
9911        let clock: Rc<RefCell<dyn Clock>> = Rc::new(RefCell::new(VirtualClock::new()));
9912        let config = ExecutionManagerConfig::default();
9913        let max_retries = config.inflight_max_retries;
9914        let mut manager = ExecutionManager::new(clock, Rc::clone(&cache), config).unwrap();
9915
9916        // The open-order check registers the client's retention requirement, as LiveNode does
9917        assert!(manager.check_open_orders(&[&client]).await.is_empty());
9918        cache.borrow_mut().update_order(&swap_events[0]).unwrap();
9919        tokio::time::pause();
9920        manager.register_submission(order.init_event(), Some(client.client_id()));
9921
9922        // Hold the finality outcome until well past the default in-flight budget
9923        let mut inflight_events = Vec::new();
9924        let mut queries_sent = 0;
9925
9926        for _ in 0..60 {
9927            tokio::time::advance(Duration::from_secs(1)).await;
9928            let result = manager.check_inflight_orders();
9929            inflight_events.extend(result.events);
9930
9931            for query in result.queries {
9932                let TradingCommand::QueryOrder(query) = query else {
9933                    panic!("expected QueryOrder, was {query:?}");
9934                };
9935
9936                client.query_order(query).unwrap();
9937                queries_sent += 1;
9938            }
9939        }
9940
9941        let exhaustions = manager.take_submission_recovery_exhaustions();
9942        let status_before_finality = cache.borrow().order(&client_order_id).unwrap().status();
9943        cache.borrow_mut().update_order(&swap_events[1]).unwrap();
9944        tokio::time::resume();
9945        drop_execution_schema(&admin_pool, &schema).await;
9946
9947        assert!(inflight_events.is_empty(), "was: {inflight_events:?}");
9948        assert_eq!(queries_sent, max_retries - 1);
9949        assert!(collect_order_events(&mut receiver).is_empty());
9950        assert_eq!(
9951            exhaustions,
9952            vec![SubmissionRecoveryExhausted {
9953                trader_id: order.trader_id(),
9954                client_id: Some(client.client_id()),
9955                strategy_id: order.strategy_id(),
9956                instrument_id: order.instrument_id(),
9957                client_order_id,
9958                source: SubmissionRecoverySource::Inflight,
9959                retry_count: max_retries,
9960                ts_event: UnixNanos::default(),
9961            }]
9962        );
9963        assert_eq!(status_before_finality, OrderStatus::Submitted);
9964        let cache = cache.borrow();
9965        let resolved = cache.order(&client_order_id).unwrap();
9966
9967        if reverted {
9968            let OrderEventAny::Rejected(rejected) = &swap_events[1] else {
9969                panic!("expected OrderRejected, was {:?}", swap_events[1]);
9970            };
9971
9972            assert_eq!(
9973                rejected.reason.as_str(),
9974                format!("Transaction {expected_hash} reverted on-chain")
9975            );
9976            assert!(!rejected.reconciliation);
9977            assert_eq!(resolved.status(), OrderStatus::Rejected);
9978            assert_eq!(
9979                resolved.filled_qty(),
9980                Quantity::zero(order.quantity().precision)
9981            );
9982        } else {
9983            assert!(matches!(&swap_events[1], OrderEventAny::Filled(_)));
9984            assert_eq!(resolved.status(), OrderStatus::Filled);
9985            assert_eq!(resolved.filled_qty(), order.quantity());
9986        }
9987    }
9988
9989    #[tokio::test]
9990    async fn finalized_swap_emits_exact_fill_once_and_refreshes_wallet() {
9991        let min_amount_out = expected_min_amount_out(50);
9992        let (expected_hash, _) = expected_swap_tx(min_amount_out).await;
9993        let state = finalized_swap_rpc_state(expected_hash, min_amount_out);
9994        let Some((admin_pool, schema, mut client, state, _)) =
9995            swap_client_with_database("execution_submit_fill_test", state).await
9996        else {
9997            return;
9998        };
9999        let order = test_market_sell_order(test_pool().instrument_id);
10000        let mut receiver = start_with_events(&mut client);
10001
10002        let plan = client
10003            .prepare_swap(&submit_order_cmd(&order), &order)
10004            .unwrap();
10005        execute_swap(
10006            plan,
10007            client.transaction_executor().unwrap(),
10008            client.emitter.clone(),
10009            client.transaction_limits.max_quote_age_blocks,
10010            client.transaction_limits.deadline_seconds,
10011        )
10012        .await
10013        .unwrap();
10014
10015        let (nonce, wallet_address, transaction_to, transaction_input, transaction_value): (
10016            i64,
10017            String,
10018            String,
10019            String,
10020            String,
10021        ) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
10022            "SELECT nonce, wallet_address, transaction_to, transaction_input, transaction_value \
10023             FROM {schema}.execution_intent"
10024        )))
10025        .fetch_one(&admin_pool)
10026        .await
10027        .unwrap();
10028        let finalized_block = client
10029            .http_rpc_client
10030            .block_by_number(FIXTURE_BLOCK + 1, true)
10031            .await
10032            .unwrap();
10033        let finalized_transaction = finalized_block
10034            .transactions
10035            .iter()
10036            .find(|transaction| transaction.hash == expected_hash)
10037            .unwrap();
10038        assert_eq!(finalized_transaction.from.to_string(), wallet_address);
10039        assert_eq!(finalized_transaction.nonce, u64::try_from(nonce).unwrap());
10040        assert_eq!(
10041            finalized_transaction.to,
10042            Some(Address::from_str(&transaction_to).unwrap())
10043        );
10044        assert_eq!(
10045            finalized_transaction.input.as_ref(),
10046            hex::decode(transaction_input.strip_prefix("0x").unwrap()).unwrap()
10047        );
10048        assert_eq!(
10049            finalized_transaction.value,
10050            U256::from_str(&transaction_value).unwrap()
10051        );
10052
10053        let mut order_events = Vec::new();
10054        let mut account_states = Vec::new();
10055
10056        while let Ok(event) = receiver.try_recv() {
10057            match event {
10058                ExecutionEvent::Order(event) => order_events.push(event),
10059                ExecutionEvent::Account(state) => account_states.push(state),
10060                other => panic!("unexpected execution event: {other:?}"),
10061            }
10062        }
10063        assert_eq!(order_events.len(), 2, "was: {order_events:?}");
10064        assert!(matches!(&order_events[0], OrderEventAny::Submitted(_)));
10065        let OrderEventAny::Filled(fill) = &order_events[1] else {
10066            panic!("expected OrderFilled, was {:?}", order_events[1]);
10067        };
10068        let expected_commission = Money::from_u256(
10069            U256::from(50_112_u64) * U256::from(100_000_000_u64),
10070            test_pool().chain.native_currency(),
10071        )
10072        .unwrap();
10073        assert_eq!(fill.client_order_id, order.client_order_id());
10074        assert_eq!(fill.venue_order_id.as_str(), expected_hash.to_string());
10075        assert_eq!(fill.order_side, OrderSide::Sell);
10076        assert_eq!(fill.last_qty, Quantity::from("0.001"));
10077        assert_eq!(fill.last_px, Price::from("1000"));
10078        assert_eq!(fill.currency.code, "USDC");
10079        assert_eq!(fill.commission, Some(expected_commission));
10080        assert_eq!(fill.liquidity_side, LiquiditySide::Taker);
10081        assert_eq!(account_states.len(), 1);
10082        let account_state = &account_states[0];
10083        assert_eq!(account_state.account_id, AccountId::from("BLOCKCHAIN-001"));
10084        assert_eq!(account_state.account_type, AccountType::Wallet);
10085        assert_eq!(account_state.base_currency, None);
10086        assert_eq!(account_state.balances.len(), 3);
10087        assert!(account_state.margins.is_empty());
10088        assert!(account_state.is_reported);
10089        assert_eq!(
10090            account_state.balances,
10091            client.wallet_balance.lock().as_account_balances().unwrap()
10092        );
10093
10094        let (fill_emitted, active): (bool, bool) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
10095            "SELECT fill_emitted, active FROM {schema}.execution_intent"
10096        )))
10097        .fetch_one(&admin_pool)
10098        .await
10099        .unwrap();
10100        assert!(fill_emitted);
10101        assert!(!active);
10102
10103        let database = client.cache.database.as_ref().unwrap().clone();
10104        let payload_keys = client.payload_keys.clone();
10105        let restart_config = client.config.clone();
10106        drop(client);
10107        let (mut restarted, _) = swap_client_with_cache(restart_config);
10108        restarted.cache.database = Some(database);
10109        restarted.payload_keys = payload_keys;
10110        restarted.signer = Some(Arc::new(
10111            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
10112        ));
10113        let mut restart_receiver = start_with_events(&mut restarted);
10114        restarted.reconcile_unresolved_execution().await.unwrap();
10115        restarted.reconcile_unresolved_execution().await.unwrap();
10116        assert!(collect_order_events(&mut restart_receiver).is_empty());
10117        let requests = state.recorded_requests();
10118        assert_eq!(
10119            requests
10120                .iter()
10121                .filter(|request| request["method"] == "eth_sendRawTransaction")
10122                .count(),
10123            1
10124        );
10125        assert_eq!(
10126            requests
10127                .iter()
10128                .filter(|request| {
10129                    request["method"] == "eth_call"
10130                        && request["params"][0]["data"]
10131                            .as_str()
10132                            .is_some_and(|data| data.starts_with(BALANCE_OF_SELECTOR))
10133                })
10134                .count(),
10135            9
10136        );
10137        assert_eq!(
10138            requests
10139                .iter()
10140                .filter(|request| request["method"] == "eth_getBalance")
10141                .count(),
10142            6
10143        );
10144
10145        drop_execution_schema(&admin_pool, &schema).await;
10146    }
10147
10148    #[tokio::test]
10149    async fn restart_emits_committed_swap_from_verified_inclusion_header() {
10150        let min_amount_out = expected_min_amount_out(50);
10151        let (expected_hash, _) = expected_swap_tx(min_amount_out).await;
10152        let state = finalized_swap_rpc_state(expected_hash, min_amount_out);
10153        let Some((admin_pool, schema, mut client, _, _)) =
10154            swap_client_with_database("execution_committed_fill_restart_test", state).await
10155        else {
10156            return;
10157        };
10158        let order = test_market_sell_order(test_pool().instrument_id);
10159        let mut receiver = start_with_events(&mut client);
10160        let plan = client
10161            .prepare_swap(&submit_order_cmd(&order), &order)
10162            .unwrap();
10163
10164        execute_swap(
10165            plan,
10166            client.transaction_executor().unwrap(),
10167            client.emitter.clone(),
10168            client.transaction_limits.max_quote_age_blocks,
10169            client.transaction_limits.deadline_seconds,
10170        )
10171        .await
10172        .unwrap();
10173        assert_swap_submitted_and_filled(&collect_order_events(&mut receiver));
10174        let ancestry_range: (i64, i64) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
10175            "SELECT height_start, height_end FROM {schema}.execution_verification_decision \
10176             WHERE decision_class = 'finality' AND read_class = 'numbered_block' \
10177               AND height_end > height_start"
10178        )))
10179        .fetch_one(&admin_pool)
10180        .await
10181        .unwrap();
10182        assert_eq!(
10183            ancestry_range,
10184            ((FIXTURE_BLOCK + 1) as i64, (FIXTURE_BLOCK + 2) as i64)
10185        );
10186
10187        sqlx::query(sqlx::AssertSqlSafe(format!(
10188            "UPDATE {schema}.execution_intent SET fill_emitted = FALSE, active = TRUE"
10189        )))
10190        .execute(&admin_pool)
10191        .await
10192        .unwrap();
10193        let database = client.cache.database.as_ref().unwrap().clone();
10194        let payload_keys = client.payload_keys.clone();
10195        let restart_config = client.config.clone();
10196        drop(client);
10197        let (mut restarted, _) = swap_client_with_cache(restart_config);
10198        restarted.cache.database = Some(database);
10199        restarted.payload_keys = payload_keys;
10200        restarted.signer = Some(Arc::new(
10201            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
10202        ));
10203        let mut restart_receiver = start_with_events(&mut restarted);
10204
10205        restarted.reconcile_unresolved_execution().await.unwrap();
10206
10207        let events = collect_order_events(&mut restart_receiver);
10208        let (fill_emitted, active): (bool, bool) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
10209            "SELECT fill_emitted, active FROM {schema}.execution_intent"
10210        )))
10211        .fetch_one(&admin_pool)
10212        .await
10213        .unwrap();
10214        assert_eq!(events.len(), 1, "was: {events:?}");
10215        assert!(matches!(&events[0], OrderEventAny::Filled(_)));
10216        assert!(fill_emitted);
10217        assert!(!active);
10218
10219        drop_execution_schema(&admin_pool, &schema).await;
10220    }
10221
10222    #[tokio::test]
10223    async fn finalized_swap_ignores_unrelated_swap_logs() {
10224        let min_amount_out = expected_min_amount_out(50);
10225        let (expected_hash, _) = expected_swap_tx(min_amount_out).await;
10226        let receipt = finalized_swap_receipt_with_unrelated_swap(expected_hash);
10227        let state = finalized_swap_rpc_state(expected_hash, min_amount_out)
10228            .with_response("eth_getTransactionReceipt", &receipt);
10229        let Some((admin_pool, schema, mut client, _, _)) =
10230            swap_client_with_database("execution_submit_unrelated_log_test", state).await
10231        else {
10232            return;
10233        };
10234        let order = test_market_sell_order(test_pool().instrument_id);
10235        let mut receiver = start_with_events(&mut client);
10236
10237        client.submit_order(submit_order_cmd(&order)).unwrap();
10238        await_pending_tasks(&client).await;
10239
10240        let events = collect_order_events(&mut receiver);
10241        assert_swap_submitted_and_filled(&events);
10242
10243        drop_execution_schema(&admin_pool, &schema).await;
10244    }
10245
10246    #[tokio::test]
10247    async fn prepare_swap_buy_accepts_quote_spend_exact_boundary() {
10248        let amount_in = expected_buy_amount_in();
10249        let min_amount_out = expected_buy_min_amount_out(50);
10250        let (expected_hash, _) = expected_buy_swap_tx(min_amount_out, amount_in).await;
10251        let state = finalized_buy_swap_rpc_state(expected_hash, min_amount_out, amount_in);
10252        let max_amount = amount_in.to_string();
10253        let Some((admin_pool, schema, client, _, cache)) = swap_client_with_database_config(
10254            "execution_prepare_buy_test",
10255            state,
10256            move |http_rpc_url| {
10257                let mut config = buy_test_config(http_rpc_url);
10258                config.quote_spend_limits.as_mut().unwrap()[0].max_amount = max_amount;
10259                config
10260            },
10261        )
10262        .await
10263        else {
10264            return;
10265        };
10266        let order = test_market_buy_order(test_pool().instrument_id);
10267        cache
10268            .borrow_mut()
10269            .add_order(order.clone(), None, None, true)
10270            .unwrap();
10271
10272        let plan = client
10273            .prepare_swap(&submit_order_cmd(&order), &order)
10274            .unwrap();
10275
10276        assert_eq!(plan.token_in, USDC_ADDRESS);
10277        assert_eq!(plan.token_out, WETH_ADDRESS);
10278        assert_eq!(plan.amount_in, amount_in);
10279        assert_eq!(plan.min_amount_out, min_amount_out);
10280        assert_ne!(plan.token_in, WETH_ADDRESS);
10281
10282        drop_execution_schema(&admin_pool, &schema).await;
10283    }
10284
10285    #[tokio::test]
10286    async fn submit_order_broadcasts_buy_swap() {
10287        let amount_in = expected_buy_amount_in();
10288        let min_amount_out = expected_buy_min_amount_out(50);
10289        let (expected_hash, expected_raw) = expected_buy_swap_tx(min_amount_out, amount_in).await;
10290        let state = finalized_buy_swap_rpc_state(expected_hash, min_amount_out, amount_in);
10291        let Some((admin_pool, schema, mut client, state, cache)) =
10292            swap_client_with_buy_database("execution_submit_buy_success_test", state).await
10293        else {
10294            return;
10295        };
10296        let order = test_market_buy_order(test_pool().instrument_id);
10297        cache
10298            .borrow_mut()
10299            .add_order(order.clone(), None, None, true)
10300            .unwrap();
10301        let mut receiver = start_with_events(&mut client);
10302
10303        client.submit_order(submit_order_cmd(&order)).unwrap();
10304        await_pending_tasks(&client).await;
10305
10306        let events = collect_order_events(&mut receiver);
10307        assert_swap_submitted_and_filled(&events);
10308        let broadcasts: Vec<_> = state
10309            .recorded_requests()
10310            .into_iter()
10311            .filter(|request| request["method"] == "eth_sendRawTransaction")
10312            .collect();
10313        assert_eq!(broadcasts.len(), 1);
10314        assert_eq!(broadcasts[0]["params"][0].as_str().unwrap(), expected_raw);
10315
10316        drop_execution_schema(&admin_pool, &schema).await;
10317    }
10318
10319    #[tokio::test]
10320    async fn finalized_buy_swap_emits_fill_from_output_leg() {
10321        let amount_in = expected_buy_amount_in();
10322        let min_amount_out = expected_buy_min_amount_out(50);
10323        let (expected_hash, _) = expected_buy_swap_tx(min_amount_out, amount_in).await;
10324        let state = finalized_buy_swap_rpc_state(expected_hash, min_amount_out, amount_in);
10325        let Some((admin_pool, schema, mut client, state, cache)) =
10326            swap_client_with_buy_database("execution_submit_buy_fill_test", state).await
10327        else {
10328            return;
10329        };
10330        let order = test_market_buy_order(test_pool().instrument_id);
10331        cache
10332            .borrow_mut()
10333            .add_order(order.clone(), None, None, true)
10334            .unwrap();
10335        let mut receiver = start_with_events(&mut client);
10336        let plan = client
10337            .prepare_swap(&submit_order_cmd(&order), &order)
10338            .unwrap();
10339
10340        execute_swap(
10341            plan,
10342            client.transaction_executor().unwrap(),
10343            client.emitter.clone(),
10344            client.transaction_limits.max_quote_age_blocks,
10345            client.transaction_limits.deadline_seconds,
10346        )
10347        .await
10348        .unwrap();
10349
10350        let mut order_events = Vec::new();
10351
10352        while let Ok(event) = receiver.try_recv() {
10353            if let ExecutionEvent::Order(event) = event {
10354                order_events.push(event);
10355            }
10356        }
10357        assert_eq!(order_events.len(), 2, "was: {order_events:?}");
10358        assert!(matches!(&order_events[0], OrderEventAny::Submitted(_)));
10359        let OrderEventAny::Filled(fill) = &order_events[1] else {
10360            panic!("expected OrderFilled, was {:?}", order_events[1]);
10361        };
10362        let expected_commission = Money::from_u256(
10363            U256::from(50_112_u64) * U256::from(100_000_000_u64),
10364            test_pool().chain.native_currency(),
10365        )
10366        .unwrap();
10367        assert_eq!(fill.client_order_id, order.client_order_id());
10368        assert_eq!(fill.venue_order_id.as_str(), expected_hash.to_string());
10369        assert_eq!(fill.order_side, OrderSide::Buy);
10370        assert_eq!(fill.last_qty, Quantity::from("0.001"));
10371        assert_eq!(fill.currency.code, "USDC");
10372        assert_eq!(fill.commission, Some(expected_commission));
10373        assert_eq!(fill.liquidity_side, LiquiditySide::Taker);
10374        assert_eq!(
10375            Money::from_decimal(
10376                fill.last_qty.as_decimal() * fill.last_px.as_decimal(),
10377                fill.currency,
10378            )
10379            .unwrap(),
10380            Money::from_u256(amount_in, fill.currency).unwrap()
10381        );
10382
10383        let (fill_emitted, active): (bool, bool) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
10384            "SELECT fill_emitted, active FROM {schema}.execution_intent"
10385        )))
10386        .fetch_one(&admin_pool)
10387        .await
10388        .unwrap();
10389        assert!(fill_emitted);
10390        assert!(!active);
10391
10392        let database = client.cache.database.as_ref().unwrap().clone();
10393        let payload_keys = client.payload_keys.clone();
10394        let restart_config = client.config.clone();
10395        drop(client);
10396        let (mut restarted, _) = swap_client_with_cache(restart_config);
10397        restarted.cache.database = Some(database);
10398        restarted.payload_keys = payload_keys;
10399        restarted.signer = Some(Arc::new(
10400            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
10401        ));
10402        let mut restart_receiver = start_with_events(&mut restarted);
10403        restarted.reconcile_unresolved_execution().await.unwrap();
10404        restarted.reconcile_unresolved_execution().await.unwrap();
10405        assert!(collect_order_events(&mut restart_receiver).is_empty());
10406        assert_eq!(
10407            state
10408                .recorded_requests()
10409                .iter()
10410                .filter(|request| request["method"] == "eth_sendRawTransaction")
10411                .count(),
10412            1
10413        );
10414
10415        drop_execution_schema(&admin_pool, &schema).await;
10416    }
10417
10418    #[tokio::test]
10419    async fn finalized_buy_swap_cancels_remainder_when_output_is_short() {
10420        let amount_in = expected_buy_amount_in();
10421        let min_amount_out = expected_buy_min_amount_out(50);
10422        let (expected_hash, _) = expected_buy_swap_tx(min_amount_out, amount_in).await;
10423        let short_base_out = min_amount_out;
10424        let mut state = finalized_buy_swap_rpc_state(expected_hash, min_amount_out, amount_in);
10425        state = state.with_response(
10426            "eth_getTransactionReceipt",
10427            &finalized_buy_swap_receipt_with_base_out(expected_hash, amount_in, short_base_out),
10428        );
10429        let Some((admin_pool, schema, mut client, _, cache)) =
10430            swap_client_with_buy_database("execution_submit_buy_short_fill_test", state).await
10431        else {
10432            return;
10433        };
10434        let order = test_market_buy_order(test_pool().instrument_id);
10435        cache
10436            .borrow_mut()
10437            .add_order(order.clone(), None, None, true)
10438            .unwrap();
10439        let mut receiver = start_with_events(&mut client);
10440        let plan = client
10441            .prepare_swap(&submit_order_cmd(&order), &order)
10442            .unwrap();
10443
10444        execute_swap(
10445            plan,
10446            client.transaction_executor().unwrap(),
10447            client.emitter.clone(),
10448            client.transaction_limits.max_quote_age_blocks,
10449            client.transaction_limits.deadline_seconds,
10450        )
10451        .await
10452        .unwrap();
10453
10454        let events = collect_order_events(&mut receiver);
10455        assert_eq!(events.len(), 3, "was: {events:?}");
10456        assert!(matches!(&events[0], OrderEventAny::Submitted(_)));
10457        let OrderEventAny::Filled(fill) = &events[1] else {
10458            panic!("expected OrderFilled, was {:?}", events[1]);
10459        };
10460        let expected_qty = raw_amount_to_quantity(short_base_out, 18).unwrap();
10461        assert_eq!(fill.order_side, OrderSide::Buy);
10462        assert_eq!(fill.last_qty, expected_qty);
10463        assert!(fill.last_qty < order.quantity());
10464        let OrderEventAny::Canceled(canceled) = &events[2] else {
10465            panic!("expected OrderCanceled, was {:?}", events[2]);
10466        };
10467        assert_eq!(canceled.client_order_id, order.client_order_id());
10468        assert_eq!(
10469            canceled.venue_order_id.as_ref().map(VenueOrderId::as_str),
10470            Some(expected_hash.to_string().as_str())
10471        );
10472
10473        drop_execution_schema(&admin_pool, &schema).await;
10474    }
10475
10476    #[tokio::test]
10477    async fn finalized_buy_swap_reports_full_output_when_above_order_quantity() {
10478        let amount_in = expected_buy_amount_in();
10479        let min_amount_out = expected_buy_min_amount_out(50);
10480        let (expected_hash, _) = expected_buy_swap_tx(min_amount_out, amount_in).await;
10481        let overshoot_base_out = expected_buy_base_amount() * U256::from(2) + U256::from(44);
10482        let mut state = finalized_buy_swap_rpc_state(expected_hash, min_amount_out, amount_in);
10483        state = state.with_response(
10484            "eth_getTransactionReceipt",
10485            &finalized_buy_swap_receipt_with_base_out(expected_hash, amount_in, overshoot_base_out),
10486        );
10487        let Some((admin_pool, schema, mut client, _, cache)) =
10488            swap_client_with_buy_database("execution_submit_buy_overshoot_fill_test", state).await
10489        else {
10490            return;
10491        };
10492        let order = test_market_buy_order(test_pool().instrument_id);
10493        cache
10494            .borrow_mut()
10495            .add_order(order.clone(), None, None, true)
10496            .unwrap();
10497        let mut receiver = start_with_events(&mut client);
10498        let plan = client
10499            .prepare_swap(&submit_order_cmd(&order), &order)
10500            .unwrap();
10501
10502        execute_swap(
10503            plan,
10504            client.transaction_executor().unwrap(),
10505            client.emitter.clone(),
10506            client.transaction_limits.max_quote_age_blocks,
10507            client.transaction_limits.deadline_seconds,
10508        )
10509        .await
10510        .unwrap();
10511
10512        let events = collect_order_events(&mut receiver);
10513        assert_eq!(events.len(), 2, "was: {events:?}");
10514        assert!(matches!(&events[0], OrderEventAny::Submitted(_)));
10515        let OrderEventAny::Filled(fill) = &events[1] else {
10516            panic!("expected OrderFilled, was {:?}", events[1]);
10517        };
10518        let expected_qty = raw_amount_to_quantity(overshoot_base_out, 18).unwrap();
10519        assert_eq!(fill.order_side, OrderSide::Buy);
10520        assert_eq!(fill.last_qty, expected_qty);
10521        assert!(fill.last_qty > order.quantity());
10522        assert_eq!(
10523            Money::from_decimal(
10524                fill.last_qty.as_decimal() * fill.last_px.as_decimal(),
10525                fill.currency,
10526            )
10527            .unwrap(),
10528            Money::from_u256(amount_in, fill.currency).unwrap()
10529        );
10530
10531        drop_execution_schema(&admin_pool, &schema).await;
10532    }
10533
10534    #[tokio::test]
10535    async fn finalized_buy_swap_quarantines_sell_oriented_log() {
10536        let amount_in = expected_buy_amount_in();
10537        let min_amount_out = expected_buy_min_amount_out(50);
10538        let (expected_hash, _) = expected_buy_swap_tx(min_amount_out, amount_in).await;
10539        let state = finalized_buy_swap_rpc_state(expected_hash, min_amount_out, amount_in)
10540            .with_response(
10541                "eth_getTransactionReceipt",
10542                &finalized_swap_receipt(expected_hash),
10543            );
10544        let Some((admin_pool, schema, mut client, _, cache)) =
10545            swap_client_with_buy_database("execution_submit_buy_sell_log_test", state).await
10546        else {
10547            return;
10548        };
10549        let order = test_market_buy_order(test_pool().instrument_id);
10550        cache
10551            .borrow_mut()
10552            .add_order(order.clone(), None, None, true)
10553            .unwrap();
10554        let mut receiver = start_with_events(&mut client);
10555        let plan = client
10556            .prepare_swap(&submit_order_cmd(&order), &order)
10557            .unwrap();
10558
10559        let error = execute_swap(
10560            plan,
10561            client.transaction_executor().unwrap(),
10562            client.emitter.clone(),
10563            client.transaction_limits.max_quote_age_blocks,
10564            client.transaction_limits.deadline_seconds,
10565        )
10566        .await
10567        .unwrap_err();
10568
10569        let events = collect_order_events(&mut receiver);
10570        assert_swap_quarantined_without_terminal_event(&events);
10571        assert!(
10572            error
10573                .to_string()
10574                .contains("does not match the persisted amount")
10575                || error.to_string().contains("is not a BUY output"),
10576            "was: {error}"
10577        );
10578        assert!(client.in_flight.lock().is_some());
10579
10580        drop_execution_schema(&admin_pool, &schema).await;
10581    }
10582
10583    #[tokio::test]
10584    async fn submit_order_applies_buy_slippage_to_base_output() {
10585        let amount_in = expected_buy_amount_in();
10586        let min_amount_out = expected_buy_min_amount_out(200);
10587        let (expected_hash, expected_raw) = expected_buy_swap_tx(min_amount_out, amount_in).await;
10588        let state = finalized_buy_swap_rpc_state(expected_hash, min_amount_out, amount_in);
10589        let Some((admin_pool, schema, mut client, state, cache)) =
10590            swap_client_with_buy_database("execution_submit_buy_slippage_test", state).await
10591        else {
10592            return;
10593        };
10594        let order = test_market_buy_order(test_pool().instrument_id);
10595        cache
10596            .borrow_mut()
10597            .add_order(order.clone(), None, None, true)
10598            .unwrap();
10599        let mut cmd = submit_order_cmd(&order);
10600        cmd.params = Some(serde_json::from_str(r#"{"slippage_bps": 200}"#).unwrap());
10601        let mut receiver = start_with_events(&mut client);
10602
10603        client.submit_order(cmd).unwrap();
10604        await_pending_tasks(&client).await;
10605
10606        let events = collect_order_events(&mut receiver);
10607        assert_swap_submitted_and_filled(&events);
10608        let broadcasts: Vec<_> = state
10609            .recorded_requests()
10610            .into_iter()
10611            .filter(|request| request["method"] == "eth_sendRawTransaction")
10612            .collect();
10613        assert_eq!(broadcasts.len(), 1);
10614        assert_eq!(broadcasts[0]["params"][0].as_str().unwrap(), expected_raw);
10615
10616        drop_execution_schema(&admin_pool, &schema).await;
10617    }
10618
10619    #[tokio::test]
10620    async fn submit_order_denies_buy_on_insufficient_quote_balance() {
10621        let amount_in = expected_buy_amount_in();
10622        let min_amount_out = expected_buy_min_amount_out(50);
10623        let (expected_hash, _) = expected_buy_swap_tx(min_amount_out, amount_in).await;
10624        let state = finalized_buy_swap_rpc_state(expected_hash, min_amount_out, amount_in)
10625            .with_call_response(BALANCE_OF_SELECTOR, CALL_ZERO);
10626        let Some((admin_pool, schema, mut client, _, cache)) =
10627            swap_client_with_buy_database("execution_submit_buy_balance_test", state).await
10628        else {
10629            return;
10630        };
10631        let order = test_market_buy_order(test_pool().instrument_id);
10632        cache
10633            .borrow_mut()
10634            .add_order(order.clone(), None, None, true)
10635            .unwrap();
10636        let mut receiver = start_with_events(&mut client);
10637
10638        client.submit_order(submit_order_cmd(&order)).unwrap();
10639        await_pending_tasks(&client).await;
10640
10641        let events = collect_order_events(&mut receiver);
10642        assert_eq!(events.len(), 1);
10643        let OrderEventAny::Denied(denied) = &events[0] else {
10644            panic!("expected OrderDenied, was {:?}", events[0]);
10645        };
10646        assert!(
10647            denied.reason.contains("is below the swap amount"),
10648            "was: {}",
10649            denied.reason
10650        );
10651
10652        drop_execution_schema(&admin_pool, &schema).await;
10653    }
10654
10655    #[tokio::test]
10656    async fn finalized_swap_without_log_stays_quarantined() {
10657        let state = swap_rpc_state()
10658            .await
10659            .with_response("eth_getTransactionReceipt", RECEIPT_SUCCESS);
10660        let Some((admin_pool, schema, mut client, _, _)) =
10661            swap_client_with_database("execution_submit_missing_log_test", state).await
10662        else {
10663            return;
10664        };
10665        let order = test_market_sell_order(test_pool().instrument_id);
10666        let mut receiver = start_with_events(&mut client);
10667
10668        client.submit_order(submit_order_cmd(&order)).unwrap();
10669        await_pending_tasks(&client).await;
10670
10671        let events = collect_order_events(&mut receiver);
10672        let (status, terminal_emitted, active): (String, bool, bool) =
10673            sqlx::query_as(sqlx::AssertSqlSafe(format!(
10674                "SELECT status, terminal_emitted, active FROM {schema}.execution_intent"
10675            )))
10676            .fetch_one(&admin_pool)
10677            .await
10678            .unwrap();
10679
10680        assert_swap_quarantined_without_terminal_event(&events);
10681        assert_eq!(status, "broadcast");
10682        assert!(!terminal_emitted);
10683        assert!(active);
10684        assert!(client.in_flight.lock().is_some());
10685
10686        drop_execution_schema(&admin_pool, &schema).await;
10687    }
10688
10689    #[tokio::test]
10690    async fn finalized_swap_refresh_failure_stays_owned_for_reconciliation() {
10691        let min_amount_out = expected_min_amount_out(50);
10692        let (expected_hash, _) = expected_swap_tx(min_amount_out).await;
10693        let state = finalized_swap_rpc_state(expected_hash, min_amount_out).with_response_sequence(
10694            "eth_getBalance",
10695            &[
10696                GET_BALANCE,
10697                GET_BALANCE,
10698                GET_BALANCE,
10699                RPC_METHOD_NOT_FOUND,
10700                RPC_METHOD_NOT_FOUND,
10701                RPC_METHOD_NOT_FOUND,
10702            ],
10703        );
10704        let Some((admin_pool, schema, mut client, _state, _)) =
10705            swap_client_with_database("execution_submit_refresh_fail_test", state).await
10706        else {
10707            return;
10708        };
10709        let order = test_market_sell_order(test_pool().instrument_id);
10710        let mut receiver = start_with_events(&mut client);
10711        let plan = client
10712            .prepare_swap(&submit_order_cmd(&order), &order)
10713            .unwrap();
10714
10715        let error = execute_swap(
10716            plan,
10717            client.transaction_executor().unwrap(),
10718            client.emitter.clone(),
10719            client.transaction_limits.max_quote_age_blocks,
10720            client.transaction_limits.deadline_seconds,
10721        )
10722        .await
10723        .unwrap_err();
10724        let events = collect_order_events(&mut receiver);
10725        let (status, fill_emitted, active): (String, bool, bool) =
10726            sqlx::query_as(sqlx::AssertSqlSafe(format!(
10727                "SELECT status, fill_emitted, active FROM {schema}.execution_intent"
10728            )))
10729            .fetch_one(&admin_pool)
10730            .await
10731            .unwrap();
10732
10733        assert!(
10734            error
10735                .to_string()
10736                .contains("finalized native balance verification is locally invalid"),
10737            "was: {error}"
10738        );
10739        assert_eq!(events.len(), 1, "was: {events:?}");
10740        assert!(matches!(&events[0], OrderEventAny::Submitted(_)));
10741        assert_eq!(status, "broadcast");
10742        assert!(!fill_emitted);
10743        assert!(active);
10744        assert!(client.in_flight.lock().is_some());
10745
10746        drop_execution_schema(&admin_pool, &schema).await;
10747    }
10748
10749    #[tokio::test]
10750    async fn submit_order_denies_router_factory_mismatch_before_signing() {
10751        let state = swap_rpc_state()
10752            .await
10753            .with_call_response(FACTORY_SELECTOR, CALL_ZERO);
10754        let Some((admin_pool, schema, mut client, state, _)) =
10755            swap_client_with_database("execution_submit_factory_test", state).await
10756        else {
10757            return;
10758        };
10759        let order = test_market_sell_order(test_pool().instrument_id);
10760        let mut receiver = start_with_events(&mut client);
10761
10762        client.submit_order(submit_order_cmd(&order)).unwrap();
10763        await_pending_tasks(&client).await;
10764
10765        let events = collect_order_events(&mut receiver);
10766        assert_eq!(events.len(), 1);
10767        let OrderEventAny::Denied(denied) = &events[0] else {
10768            panic!("expected OrderDenied, was {:?}", events[0]);
10769        };
10770        assert!(
10771            denied
10772                .reason
10773                .contains("swap deployment manifest verification disagreed"),
10774            "was: {}",
10775            denied.reason
10776        );
10777        let requests = state.recorded_requests();
10778        assert!(
10779            requests
10780                .iter()
10781                .all(|request| request["method"] != "eth_getTransactionCount")
10782        );
10783        assert!(
10784            requests
10785                .iter()
10786                .all(|request| request["method"] != "eth_sendRawTransaction")
10787        );
10788
10789        drop_execution_schema(&admin_pool, &schema).await;
10790    }
10791
10792    #[tokio::test]
10793    async fn submit_order_denies_router_weth_mismatch_before_signing() {
10794        let state = swap_rpc_state()
10795            .await
10796            .with_call_response(WETH9_SELECTOR, CALL_ZERO);
10797        let Some((admin_pool, schema, mut client, state, _)) =
10798            swap_client_with_database("execution_submit_weth_test", state).await
10799        else {
10800            return;
10801        };
10802        let order = test_market_sell_order(test_pool().instrument_id);
10803        let mut receiver = start_with_events(&mut client);
10804
10805        client.submit_order(submit_order_cmd(&order)).unwrap();
10806        await_pending_tasks(&client).await;
10807
10808        let events = collect_order_events(&mut receiver);
10809        assert_eq!(events.len(), 1);
10810        let OrderEventAny::Denied(denied) = &events[0] else {
10811            panic!("expected OrderDenied, was {:?}", events[0]);
10812        };
10813        assert!(
10814            denied
10815                .reason
10816                .contains("swap deployment manifest verification disagreed"),
10817            "was: {}",
10818            denied.reason
10819        );
10820        let requests = state.recorded_requests();
10821        assert!(
10822            requests
10823                .iter()
10824                .all(|request| request["method"] != "eth_getTransactionCount")
10825        );
10826        assert!(
10827            requests
10828                .iter()
10829                .all(|request| request["method"] != "eth_sendRawTransaction")
10830        );
10831
10832        drop_execution_schema(&admin_pool, &schema).await;
10833    }
10834
10835    #[tokio::test]
10836    async fn submit_order_denies_factory_pool_mismatch_before_signing() {
10837        let state = swap_rpc_state()
10838            .await
10839            .with_call_response(GET_POOL_SELECTOR, CALL_ZERO);
10840        let Some((admin_pool, schema, mut client, state, _)) =
10841            swap_client_with_database("execution_submit_pool_identity_test", state).await
10842        else {
10843            return;
10844        };
10845        let order = test_market_sell_order(test_pool().instrument_id);
10846        let mut receiver = start_with_events(&mut client);
10847
10848        client.submit_order(submit_order_cmd(&order)).unwrap();
10849        await_pending_tasks(&client).await;
10850
10851        let events = collect_order_events(&mut receiver);
10852        assert_eq!(events.len(), 1);
10853        let OrderEventAny::Denied(denied) = &events[0] else {
10854            panic!("expected OrderDenied, was {:?}", events[0]);
10855        };
10856        assert!(
10857            denied
10858                .reason
10859                .contains("swap deployment manifest verification disagreed"),
10860            "was: {}",
10861            denied.reason
10862        );
10863        let requests = state.recorded_requests();
10864        assert!(
10865            requests
10866                .iter()
10867                .all(|request| request["method"] != "eth_getTransactionCount")
10868        );
10869        assert!(
10870            requests
10871                .iter()
10872                .all(|request| request["method"] != "eth_sendRawTransaction")
10873        );
10874
10875        drop_execution_schema(&admin_pool, &schema).await;
10876    }
10877
10878    #[tokio::test]
10879    async fn submit_order_denies_cached_token_decimal_mismatch_before_signing() {
10880        let state = swap_rpc_state().await.with_contract_call_response(
10881            WETH,
10882            DECIMALS_SELECTOR,
10883            CALL_DECIMALS_6,
10884        );
10885        let Some((admin_pool, schema, mut client, state, _)) =
10886            swap_client_with_database("execution_submit_decimals_test", state).await
10887        else {
10888            return;
10889        };
10890        let order = test_market_sell_order(test_pool().instrument_id);
10891        let mut receiver = start_with_events(&mut client);
10892
10893        client.submit_order(submit_order_cmd(&order)).unwrap();
10894        await_pending_tasks(&client).await;
10895
10896        let events = collect_order_events(&mut receiver);
10897        assert_eq!(events.len(), 1);
10898        let OrderEventAny::Denied(denied) = &events[0] else {
10899            panic!("expected OrderDenied, was {:?}", events[0]);
10900        };
10901        assert!(
10902            denied
10903                .reason
10904                .contains("swap deployment manifest verification disagreed"),
10905            "was: {}",
10906            denied.reason
10907        );
10908        let requests = state.recorded_requests();
10909        assert!(
10910            requests
10911                .iter()
10912                .all(|request| request["method"] != "eth_getTransactionCount")
10913        );
10914        assert!(
10915            requests
10916                .iter()
10917                .all(|request| request["method"] != "eth_sendRawTransaction")
10918        );
10919
10920        drop_execution_schema(&admin_pool, &schema).await;
10921    }
10922
10923    #[tokio::test]
10924    async fn submit_order_denies_on_insufficient_router_allowance() {
10925        let state = swap_rpc_state()
10926            .await
10927            .with_call_response(ALLOWANCE_SELECTOR, CALL_ZERO);
10928        let Some((admin_pool, schema, mut client, state, _)) =
10929            swap_client_with_database("execution_submit_allowance_test", state).await
10930        else {
10931            return;
10932        };
10933        let order = test_market_sell_order(test_pool().instrument_id);
10934        let mut receiver = start_with_events(&mut client);
10935
10936        client.submit_order(submit_order_cmd(&order)).unwrap();
10937        await_pending_tasks(&client).await;
10938
10939        let events = collect_order_events(&mut receiver);
10940        assert_eq!(events.len(), 1);
10941        let OrderEventAny::Denied(denied) = &events[0] else {
10942            panic!("expected OrderDenied, was {:?}", events[0]);
10943        };
10944        assert!(
10945            denied.reason.contains("below the swap amount"),
10946            "was: {}",
10947            denied.reason
10948        );
10949        let requests = state.recorded_requests();
10950        assert!(
10951            requests
10952                .iter()
10953                .all(|request| request["method"] != "eth_sendRawTransaction"),
10954            "no broadcast may follow a pre-trade denial"
10955        );
10956        let row_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
10957            "SELECT COUNT(*) FROM {schema}.execution_intent"
10958        )))
10959        .fetch_one(&admin_pool)
10960        .await
10961        .unwrap();
10962        assert_eq!(row_count, 0);
10963        assert!(client.in_flight.lock().is_none());
10964
10965        drop_execution_schema(&admin_pool, &schema).await;
10966    }
10967
10968    #[tokio::test]
10969    async fn submit_order_denies_on_insufficient_input_balance() {
10970        let state = swap_rpc_state()
10971            .await
10972            .with_call_response(BALANCE_OF_SELECTOR, CALL_ZERO);
10973        let Some((admin_pool, schema, mut client, _, _)) =
10974            swap_client_with_database("execution_submit_balance_test", state).await
10975        else {
10976            return;
10977        };
10978        let order = test_market_sell_order(test_pool().instrument_id);
10979        let mut receiver = start_with_events(&mut client);
10980
10981        client.submit_order(submit_order_cmd(&order)).unwrap();
10982        await_pending_tasks(&client).await;
10983
10984        let events = collect_order_events(&mut receiver);
10985        assert_eq!(events.len(), 1);
10986        let OrderEventAny::Denied(denied) = &events[0] else {
10987            panic!("expected OrderDenied, was {:?}", events[0]);
10988        };
10989        assert!(
10990            denied.reason.contains("is below the swap amount"),
10991            "was: {}",
10992            denied.reason
10993        );
10994
10995        drop_execution_schema(&admin_pool, &schema).await;
10996    }
10997
10998    #[tokio::test]
10999    async fn submit_order_denies_on_insufficient_native_balance() {
11000        let state = swap_rpc_state()
11001            .await
11002            .with_response("eth_getBalance", GET_BALANCE_INSUFFICIENT);
11003        let Some((admin_pool, schema, mut client, state, _)) =
11004            swap_client_with_database("execution_submit_native_balance_test", state).await
11005        else {
11006            return;
11007        };
11008        let order = test_market_sell_order(test_pool().instrument_id);
11009        let mut receiver = start_with_events(&mut client);
11010
11011        client.submit_order(submit_order_cmd(&order)).unwrap();
11012        await_pending_tasks(&client).await;
11013
11014        let events = collect_order_events(&mut receiver);
11015        assert_eq!(events.len(), 1);
11016        let OrderEventAny::Denied(denied) = &events[0] else {
11017            panic!("expected OrderDenied, was {:?}", events[0]);
11018        };
11019        assert!(
11020            denied.reason.contains("below maximum transaction cost"),
11021            "was: {}",
11022            denied.reason
11023        );
11024        assert!(
11025            state
11026                .recorded_requests()
11027                .iter()
11028                .all(|request| request["method"] != "eth_sendRawTransaction"),
11029            "no broadcast may follow an insufficient native balance"
11030        );
11031        let (row_count, status): (i64, Option<String>) = sqlx::query_as(sqlx::AssertSqlSafe(
11032            format!("SELECT COUNT(*), MAX(status) FROM {schema}.execution_intent"),
11033        ))
11034        .fetch_one(&admin_pool)
11035        .await
11036        .unwrap();
11037        assert_eq!(row_count, 1);
11038        assert_eq!(status.as_deref(), Some("recoverable"));
11039        assert!(client.in_flight.lock().is_none());
11040
11041        drop_execution_schema(&admin_pool, &schema).await;
11042    }
11043
11044    #[tokio::test]
11045    async fn submit_order_denies_on_stale_quote() {
11046        let Some((admin_pool, schema, mut client, state, cache)) =
11047            swap_client_with_database("execution_submit_stale_quote_test", swap_rpc_state().await)
11048                .await
11049        else {
11050            return;
11051        };
11052        let pool = test_pool();
11053        cache
11054            .borrow_mut()
11055            .add_pool_profiler(test_profiler(&pool, FIXTURE_BLOCK - 101))
11056            .unwrap();
11057        let order = test_market_sell_order(pool.instrument_id);
11058        let mut receiver = start_with_events(&mut client);
11059
11060        client.submit_order(submit_order_cmd(&order)).unwrap();
11061        await_pending_tasks(&client).await;
11062
11063        let events = collect_order_events(&mut receiver);
11064        assert_eq!(events.len(), 1);
11065        let OrderEventAny::Denied(denied) = &events[0] else {
11066            panic!("expected OrderDenied, was {:?}", events[0]);
11067        };
11068        assert!(
11069            denied.reason.contains("Stale quote"),
11070            "was: {}",
11071            denied.reason
11072        );
11073        let requests = state.recorded_requests();
11074        assert!(
11075            requests
11076                .iter()
11077                .all(|request| request["method"] != "eth_sendRawTransaction"),
11078            "no broadcast may follow a pre-trade denial"
11079        );
11080
11081        drop_execution_schema(&admin_pool, &schema).await;
11082    }
11083
11084    #[tokio::test]
11085    async fn submit_order_denies_quote_ahead_of_chain_head() {
11086        let Some((admin_pool, schema, mut client, state, cache)) =
11087            swap_client_with_database("execution_submit_ahead_quote_test", swap_rpc_state().await)
11088                .await
11089        else {
11090            return;
11091        };
11092        let pool = test_pool();
11093        cache
11094            .borrow_mut()
11095            .add_pool_profiler(test_profiler(&pool, FIXTURE_BLOCK + 1))
11096            .unwrap();
11097        let order = test_market_sell_order(pool.instrument_id);
11098        let mut receiver = start_with_events(&mut client);
11099
11100        client.submit_order(submit_order_cmd(&order)).unwrap();
11101        await_pending_tasks(&client).await;
11102
11103        let events = collect_order_events(&mut receiver);
11104        assert_eq!(events.len(), 1);
11105        let OrderEventAny::Denied(denied) = &events[0] else {
11106            panic!("expected OrderDenied, was {:?}", events[0]);
11107        };
11108        assert!(
11109            denied.reason.contains("is ahead of the latest block"),
11110            "was: {}",
11111            denied.reason
11112        );
11113        let requests = state.recorded_requests();
11114        assert!(
11115            requests
11116                .iter()
11117                .all(|request| request["method"] != "eth_sendRawTransaction"),
11118            "no broadcast may follow a pre-trade denial"
11119        );
11120
11121        drop_execution_schema(&admin_pool, &schema).await;
11122    }
11123
11124    #[tokio::test]
11125    async fn submit_order_denies_on_deadline_overflow() {
11126        let Some((admin_pool, schema, mut client, state, _)) = swap_client_with_database(
11127            "execution_submit_deadline_overflow_test",
11128            swap_rpc_state().await,
11129        )
11130        .await
11131        else {
11132            return;
11133        };
11134        client.transaction_limits.deadline_seconds = u64::MAX;
11135        let order = test_market_sell_order(test_pool().instrument_id);
11136        let mut receiver = start_with_events(&mut client);
11137
11138        client.submit_order(submit_order_cmd(&order)).unwrap();
11139        await_pending_tasks(&client).await;
11140
11141        let events = collect_order_events(&mut receiver);
11142        assert_eq!(events.len(), 1);
11143        let OrderEventAny::Denied(denied) = &events[0] else {
11144            panic!("expected OrderDenied, was {:?}", events[0]);
11145        };
11146        assert!(
11147            denied.reason.contains("deadline overflow"),
11148            "was: {}",
11149            denied.reason
11150        );
11151        let requests = state.recorded_requests();
11152        assert!(
11153            requests
11154                .iter()
11155                .all(|request| request["method"] != "eth_sendRawTransaction"),
11156            "no broadcast may follow a pre-trade denial"
11157        );
11158        assert!(client.in_flight.lock().is_none());
11159
11160        drop_execution_schema(&admin_pool, &schema).await;
11161    }
11162
11163    #[tokio::test]
11164    async fn submit_order_reconciles_node_rejection_to_finalized_receipt() {
11165        let state = swap_rpc_state()
11166            .await
11167            .with_response("eth_sendRawTransaction", SEND_RAW_TRANSACTION_REJECTED);
11168        let Some((admin_pool, schema, mut client, _state, _)) =
11169            swap_client_with_database("execution_submit_node_rejected_test", state).await
11170        else {
11171            return;
11172        };
11173        let order = test_market_sell_order(test_pool().instrument_id);
11174        let mut receiver = start_with_events(&mut client);
11175
11176        client.submit_order(submit_order_cmd(&order)).unwrap();
11177        await_pending_tasks(&client).await;
11178
11179        let events = collect_order_events(&mut receiver);
11180        assert_swap_submitted_and_filled(&events);
11181        let (purpose, status, client_order_id): (String, String, Option<String>) =
11182            sqlx::query_as(sqlx::AssertSqlSafe(format!(
11183                "SELECT purpose, status, client_order_id FROM {schema}.execution_intent"
11184            )))
11185            .fetch_one(&admin_pool)
11186            .await
11187            .unwrap();
11188        assert_eq!(purpose, "swap");
11189        assert_eq!(status, "finalized");
11190        assert_eq!(
11191            client_order_id.as_deref(),
11192            Some(order.client_order_id().as_str())
11193        );
11194        assert!(client.in_flight.lock().is_none());
11195
11196        drop_execution_schema(&admin_pool, &schema).await;
11197    }
11198
11199    #[tokio::test]
11200    async fn submit_order_acknowledges_uncertain_nonce_too_low() {
11201        let state = swap_rpc_state()
11202            .await
11203            .with_response("eth_sendRawTransaction", SEND_RAW_TRANSACTION_NONCE_TOO_LOW);
11204        let Some((admin_pool, schema, mut client, _state, _)) =
11205            swap_client_with_database("execution_submit_nonce_too_low_test", state).await
11206        else {
11207            return;
11208        };
11209        let order = test_market_sell_order(test_pool().instrument_id);
11210        let mut receiver = start_with_events(&mut client);
11211
11212        client.submit_order(submit_order_cmd(&order)).unwrap();
11213        await_pending_tasks(&client).await;
11214
11215        let events = collect_order_events(&mut receiver);
11216        assert_swap_submitted_and_filled(&events);
11217        let (purpose, status, client_order_id): (String, String, Option<String>) =
11218            sqlx::query_as(sqlx::AssertSqlSafe(format!(
11219                "SELECT purpose, status, client_order_id FROM {schema}.execution_intent"
11220            )))
11221            .fetch_one(&admin_pool)
11222            .await
11223            .unwrap();
11224        assert_eq!(purpose, "swap");
11225        assert_eq!(status, "finalized");
11226        assert_eq!(
11227            client_order_id.as_deref(),
11228            Some(order.client_order_id().as_str())
11229        );
11230        assert!(client.in_flight.lock().is_none());
11231
11232        drop_execution_schema(&admin_pool, &schema).await;
11233    }
11234
11235    #[tokio::test]
11236    async fn submit_order_rejected_on_reverted_receipt() {
11237        let expected_min_out = expected_min_amount_out(50);
11238        let (expected_hash, _) = expected_swap_tx(expected_min_out).await;
11239        let block = finalized_swap_block(expected_hash, expected_min_out);
11240        let receipt = receipt_with_transaction_hash(RECEIPT_REVERTED, expected_hash);
11241        let state = with_finalized_identity(
11242            swap_rpc_state()
11243                .await
11244                .with_response("eth_getTransactionReceipt", &receipt),
11245            &block,
11246            &receipt,
11247        );
11248        let Some((admin_pool, schema, mut client, _state, _)) =
11249            swap_client_with_database("execution_submit_reverted_test", state).await
11250        else {
11251            return;
11252        };
11253        let order = test_market_sell_order(test_pool().instrument_id);
11254        let mut receiver = start_with_events(&mut client);
11255        client.submit_order(submit_order_cmd(&order)).unwrap();
11256        await_pending_tasks(&client).await;
11257
11258        let events = collect_order_events(&mut receiver);
11259        assert_eq!(events.len(), 2);
11260        assert!(
11261            matches!(&events[0], OrderEventAny::Submitted(_)),
11262            "was: {:?}",
11263            events[0]
11264        );
11265        let OrderEventAny::Rejected(rejected) = &events[1] else {
11266            panic!("expected OrderRejected, was {:?}", events[1]);
11267        };
11268        assert!(
11269            rejected.reason.contains("reverted on-chain"),
11270            "was: {}",
11271            rejected.reason
11272        );
11273
11274        let record = client
11275            .cache
11276            .get_execution_transaction(42161, &expected_hash.to_string())
11277            .await
11278            .unwrap()
11279            .unwrap();
11280        assert_eq!(record.purpose, "swap");
11281        assert_eq!(record.status, "reverted");
11282        assert_eq!(
11283            record.client_order_id.as_deref(),
11284            Some(order.client_order_id().as_str())
11285        );
11286        assert!(client.in_flight.lock().is_none());
11287
11288        drop_execution_schema(&admin_pool, &schema).await;
11289    }
11290
11291    #[tokio::test]
11292    async fn submit_order_acknowledges_ambiguous_broadcast() {
11293        // A response the client cannot parse classifies as an ambiguous broadcast failure:
11294        // the transaction may be live, so the durable intent remains submitted and occupied
11295        let state = swap_rpc_state()
11296            .await
11297            .with_response("eth_sendRawTransaction", "not json");
11298        let Some((admin_pool, schema, mut client, _state, _)) =
11299            swap_client_with_database("execution_submit_ambiguous_test", state).await
11300        else {
11301            return;
11302        };
11303        let order = test_market_sell_order(test_pool().instrument_id);
11304        let mut receiver = start_with_events(&mut client);
11305
11306        client.submit_order(submit_order_cmd(&order)).unwrap();
11307        await_pending_tasks(&client).await;
11308
11309        let events = collect_order_events(&mut receiver);
11310        assert_swap_submitted_and_filled(&events);
11311
11312        let record = client
11313            .cache
11314            .get_execution_transaction(
11315                42161,
11316                &expected_swap_tx(expected_min_amount_out(50))
11317                    .await
11318                    .0
11319                    .to_string(),
11320            )
11321            .await
11322            .unwrap()
11323            .unwrap();
11324        assert_eq!(record.purpose, "swap");
11325        assert_eq!(record.status, "finalized");
11326        assert!(client.in_flight.lock().is_none());
11327
11328        drop_execution_schema(&admin_pool, &schema).await;
11329    }
11330
11331    #[tokio::test]
11332    async fn submit_order_acknowledges_broadcast_hash_mismatch() {
11333        // The static fixture's hash differs from the locally signed hash, so acceptance of the
11334        // signed transaction is unverified until the independent receipt reaches finality
11335        let state = swap_rpc_state_for_mismatch().await;
11336        let Some((admin_pool, schema, mut client, _state, _)) =
11337            swap_client_with_database("execution_submit_hash_mismatch_test", state).await
11338        else {
11339            return;
11340        };
11341        let order = test_market_sell_order(test_pool().instrument_id);
11342        let mut receiver = start_with_events(&mut client);
11343
11344        client.submit_order(submit_order_cmd(&order)).unwrap();
11345        await_pending_tasks(&client).await;
11346
11347        let events = collect_order_events(&mut receiver);
11348        assert_swap_submitted_and_filled(&events);
11349
11350        let record = client
11351            .cache
11352            .get_execution_transaction(
11353                42161,
11354                &expected_swap_tx(expected_min_amount_out(50))
11355                    .await
11356                    .0
11357                    .to_string(),
11358            )
11359            .await
11360            .unwrap()
11361            .unwrap();
11362        assert_eq!(record.purpose, "swap");
11363        assert_eq!(record.status, "finalized");
11364        assert!(client.in_flight.lock().is_none());
11365
11366        drop_execution_schema(&admin_pool, &schema).await;
11367    }
11368
11369    #[tokio::test]
11370    async fn submit_order_persists_authenticated_envelope_before_broadcast() {
11371        let state = swap_rpc_state().await;
11372        let Some((admin_pool, schema, mut client, state, _)) =
11373            swap_client_with_database("protected_submit_test", state).await
11374        else {
11375            return;
11376        };
11377        let order = test_market_sell_order(test_pool().instrument_id);
11378        let mut receiver = start_with_events(&mut client);
11379
11380        client.submit_order(submit_order_cmd(&order)).unwrap();
11381        await_pending_tasks(&client).await;
11382
11383        let events = collect_order_events(&mut receiver);
11384        assert_swap_submitted_and_filled(&events);
11385        let representations: Vec<(bool, bool)> = sqlx::query_as(sqlx::AssertSqlSafe(format!(
11386            "SELECT raw_transaction IS NULL, sealed_transaction IS NOT NULL \
11387             FROM {schema}.execution_transaction_hash WHERE payload_expected"
11388        )))
11389        .fetch_all(&admin_pool)
11390        .await
11391        .unwrap();
11392        let broadcasts = state
11393            .recorded_requests()
11394            .into_iter()
11395            .filter(|request| request["method"] == "eth_sendRawTransaction")
11396            .count();
11397        assert_eq!(representations, vec![(true, true)]);
11398        assert_eq!(broadcasts, 1);
11399
11400        drop_execution_schema(&admin_pool, &schema).await;
11401    }
11402
11403    #[tokio::test]
11404    async fn protected_persistence_failure_prevents_broadcast_and_acknowledgment() {
11405        let state = swap_rpc_state().await;
11406        let Some((admin_pool, schema, mut client, state, _)) =
11407            swap_client_with_database("protected_persist_failure_test", state).await
11408        else {
11409            return;
11410        };
11411
11412        for statement in [
11413            format!(
11414                "CREATE FUNCTION {schema}.reject_protected_payload() RETURNS trigger \
11415                 LANGUAGE plpgsql AS 'BEGIN RAISE EXCEPTION ''test payload rejection''; END'"
11416            ),
11417            format!(
11418                "CREATE TRIGGER reject_protected_payload BEFORE INSERT ON \
11419                 {schema}.execution_transaction_hash FOR EACH ROW \
11420                 EXECUTE FUNCTION {schema}.reject_protected_payload()"
11421            ),
11422        ] {
11423            sqlx::query(sqlx::AssertSqlSafe(statement))
11424                .execute(&admin_pool)
11425                .await
11426                .unwrap();
11427        }
11428        let order = test_market_sell_order(test_pool().instrument_id);
11429        let mut receiver = start_with_events(&mut client);
11430
11431        client.submit_order(submit_order_cmd(&order)).unwrap();
11432        await_pending_tasks(&client).await;
11433
11434        let events = collect_order_events(&mut receiver);
11435        assert!(
11436            events
11437                .iter()
11438                .all(|event| !matches!(event, OrderEventAny::Submitted(_)))
11439        );
11440        let broadcasts = state
11441            .recorded_requests()
11442            .into_iter()
11443            .filter(|request| request["method"] == "eth_sendRawTransaction")
11444            .count();
11445        let payload_rows: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
11446            "SELECT COUNT(*) FROM {schema}.execution_transaction_hash"
11447        )))
11448        .fetch_one(&admin_pool)
11449        .await
11450        .unwrap();
11451        assert_eq!(broadcasts, 0);
11452        assert_eq!(payload_rows, 0);
11453
11454        drop_execution_schema(&admin_pool, &schema).await;
11455    }
11456
11457    #[tokio::test]
11458    async fn submit_order_reservation_failure_denies_without_broadcast_and_releases_slot() {
11459        let Some((admin_pool, schema, mut client, state, cache)) =
11460            swap_client_with_database("execution_submit_persist_fail_test", swap_rpc_state().await)
11461                .await
11462        else {
11463            return;
11464        };
11465        sqlx::query(sqlx::AssertSqlSafe(format!(
11466            "DROP TABLE {schema}.execution_intent CASCADE"
11467        )))
11468        .execute(&admin_pool)
11469        .await
11470        .unwrap();
11471        let pool = test_pool();
11472        let first = test_market_sell_order(pool.instrument_id);
11473        let second = market_sell_order_with_id(pool.instrument_id, "O-SWAP-002");
11474        cache
11475            .borrow_mut()
11476            .add_order(second.clone(), None, None, false)
11477            .unwrap();
11478        let mut receiver = start_with_events(&mut client);
11479
11480        client.submit_order(submit_order_cmd(&first)).unwrap();
11481        await_pending_tasks(&client).await;
11482
11483        let events = collect_order_events(&mut receiver);
11484        assert_eq!(events.len(), 1);
11485        let OrderEventAny::Denied(denied) = &events[0] else {
11486            panic!("expected OrderDenied, was {:?}", events[0]);
11487        };
11488        assert!(
11489            denied
11490                .reason
11491                .contains("Execution intent reservation failed before commit"),
11492            "was: {}",
11493            denied.reason
11494        );
11495        client.submit_order(submit_order_cmd(&second)).unwrap();
11496        await_pending_tasks(&client).await;
11497        let retry_events = collect_order_events(&mut receiver);
11498        assert_eq!(retry_events.len(), 1);
11499        let OrderEventAny::Denied(retry_denied) = &retry_events[0] else {
11500            panic!("expected OrderDenied, was {:?}", retry_events[0]);
11501        };
11502        assert_eq!(
11503            retry_denied.reason,
11504            "Execution intent reservation failed before commit"
11505        );
11506        let broadcasts = state
11507            .recorded_requests()
11508            .into_iter()
11509            .filter(|request| request["method"] == "eth_sendRawTransaction")
11510            .count();
11511        assert_eq!(broadcasts, 0);
11512        assert!(client.in_flight.lock().is_none());
11513
11514        drop_execution_schema(&admin_pool, &schema).await;
11515    }
11516
11517    #[tokio::test]
11518    async fn submit_order_reservation_commit_failure_keeps_preparing_slot() {
11519        let Some((admin_pool, schema, mut client, state, cache)) = swap_client_with_database(
11520            "execution_submit_reservation_commit_fail_test",
11521            swap_rpc_state().await,
11522        )
11523        .await
11524        else {
11525            return;
11526        };
11527        install_reservation_commit_rejection(&admin_pool, &schema).await;
11528        let pool = test_pool();
11529        let first = test_market_sell_order(pool.instrument_id);
11530        let second = market_sell_order_with_id(pool.instrument_id, "O-SWAP-002");
11531        cache
11532            .borrow_mut()
11533            .add_order(second.clone(), None, None, false)
11534            .unwrap();
11535        let mut receiver = start_with_events(&mut client);
11536
11537        client.submit_order(submit_order_cmd(&first)).unwrap();
11538        await_pending_tasks(&client).await;
11539        client.submit_order(submit_order_cmd(&second)).unwrap();
11540        await_pending_tasks(&client).await;
11541
11542        let events = collect_order_events(&mut receiver);
11543        let submitted = events
11544            .iter()
11545            .filter(|event| matches!(event, OrderEventAny::Submitted(_)))
11546            .count();
11547        let denied_commit = events
11548            .iter()
11549            .filter(|event| {
11550                matches!(event, OrderEventAny::Denied(denied) if denied.reason == "Execution intent reservation commit outcome is unknown; reconciliation is required")
11551            })
11552            .count();
11553        let denied_in_flight = events
11554            .iter()
11555            .filter(|event| {
11556                matches!(event, OrderEventAny::Denied(denied) if denied.reason.contains("at most one transaction can be in flight"))
11557            })
11558            .count();
11559        let requests = state.recorded_requests();
11560        let intent_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
11561            "SELECT COUNT(*) FROM {schema}.execution_intent"
11562        )))
11563        .fetch_one(&admin_pool)
11564        .await
11565        .unwrap();
11566        let signed_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
11567            "SELECT COUNT(*) FROM {schema}.execution_transaction_hash"
11568        )))
11569        .fetch_one(&admin_pool)
11570        .await
11571        .unwrap();
11572
11573        assert_eq!(submitted, 0, "was: {events:?}");
11574        assert_eq!(denied_commit, 1, "was: {events:?}");
11575        assert_eq!(denied_in_flight, 1, "was: {events:?}");
11576        assert!(matches!(
11577            *client.in_flight.lock(),
11578            Some(InFlightSlot::Preparing(TransactionPurpose::Swap))
11579        ));
11580        assert_eq!(intent_count, 0);
11581        assert_eq!(signed_count, 0);
11582        assert!(
11583            requests
11584                .iter()
11585                .all(|request| request["method"] != "eth_getTransactionCount")
11586        );
11587        assert!(
11588            requests
11589                .iter()
11590                .all(|request| request["method"] != "eth_sendRawTransaction")
11591        );
11592
11593        drop_execution_schema(&admin_pool, &schema).await;
11594    }
11595
11596    #[tokio::test]
11597    async fn protected_payload_migration_failure_keeps_plaintext_and_blocks_ready() {
11598        let Some((admin_pool, schema, client, _)) = execution_client_with_unprotected_database(
11599            "protected_payload_migration_failure",
11600            execution_rpc_state(),
11601        )
11602        .await
11603        else {
11604            return;
11605        };
11606        let database = client.cache.database.as_ref().unwrap();
11607        let (intent, _) = persist_invalid_test_swap(database, None).await;
11608        let keys = payload_test_keys([0x31; 32], vec![], "migration-failure");
11609        let error = database
11610            .ensure_execution_payload_storage(&keys)
11611            .await
11612            .unwrap_err();
11613
11614        assert!(
11615            error
11616                .to_string()
11617                .contains("failed to authenticate execution payload")
11618        );
11619        let row = database
11620            .get_execution_transaction_hashes(intent.id)
11621            .await
11622            .unwrap()
11623            .pop()
11624            .unwrap();
11625        let operation: String = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
11626            "SELECT operation FROM {schema}.execution_payload_state"
11627        )))
11628        .fetch_one(&admin_pool)
11629        .await
11630        .unwrap();
11631        assert_eq!(operation, "migrate");
11632        assert!(row.raw_transaction.is_some());
11633        assert!(row.sealed_transaction.is_none());
11634
11635        drop_execution_schema(&admin_pool, &schema).await;
11636    }
11637
11638    #[tokio::test]
11639    async fn protected_payload_migration_restart_restore_rewrap_and_rollback() {
11640        let Some((admin_pool, pg_config)) =
11641            connect_test_postgres("protected payload lifecycle").await
11642        else {
11643            return;
11644        };
11645        let schema = format!("protected_payload_lifecycle_{}", std::process::id());
11646        setup_execution_schema(&admin_pool, &schema).await;
11647        let options: sqlx::postgres::PgConnectOptions = pg_config.into();
11648        let options = options.options([("search_path", schema.clone())]);
11649        let database = connect_test_database(options.clone()).await.unwrap();
11650        database
11651            .ensure_execution_transaction_schema()
11652            .await
11653            .unwrap();
11654        let intent = reserve_test_wrap_intent(&database).await;
11655        database
11656            .assign_execution_intent_nonce(intent.id, 7)
11657            .await
11658            .unwrap();
11659        let transaction = build_eip1559_transaction(
11660            42161,
11661            7,
11662            78_000,
11663            130_000_000,
11664            10_000_000,
11665            WETH_ADDRESS,
11666            U256::from(1_u64),
11667            Bytes::from(hex::decode("d0e30db0").unwrap()),
11668        );
11669        let (transaction_hash, raw_transaction) = sign_eip1559_transaction(
11670            transaction,
11671            &PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
11672        )
11673        .await
11674        .unwrap();
11675        database
11676            .add_execution_transaction_hash(
11677                intent.id,
11678                42161,
11679                &transaction_hash.to_string(),
11680                &raw_transaction,
11681            )
11682            .await
11683            .unwrap();
11684        let keys = payload_test_keys([0x41; 32], vec![], "restore-a");
11685
11686        database
11687            .ensure_execution_payload_storage(&keys)
11688            .await
11689            .unwrap();
11690        let operation: String = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
11691            "SELECT operation FROM {schema}.execution_payload_state"
11692        )))
11693        .fetch_one(&admin_pool)
11694        .await
11695        .unwrap();
11696        assert_eq!(operation, "ready");
11697        drop(database);
11698
11699        let restarted = connect_test_database(options).await.unwrap();
11700        restarted
11701            .ensure_execution_payload_storage(&keys)
11702            .await
11703            .unwrap();
11704        let row = restarted
11705            .get_execution_transaction_hashes(intent.id)
11706            .await
11707            .unwrap()
11708            .pop()
11709            .unwrap();
11710        assert!(row.raw_transaction.is_none());
11711        let sealed_transaction = row.sealed_transaction.clone().unwrap();
11712        let stored_intent = restarted.get_execution_intent(intent.id).await.unwrap();
11713        let context = payload_context(&stored_intent, &row, keys.deployment_id()).unwrap();
11714        let alternate_envelope = keys.seal(&raw_transaction, &context).unwrap();
11715        assert_ne!(alternate_envelope, sealed_transaction);
11716        let repeated = restarted
11717            .add_execution_transaction_envelope(
11718                intent.id,
11719                42161,
11720                &transaction_hash.to_string(),
11721                &alternate_envelope,
11722            )
11723            .await
11724            .unwrap();
11725        let check = restarted
11726            .check_execution_payload_storage(Some(&keys), None, 1)
11727            .await
11728            .unwrap();
11729        assert_eq!(
11730            repeated.sealed_transaction,
11731            Some(sealed_transaction.clone())
11732        );
11733        assert_eq!(check.plaintext_rows, 0);
11734        assert_eq!(check.original_rows, 1);
11735        assert_eq!(check.replacement_rows, 0);
11736        assert_eq!(check.authenticated_rows, 1);
11737        assert!(!check.read_roles.is_empty());
11738
11739        sqlx::query(sqlx::AssertSqlSafe(format!(
11740            "UPDATE {schema}.execution_payload_key_state SET seals = 4294967295"
11741        )))
11742        .execute(&admin_pool)
11743        .await
11744        .unwrap();
11745        let exhausted = restarted
11746            .reserve_execution_payload_seal(&keys)
11747            .await
11748            .unwrap_err();
11749        assert!(exhausted.to_string().contains("seal limit"));
11750        let seals: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
11751            "SELECT seals FROM {schema}.execution_payload_key_state"
11752        )))
11753        .fetch_one(&admin_pool)
11754        .await
11755        .unwrap();
11756        assert_eq!(seals, 4_294_967_295);
11757        sqlx::query(sqlx::AssertSqlSafe(format!(
11758            "UPDATE {schema}.execution_payload_key_state SET seals = 1"
11759        )))
11760        .execute(&admin_pool)
11761        .await
11762        .unwrap();
11763
11764        let missing_key = restarted
11765            .check_execution_payload_storage(None, None, 1)
11766            .await
11767            .unwrap_err();
11768        assert!(
11769            missing_key
11770                .to_string()
11771                .contains("no payload key is configured")
11772        );
11773        let restored_elsewhere = payload_test_keys([0x41; 32], vec![], "restore-b");
11774        let wrong_context = restarted
11775            .ensure_execution_payload_storage(&restored_elsewhere)
11776            .await
11777            .unwrap_err();
11778        assert!(wrong_context.to_string().contains("deployment ID"));
11779
11780        sqlx::query(sqlx::AssertSqlSafe(format!(
11781            "UPDATE {schema}.execution_transaction_hash \
11782             SET sealed_transaction = set_byte(sealed_transaction, octet_length(sealed_transaction) - 1, \
11783                 get_byte(sealed_transaction, octet_length(sealed_transaction) - 1) # 1)"
11784        )))
11785        .execute(&admin_pool)
11786        .await
11787        .unwrap();
11788        assert!(
11789            restarted
11790                .check_execution_payload_storage(Some(&keys), None, 1)
11791                .await
11792                .is_err()
11793        );
11794        sqlx::query(sqlx::AssertSqlSafe(format!(
11795            "UPDATE {schema}.execution_transaction_hash SET sealed_transaction = $1"
11796        )))
11797        .bind(&sealed_transaction)
11798        .execute(&admin_pool)
11799        .await
11800        .unwrap();
11801
11802        for statement in [
11803            format!(
11804                "ALTER TABLE {schema}.execution_transaction_hash \
11805                 DROP CONSTRAINT execution_transaction_payload_protected_check"
11806            ),
11807            format!("UPDATE {schema}.execution_transaction_hash SET sealed_transaction = NULL"),
11808        ] {
11809            sqlx::query(sqlx::AssertSqlSafe(statement))
11810                .execute(&admin_pool)
11811                .await
11812                .unwrap();
11813        }
11814        assert!(
11815            restarted
11816                .check_execution_payload_storage(Some(&keys), None, 1)
11817                .await
11818                .is_err()
11819        );
11820        sqlx::query(sqlx::AssertSqlSafe(format!(
11821            "UPDATE {schema}.execution_transaction_hash SET sealed_transaction = $1"
11822        )))
11823        .bind(&sealed_transaction)
11824        .execute(&admin_pool)
11825        .await
11826        .unwrap();
11827        sqlx::query(sqlx::AssertSqlSafe(format!(
11828            "ALTER TABLE {schema}.execution_transaction_hash \
11829             ADD CONSTRAINT execution_transaction_payload_protected_check CHECK ( \
11830                 (payload_expected AND raw_transaction IS NULL AND sealed_transaction IS NOT NULL) \
11831                 OR (NOT payload_expected AND raw_transaction IS NULL AND sealed_transaction IS NULL) \
11832             )"
11833        )))
11834        .execute(&admin_pool)
11835        .await
11836        .unwrap();
11837
11838        let rotated = payload_test_keys([0x52; 32], vec![[0x41; 32]], "restore-a");
11839        restarted
11840            .rewrap_execution_payload_storage(&rotated, 1)
11841            .await
11842            .unwrap();
11843        let rotated_check = restarted
11844            .check_execution_payload_storage(Some(&rotated), None, 1)
11845            .await
11846            .unwrap();
11847        assert_eq!(rotated_check.authenticated_rows, 1);
11848        assert_eq!(rotated_check.key_ids.len(), 1);
11849
11850        restarted
11851            .rollback_execution_payload_storage(&rotated, 1)
11852            .await
11853            .unwrap();
11854        let rolled_back = restarted
11855            .get_execution_transaction_hashes(intent.id)
11856            .await
11857            .unwrap()
11858            .pop()
11859            .unwrap();
11860        assert_eq!(
11861            rolled_back.raw_transaction.as_deref(),
11862            Some(raw_transaction.as_slice())
11863        );
11864        assert!(rolled_back.sealed_transaction.is_none());
11865        let legacy_check = restarted
11866            .check_execution_payload_storage(None, None, 1)
11867            .await
11868            .unwrap();
11869        assert!(!legacy_check.protected);
11870        assert_eq!(legacy_check.plaintext_rows, 1);
11871        assert_eq!(legacy_check.authenticated_rows, 1);
11872
11873        drop_execution_schema(&admin_pool, &schema).await;
11874    }
11875
11876    #[tokio::test]
11877    async fn protected_payload_storage_authenticates_multiple_execution_identities() {
11878        let Some((admin_pool, pg_config)) =
11879            connect_test_postgres("protected payload multiple identities").await
11880        else {
11881            return;
11882        };
11883        let schema = format!("protected_payload_identities_{}", std::process::id());
11884        setup_execution_schema(&admin_pool, &schema).await;
11885        let options: sqlx::postgres::PgConnectOptions = pg_config.into();
11886        let database = connect_test_database(options.options([("search_path", schema.clone())]))
11887            .await
11888            .unwrap();
11889        database
11890            .ensure_execution_transaction_schema()
11891            .await
11892            .unwrap();
11893
11894        let private_keys = [
11895            TEST_PRIVATE_KEY,
11896            "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d",
11897        ];
11898
11899        for (index, private_key) in private_keys.into_iter().enumerate() {
11900            let signer = PrivateKeySigner::from_str(private_key).unwrap();
11901            let nonce = 7 + u64::try_from(index).unwrap();
11902            let intent =
11903                reserve_test_wrap_intent_for_wallet(&database, &signer.address().to_string()).await;
11904            database
11905                .assign_execution_intent_nonce(intent.id, nonce)
11906                .await
11907                .unwrap();
11908            let transaction = build_eip1559_transaction(
11909                42161,
11910                nonce,
11911                78_000,
11912                130_000_000,
11913                10_000_000,
11914                WETH_ADDRESS,
11915                U256::from(1_u64),
11916                Bytes::from(hex::decode("d0e30db0").unwrap()),
11917            );
11918            let (transaction_hash, raw_transaction) =
11919                sign_eip1559_transaction(transaction, &signer)
11920                    .await
11921                    .unwrap();
11922            database
11923                .add_execution_transaction_hash(
11924                    intent.id,
11925                    42161,
11926                    &transaction_hash.to_string(),
11927                    &raw_transaction,
11928                )
11929                .await
11930                .unwrap();
11931        }
11932
11933        let keys = payload_test_keys([0x61; 32], vec![], "multiple-identities");
11934        database
11935            .ensure_execution_payload_storage(&keys)
11936            .await
11937            .unwrap();
11938        let lease = database
11939            .require_execution_payload_storage(
11940                &keys,
11941                PayloadPolicy {
11942                    chain_id: 42161,
11943                    signer: Address::from_str(WALLET).unwrap(),
11944                    gas_limit: 1_000_000,
11945                    max_fee_per_gas: 1_000_000_000,
11946                },
11947                1,
11948            )
11949            .await
11950            .unwrap();
11951        drop(lease);
11952        let check = database
11953            .check_execution_payload_storage(Some(&keys), None, 1)
11954            .await
11955            .unwrap();
11956
11957        assert!(check.protected);
11958        assert_eq!(check.plaintext_rows, 0);
11959        assert_eq!(check.original_rows, 2);
11960        assert_eq!(check.replacement_rows, 0);
11961        assert_eq!(check.authenticated_rows, 2);
11962        assert_eq!(check.key_ids.len(), 1);
11963
11964        drop_execution_schema(&admin_pool, &schema).await;
11965    }
11966
11967    #[rstest]
11968    #[case::finalized(TransactionStatus::Finalized)]
11969    #[case::reverted(TransactionStatus::Reverted)]
11970    #[tokio::test]
11971    async fn protected_payload_storage_ignores_current_policy_for_released_terminal_history(
11972        #[case] status: TransactionStatus,
11973    ) {
11974        let Some((admin_pool, pg_config)) =
11975            connect_test_postgres("protected payload terminal history").await
11976        else {
11977            return;
11978        };
11979        let schema = format!(
11980            "protected_payload_terminal_{}_{}",
11981            status.as_str(),
11982            std::process::id()
11983        );
11984        setup_execution_schema(&admin_pool, &schema).await;
11985        let options: sqlx::postgres::PgConnectOptions = pg_config.into();
11986        let database = connect_test_database(options.options([("search_path", schema.clone())]))
11987            .await
11988            .unwrap();
11989        database
11990            .ensure_execution_transaction_schema()
11991            .await
11992            .unwrap();
11993        let (intent, transaction_hash, _) = persist_test_wrap_broadcast(&database, None).await;
11994        database
11995            .record_execution_status(
11996                intent.id,
11997                &transaction_hash.to_string(),
11998                status,
11999                None,
12000                None,
12001                None,
12002                None,
12003                None,
12004            )
12005            .await
12006            .unwrap();
12007        database
12008            .mark_execution_event_emitted(intent.id, "terminal")
12009            .await
12010            .unwrap();
12011        let keys = payload_test_keys([0x62; 32], vec![], "terminal-history");
12012        database
12013            .ensure_execution_payload_storage(&keys)
12014            .await
12015            .unwrap();
12016
12017        let lease = database
12018            .require_execution_payload_storage(
12019                &keys,
12020                PayloadPolicy {
12021                    chain_id: 42161,
12022                    signer: Address::from_str(WALLET).unwrap(),
12023                    gas_limit: 1,
12024                    max_fee_per_gas: 1,
12025                },
12026                1,
12027            )
12028            .await
12029            .unwrap();
12030        drop(lease);
12031        let check = database
12032            .check_execution_payload_storage(Some(&keys), None, 1)
12033            .await
12034            .unwrap();
12035        let active_intent = reserve_test_wrap_intent(&database).await;
12036        database
12037            .assign_execution_intent_nonce(active_intent.id, 8)
12038            .await
12039            .unwrap();
12040        let active_intent = database
12041            .get_execution_intent(active_intent.id)
12042            .await
12043            .unwrap();
12044        let active_transaction = build_eip1559_transaction(
12045            42161,
12046            8,
12047            78_000,
12048            130_000_000,
12049            10_000_000,
12050            WETH_ADDRESS,
12051            U256::from(1_u64),
12052            Bytes::from(hex::decode("d0e30db0").unwrap()),
12053        );
12054        let (active_hash, active_raw_transaction) = sign_eip1559_transaction(
12055            active_transaction,
12056            &PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
12057        )
12058        .await
12059        .unwrap();
12060        persist_test_payload(
12061            &database,
12062            Some(&keys),
12063            &active_intent,
12064            active_hash,
12065            &active_raw_transaction,
12066        )
12067        .await;
12068        let active_error = match database
12069            .require_execution_payload_storage(
12070                &keys,
12071                PayloadPolicy {
12072                    chain_id: 42161,
12073                    signer: Address::from_str(WALLET).unwrap(),
12074                    gas_limit: 1,
12075                    max_fee_per_gas: 1,
12076                },
12077                1,
12078            )
12079            .await
12080        {
12081            Ok(_) => panic!("active execution payload unexpectedly passed current policy"),
12082            Err(e) => e,
12083        };
12084        let active_error = format!("{active_error:#}");
12085
12086        assert!(check.protected);
12087        assert_eq!(check.authenticated_rows, 1);
12088        assert!(
12089            active_error.contains(&format!(
12090                "execution intent {} transaction {active_hash} violates current execution policy",
12091                active_intent.id
12092            )),
12093            "was: {active_error}"
12094        );
12095        assert!(
12096            active_error.contains("gas limit 78000 exceeds configured ceiling 1"),
12097            "was: {active_error}"
12098        );
12099
12100        drop_execution_schema(&admin_pool, &schema).await;
12101    }
12102
12103    #[allow(unsafe_code)] // env-var mutation uses the test client's dedicated signer variable
12104    #[tokio::test]
12105    async fn rollback_blocks_execution_until_protection_and_full_check_succeed() {
12106        let state = ready_rpc_state();
12107        let Some((admin_pool, schema, mut client, state)) =
12108            execution_client_with_database("payload_rollback_reactivation", state).await
12109        else {
12110            return;
12111        };
12112        // SAFETY: this test binary owns the dedicated test signer variable
12113        unsafe { std::env::set_var("BLOCKCHAIN_TEST_PRIVATE_KEY", TEST_PRIVATE_KEY) };
12114        client.disconnect().await.unwrap();
12115        let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel();
12116        replace_exec_event_sender(sender);
12117        client.start().unwrap();
12118
12119        client.rollback_payload_storage(1).await.unwrap();
12120        let unprotected = client.check_payload_storage(1).await.unwrap();
12121        let error = client.connect().await.unwrap_err();
12122
12123        assert!(!unprotected.protected);
12124        assert_eq!(unprotected.plaintext_rows, 0);
12125        assert_eq!(unprotected.authenticated_rows, 0);
12126        assert!(
12127            error
12128                .to_string()
12129                .contains("Postgres execution requires protected payload storage"),
12130            "was: {error}"
12131        );
12132        assert!(client.signer.is_none());
12133        assert!(state.recorded_requests().is_empty());
12134
12135        client.protect_payload_storage().await.unwrap();
12136        let protected = client.check_payload_storage(1).await.unwrap();
12137        assert!(protected.protected);
12138        assert_eq!(protected.plaintext_rows, 0);
12139        assert_eq!(protected.authenticated_rows, 0);
12140
12141        client.connect().await.unwrap();
12142
12143        assert!(client.is_connected());
12144        assert!(client.transaction_executor().is_ok());
12145        assert!(!state.recorded_requests().is_empty());
12146
12147        drop(client);
12148        drop_execution_schema(&admin_pool, &schema).await;
12149    }
12150
12151    #[rstest]
12152    #[case::active_key(true)]
12153    #[case::deployment_identity(false)]
12154    #[tokio::test]
12155    async fn postgres_connect_rejects_missing_payload_identity_before_rpc(
12156        #[case] remove_active_key: bool,
12157    ) {
12158        let test_name = if remove_active_key {
12159            "payload_connect_missing_active_key"
12160        } else {
12161            "payload_connect_missing_deployment"
12162        };
12163        let Some((admin_pool, schema, mut client, state)) =
12164            execution_client_with_database(test_name, ready_rpc_state()).await
12165        else {
12166            return;
12167        };
12168        client.disconnect().await.unwrap();
12169        client.payload_keys = None;
12170        if remove_active_key {
12171            client.config.payload_key_env = None;
12172            client.config.payload_deployment_id = None;
12173        } else {
12174            client.config.payload_deployment_id = None;
12175        }
12176
12177        let error = client.connect().await.unwrap_err();
12178
12179        assert!(
12180            error.to_string().contains(if remove_active_key {
12181                "Postgres execution requires an active payload key and deployment identity"
12182            } else {
12183                "Payload deployment ID is required"
12184            }),
12185            "was: {error}"
12186        );
12187        assert!(client.signer.is_none());
12188        assert!(client.payload_keys.is_none());
12189        assert!(state.recorded_requests().is_empty());
12190
12191        drop(client);
12192        drop_execution_schema(&admin_pool, &schema).await;
12193    }
12194
12195    #[tokio::test]
12196    async fn postgres_connect_rejects_missing_envelope_key_before_rpc() {
12197        let Some((admin_pool, schema, mut client, state)) = execution_client_with_database(
12198            "payload_connect_missing_envelope_key",
12199            ready_rpc_state(),
12200        )
12201        .await
12202        else {
12203            return;
12204        };
12205        let database = client.cache.database.as_ref().unwrap().clone();
12206        persist_test_wrap_broadcast(&database, client.payload_keys.as_deref()).await;
12207        client.disconnect().await.unwrap();
12208        client.payload_keys = None;
12209        let keys = set_test_payload_key(&mut client, [0xb6; 32], &schema);
12210        sqlx::query(sqlx::AssertSqlSafe(format!(
12211            "UPDATE {schema}.execution_payload_state SET active_key_id = $1 \
12212             WHERE component = 'signed_transactions'"
12213        )))
12214        .bind(keys.active_key_id().as_slice())
12215        .execute(&admin_pool)
12216        .await
12217        .unwrap();
12218
12219        let error = client.connect().await.unwrap_err();
12220
12221        assert!(
12222            error
12223                .to_string()
12224                .contains("Stored execution payload requires unavailable key"),
12225            "was: {error}"
12226        );
12227        assert!(client.signer.is_none());
12228        assert!(client.payload_keys.is_none());
12229        assert!(state.recorded_requests().is_empty());
12230
12231        drop(client);
12232        drop_execution_schema(&admin_pool, &schema).await;
12233    }
12234
12235    #[tokio::test]
12236    async fn payload_action_lease_blocks_rewrap_transition_until_release() {
12237        let Some((admin_pool, schema, client, _)) = execution_client_with_unprotected_database(
12238            "payload_action_lease",
12239            execution_rpc_state(),
12240        )
12241        .await
12242        else {
12243            return;
12244        };
12245        let database = client.cache.database.as_ref().unwrap();
12246        let keys = payload_test_keys([0x71; 32], vec![], "action-lease");
12247        database
12248            .ensure_execution_payload_storage(&keys)
12249            .await
12250            .unwrap();
12251        let lease = database
12252            .acquire_execution_payload_lease(&keys)
12253            .await
12254            .unwrap();
12255        let rotated = payload_test_keys([0x72; 32], vec![[0x71; 32]], "action-lease");
12256        let mut operation = Box::pin(database.rewrap_execution_payload_storage(&rotated, 1));
12257
12258        assert!(
12259            tokio::time::timeout(Duration::from_millis(50), &mut operation)
12260                .await
12261                .is_err()
12262        );
12263        let state: String = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
12264            "SELECT operation FROM {schema}.execution_payload_state"
12265        )))
12266        .fetch_one(&admin_pool)
12267        .await
12268        .unwrap();
12269        assert_eq!(state, "ready");
12270
12271        drop(lease);
12272        tokio::time::timeout(Duration::from_secs(2), operation)
12273            .await
12274            .unwrap()
12275            .unwrap();
12276        let state: String = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
12277            "SELECT operation FROM {schema}.execution_payload_state"
12278        )))
12279        .fetch_one(&admin_pool)
12280        .await
12281        .unwrap();
12282        assert_eq!(state, "ready");
12283
12284        drop_execution_schema(&admin_pool, &schema).await;
12285    }
12286
12287    #[tokio::test]
12288    async fn payload_infrastructure_checks_are_scoped_to_the_execution_schema() {
12289        let Some((admin_pool, pg_config)) = connect_test_postgres("payload schema isolation").await
12290        else {
12291            return;
12292        };
12293        let schema_a = format!("payload_schema_a_{}", std::process::id());
12294        let schema_b = format!("payload_schema_b_{}", std::process::id());
12295        setup_execution_schema(&admin_pool, &schema_a).await;
12296        setup_execution_schema(&admin_pool, &schema_b).await;
12297        let options: sqlx::postgres::PgConnectOptions = pg_config.into();
12298        let database_a =
12299            connect_test_database(options.clone().options([("search_path", schema_a.clone())]))
12300                .await
12301                .unwrap();
12302        let database_b =
12303            connect_test_database(options.options([("search_path", schema_b.clone())]))
12304                .await
12305                .unwrap();
12306        database_a
12307            .ensure_execution_transaction_schema()
12308            .await
12309            .unwrap();
12310        database_b
12311            .ensure_execution_transaction_schema()
12312            .await
12313            .unwrap();
12314        let keys_a = payload_test_keys([0x91; 32], vec![], "schema-a");
12315        let keys_b = payload_test_keys([0x92; 32], vec![], "schema-b");
12316
12317        database_a
12318            .ensure_execution_payload_storage(&keys_a)
12319            .await
12320            .unwrap();
12321        database_b
12322            .ensure_execution_payload_storage(&keys_b)
12323            .await
12324            .unwrap();
12325
12326        for statement in [
12327            format!(
12328                "DROP TRIGGER execution_transaction_payload_fence ON \
12329                 {schema_a}.execution_transaction_hash"
12330            ),
12331            format!(
12332                "ALTER TABLE {schema_a}.execution_transaction_hash \
12333                 DROP CONSTRAINT execution_transaction_payload_protected_check"
12334            ),
12335        ] {
12336            sqlx::query(sqlx::AssertSqlSafe(statement))
12337                .execute(&admin_pool)
12338                .await
12339                .unwrap();
12340        }
12341
12342        let error = database_a
12343            .ensure_execution_payload_storage(&keys_a)
12344            .await
12345            .unwrap_err();
12346
12347        assert!(
12348            error
12349                .to_string()
12350                .contains("without its write fence or constraint"),
12351            "was: {error}"
12352        );
12353
12354        drop_execution_schema(&admin_pool, &schema_a).await;
12355        drop_execution_schema(&admin_pool, &schema_b).await;
12356    }
12357
12358    #[tokio::test]
12359    async fn execution_fresh_schema_preserves_nullable_wallet_address() {
12360        let Some((admin_pool, _)) = connect_test_postgres("fresh execution schema").await else {
12361            return;
12362        };
12363        let schema = format!("execution_fresh_test_{}", std::process::id());
12364        let mut transaction = admin_pool.begin().await.unwrap();
12365        sqlx::query(sqlx::AssertSqlSafe(format!("CREATE SCHEMA {schema}")))
12366            .execute(&mut *transaction)
12367            .await
12368            .unwrap();
12369        sqlx::query(sqlx::AssertSqlSafe(format!(
12370            "SET LOCAL search_path TO {schema}"
12371        )))
12372        .execute(&mut *transaction)
12373        .await
12374        .unwrap();
12375        sqlx::query("CREATE TABLE chain (chain_id INTEGER PRIMARY KEY)")
12376            .execute(&mut *transaction)
12377            .await
12378            .unwrap();
12379        sqlx::query("INSERT INTO chain (chain_id) VALUES (42161)")
12380            .execute(&mut *transaction)
12381            .await
12382            .unwrap();
12383        sqlx::query(sqlx::AssertSqlSafe(execution_transaction_create_sql()))
12384            .execute(&mut *transaction)
12385            .await
12386            .unwrap();
12387        sqlx::query(
12388            "INSERT INTO execution_transaction \
12389             (chain_id, nonce, transaction_hash, purpose, status) \
12390             VALUES (42161, 7, '0xfresh-legacy', 'wrap', 'rejected')",
12391        )
12392        .execute(&mut *transaction)
12393        .await
12394        .unwrap();
12395
12396        let is_nullable: String = sqlx::query_scalar(
12397            "SELECT is_nullable FROM information_schema.columns \
12398             WHERE table_schema = $1 AND table_name = 'execution_transaction' \
12399             AND column_name = 'wallet_address'",
12400        )
12401        .bind(&schema)
12402        .fetch_one(&mut *transaction)
12403        .await
12404        .unwrap();
12405        let wallet_address: Option<String> = sqlx::query_scalar(
12406            "SELECT wallet_address FROM execution_transaction \
12407             WHERE transaction_hash = '0xfresh-legacy'",
12408        )
12409        .fetch_one(&mut *transaction)
12410        .await
12411        .unwrap();
12412
12413        assert_eq!(is_nullable, "YES");
12414        assert_eq!(wallet_address, None);
12415        transaction.rollback().await.unwrap();
12416    }
12417
12418    #[tokio::test]
12419    async fn execution_schema_migration_preserves_existing_rows() {
12420        let Some((admin_pool, pg_config)) =
12421            connect_test_postgres("execution schema migration").await
12422        else {
12423            return;
12424        };
12425        let schema = format!("execution_migration_test_{}", std::process::id());
12426        setup_execution_schema(&admin_pool, &schema).await;
12427        sqlx::query(sqlx::AssertSqlSafe(format!(
12428            "INSERT INTO {schema}.execution_transaction \
12429             (chain_id, nonce, transaction_hash, purpose, status) \
12430             VALUES (42161, 7, '0xlegacy', 'wrap', 'rejected')"
12431        )))
12432        .execute(&admin_pool)
12433        .await
12434        .unwrap();
12435
12436        let db_options: sqlx::postgres::PgConnectOptions = pg_config.into();
12437        let db_options = db_options.options([("search_path", schema.clone())]);
12438        let database = connect_test_database(db_options).await.unwrap();
12439        database
12440            .ensure_execution_transaction_schema()
12441            .await
12442            .unwrap();
12443        let legacy = database
12444            .get_execution_transaction(42161, "0xlegacy")
12445            .await
12446            .unwrap()
12447            .unwrap();
12448        assert_eq!(legacy.purpose, "wrap");
12449        assert_eq!(legacy.status, "rejected");
12450        assert_eq!(legacy.client_order_id, None);
12451        assert_eq!(legacy.wallet_address, None);
12452        let is_nullable: String = sqlx::query_scalar(
12453            "SELECT is_nullable FROM information_schema.columns \
12454             WHERE table_schema = $1 AND table_name = 'execution_transaction' \
12455             AND column_name = 'wallet_address'",
12456        )
12457        .bind(&schema)
12458        .fetch_one(&admin_pool)
12459        .await
12460        .unwrap();
12461        let fence_error = database
12462            .add_execution_transaction(
12463                42161,
12464                WALLET,
12465                8,
12466                "0xswap",
12467                "swap",
12468                "pending",
12469                Some("O-SWAP-001"),
12470            )
12471            .await
12472            .err()
12473            .unwrap();
12474        assert!(
12475            fence_error
12476                .to_string()
12477                .contains("Legacy execution writer refused"),
12478            "was: {fence_error}"
12479        );
12480        assert_eq!(is_nullable, "YES");
12481
12482        drop_execution_schema(&admin_pool, &schema).await;
12483    }
12484
12485    #[tokio::test]
12486    async fn execution_schema_migration_drops_legacy_wallet_not_null() {
12487        let Some((admin_pool, pg_config)) =
12488            connect_test_postgres("legacy wallet address schema migration").await
12489        else {
12490            return;
12491        };
12492        let schema = format!("execution_wallet_migration_test_{}", std::process::id());
12493        setup_execution_schema(&admin_pool, &schema).await;
12494        sqlx::query(sqlx::AssertSqlSafe(format!(
12495            "ALTER TABLE {schema}.execution_transaction \
12496             ADD COLUMN wallet_address TEXT NOT NULL"
12497        )))
12498        .execute(&admin_pool)
12499        .await
12500        .unwrap();
12501        sqlx::query(sqlx::AssertSqlSafe(format!(
12502            "INSERT INTO {schema}.execution_transaction \
12503             (chain_id, wallet_address, nonce, transaction_hash, purpose, status) \
12504             VALUES (42161, '{WALLET}', 7, '0xlegacy-wallet', 'wrap', 'rejected')"
12505        )))
12506        .execute(&admin_pool)
12507        .await
12508        .unwrap();
12509
12510        let db_options: sqlx::postgres::PgConnectOptions = pg_config.into();
12511        let db_options = db_options.options([("search_path", schema.clone())]);
12512        let database = connect_test_database(db_options).await.unwrap();
12513        database
12514            .ensure_execution_transaction_schema()
12515            .await
12516            .unwrap();
12517        let legacy = database
12518            .get_execution_transaction(42161, "0xlegacy-wallet")
12519            .await
12520            .unwrap()
12521            .unwrap();
12522        let is_nullable: String = sqlx::query_scalar(
12523            "SELECT is_nullable FROM information_schema.columns \
12524             WHERE table_schema = $1 AND table_name = 'execution_transaction' \
12525             AND column_name = 'wallet_address'",
12526        )
12527        .bind(&schema)
12528        .fetch_one(&admin_pool)
12529        .await
12530        .unwrap();
12531
12532        assert_eq!(legacy.wallet_address.as_deref(), Some(WALLET));
12533        assert_eq!(is_nullable, "YES");
12534        drop_execution_schema(&admin_pool, &schema).await;
12535    }
12536
12537    #[tokio::test]
12538    async fn execution_schema_migration_refuses_unresolved_legacy_rows() {
12539        let Some((admin_pool, pg_config)) =
12540            connect_test_postgres("unsafe execution schema migration").await
12541        else {
12542            return;
12543        };
12544        let schema = format!("execution_unsafe_migration_test_{}", std::process::id());
12545        setup_execution_schema(&admin_pool, &schema).await;
12546        sqlx::query(sqlx::AssertSqlSafe(format!(
12547            "INSERT INTO {schema}.execution_transaction \
12548             (chain_id, nonce, transaction_hash, purpose, status) \
12549             VALUES (42161, 7, '0xunresolved', 'wrap', 'pending')"
12550        )))
12551        .execute(&admin_pool)
12552        .await
12553        .unwrap();
12554        let db_options: sqlx::postgres::PgConnectOptions = pg_config.into();
12555        let db_options = db_options.options([("search_path", schema.clone())]);
12556        let database = connect_test_database(db_options).await.unwrap();
12557
12558        let error = database
12559            .ensure_execution_transaction_schema()
12560            .await
12561            .unwrap_err();
12562        let legacy_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
12563            "SELECT COUNT(*) FROM {schema}.execution_transaction \
12564             WHERE transaction_hash = '0xunresolved' AND status = 'pending'"
12565        )))
12566        .fetch_one(&admin_pool)
12567        .await
12568        .unwrap();
12569        let v2_table: Option<String> = sqlx::query_scalar("SELECT to_regclass($1)::TEXT")
12570            .bind(format!("{schema}.execution_intent"))
12571            .fetch_one(&admin_pool)
12572            .await
12573            .unwrap();
12574
12575        assert!(
12576            error
12577                .to_string()
12578                .contains("Cannot safely migrate 1 unresolved execution schema version 1"),
12579            "was: {error}"
12580        );
12581        assert_eq!(legacy_count, 1);
12582        assert_eq!(v2_table, None);
12583
12584        drop_execution_schema(&admin_pool, &schema).await;
12585    }
12586
12587    #[rstest]
12588    fn receipt_max_polls_derives_from_timeout() {
12589        assert_eq!(receipt_max_polls(0), 1);
12590        assert_eq!(receipt_max_polls(1), 1);
12591        assert_eq!(receipt_max_polls(60), 60);
12592        assert_eq!(receipt_max_polls(u64::MAX), u32::MAX);
12593        assert_eq!(receipt_timeout(0), Duration::from_secs(1));
12594        assert_eq!(receipt_timeout(60), Duration::from_secs(60));
12595        assert_eq!(
12596            receipt_timeout(u64::MAX),
12597            Duration::from_secs(u64::from(u32::MAX))
12598        );
12599    }
12600
12601    #[rstest]
12602    fn submit_order_errors_when_order_not_cached() {
12603        let client = swap_client_with_cache(test_config("http://127.0.0.1:1".to_string())).0;
12604        let mut cmd = submit_order_cmd(&test_market_sell_order(test_pool().instrument_id));
12605        cmd.client_order_id = ClientOrderId::from("O-UNKNOWN");
12606
12607        let error = client.submit_order(cmd).unwrap_err();
12608
12609        assert!(!error.to_string().is_empty());
12610    }
12611
12612    #[tokio::test]
12613    async fn submit_order_denies_pool_fee_above_uint24() {
12614        let cache = Rc::new(RefCell::new(Cache::default()));
12615        let mut pool = test_pool();
12616        pool.fee = Some(16_777_216); // 2^24, above the uint24 calldata range
12617        cache.borrow_mut().add_pool(pool.clone()).unwrap();
12618        let order = test_market_sell_order(pool.instrument_id);
12619        cache
12620            .borrow_mut()
12621            .add_order(order.clone(), None, None, false)
12622            .unwrap();
12623        let core = ExecutionClientCore::new(
12624            TraderId::from("TRADER-001"),
12625            ClientId::from("BLOCKCHAIN-001"),
12626            *BLOCKCHAIN_VENUE,
12627            OmsType::Netting,
12628            AccountId::from("BLOCKCHAIN-001"),
12629            AccountType::Wallet,
12630            None,
12631            cache,
12632        );
12633        let mut client =
12634            BlockchainExecutionClient::new(core, test_config("http://127.0.0.1:1".to_string()))
12635                .unwrap();
12636        let mut receiver = start_with_events(&mut client);
12637
12638        client.submit_order(submit_order_cmd(&order)).unwrap();
12639
12640        let events = collect_order_events(&mut receiver);
12641        assert_eq!(events.len(), 1);
12642        let OrderEventAny::Denied(denied) = &events[0] else {
12643            panic!("expected OrderDenied, was {:?}", events[0]);
12644        };
12645        assert!(
12646            denied.reason.contains("exceeds uint24"),
12647            "was: {}",
12648            denied.reason
12649        );
12650    }
12651
12652    #[tokio::test]
12653    async fn submit_order_denies_uninitialized_profiler() {
12654        let cache = Rc::new(RefCell::new(Cache::default()));
12655        let pool = test_pool();
12656        cache.borrow_mut().add_pool(pool.clone()).unwrap();
12657        let order = test_market_sell_order(pool.instrument_id);
12658        cache
12659            .borrow_mut()
12660            .add_order(order.clone(), None, None, false)
12661            .unwrap();
12662        cache
12663            .borrow_mut()
12664            .add_pool_profiler(PoolProfiler::new(Arc::new(pool)))
12665            .unwrap();
12666        let core = ExecutionClientCore::new(
12667            TraderId::from("TRADER-001"),
12668            ClientId::from("BLOCKCHAIN-001"),
12669            *BLOCKCHAIN_VENUE,
12670            OmsType::Netting,
12671            AccountId::from("BLOCKCHAIN-001"),
12672            AccountType::Wallet,
12673            None,
12674            cache,
12675        );
12676        let mut client =
12677            BlockchainExecutionClient::new(core, test_config("http://127.0.0.1:1".to_string()))
12678                .unwrap();
12679        let mut receiver = start_with_events(&mut client);
12680
12681        client.submit_order(submit_order_cmd(&order)).unwrap();
12682
12683        let events = collect_order_events(&mut receiver);
12684        assert_eq!(events.len(), 1);
12685        let OrderEventAny::Denied(denied) = &events[0] else {
12686            panic!("expected OrderDenied, was {:?}", events[0]);
12687        };
12688        assert!(
12689            denied.reason.contains("is not initialized"),
12690            "was: {}",
12691            denied.reason
12692        );
12693    }
12694
12695    #[tokio::test]
12696    async fn submit_order_denies_when_quote_cannot_fill_order() {
12697        let cache = Rc::new(RefCell::new(Cache::default()));
12698        let pool = test_pool();
12699        cache.borrow_mut().add_pool(pool.clone()).unwrap();
12700        let order = test_market_sell_order(pool.instrument_id);
12701        cache
12702            .borrow_mut()
12703            .add_order(order.clone(), None, None, false)
12704            .unwrap();
12705        // A narrow one-unit-of-liquidity position cannot fill the order
12706        cache
12707            .borrow_mut()
12708            .add_pool_profiler(test_profiler_with_range(
12709                &pool,
12710                FIXTURE_BLOCK,
12711                FIXTURE_BLOCK_HASH,
12712                U160::from(1u128 << 96),
12713                -10,
12714                10,
12715                1,
12716            ))
12717            .unwrap();
12718        let core = ExecutionClientCore::new(
12719            TraderId::from("TRADER-001"),
12720            ClientId::from("BLOCKCHAIN-001"),
12721            *BLOCKCHAIN_VENUE,
12722            OmsType::Netting,
12723            AccountId::from("BLOCKCHAIN-001"),
12724            AccountType::Wallet,
12725            None,
12726            cache,
12727        );
12728        let mut client =
12729            BlockchainExecutionClient::new(core, test_config("http://127.0.0.1:1".to_string()))
12730                .unwrap();
12731        let mut receiver = start_with_events(&mut client);
12732
12733        client.submit_order(submit_order_cmd(&order)).unwrap();
12734
12735        let events = collect_order_events(&mut receiver);
12736        assert_eq!(events.len(), 1);
12737        let OrderEventAny::Denied(denied) = &events[0] else {
12738            panic!("expected OrderDenied, was {:?}", events[0]);
12739        };
12740        assert!(
12741            denied.reason.contains("cannot fill the order"),
12742            "was: {}",
12743            denied.reason
12744        );
12745    }
12746
12747    #[tokio::test]
12748    async fn submit_order_applies_slippage_param_override_at_ceiling() {
12749        let Some((admin_pool, schema, mut client, state, _)) = swap_client_with_database(
12750            "execution_submit_slippage_override_test",
12751            swap_rpc_state_with_min_amount_out(expected_min_amount_out(200)).await,
12752        )
12753        .await
12754        else {
12755            return;
12756        };
12757        let order = test_market_sell_order(test_pool().instrument_id);
12758        let mut cmd = submit_order_cmd(&order);
12759        // Exactly at the configured ceiling (200 bps): accepted, not denied
12760        cmd.params = Some(serde_json::from_str(r#"{"slippage_bps": 200}"#).unwrap());
12761        let mut receiver = start_with_events(&mut client);
12762        let expected_min_out = expected_min_amount_out(200);
12763
12764        client.submit_order(cmd).unwrap();
12765        await_pending_tasks(&client).await;
12766
12767        let events = collect_order_events(&mut receiver);
12768        assert_swap_submitted_and_filled(&events);
12769
12770        let (_, expected_raw) = expected_swap_tx(expected_min_out).await;
12771        let broadcasts: Vec<_> = state
12772            .recorded_requests()
12773            .into_iter()
12774            .filter(|request| request["method"] == "eth_sendRawTransaction")
12775            .collect();
12776        assert_eq!(broadcasts.len(), 1);
12777        assert_eq!(broadcasts[0]["params"][0].as_str().unwrap(), expected_raw);
12778
12779        drop_execution_schema(&admin_pool, &schema).await;
12780    }
12781
12782    #[tokio::test]
12783    async fn submit_order_accepts_quote_fresh_at_max_age_boundary() {
12784        let min_amount_out = expected_min_amount_out(50);
12785        let (tx_hash, _) = expected_swap_tx(min_amount_out).await;
12786        let head = finalized_swap_block(tx_hash, min_amount_out);
12787        let state = swap_rpc_state()
12788            .await
12789            .with_response("eth_getBlockByNumber", &head)
12790            .with_parameter_response("eth_getBlockByNumber", FIXTURE_BLOCK_PARAM, BLOCK_BY_NUMBER);
12791        let Some((admin_pool, schema, mut client, state, cache)) =
12792            swap_client_with_database_config(
12793                "execution_submit_fresh_boundary_test",
12794                state,
12795                |http_rpc_url| {
12796                    let mut config = test_config(http_rpc_url);
12797                    config.max_quote_age_blocks = Some(1);
12798                    config
12799                },
12800            )
12801            .await
12802        else {
12803            return;
12804        };
12805        let pool = test_pool();
12806        cache
12807            .borrow_mut()
12808            .add_pool_profiler(test_profiler_at_block(
12809                &pool,
12810                FIXTURE_BLOCK,
12811                FIXTURE_BLOCK_HASH,
12812            ))
12813            .unwrap();
12814        let order = test_market_sell_order(pool.instrument_id);
12815        let mut receiver = start_with_events(&mut client);
12816
12817        client.submit_order(submit_order_cmd(&order)).unwrap();
12818        await_pending_tasks(&client).await;
12819
12820        let events = collect_order_events(&mut receiver);
12821        assert_eq!(events.len(), 1, "was: {events:?}");
12822        assert!(matches!(&events[0], OrderEventAny::Submitted(_)));
12823        assert_eq!(
12824            state
12825                .recorded_requests()
12826                .iter()
12827                .filter(|request| request["method"] == "eth_sendRawTransaction")
12828                .count(),
12829            1
12830        );
12831
12832        drop_execution_schema(&admin_pool, &schema).await;
12833    }
12834
12835    #[tokio::test]
12836    async fn submit_order_sells_base_token_from_token1_position() {
12837        // A pool with the token order reversed relative to the convention: USDC is token0
12838        // and WETH is token1, so the base token (WETH, by token priority) sits in the
12839        // token1 position and the swap quotes zero_for_one = false
12840        let chain = Arc::new(chains::ARBITRUM.clone());
12841        let dex = Arc::clone(&UNISWAP_V3.dex);
12842        let usdc = Token::new(
12843            Arc::clone(&chain),
12844            address!("af88d065e77c8cC2239327C5EDb3A432268e5831"),
12845            "USD Coin".to_string(),
12846            "USDC".to_string(),
12847            6,
12848        );
12849        let weth = Token::new(
12850            Arc::clone(&chain),
12851            address!("82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
12852            "Wrapped Ether".to_string(),
12853            "WETH".to_string(),
12854            18,
12855        );
12856        let pool = Pool::new(
12857            chain,
12858            dex,
12859            address!("C6962004f452bE9203591991D15f6b388e09E8D0"),
12860            PoolIdentifier::from_address(address!("C6962004f452bE9203591991D15f6b388e09E8D0")),
12861            55_000_000,
12862            usdc,
12863            weth,
12864            Some(500),
12865            Some(10),
12866            UnixNanos::default(),
12867        );
12868
12869        let cache = Rc::new(RefCell::new(Cache::default()));
12870        cache.borrow_mut().add_pool(pool.clone()).unwrap();
12871        let order = test_market_sell_order(pool.instrument_id);
12872        cache
12873            .borrow_mut()
12874            .add_order(order.clone(), None, None, false)
12875            .unwrap();
12876        // Asymmetric price so the zero_for_one direction changes the quote
12877        cache
12878            .borrow_mut()
12879            .add_pool_profiler(test_profiler_with_state(
12880                &pool,
12881                FIXTURE_BLOCK,
12882                U160::from(2u128 << 96),
12883                TEST_LIQUIDITY,
12884            ))
12885            .unwrap();
12886
12887        let (admin_pool, pg_config) = match connect_test_postgres("orientation").await {
12888            Some(setup) => setup,
12889            None => return,
12890        };
12891        let schema = format!("execution_submit_orientation_test_{}", std::process::id());
12892        setup_execution_schema(&admin_pool, &schema).await;
12893        let db_options: sqlx::postgres::PgConnectOptions = pg_config.into();
12894        let db_options = db_options.options([("search_path", schema.clone())]);
12895        let database = connect_test_database(db_options).await.unwrap();
12896
12897        let state = swap_rpc_state()
12898            .await
12899            .with_call_response(POOL_TOKEN0_SELECTOR, CALL_USDC)
12900            .with_call_response(POOL_TOKEN1_SELECTOR, CALL_WETH)
12901            .with_response("eth_getTransactionReceipt", RECEIPT_NULL);
12902        let addr = start_mock_rpc_server(state.clone()).await;
12903        let core = ExecutionClientCore::new(
12904            TraderId::from("TRADER-001"),
12905            ClientId::from("BLOCKCHAIN-001"),
12906            *BLOCKCHAIN_VENUE,
12907            OmsType::Netting,
12908            AccountId::from("BLOCKCHAIN-001"),
12909            AccountType::Wallet,
12910            None,
12911            cache,
12912        );
12913        let mut config = test_config(format!("http://{addr}"));
12914        let result = |response: &str| {
12915            serde_json::from_str::<serde_json::Value>(response).unwrap()["result"]
12916                .as_str()
12917                .unwrap()
12918                .to_string()
12919        };
12920        {
12921            let verification = config.verification.as_mut().unwrap();
12922            let pool_identity = &mut verification.deployment_manifest.pools[0];
12923            pool_identity.token0 = USDC.to_string();
12924            pool_identity.token1 = WETH.to_string();
12925            let pool_contract = verification
12926                .deployment_manifest
12927                .contracts
12928                .iter_mut()
12929                .find(|contract| contract.role == BlockchainContractRole::Pool)
12930                .unwrap();
12931
12932            for probe in &mut pool_contract.probes {
12933                if probe.call_data.starts_with(POOL_TOKEN0_SELECTOR) {
12934                    probe.expected_output = result(CALL_USDC);
12935                } else if probe.call_data.starts_with(POOL_TOKEN1_SELECTOR) {
12936                    probe.expected_output = result(CALL_WETH);
12937                }
12938            }
12939        }
12940        refresh_test_manifest_digest(&mut config);
12941        let mut client = BlockchainExecutionClient::new(core, config).unwrap();
12942        client.cache.database = Some(database);
12943        client
12944            .cache
12945            .ensure_execution_transaction_schema()
12946            .await
12947            .unwrap();
12948        protect_test_storage(&mut client, &schema).await;
12949        initialize_test_verification_ledger(&client).await;
12950        client.signer = Some(Arc::new(
12951            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
12952        ));
12953        client.core.set_connected();
12954        let mut receiver = start_with_events(&mut client);
12955
12956        let profiler = test_profiler_with_state(
12957            &pool,
12958            FIXTURE_BLOCK,
12959            U160::from(2u128 << 96),
12960            TEST_LIQUIDITY,
12961        );
12962        let quote = profiler
12963            .swap_exact_in(U256::from(1_000_000_000_000_000u64), false, None)
12964            .unwrap();
12965        let quoted = exact_output_amount(&quote, false).unwrap();
12966        let expected_min_out = derive_min_amount_out(quoted, 50).unwrap();
12967        let wrong_direction_quote = profiler
12968            .swap_exact_in(U256::from(1_000_000_000_000_000u64), true, None)
12969            .unwrap();
12970        let wrong_direction_out = exact_output_amount(&wrong_direction_quote, true).unwrap();
12971        assert_ne!(
12972            quoted, wrong_direction_out,
12973            "the asymmetric price must make the quote direction observable"
12974        );
12975        assert_ne!(
12976            expected_min_out,
12977            expected_min_amount_out(50),
12978            "the profiler quote must remain distinct from the independent quote fixture"
12979        );
12980
12981        client.submit_order(submit_order_cmd(&order)).unwrap();
12982        await_pending_tasks(&client).await;
12983
12984        let events = collect_order_events(&mut receiver);
12985        assert_eq!(events.len(), 1);
12986        assert!(
12987            matches!(&events[0], OrderEventAny::Submitted(_)),
12988            "was: {:?}",
12989            events[0]
12990        );
12991
12992        let (_, expected_raw) = expected_swap_tx(expected_min_amount_out(50)).await;
12993        let broadcasts: Vec<_> = state
12994            .recorded_requests()
12995            .into_iter()
12996            .filter(|request| request["method"] == "eth_sendRawTransaction")
12997            .collect();
12998        assert_eq!(broadcasts.len(), 1);
12999        assert_eq!(broadcasts[0]["params"][0].as_str().unwrap(), expected_raw);
13000
13001        drop_execution_schema(&admin_pool, &schema).await;
13002    }
13003
13004    #[tokio::test]
13005    async fn submit_order_denies_on_chain_mismatch() {
13006        let state = swap_rpc_state()
13007            .await
13008            .with_response("eth_chainId", CHAIN_ID_ETHEREUM);
13009        let Some((admin_pool, schema, mut client, state, _)) =
13010            swap_client_with_database("execution_submit_chain_mismatch_test", state).await
13011        else {
13012            return;
13013        };
13014        let order = test_market_sell_order(test_pool().instrument_id);
13015        let mut receiver = start_with_events(&mut client);
13016
13017        client.submit_order(submit_order_cmd(&order)).unwrap();
13018        await_pending_tasks(&client).await;
13019
13020        let events = collect_order_events(&mut receiver);
13021        assert_eq!(events.len(), 1);
13022        let OrderEventAny::Denied(denied) = &events[0] else {
13023            panic!("expected OrderDenied, was {:?}", events[0]);
13024        };
13025        assert!(
13026            denied
13027                .reason
13028                .contains("pre-sign chain ID verification disagreed"),
13029            "was: {}",
13030            denied.reason
13031        );
13032        let requests = state.recorded_requests();
13033        assert!(
13034            requests
13035                .iter()
13036                .all(|request| request["method"] != "eth_sendRawTransaction"),
13037            "no broadcast may follow a chain mismatch"
13038        );
13039        assert!(client.in_flight.lock().is_none());
13040
13041        drop_execution_schema(&admin_pool, &schema).await;
13042    }
13043
13044    #[tokio::test]
13045    async fn submit_order_finality_timeout_marks_dropped_and_keeps_ownership() {
13046        let receipt_release = Arc::new(tokio::sync::Semaphore::new(0));
13047        let state = swap_rpc_state()
13048            .await
13049            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
13050            .with_response_release("eth_getTransactionReceipt", Arc::clone(&receipt_release));
13051        let Some((admin_pool, schema, mut client, state, _)) =
13052            swap_client_with_database("execution_submit_inclusion_timeout_test", state).await
13053        else {
13054            return;
13055        };
13056        client.transaction_limits.receipt_timeout_secs = 1;
13057        let order = test_market_sell_order(test_pool().instrument_id);
13058        let mut receiver = start_with_events(&mut client);
13059
13060        client.submit_order(submit_order_cmd(&order)).unwrap();
13061        await_recorded_requests(&state, "eth_getTransactionReceipt", 3).await;
13062        tokio::time::timeout(Duration::from_secs(3), await_pending_tasks(&client))
13063            .await
13064            .unwrap();
13065        receipt_release.add_permits(3);
13066
13067        // Broadcast acceptance was observed, so the order is submitted; the unobserved
13068        // inclusion justifies no further event
13069        let events = collect_order_events(&mut receiver);
13070        assert_eq!(events.len(), 1);
13071        assert!(
13072            matches!(&events[0], OrderEventAny::Submitted(_)),
13073            "was: {:?}",
13074            events[0]
13075        );
13076
13077        let record = client
13078            .cache
13079            .get_execution_transaction(
13080                42161,
13081                &expected_swap_tx(expected_min_amount_out(50))
13082                    .await
13083                    .0
13084                    .to_string(),
13085            )
13086            .await
13087            .unwrap()
13088            .unwrap();
13089        assert_eq!(record.status, "dropped");
13090        assert!(client.in_flight.lock().is_some());
13091
13092        drop_execution_schema(&admin_pool, &schema).await;
13093    }
13094
13095    #[tokio::test]
13096    async fn included_receipt_finality_timeout_marks_dropped_and_keeps_ownership() {
13097        let state = execution_rpc_state()
13098            .with_response("eth_getTransactionCount", TRANSACTION_COUNT)
13099            .with_response("eth_estimateGas", ESTIMATE_GAS)
13100            .with_response("eth_call", CALL_BALANCE)
13101            .with_response("eth_getTransactionReceipt", RECEIPT_SUCCESS)
13102            .with_parameter_response("eth_getBlockByNumber", "finalized", BLOCK_BY_NUMBER)
13103            .with_send_raw_transaction_echo();
13104        let Some((admin_pool, schema, mut client, _)) =
13105            execution_client_with_database("execution_included_timeout_test", state).await
13106        else {
13107            return;
13108        };
13109        client.transaction_limits.receipt_timeout_secs = 1;
13110
13111        let error = client
13112            .wrap(U256::from(1_000_000_000_000_000_u64))
13113            .await
13114            .unwrap_err();
13115        let transitions: Vec<String> = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
13116            "SELECT to_status FROM {schema}.execution_transaction_transition ORDER BY id"
13117        )))
13118        .fetch_all(&admin_pool)
13119        .await
13120        .unwrap();
13121
13122        assert!(
13123            error.to_string().contains("Timed out awaiting finality"),
13124            "was: {error}"
13125        );
13126        assert_eq!(transitions, ["prepared", "signed", "broadcast", "dropped"]);
13127        assert!(client.in_flight.lock().is_some());
13128
13129        drop_execution_schema(&admin_pool, &schema).await;
13130    }
13131
13132    #[tokio::test]
13133    async fn submit_order_single_in_flight_rejects_concurrent_swap() {
13134        let broadcast_release = Arc::new(tokio::sync::Semaphore::new(0));
13135        let state = swap_rpc_state()
13136            .await
13137            .with_response_release("eth_sendRawTransaction", Arc::clone(&broadcast_release));
13138        let Some((admin_pool, schema, mut client, state, cache)) =
13139            swap_client_with_database("execution_submit_concurrent_test", state).await
13140        else {
13141            return;
13142        };
13143        let pool = test_pool();
13144        let first = test_market_sell_order(pool.instrument_id);
13145        let second = OrderTestBuilder::new(OrderType::Market)
13146            .trader_id(TraderId::from("TRADER-001"))
13147            .strategy_id(StrategyId::from("S-001"))
13148            .instrument_id(pool.instrument_id)
13149            .client_order_id(ClientOrderId::from("O-SWAP-002"))
13150            .side(OrderSide::Sell)
13151            .quantity(Quantity::from("0.001"))
13152            .build();
13153        cache
13154            .borrow_mut()
13155            .add_order(second.clone(), None, None, false)
13156            .unwrap();
13157        let mut receiver = start_with_events(&mut client);
13158
13159        client.submit_order(submit_order_cmd(&first)).unwrap();
13160        await_recorded_requests(&state, "eth_sendRawTransaction", 1).await;
13161        client.submit_order(submit_order_cmd(&second)).unwrap();
13162        let event = tokio::time::timeout(TEST_TIMEOUT, receiver.recv())
13163            .await
13164            .unwrap()
13165            .unwrap();
13166        let ExecutionEvent::Order(OrderEventAny::Denied(denied)) = event else {
13167            panic!("expected OrderDenied, was {event:?}");
13168        };
13169        assert_eq!(denied.client_order_id, second.client_order_id());
13170        assert!(
13171            denied
13172                .reason
13173                .contains("at most one transaction can be in flight"),
13174            "was: {}",
13175            denied.reason
13176        );
13177        broadcast_release.add_permits(1);
13178        await_pending_tasks(&client).await;
13179
13180        let events = collect_order_events(&mut receiver);
13181        assert_swap_submitted_and_filled(&events);
13182
13183        let broadcasts = state
13184            .recorded_requests()
13185            .into_iter()
13186            .filter(|request| request["method"] == "eth_sendRawTransaction")
13187            .count();
13188        assert_eq!(broadcasts, 1);
13189        let row_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
13190            "SELECT COUNT(*) FROM {schema}.execution_intent"
13191        )))
13192        .fetch_one(&admin_pool)
13193        .await
13194        .unwrap();
13195        assert_eq!(row_count, 1);
13196        assert!(client.in_flight.lock().is_none());
13197
13198        drop_execution_schema(&admin_pool, &schema).await;
13199    }
13200
13201    #[tokio::test]
13202    async fn preflight_ready_when_all_checks_pass() {
13203        let (client, state) = client_with_mock_rpc(ready_rpc_state()).await;
13204        let pool = test_pool();
13205
13206        let report = client.preflight(&pool.instrument_id).await.unwrap();
13207
13208        assert!(report.ready, "issues: {:?}", report.issues);
13209        assert!(report.issues.is_empty());
13210        assert_eq!(report.expected_chain_id, 42161);
13211        assert_eq!(report.actual_chain_id, 42161);
13212        assert!(report.chain_id_matches);
13213        assert_eq!(
13214            report.pool.address,
13215            address!("C6962004f452bE9203591991D15f6b388e09E8D0")
13216        );
13217        assert!(report.pool.has_deployed_code);
13218        assert_eq!(report.pool.fee, Some(500));
13219        assert_eq!(
13220            report.pool.base_token,
13221            address!("82aF49447D8a07e3bd95BD0d56f35241523fBab1")
13222        );
13223        assert_eq!(
13224            report.pool.quote_token,
13225            address!("af88d065e77c8cC2239327C5EDb3A432268e5831")
13226        );
13227        assert_eq!(report.routers.len(), 1);
13228        assert!(report.routers[0].has_deployed_code);
13229        assert_eq!(report.tokens.len(), 2);
13230        assert_eq!(
13231            report.tokens[0].wallet_balance,
13232            U256::from(500_000_000_000_000_000u64)
13233        );
13234        assert_eq!(
13235            report.tokens[0].router_allowances,
13236            vec![(
13237                address!("E592427A0AEce92De3Edee1F18E0157C05861564"),
13238                U256::from(1_000_000_000_000_000_000u64)
13239            )]
13240        );
13241        assert_eq!(
13242            report.native_balance_wei,
13243            U256::from(1_000_000_000_000_000_000u64)
13244        );
13245        assert_eq!(report.base_fee_per_gas_wei, 100_000_000);
13246        assert_eq!(report.max_priority_fee_per_gas_wei, 10_000_000);
13247        assert_eq!(report.derived_max_fee_per_gas_wei, 130_000_000);
13248        assert!(report.fee_within_ceiling);
13249
13250        let requests = state.recorded_requests();
13251        for method in ["eth_getCode", "eth_call", "eth_getBalance"] {
13252            let matching: Vec<_> = requests
13253                .iter()
13254                .filter(|request| request["method"] == method)
13255                .collect();
13256            assert!(!matching.is_empty(), "method {method}");
13257            assert!(
13258                matching
13259                    .iter()
13260                    .all(|request| request["params"][1] == "latest"),
13261                "method {method}: {matching:?}"
13262            );
13263        }
13264    }
13265
13266    #[tokio::test]
13267    async fn preflight_not_ready_without_pool_fee() {
13268        let addr = start_mock_rpc_server(ready_rpc_state()).await;
13269        let mut pool = test_pool();
13270        pool.fee = None;
13271        let client = test_client_from_config(test_config(format!("http://{addr}")), pool.clone());
13272
13273        let report = client.preflight(&pool.instrument_id).await.unwrap();
13274
13275        assert!(!report.ready);
13276        assert_eq!(report.pool.fee, None);
13277        assert_eq!(report.issues, vec!["Pool fee tier is missing"]);
13278    }
13279
13280    #[tokio::test]
13281    async fn preflight_not_ready_on_wrong_chain() {
13282        let state = ready_rpc_state().with_response("eth_chainId", CHAIN_ID_ETHEREUM);
13283        let (client, _) = client_with_mock_rpc(state).await;
13284        let pool = test_pool();
13285
13286        let report = client.preflight(&pool.instrument_id).await.unwrap();
13287
13288        assert!(!report.ready);
13289        assert!(!report.chain_id_matches);
13290        assert!(
13291            report
13292                .issues
13293                .iter()
13294                .any(|issue| issue.contains("Chain ID mismatch"))
13295        );
13296    }
13297
13298    #[tokio::test]
13299    async fn preflight_not_ready_without_deployed_code() {
13300        let state = ready_rpc_state().with_response("eth_getCode", GET_CODE_EMPTY);
13301        let (client, _) = client_with_mock_rpc(state).await;
13302        let pool = test_pool();
13303
13304        let report = client.preflight(&pool.instrument_id).await.unwrap();
13305
13306        assert!(!report.ready);
13307        assert!(!report.pool.has_deployed_code);
13308        assert!(!report.routers[0].has_deployed_code);
13309        assert!(report.tokens.iter().all(|t| !t.has_deployed_code));
13310        assert!(
13311            report
13312                .issues
13313                .iter()
13314                .any(|issue| issue.contains("No deployed bytecode at router address"))
13315        );
13316    }
13317
13318    #[tokio::test]
13319    async fn preflight_not_ready_with_zero_balances_and_allowance() {
13320        let state = ready_rpc_state()
13321            .with_response("eth_getBalance", GET_BALANCE_ZERO)
13322            .with_call_response(BALANCE_OF_SELECTOR, CALL_ZERO)
13323            .with_call_response(ALLOWANCE_SELECTOR, CALL_ZERO);
13324        let (client, _) = client_with_mock_rpc(state).await;
13325        let pool = test_pool();
13326
13327        let report = client.preflight(&pool.instrument_id).await.unwrap();
13328
13329        assert!(!report.ready);
13330        assert!(
13331            report
13332                .issues
13333                .iter()
13334                .any(|issue| issue.contains("Native currency balance is zero"))
13335        );
13336        assert!(
13337            report
13338                .issues
13339                .iter()
13340                .any(|issue| issue.contains("balance is zero"))
13341        );
13342        assert!(
13343            report
13344                .issues
13345                .iter()
13346                .any(|issue| issue.contains("No router allowance"))
13347        );
13348    }
13349
13350    #[tokio::test]
13351    async fn preflight_not_ready_when_fees_exceed_ceiling() {
13352        let addr = start_mock_rpc_server(ready_rpc_state()).await;
13353        let mut config = test_config(format!("http://{addr}"));
13354        config.max_fee_per_gas_wei = 1;
13355        let pool = test_pool();
13356        let client = test_client_from_config(config, pool.clone());
13357
13358        let report = client.preflight(&pool.instrument_id).await.unwrap();
13359
13360        assert!(!report.ready);
13361        assert!(!report.fee_within_ceiling);
13362        assert_eq!(report.derived_max_fee_per_gas_wei, 130_000_000);
13363        assert!(
13364            report
13365                .issues
13366                .iter()
13367                .any(|issue| issue.contains("exceeds ceiling"))
13368        );
13369    }
13370
13371    #[rstest]
13372    fn resolve_pool_rejects_unknown_pool() {
13373        let client = test_client("http://127.0.0.1:1".to_string());
13374        let unknown: InstrumentId = "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45.Arbitrum:UniswapV3"
13375            .parse()
13376            .unwrap();
13377
13378        let error = client.resolve_pool(&unknown).unwrap_err();
13379
13380        assert!(error.to_string().contains("Unknown pool"), "was: {error}");
13381    }
13382
13383    #[rstest]
13384    fn resolve_pool_rejects_mismatched_chain() {
13385        let client = test_client("http://127.0.0.1:1".to_string());
13386        let ethereum_pool: InstrumentId =
13387            "0xC6962004f452bE9203591991D15f6b388e09E8D0.Ethereum:UniswapV3"
13388                .parse()
13389                .unwrap();
13390
13391        let error = client.resolve_pool(&ethereum_pool).unwrap_err();
13392
13393        assert!(
13394            error
13395                .to_string()
13396                .contains("does not match the client chain"),
13397            "was: {error}"
13398        );
13399    }
13400
13401    #[rstest]
13402    fn resolve_pool_rejects_unsupported_dex() {
13403        let client = test_client("http://127.0.0.1:1".to_string());
13404        let v4_pool: InstrumentId = "0xC6962004f452bE9203591991D15f6b388e09E8D0.Arbitrum:UniswapV4"
13405            .parse()
13406            .unwrap();
13407
13408        let error = client.resolve_pool(&v4_pool).unwrap_err();
13409
13410        assert!(
13411            error.to_string().contains("only UniswapV3 is supported"),
13412            "was: {error}"
13413        );
13414    }
13415
13416    #[rstest]
13417    fn resolve_pool_rejects_pool_id_identifier() {
13418        let client = test_client("http://127.0.0.1:1".to_string());
13419        let pool_id: InstrumentId =
13420            "0x0000000000000000000000000000000000000000000000000000000000000000.Arbitrum:UniswapV3"
13421                .parse()
13422                .unwrap();
13423
13424        let error = client.resolve_pool(&pool_id).unwrap_err();
13425
13426        assert!(
13427            error
13428                .to_string()
13429                .contains("only address identifiers are supported"),
13430            "was: {error}"
13431        );
13432    }
13433
13434    #[rstest]
13435    fn resolve_pool_rejects_ambiguous_token_priority() {
13436        let chain = Arc::new(chains::ARBITRUM.clone());
13437        let dex = Arc::clone(&UNISWAP_V3.dex);
13438        let token_a = Token::new(
13439            Arc::clone(&chain),
13440            address!("1111111111111111111111111111111111111111"),
13441            "Token A".to_string(),
13442            "TOKA".to_string(),
13443            18,
13444        );
13445        let token_b = Token::new(
13446            Arc::clone(&chain),
13447            address!("2222222222222222222222222222222222222222"),
13448            "Token B".to_string(),
13449            "TOKB".to_string(),
13450            18,
13451        );
13452        let pool = Pool::new(
13453            chain,
13454            dex,
13455            address!("3333333333333333333333333333333333333333"),
13456            PoolIdentifier::from_address(address!("3333333333333333333333333333333333333333")),
13457            55_000_000,
13458            token_a,
13459            token_b,
13460            Some(500),
13461            Some(10),
13462            UnixNanos::default(),
13463        );
13464        let client =
13465            test_client_from_config(test_config("http://127.0.0.1:1".to_string()), pool.clone());
13466
13467        let error = client.resolve_pool(&pool.instrument_id).unwrap_err();
13468
13469        assert!(error.to_string().contains("ambiguous"), "was: {error}");
13470    }
13471
13472    #[rstest]
13473    fn new_parses_pair_specific_quote_spend_limits_with_distinct_precisions() {
13474        let mut config = buy_test_config("http://127.0.0.1:1".to_string());
13475        config.quote_spend_limits = Some(vec![
13476            quote_spend_limit(WETH, USDC, 18, &U256::MAX.to_string()),
13477            quote_spend_limit(USDC, WETH, 6, "1000000000"),
13478        ]);
13479
13480        let client = test_client_from_config(config, test_pool());
13481        let sell_ceiling = client
13482            .transaction_limits
13483            .quote_spend_limits
13484            .get(&(WETH_ADDRESS, USDC_ADDRESS))
13485            .unwrap();
13486        let buy_ceiling = client
13487            .transaction_limits
13488            .quote_spend_limits
13489            .get(&(USDC_ADDRESS, WETH_ADDRESS))
13490            .unwrap();
13491
13492        assert_eq!(sell_ceiling.spend_token, WETH_ADDRESS);
13493        assert_eq!(sell_ceiling.spend_token_decimals, 18);
13494        assert_eq!(sell_ceiling.max_amount, U256::MAX);
13495        assert_eq!(buy_ceiling.spend_token, USDC_ADDRESS);
13496        assert_eq!(buy_ceiling.spend_token_decimals, 6);
13497        assert_eq!(buy_ceiling.max_amount, U256::from(1_000_000_000u64));
13498    }
13499
13500    #[rstest]
13501    fn new_rejects_quote_spend_limit_token_pair_mismatch() {
13502        let mut config = buy_test_config("http://127.0.0.1:1".to_string());
13503        config.quote_spend_limits.as_mut().unwrap()[0].spend_token = WETH.to_string();
13504
13505        let error = test_client_result(config, test_pool()).unwrap_err();
13506
13507        assert!(
13508            error
13509                .to_string()
13510                .contains("`spend_token` must match `token_in`"),
13511            "was: {error}"
13512        );
13513    }
13514
13515    #[rstest]
13516    fn new_rejects_quote_spend_limit_pair_outside_allowlist() {
13517        let mut config = buy_test_config("http://127.0.0.1:1".to_string());
13518        config.quote_spend_limits = Some(vec![quote_spend_limit(
13519            USDC,
13520            "0x1111111111111111111111111111111111111111",
13521            6,
13522            "1000000000",
13523        )]);
13524
13525        let error = test_client_result(config, test_pool()).unwrap_err();
13526
13527        assert!(
13528            error
13529                .to_string()
13530                .contains("is not in the `allowed_token_pairs` allowlist"),
13531            "was: {error}"
13532        );
13533    }
13534
13535    #[rstest]
13536    fn new_rejects_duplicate_quote_spend_pairs() {
13537        let mut config = buy_test_config("http://127.0.0.1:1".to_string());
13538        config.quote_spend_limits = Some(vec![
13539            quote_spend_limit(USDC, WETH, 6, "1000000000"),
13540            quote_spend_limit(USDC, WETH, 6, "2000000000"),
13541        ]);
13542
13543        let error = test_client_result(config, test_pool()).unwrap_err();
13544
13545        assert!(
13546            error
13547                .to_string()
13548                .contains("Duplicate quote spend limit for token pair"),
13549            "was: {error}"
13550        );
13551    }
13552
13553    #[rstest]
13554    #[case::empty("")]
13555    #[case::signed("-1")]
13556    #[case::fractional("1.5")]
13557    #[case::hexadecimal("0x10")]
13558    #[case::overflow(
13559        "115792089237316195423570985008687907853269984665640564039457584007913129639936"
13560    )]
13561    fn new_rejects_invalid_quote_spend_max_amount(#[case] max_amount: &str) {
13562        let mut config = buy_test_config("http://127.0.0.1:1".to_string());
13563        config.quote_spend_limits.as_mut().unwrap()[0].max_amount = max_amount.to_string();
13564
13565        let error = test_client_result(config, test_pool()).unwrap_err();
13566
13567        assert!(
13568            error.to_string().contains("Quote spend limit `max_amount`"),
13569            "was: {error}"
13570        );
13571    }
13572
13573    #[rstest]
13574    #[case::allowed_token_pairs("allowed_token_pairs")]
13575    #[case::slippage_bps("slippage_bps")]
13576    #[case::max_slippage_bps("max_slippage_bps")]
13577    #[case::max_order_amount("max_order_amount")]
13578    #[case::deadline_seconds("deadline_seconds")]
13579    #[case::max_quote_age_blocks("max_quote_age_blocks")]
13580    #[case::receipt_timeout_secs("receipt_timeout_secs")]
13581    fn new_rejects_each_missing_transaction_limit(#[case] missing: &str) {
13582        let mut config = test_config("http://127.0.0.1:1".to_string());
13583        match missing {
13584            "allowed_token_pairs" => config.allowed_token_pairs = None,
13585            "slippage_bps" => config.slippage_bps = None,
13586            "max_slippage_bps" => config.max_slippage_bps = None,
13587            "max_order_amount" => config.max_order_amount = None,
13588            "deadline_seconds" => config.deadline_seconds = None,
13589            "max_quote_age_blocks" => config.max_quote_age_blocks = None,
13590            "receipt_timeout_secs" => config.receipt_timeout_secs = None,
13591            _ => unreachable!(),
13592        }
13593
13594        let error = test_client_result(config, test_pool()).unwrap_err();
13595
13596        assert_eq!(
13597            error.to_string(),
13598            "Blockchain execution transaction limits are required: allowed_token_pairs, slippage_bps, max_slippage_bps, max_order_amount, deadline_seconds, max_quote_age_blocks, receipt_timeout_secs"
13599        );
13600    }
13601
13602    #[rstest]
13603    fn new_rejects_empty_router_allowlist() {
13604        let mut config = test_config("http://127.0.0.1:1".to_string());
13605        config.router_addresses = Vec::new();
13606
13607        let error = test_client_result(config, test_pool()).unwrap_err();
13608
13609        assert!(
13610            error.to_string().contains("at least one router address"),
13611            "was: {error}"
13612        );
13613    }
13614
13615    #[tokio::test]
13616    async fn wrap_refuses_without_durable_store() {
13617        let (mut client, _) = client_with_mock_rpc(ready_rpc_state()).await;
13618        client.core.set_connected();
13619
13620        let error = client.wrap(U256::from(1_000u64)).await.unwrap_err();
13621
13622        assert!(
13623            error.to_string().contains("No durable store configured"),
13624            "was: {error}"
13625        );
13626    }
13627
13628    #[tokio::test]
13629    async fn approve_rejects_router_outside_allowlist() {
13630        let (mut client, state) = client_with_mock_rpc(ready_rpc_state()).await;
13631
13632        let error = client
13633            .approve(
13634                WETH_ADDRESS,
13635                U256::from(1_000u64),
13636                address!("68b3465833fb72A70ecDF485E0e4C7bD8665Fc45"),
13637            )
13638            .await
13639            .err()
13640            .unwrap();
13641
13642        assert!(
13643            error
13644                .to_string()
13645                .contains("not in the configured `router_addresses` allowlist"),
13646            "was: {error}"
13647        );
13648        assert!(state.recorded_requests().is_empty());
13649    }
13650
13651    #[tokio::test]
13652    async fn approve_rejects_token_outside_input_allowlist() {
13653        let (mut client, state) = client_with_mock_rpc(ready_rpc_state()).await;
13654
13655        let error = client
13656            .approve(USDC_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
13657            .await
13658            .unwrap_err();
13659
13660        assert!(
13661            error
13662                .to_string()
13663                .contains("is not an input token in the configured `allowed_token_pairs`"),
13664            "was: {error}"
13665        );
13666        assert!(state.recorded_requests().is_empty());
13667    }
13668
13669    #[tokio::test]
13670    async fn in_flight_guard_rejects_second_transaction() {
13671        let (mut client, _) = client_with_mock_rpc(ready_rpc_state()).await;
13672        client.core.set_connected();
13673        *client.in_flight.lock() = Some(InFlightSlot::AwaitingFinality(InFlightTransaction {
13674            intent_id: 1,
13675            nonce: 7,
13676            tx_hash: B256::ZERO,
13677            purpose: TransactionPurpose::Wrap,
13678        }));
13679
13680        let error = client.wrap(U256::from(1_000u64)).await.unwrap_err();
13681
13682        assert!(
13683            error.to_string().contains("still awaiting finality"),
13684            "was: {error}"
13685        );
13686    }
13687
13688    #[tokio::test]
13689    async fn wrap_rejects_zero_amount() {
13690        let (mut client, _) = client_with_mock_rpc(ready_rpc_state()).await;
13691
13692        let error = client.wrap(U256::ZERO).await.unwrap_err();
13693
13694        assert!(
13695            error.to_string().contains("Wrap amount must be positive"),
13696            "was: {error}"
13697        );
13698    }
13699
13700    #[tokio::test]
13701    async fn wrap_rejects_code_free_target_before_broadcast() {
13702        let state = execution_rpc_state().with_response("eth_getCode", GET_CODE_EMPTY);
13703        let Some((admin_pool, schema, mut client, state)) =
13704            execution_client_with_database("execution_wrap_code_free_test", state).await
13705        else {
13706            return;
13707        };
13708
13709        let error = client
13710            .wrap(U256::from(1_000_000_000_000_000u64))
13711            .await
13712            .unwrap_err();
13713
13714        assert!(
13715            error
13716                .to_string()
13717                .contains("pre-sign deployment manifest verification disagreed"),
13718            "was: {error}"
13719        );
13720        assert!(client.in_flight.lock().is_none());
13721        let requests = state.recorded_requests();
13722        assert_eq!(
13723            requests
13724                .iter()
13725                .filter(|request| request["method"] == "eth_getCode")
13726                .count(),
13727            3
13728        );
13729        assert!(
13730            requests
13731                .iter()
13732                .all(|request| request["method"] != "eth_sendRawTransaction")
13733        );
13734
13735        drop_execution_schema(&admin_pool, &schema).await;
13736    }
13737
13738    #[tokio::test]
13739    async fn wrap_rejects_unrelated_target_before_broadcast() {
13740        let state = execution_rpc_state().with_response("eth_call", CALL_EMPTY);
13741        let Some((admin_pool, schema, mut client, state)) =
13742            execution_client_with_database("execution_wrap_unrelated_test", state).await
13743        else {
13744            return;
13745        };
13746
13747        let error = client
13748            .wrap(U256::from(1_000_000_000_000_000u64))
13749            .await
13750            .unwrap_err();
13751
13752        assert!(
13753            error
13754                .to_string()
13755                .contains("pre-sign wrapped token probe verification is unavailable"),
13756            "was: {error}"
13757        );
13758        assert!(client.in_flight.lock().is_none());
13759        let requests = state.recorded_requests();
13760        assert!(
13761            requests
13762                .iter()
13763                .all(|request| request["method"] != "eth_sendRawTransaction")
13764        );
13765
13766        drop_execution_schema(&admin_pool, &schema).await;
13767    }
13768
13769    #[tokio::test]
13770    async fn wrap_rejects_included_transaction_without_balance_delta() {
13771        let state = broadcast_rpc_state().with_response_sequence("eth_call", &[CALL_BALANCE; 9]);
13772        let Some((admin_pool, schema, mut client, state)) =
13773            execution_client_with_database("execution_wrap_no_delta_test", state).await
13774        else {
13775            return;
13776        };
13777
13778        let error = client
13779            .wrap(U256::from(1_000_000_000_000_000u64))
13780            .await
13781            .unwrap_err();
13782
13783        assert!(
13784            error.to_string().contains("did not increase"),
13785            "was: {error}"
13786        );
13787        let in_flight = awaiting_in_flight(&client);
13788        assert_eq!(in_flight.purpose, TransactionPurpose::Wrap);
13789        assert_eq!(
13790            execution_intent_markers(&admin_pool, &schema).await,
13791            vec![("wrap".into(), "broadcast".into(), false, true)]
13792        );
13793        let nonce_state = sqlx::query_as::<_, (i64, i64)>(sqlx::AssertSqlSafe(format!(
13794            "SELECT next_canonical_nonce, revision FROM {schema}.execution_verification_nonce"
13795        )))
13796        .fetch_one(&admin_pool)
13797        .await
13798        .unwrap();
13799        assert_eq!(nonce_state, (7, 0));
13800        let broadcasts = state
13801            .recorded_requests()
13802            .into_iter()
13803            .filter(|request| request["method"] == "eth_sendRawTransaction")
13804            .count();
13805        assert_eq!(broadcasts, 1);
13806
13807        let expected_hash = expected_wrap_tx_hash(U256::from(1_000_000_000_000_000u64)).await;
13808        let block = finalized_wrap_block(expected_hash);
13809        let receipt = receipt_with_transaction_hash(RECEIPT_SUCCESS, expected_hash);
13810        let restart_state = with_finalized_identity(
13811            execution_rpc_state()
13812                .with_response("eth_getTransactionReceipt", &receipt)
13813                .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block)
13814                .with_response_sequence("eth_call", &[CALL_BALANCE; 6]),
13815            &block,
13816            &receipt,
13817        );
13818        let addr = start_mock_rpc_server(restart_state).await;
13819        let error = later_reconnect(client, format!("http://{addr}")).await;
13820        assert!(
13821            error.to_string().contains("did not increase"),
13822            "was: {error}"
13823        );
13824        assert_eq!(
13825            execution_intent_markers(&admin_pool, &schema).await,
13826            vec![("wrap".into(), "broadcast".into(), false, true)]
13827        );
13828        let nonce_state = sqlx::query_as::<_, (i64, i64)>(sqlx::AssertSqlSafe(format!(
13829            "SELECT next_canonical_nonce, revision FROM {schema}.execution_verification_nonce"
13830        )))
13831        .fetch_one(&admin_pool)
13832        .await
13833        .unwrap();
13834        assert_eq!(nonce_state, (7, 0));
13835
13836        drop_execution_schema(&admin_pool, &schema).await;
13837    }
13838
13839    #[tokio::test]
13840    async fn wrap_reports_inclusion_when_postcondition_read_fails() {
13841        let state = broadcast_rpc_state().with_response_sequence(
13842            "eth_call",
13843            &[
13844                CALL_BALANCE,
13845                CALL_BALANCE,
13846                CALL_BALANCE,
13847                CALL_BALANCE,
13848                CALL_BALANCE,
13849                CALL_BALANCE,
13850                RPC_METHOD_NOT_FOUND,
13851                RPC_METHOD_NOT_FOUND,
13852                RPC_METHOD_NOT_FOUND,
13853            ],
13854        );
13855        let Some((admin_pool, schema, mut client, _)) =
13856            execution_client_with_database("execution_wrap_postcondition_rpc_test", state).await
13857        else {
13858            return;
13859        };
13860
13861        let error = client
13862            .wrap(U256::from(1_000_000_000_000_000u64))
13863            .await
13864            .unwrap_err();
13865
13866        let message = error.to_string();
13867        assert!(
13868            message.contains("failed to verify WETH balance after included transaction 0x"),
13869            "was: {message}"
13870        );
13871        assert!(message.contains("at block 30346561"), "was: {message}");
13872        let in_flight = awaiting_in_flight(&client);
13873        assert_eq!(in_flight.purpose, TransactionPurpose::Wrap);
13874        assert_eq!(
13875            execution_intent_markers(&admin_pool, &schema).await,
13876            vec![("wrap".into(), "broadcast".into(), false, true)]
13877        );
13878
13879        drop_execution_schema(&admin_pool, &schema).await;
13880    }
13881
13882    #[tokio::test]
13883    async fn approve_rejects_false_return_before_broadcast() {
13884        let state = execution_rpc_state().with_response("eth_call", CALL_ZERO);
13885        let Some((admin_pool, schema, mut client, state)) =
13886            execution_client_with_database("execution_approve_false_test", state).await
13887        else {
13888            return;
13889        };
13890
13891        let error = client
13892            .approve(WETH_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
13893            .await
13894            .unwrap_err();
13895
13896        assert!(error.to_string().contains("returned false"), "was: {error}");
13897        assert!(client.in_flight.lock().is_none());
13898        let requests = state.recorded_requests();
13899        let approval_calls = requests
13900            .iter()
13901            .filter(|request| {
13902                request["method"] == "eth_call"
13903                    && request["params"][0]["data"]
13904                        .as_str()
13905                        .is_some_and(|data| data.starts_with("0x095ea7b3"))
13906            })
13907            .collect::<Vec<_>>();
13908        assert_eq!(approval_calls.len(), 3);
13909        for request in approval_calls {
13910            assert_eq!(
13911                request["params"][0]["from"]
13912                    .as_str()
13913                    .unwrap()
13914                    .parse::<Address>()
13915                    .unwrap(),
13916                WALLET.parse::<Address>().unwrap()
13917            );
13918            assert_eq!(request["params"][1], FIXTURE_BLOCK_PARAM);
13919        }
13920        assert!(
13921            requests
13922                .iter()
13923                .all(|request| request["method"] != "eth_getTransactionCount")
13924        );
13925        assert!(
13926            requests
13927                .iter()
13928                .all(|request| request["method"] != "eth_sendRawTransaction")
13929        );
13930
13931        drop_execution_schema(&admin_pool, &schema).await;
13932    }
13933
13934    #[tokio::test]
13935    async fn approve_rejects_router_with_wrong_factory_before_signing() {
13936        let state = ready_rpc_state().with_call_response(FACTORY_SELECTOR, CALL_ZERO);
13937        let Some((admin_pool, schema, mut client, state)) =
13938            execution_client_with_database("execution_approve_factory_test", state).await
13939        else {
13940            return;
13941        };
13942
13943        let error = client
13944            .approve(WETH_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
13945            .await
13946            .unwrap_err();
13947
13948        assert!(
13949            error
13950                .to_string()
13951                .contains("pre-sign deployment manifest verification disagreed"),
13952            "was: {error}"
13953        );
13954        let requests = state.recorded_requests();
13955        assert!(
13956            requests
13957                .iter()
13958                .all(|request| request["method"] != "eth_getTransactionCount")
13959        );
13960        assert!(
13961            requests
13962                .iter()
13963                .all(|request| request["method"] != "eth_sendRawTransaction")
13964        );
13965
13966        drop_execution_schema(&admin_pool, &schema).await;
13967    }
13968
13969    #[tokio::test]
13970    async fn approve_rejects_router_with_wrong_weth_before_signing() {
13971        let state = ready_rpc_state().with_call_response(WETH9_SELECTOR, CALL_ZERO);
13972        let Some((admin_pool, schema, mut client, state)) =
13973            execution_client_with_database("execution_approve_weth_test", state).await
13974        else {
13975            return;
13976        };
13977
13978        let error = client
13979            .approve(WETH_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
13980            .await
13981            .unwrap_err();
13982
13983        assert!(
13984            error
13985                .to_string()
13986                .contains("pre-sign deployment manifest verification disagreed"),
13987            "was: {error}"
13988        );
13989        let requests = state.recorded_requests();
13990        assert!(
13991            requests
13992                .iter()
13993                .all(|request| request["method"] != "eth_getTransactionCount")
13994        );
13995        assert!(
13996            requests
13997                .iter()
13998                .all(|request| request["method"] != "eth_sendRawTransaction")
13999        );
14000
14001        drop_execution_schema(&admin_pool, &schema).await;
14002    }
14003
14004    #[tokio::test]
14005    async fn approve_rejects_nonzero_to_nonzero_transition_before_signing() {
14006        let state = ready_rpc_state();
14007        let Some((admin_pool, schema, mut client, state)) =
14008            execution_client_with_database("execution_approve_nonzero_test", state).await
14009        else {
14010            return;
14011        };
14012
14013        let error = client
14014            .approve(WETH_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
14015            .await
14016            .unwrap_err();
14017
14018        assert!(
14019            error.to_string().contains("approve zero before setting"),
14020            "was: {error}"
14021        );
14022        let requests = state.recorded_requests();
14023        assert!(
14024            requests
14025                .iter()
14026                .all(|request| request["method"] != "eth_getTransactionCount")
14027        );
14028        assert!(
14029            requests
14030                .iter()
14031                .all(|request| request["method"] != "eth_sendRawTransaction")
14032        );
14033
14034        drop_execution_schema(&admin_pool, &schema).await;
14035    }
14036
14037    #[tokio::test]
14038    async fn approve_zero_revokes_under_unlimited_policy() {
14039        let state = broadcast_rpc_state()
14040            .with_response("eth_call", CALL_BOOL_TRUE)
14041            .with_call_response_sequence(
14042                ALLOWANCE_SELECTOR,
14043                &[
14044                    CALL_ALLOWANCE,
14045                    CALL_ALLOWANCE,
14046                    CALL_ALLOWANCE,
14047                    CALL_ZERO,
14048                    CALL_ZERO,
14049                    CALL_ZERO,
14050                ],
14051            );
14052        let Some((admin_pool, schema, mut client, state)) =
14053            execution_client_with_database("execution_approve_revoke_test", state).await
14054        else {
14055            return;
14056        };
14057        client.config.unlimited_approval = true;
14058        client.transaction_limits.receipt_timeout_secs = TEST_TIMEOUT.as_secs();
14059
14060        let tx_hash = client
14061            .approve(WETH_ADDRESS, U256::ZERO, ROUTER_ADDRESS)
14062            .await
14063            .unwrap();
14064
14065        assert_eq!(tx_hash, expected_approve_tx_hash(U256::ZERO).await);
14066        let approve_data = state
14067            .recorded_requests()
14068            .into_iter()
14069            .find_map(|request| {
14070                (request["method"] == "eth_estimateGas")
14071                    .then(|| request["params"][0]["data"].as_str().map(str::to_owned))
14072                    .flatten()
14073            })
14074            .unwrap();
14075        assert!(approve_data.starts_with("0x095ea7b3"));
14076        assert!(approve_data.ends_with(&"0".repeat(64)));
14077        assert_eq!(
14078            state
14079                .recorded_requests()
14080                .iter()
14081                .filter(|request| {
14082                    request["method"] == "eth_call"
14083                        && request["params"][0]["data"]
14084                            .as_str()
14085                            .is_some_and(|data| data.starts_with(FACTORY_SELECTOR))
14086                })
14087                .count(),
14088            12
14089        );
14090        assert_eq!(
14091            execution_intent_markers(&admin_pool, &schema).await,
14092            vec![("approve".into(), "finalized".into(), true, false)]
14093        );
14094
14095        drop_execution_schema(&admin_pool, &schema).await;
14096    }
14097
14098    #[tokio::test]
14099    async fn approve_accepts_empty_return_with_sufficient_allowance() {
14100        let state = broadcast_rpc_state()
14101            .with_response("eth_call", CALL_EMPTY)
14102            .with_call_response_sequence(
14103                ALLOWANCE_SELECTOR,
14104                &[
14105                    CALL_ZERO,
14106                    CALL_ZERO,
14107                    CALL_ZERO,
14108                    CALL_ALLOWANCE_1000,
14109                    CALL_ALLOWANCE_1000,
14110                    CALL_ALLOWANCE_1000,
14111                ],
14112            );
14113        let Some((admin_pool, schema, mut client, _)) =
14114            execution_client_with_database("execution_approve_empty_test", state).await
14115        else {
14116            return;
14117        };
14118
14119        let tx_hash = client
14120            .approve(WETH_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
14121            .await
14122            .unwrap();
14123
14124        let record = client
14125            .cache
14126            .get_execution_transaction(42161, &tx_hash.to_string())
14127            .await
14128            .unwrap()
14129            .unwrap();
14130        assert_eq!(record.purpose, "approve");
14131        assert_eq!(record.status, "finalized");
14132        assert!(client.in_flight.lock().is_none());
14133        assert_eq!(
14134            execution_intent_markers(&admin_pool, &schema).await,
14135            vec![("approve".into(), "finalized".into(), true, false)]
14136        );
14137
14138        drop_execution_schema(&admin_pool, &schema).await;
14139    }
14140
14141    #[tokio::test]
14142    async fn approve_rejects_empty_return_with_insufficient_allowance() {
14143        let state = broadcast_rpc_state()
14144            .with_response("eth_call", CALL_EMPTY)
14145            .with_call_response_sequence(ALLOWANCE_SELECTOR, &[CALL_ZERO; 6]);
14146        let Some((admin_pool, schema, mut client, _)) =
14147            execution_client_with_database("execution_approve_insufficient_test", state).await
14148        else {
14149            return;
14150        };
14151
14152        let error = client
14153            .approve(WETH_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
14154            .await
14155            .unwrap_err();
14156
14157        assert!(
14158            error
14159                .to_string()
14160                .contains("does not equal the requested amount"),
14161            "was: {error}"
14162        );
14163        let in_flight = awaiting_in_flight(&client);
14164        assert_eq!(in_flight.purpose, TransactionPurpose::Approve);
14165        assert_eq!(
14166            execution_intent_markers(&admin_pool, &schema).await,
14167            vec![("approve".into(), "broadcast".into(), false, true)]
14168        );
14169        let nonce_state = sqlx::query_as::<_, (i64, i64)>(sqlx::AssertSqlSafe(format!(
14170            "SELECT next_canonical_nonce, revision FROM {schema}.execution_verification_nonce"
14171        )))
14172        .fetch_one(&admin_pool)
14173        .await
14174        .unwrap();
14175        assert_eq!(nonce_state, (7, 0));
14176
14177        let expected_hash = expected_approve_tx_hash(U256::from(1_000u64)).await;
14178        let block = finalized_approve_block(expected_hash, U256::from(1_000u64));
14179        let receipt = receipt_with_transaction_hash(RECEIPT_SUCCESS, expected_hash);
14180        let restart_state = with_finalized_identity(
14181            execution_rpc_state()
14182                .with_response("eth_getTransactionReceipt", &receipt)
14183                .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block)
14184                .with_call_response(ALLOWANCE_SELECTOR, CALL_ZERO),
14185            &block,
14186            &receipt,
14187        );
14188        let addr = start_mock_rpc_server(restart_state).await;
14189        let error = later_reconnect(client, format!("http://{addr}")).await;
14190        assert!(
14191            error
14192                .to_string()
14193                .contains("does not equal the requested amount"),
14194            "was: {error}"
14195        );
14196        assert_eq!(
14197            execution_intent_markers(&admin_pool, &schema).await,
14198            vec![("approve".into(), "broadcast".into(), false, true)]
14199        );
14200        let nonce_state = sqlx::query_as::<_, (i64, i64)>(sqlx::AssertSqlSafe(format!(
14201            "SELECT next_canonical_nonce, revision FROM {schema}.execution_verification_nonce"
14202        )))
14203        .fetch_one(&admin_pool)
14204        .await
14205        .unwrap();
14206        assert_eq!(nonce_state, (7, 0));
14207
14208        drop_execution_schema(&admin_pool, &schema).await;
14209    }
14210
14211    #[tokio::test]
14212    async fn approve_reports_inclusion_when_postcondition_read_fails() {
14213        let state = broadcast_rpc_state()
14214            .with_response("eth_call", CALL_BOOL_TRUE)
14215            .with_call_response_sequence(
14216                ALLOWANCE_SELECTOR,
14217                &[
14218                    CALL_ZERO,
14219                    CALL_ZERO,
14220                    CALL_ZERO,
14221                    RPC_METHOD_NOT_FOUND,
14222                    RPC_METHOD_NOT_FOUND,
14223                    RPC_METHOD_NOT_FOUND,
14224                ],
14225            );
14226        let Some((admin_pool, schema, mut client, _)) =
14227            execution_client_with_database("execution_approve_postcondition_rpc_test", state).await
14228        else {
14229            return;
14230        };
14231
14232        let error = client
14233            .approve(WETH_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
14234            .await
14235            .unwrap_err();
14236
14237        let message = error.to_string();
14238        assert!(
14239            message.contains("failed to verify router allowance after included transaction 0x"),
14240            "was: {message}"
14241        );
14242        assert!(message.contains("at block 30346561"), "was: {message}");
14243        let in_flight = awaiting_in_flight(&client);
14244        assert_eq!(in_flight.purpose, TransactionPurpose::Approve);
14245        assert_eq!(
14246            execution_intent_markers(&admin_pool, &schema).await,
14247            vec![("approve".into(), "broadcast".into(), false, true)]
14248        );
14249
14250        drop_execution_schema(&admin_pool, &schema).await;
14251    }
14252
14253    #[tokio::test]
14254    async fn wallet_balance_refresh_replaces_complete_snapshot_with_exact_precision() {
14255        let state = execution_rpc_state()
14256            .with_response_sequence("eth_getBalance", &[GET_BALANCE, GET_BALANCE_ZERO])
14257            .with_response_sequence(
14258                "eth_call",
14259                &[
14260                    CALL_BALANCE_WETH,
14261                    CALL_BALANCE_USDC,
14262                    CALL_BALANCE_WETH_UPDATED,
14263                    CALL_BALANCE_USDC_UPDATED,
14264                ],
14265            );
14266        let (mut client, _, _) =
14267            client_with_token_mock_rpc(state, "BLOCKCHAIN_TEST_BALANCE_REPLACE").await;
14268        let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel();
14269        replace_exec_event_sender(sender);
14270        client.start().unwrap();
14271
14272        client.refresh_wallet_balances().await.unwrap();
14273        let balances = client.wallet_balance.lock().as_account_balances().unwrap();
14274
14275        assert_eq!(balances.len(), 3);
14276        assert_eq!(balances[0].currency.code, "ETH");
14277        assert_eq!(balances[0].currency.name, "Ethereum");
14278        assert_eq!(balances[0].currency.precision, 18);
14279        assert_eq!(balances[0].total.raw(), 1_000_000_000_000_000_000);
14280        assert_eq!(balances[0].free, balances[0].total);
14281        assert_eq!(balances[0].locked, Money::zero(balances[0].currency));
14282        assert_eq!(balances[1].currency.code, "WETH");
14283        assert_eq!(balances[1].currency.name, "Wrapped Ether");
14284        assert_eq!(balances[1].currency.precision, 18);
14285        assert_eq!(balances[1].total.raw(), 1_234_567_890_123_456_789);
14286        assert_eq!(balances[1].free, balances[1].total);
14287        assert_eq!(balances[1].locked, Money::zero(balances[1].currency));
14288        assert_eq!(balances[2].currency.code, "USDC");
14289        assert_eq!(balances[2].currency.name, "USD Coin");
14290        assert_eq!(balances[2].currency.precision, 6);
14291        assert_eq!(balances[2].total.raw(), 9_876_543_210_000_000_000);
14292        assert_eq!(balances[2].free, balances[2].total);
14293        assert_eq!(balances[2].locked, Money::zero(balances[2].currency));
14294
14295        client.refresh_wallet_balances().await.unwrap();
14296        let balances = client.wallet_balance.lock().as_account_balances().unwrap();
14297
14298        assert_eq!(balances.len(), 3);
14299        assert_eq!(client.wallet_balance.lock().token_balances.len(), 2);
14300        assert_eq!(balances[0].total.raw(), 0);
14301        assert_eq!(balances[1].total.raw(), 2_000_000_000_000_000_000);
14302        assert_eq!(balances[2].total.raw(), 12_345_670_000_000_000);
14303    }
14304
14305    #[allow(unsafe_code)] // env-var mutation in tests; unique var names avoid cross-test races
14306    #[tokio::test]
14307    async fn failed_connect_refresh_retains_snapshot_and_publishes_nothing() {
14308        let state = execution_rpc_state()
14309            .with_response_sequence("eth_getBalance", &[GET_BALANCE, GET_BALANCE_ZERO])
14310            .with_response_sequence(
14311                "eth_call",
14312                &[
14313                    CALL_BALANCE_WETH,
14314                    CALL_BALANCE_USDC,
14315                    CALL_BALANCE_WETH_UPDATED,
14316                    RPC_METHOD_NOT_FOUND,
14317                ],
14318            );
14319        let (mut client, _, _) =
14320            client_with_token_mock_rpc(state, "BLOCKCHAIN_TEST_BALANCE_ATOMIC").await;
14321        let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
14322        replace_exec_event_sender(sender);
14323        client.start().unwrap();
14324        client.refresh_wallet_balances().await.unwrap();
14325        let retained = client.wallet_balance.lock().as_account_balances().unwrap();
14326        receiver.try_recv().unwrap();
14327        // SAFETY: this variable name is unique to this test across the test binary
14328        unsafe { std::env::set_var("BLOCKCHAIN_TEST_BALANCE_ATOMIC", TEST_PRIVATE_KEY) };
14329
14330        let error = client.connect().await.unwrap_err();
14331        let failed_address = Address::from_str(USDC).unwrap();
14332
14333        assert!(
14334            error.to_string().contains(&format!(
14335                "failed to fetch token balance for {failed_address}"
14336            )),
14337            "was: {error}"
14338        );
14339        assert!(!client.is_connected());
14340        assert!(client.signer.is_none());
14341        assert_eq!(
14342            client.wallet_balance.lock().as_account_balances().unwrap(),
14343            retained
14344        );
14345        assert!(matches!(
14346            receiver.try_recv(),
14347            Err(tokio::sync::mpsc::error::TryRecvError::Empty)
14348        ));
14349    }
14350
14351    #[allow(unsafe_code)] // env-var mutation in tests; unique var names avoid cross-test races
14352    #[tokio::test]
14353    async fn failed_connect_publication_retains_snapshot() {
14354        let state = execution_rpc_state()
14355            .with_response_sequence("eth_getBalance", &[GET_BALANCE, GET_BALANCE_ZERO])
14356            .with_response_sequence(
14357                "eth_call",
14358                &[
14359                    CALL_BALANCE_WETH,
14360                    CALL_BALANCE_USDC,
14361                    CALL_BALANCE_WETH_UPDATED,
14362                    CALL_BALANCE_USDC_UPDATED,
14363                ],
14364            );
14365        let (mut client, _, _) =
14366            client_with_token_mock_rpc(state, "BLOCKCHAIN_TEST_BALANCE_PUBLICATION").await;
14367        let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
14368        replace_exec_event_sender(sender);
14369        client.start().unwrap();
14370        client.refresh_wallet_balances().await.unwrap();
14371        let retained = client.wallet_balance.lock().as_account_balances().unwrap();
14372        receiver.try_recv().unwrap();
14373        drop(receiver);
14374        // SAFETY: this variable name is unique to this test across the test binary
14375        unsafe { std::env::set_var("BLOCKCHAIN_TEST_BALANCE_PUBLICATION", TEST_PRIVATE_KEY) };
14376
14377        let error = client.connect().await.unwrap_err();
14378
14379        assert!(
14380            error.to_string().contains("Failed to send account state"),
14381            "was: {error}"
14382        );
14383        assert!(!client.is_connected());
14384        assert!(client.signer.is_none());
14385        assert_eq!(
14386            client.wallet_balance.lock().as_account_balances().unwrap(),
14387            retained
14388        );
14389    }
14390
14391    #[allow(unsafe_code)] // env-var mutation in tests; unique var names avoid cross-test races
14392    #[tokio::test]
14393    async fn connect_and_repeated_query_publish_wallet_account_state() {
14394        let state = execution_rpc_state()
14395            .with_response_sequence("eth_call", &[CALL_BALANCE_WETH, CALL_BALANCE_USDC]);
14396        let (mut client, state, cache) =
14397            client_with_token_mock_rpc(state, "BLOCKCHAIN_TEST_ACCOUNT_STATE").await;
14398        let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
14399        replace_exec_event_sender(sender);
14400        client.start().unwrap();
14401        // SAFETY: this variable name is unique to this test across the test binary
14402        unsafe { std::env::set_var("BLOCKCHAIN_TEST_ACCOUNT_STATE", TEST_PRIVATE_KEY) };
14403
14404        client.connect().await.unwrap();
14405
14406        let ExecutionEvent::Account(connected) = receiver.try_recv().unwrap() else {
14407            panic!("expected account state event")
14408        };
14409        assert_eq!(connected.account_id, AccountId::from("BLOCKCHAIN-001"));
14410        assert_eq!(connected.account_type, AccountType::Wallet);
14411        assert_eq!(connected.base_currency, None);
14412        assert_eq!(connected.balances.len(), 3);
14413        assert!(connected.margins.is_empty());
14414        assert!(connected.is_reported);
14415        cache.borrow_mut().update_account_state(&connected).unwrap();
14416
14417        let account = client.get_account().unwrap();
14418        assert!(matches!(account, AccountAny::Wallet(_)));
14419        assert_eq!(account.id(), AccountId::from("BLOCKCHAIN-001"));
14420        assert_eq!(account.last_event(), Some(connected.clone()));
14421
14422        let requests_before = state.recorded_requests().len();
14423        let query = || {
14424            QueryAccount::new(
14425                TraderId::from("TRADER-001"),
14426                Some(ClientId::from("BLOCKCHAIN-001")),
14427                AccountId::from("BLOCKCHAIN-001"),
14428                UUID4::new(),
14429                UnixNanos::default(),
14430                None,
14431                None,
14432            )
14433        };
14434        client.query_account(query()).unwrap();
14435        client.query_account(query()).unwrap();
14436
14437        let ExecutionEvent::Account(first_query) = receiver.try_recv().unwrap() else {
14438            panic!("expected first query account state event")
14439        };
14440        let ExecutionEvent::Account(second_query) = receiver.try_recv().unwrap() else {
14441            panic!("expected second query account state event")
14442        };
14443        assert!(connected.has_same_balances_and_margins(&first_query));
14444        assert!(first_query.has_same_balances_and_margins(&second_query));
14445        assert_eq!(first_query.account_id, connected.account_id);
14446        assert_eq!(first_query.account_type, connected.account_type);
14447        assert_eq!(first_query.base_currency, connected.base_currency);
14448        assert_eq!(first_query.is_reported, connected.is_reported);
14449        assert_ne!(first_query.event_id, second_query.event_id);
14450        assert_eq!(state.recorded_requests().len(), requests_before);
14451
14452        client.stop().unwrap();
14453        assert!(client.core.is_stopped());
14454        assert!(!client.is_connected());
14455        assert!(client.signer.is_none());
14456    }
14457
14458    #[tokio::test]
14459    async fn connect_rejects_on_chain_mismatch() {
14460        let state = ready_rpc_state().with_response("eth_chainId", CHAIN_ID_ETHEREUM);
14461        let (mut client, _) = client_with_mock_rpc(state).await;
14462
14463        let error = client.connect().await.unwrap_err();
14464
14465        assert!(
14466            error
14467                .to_string()
14468                .contains("Blockchain chain ID verification disagreed"),
14469            "was: {error}"
14470        );
14471    }
14472
14473    #[allow(unsafe_code)] // env-var mutation in tests; unique var names avoid cross-test races
14474    #[tokio::test]
14475    async fn connect_rejects_on_signer_wallet_mismatch() {
14476        let addr = start_mock_rpc_server(ready_rpc_state()).await;
14477        let config = test_config_with_signer_env(
14478            format!("http://{addr}"),
14479            "BLOCKCHAIN_TEST_PRIVATE_KEY_MISMATCH",
14480        );
14481        let mut client = test_client_from_config(config, test_pool());
14482        // Anvil development key #1 derives a different address than the configured wallet
14483        // SAFETY: this variable name is unique to this test across the test binary
14484        unsafe {
14485            std::env::set_var(
14486                "BLOCKCHAIN_TEST_PRIVATE_KEY_MISMATCH",
14487                "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d",
14488            )
14489        };
14490
14491        let error = client.connect().await.unwrap_err();
14492
14493        assert!(
14494            error
14495                .to_string()
14496                .contains("does not match configured wallet address"),
14497            "was: {error}"
14498        );
14499    }
14500
14501    #[allow(unsafe_code)] // env-var mutation in tests; unique var names avoid cross-test races
14502    #[tokio::test]
14503    async fn connect_rejects_missing_trace_capability_before_signer_load() {
14504        let state = ready_rpc_state().with_parameter_response(
14505            "debug_traceTransaction",
14506            &B256::ZERO.to_string(),
14507            RPC_METHOD_NOT_FOUND,
14508        );
14509        let addr = start_mock_rpc_server(state.clone()).await;
14510        let config = test_config_with_signer_env(
14511            format!("http://{addr}"),
14512            "BLOCKCHAIN_TEST_TRACE_CAPABILITY",
14513        );
14514        let mut client = test_client_from_config(config, test_pool());
14515        // SAFETY: this variable name is unique to this test across the test binary
14516        unsafe { std::env::set_var("BLOCKCHAIN_TEST_TRACE_CAPABILITY", TEST_PRIVATE_KEY) };
14517
14518        let error = client.connect().await.unwrap_err();
14519
14520        assert!(
14521            error
14522                .to_string()
14523                .contains("Blockchain call trace capability verification is locally invalid"),
14524            "was: {error}"
14525        );
14526        assert!(client.signer.is_none());
14527        assert!(
14528            state
14529                .recorded_requests()
14530                .iter()
14531                .all(|request| request["method"] != "eth_getBalance")
14532        );
14533    }
14534
14535    #[allow(unsafe_code)] // env-var mutation in tests; unique var names avoid cross-test races
14536    #[tokio::test]
14537    async fn connect_initializes_signer_from_env() {
14538        let addr = start_mock_rpc_server(ready_rpc_state()).await;
14539        let config =
14540            test_config_with_signer_env(format!("http://{addr}"), "BLOCKCHAIN_TEST_PRIVATE_KEY_OK");
14541        let mut client = test_client_from_config(config, test_pool());
14542        let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel();
14543        replace_exec_event_sender(sender);
14544        client.start().unwrap();
14545        // SAFETY: this variable name is unique to this test across the test binary
14546        unsafe { std::env::set_var("BLOCKCHAIN_TEST_PRIVATE_KEY_OK", TEST_PRIVATE_KEY) };
14547
14548        client.connect().await.unwrap();
14549
14550        let signer = client.signer.as_ref().unwrap();
14551        assert_eq!(
14552            signer.address(),
14553            address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266")
14554        );
14555    }
14556
14557    #[allow(unsafe_code)] // env-var mutation in tests; unique var names avoid cross-test races
14558    #[tokio::test]
14559    async fn reconnect_resumes_at_the_durable_finalized_tip() {
14560        let Some((admin_pool, pg_config)) =
14561            connect_test_postgres("verification ledger resume").await
14562        else {
14563            return;
14564        };
14565        let schema = format!("verification_ledger_resume_{}", std::process::id());
14566        setup_execution_schema(&admin_pool, &schema).await;
14567        let options: sqlx::postgres::PgConnectOptions = pg_config.into();
14568        let options = options.options([("search_path", schema.clone())]);
14569        let database = connect_test_database(options).await.unwrap();
14570        database
14571            .ensure_execution_transaction_schema()
14572            .await
14573            .unwrap();
14574
14575        let state = ready_rpc_state();
14576        let addr = start_mock_rpc_server(state.clone()).await;
14577        let config = test_config_with_signer_env(
14578            format!("http://{addr}"),
14579            "BLOCKCHAIN_TEST_VERIFICATION_RESUME",
14580        );
14581        let mut client = test_client_from_config(config, test_pool());
14582        client.cache.database = Some(database);
14583        protect_test_storage(&mut client, &schema).await;
14584        let finalized_headers = [
14585            ExecutionVerifiedHeader {
14586                number: FIXTURE_BLOCK,
14587                hash: FIXTURE_BLOCK_HASH.to_string(),
14588                parent_hash: "0x0000000000000000000000000000000000000000000000000000000000000001"
14589                    .to_string(),
14590                timestamp: FIXTURE_BLOCK_TIMESTAMP,
14591                base_fee_per_gas: Some(100_000_000),
14592            },
14593            ExecutionVerifiedHeader {
14594                number: FIXTURE_BLOCK + 1,
14595                hash: B256::from([0x22; 32]).to_string(),
14596                parent_hash: FIXTURE_BLOCK_HASH.to_string(),
14597                timestamp: FIXTURE_BLOCK_TIMESTAMP + 1,
14598                base_fee_per_gas: Some(100_000_000),
14599            },
14600            ExecutionVerifiedHeader {
14601                number: FIXTURE_BLOCK + 2,
14602                hash: B256::from([0x33; 32]).to_string(),
14603                parent_hash: B256::from([0x22; 32]).to_string(),
14604                timestamp: FIXTURE_BLOCK_TIMESTAMP + 2,
14605                base_fee_per_gas: Some(100_000_000),
14606            },
14607        ];
14608        initialize_test_verification_ledger_with_headers(&client, &finalized_headers).await;
14609        let verification = client.config.verification.as_ref().unwrap();
14610        let position = client
14611            .cache
14612            .database
14613            .as_ref()
14614            .unwrap()
14615            .load_execution_verification_position(
14616                42_161,
14617                WALLET,
14618                &verification.manifest_version,
14619                &verification.manifest_digest,
14620            )
14621            .await
14622            .unwrap()
14623            .unwrap();
14624        let resume = client
14625            .cache
14626            .database
14627            .as_ref()
14628            .unwrap()
14629            .load_execution_verification_resume(
14630                42_161,
14631                WALLET,
14632                &verification.manifest_version,
14633                &verification.manifest_digest,
14634            )
14635            .await
14636            .unwrap()
14637            .unwrap();
14638        assert_eq!(position.next_canonical_nonce, 7);
14639        assert_eq!(position.revision, 0);
14640        assert_eq!(position.finalized_tip, finalized_headers[2]);
14641        assert_eq!(resume.next_canonical_nonce, 7);
14642        assert_eq!(resume.revision, 0);
14643        assert_eq!(resume.finalized_headers, finalized_headers);
14644
14645        let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel();
14646        replace_exec_event_sender(sender);
14647        client.start().unwrap();
14648        // SAFETY: this variable name is unique to this test across the test binary
14649        unsafe { std::env::set_var("BLOCKCHAIN_TEST_VERIFICATION_RESUME", TEST_PRIVATE_KEY) };
14650
14651        client.connect().await.unwrap();
14652
14653        let skipped_height = format!("0x{:x}", FIXTURE_BLOCK + 1);
14654        assert!(client.is_connected());
14655        assert!(state.recorded_requests().iter().all(|request| {
14656            request["method"] != "eth_getBlockByNumber"
14657                || request["params"][0].as_str() != Some(&skipped_height)
14658        }));
14659        let header_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
14660            "SELECT COUNT(*) FROM {schema}.execution_verified_finalized_header"
14661        )))
14662        .fetch_one(&admin_pool)
14663        .await
14664        .unwrap();
14665        assert_eq!(header_count, 3);
14666
14667        drop(client);
14668        drop_execution_schema(&admin_pool, &schema).await;
14669    }
14670
14671    #[tokio::test]
14672    async fn disconnect_revokes_signer_and_blocks_execution() {
14673        let (mut client, state) = client_with_mock_rpc(ready_rpc_state()).await;
14674        client.core.set_connected();
14675        client.signer = Some(Arc::new(
14676            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
14677        ));
14678
14679        client.disconnect().await.unwrap();
14680        let error = client.wrap(U256::from(1_000u64)).await.unwrap_err();
14681
14682        assert!(!client.is_connected());
14683        assert!(client.signer.is_none());
14684        assert!(
14685            error.to_string().contains("is not connected"),
14686            "was: {error}"
14687        );
14688        assert!(state.recorded_requests().is_empty());
14689    }
14690
14691    #[allow(unsafe_code)] // env-var mutation in tests; unique var names avoid cross-test races
14692    #[tokio::test]
14693    async fn connect_releases_stale_preparing_claim_after_tasks_finish() {
14694        let addr = start_mock_rpc_server(ready_rpc_state()).await;
14695        let config = test_config_with_signer_env(
14696            format!("http://{addr}"),
14697            "BLOCKCHAIN_TEST_RECONNECT_CLAIM",
14698        );
14699        let mut client = test_client_from_config(config, test_pool());
14700        let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel();
14701        replace_exec_event_sender(sender);
14702        client.start().unwrap();
14703        // SAFETY: this variable name is unique to this test across the test binary
14704        unsafe { std::env::set_var("BLOCKCHAIN_TEST_RECONNECT_CLAIM", TEST_PRIVATE_KEY) };
14705        *client.in_flight.lock() = Some(InFlightSlot::Preparing(TransactionPurpose::Swap));
14706        client
14707            .pending_tasks
14708            .spawn(async {})
14709            .expect("stale task spawn");
14710
14711        client.connect().await.unwrap();
14712
14713        assert!(client.in_flight.lock().is_none());
14714    }
14715
14716    #[allow(unsafe_code)] // env-var mutation in tests; unique var names avoid cross-test races
14717    #[tokio::test]
14718    async fn reconnect_after_aborted_submission_releases_preparing_claim() {
14719        // The swap task blocks in its first RPC; disconnect aborts it mid-preparation, and the
14720        // following connect awaits termination and releases the stale claim
14721        let state = ready_rpc_state().with_sleep("eth_getBlockByNumber", Duration::from_secs(30));
14722        let Some((admin_pool, schema, mut client, state, _)) =
14723            swap_client_with_database("execution_reconnect_claim_test", state).await
14724        else {
14725            return;
14726        };
14727        let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel();
14728        replace_exec_event_sender(sender);
14729        client.start().unwrap();
14730        // SAFETY: this variable name is unique to this test across the test binary
14731        unsafe { std::env::set_var("BLOCKCHAIN_TEST_PRIVATE_KEY", TEST_PRIVATE_KEY) };
14732
14733        let order = test_market_sell_order(test_pool().instrument_id);
14734        client.submit_order(submit_order_cmd(&order)).unwrap();
14735        await_recorded_requests(&state, "eth_getBlockByNumber", 1).await;
14736        assert!(client.in_flight.lock().is_some());
14737
14738        client.disconnect().await.unwrap();
14739        let ready_addr = start_mock_rpc_server(ready_rpc_state()).await;
14740        let ready = test_client(format!("http://{ready_addr}"));
14741        client.http_rpc_client = ready.http_rpc_client;
14742        client.verification = ready.verification;
14743        client.connect().await.unwrap();
14744
14745        assert!(client.in_flight.lock().is_none());
14746
14747        drop_execution_schema(&admin_pool, &schema).await;
14748    }
14749
14750    #[tokio::test]
14751    async fn poll_for_receipt_returns_none_after_exhaustion() {
14752        let state = ready_rpc_state().with_response("eth_getTransactionReceipt", RECEIPT_NULL);
14753        let (client, state) = client_with_mock_rpc(state).await;
14754
14755        let receipt = poll_for_receipt(&client.http_rpc_client, &B256::ZERO, 3, Duration::ZERO)
14756            .await
14757            .unwrap();
14758
14759        assert!(receipt.is_none());
14760        let requests = state.recorded_requests();
14761        assert_eq!(requests.len(), 3);
14762    }
14763
14764    #[tokio::test]
14765    async fn poll_for_receipt_returns_last_error_when_every_poll_fails() {
14766        let state =
14767            ready_rpc_state().with_response("eth_getTransactionReceipt", RPC_METHOD_NOT_FOUND);
14768        let (client, state) = client_with_mock_rpc(state).await;
14769
14770        let error = poll_for_receipt(&client.http_rpc_client, &B256::ZERO, 3, Duration::ZERO)
14771            .await
14772            .unwrap_err();
14773
14774        assert_eq!(
14775            error.to_string(),
14776            "eth_getTransactionReceipt RPC error -32601"
14777        );
14778        let requests = state.recorded_requests();
14779        assert_eq!(requests.len(), 3);
14780    }
14781
14782    #[tokio::test]
14783    async fn poll_for_receipt_returns_none_after_pending_then_errors() {
14784        let state = ready_rpc_state().with_response_sequence(
14785            "eth_getTransactionReceipt",
14786            &[RECEIPT_NULL, RPC_METHOD_NOT_FOUND, RPC_METHOD_NOT_FOUND],
14787        );
14788        let (client, state) = client_with_mock_rpc(state).await;
14789
14790        let receipt = poll_for_receipt(&client.http_rpc_client, &B256::ZERO, 3, Duration::ZERO)
14791            .await
14792            .unwrap();
14793
14794        assert!(receipt.is_none());
14795        let requests = state.recorded_requests();
14796        assert_eq!(requests.len(), 3);
14797    }
14798
14799    #[tokio::test]
14800    async fn cancellation_during_persistence_keeps_in_flight_slot() {
14801        let Some((admin_pool, pg_config)) = connect_test_postgres("persistence cancellation").await
14802        else {
14803            return;
14804        };
14805
14806        let schema = format!("execution_persist_cancel_test_{}", std::process::id());
14807        setup_execution_schema(&admin_pool, &schema).await;
14808
14809        let db_options: sqlx::postgres::PgConnectOptions = pg_config.into();
14810        let db_options = db_options.options([("search_path", schema.clone())]);
14811        let database = connect_test_database(db_options).await.unwrap();
14812
14813        let state = signing_rpc_state();
14814        let addr = start_mock_rpc_server(state.clone()).await;
14815
14816        let mut client = test_client(format!("http://{addr}"));
14817        client.cache.database = Some(database);
14818        // Mirror the connect-time migration: tests create the pre-submission table shape
14819        client
14820            .cache
14821            .ensure_execution_transaction_schema()
14822            .await
14823            .unwrap();
14824        protect_test_storage(&mut client, &schema).await;
14825        initialize_test_verification_ledger(&client).await;
14826        client.signer = Some(Arc::new(
14827            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
14828        ));
14829        client.core.set_connected();
14830
14831        let advisory_lock = i64::from(std::process::id());
14832
14833        for statement in [
14834            format!(
14835                "CREATE FUNCTION {schema}.block_execution_hash_insert() RETURNS trigger \
14836                 LANGUAGE plpgsql AS 'BEGIN PERFORM pg_advisory_xact_lock({advisory_lock}); \
14837                 RETURN NEW; END'"
14838            ),
14839            format!(
14840                "CREATE TRIGGER block_execution_hash_insert BEFORE INSERT ON \
14841                 {schema}.execution_transaction_hash FOR EACH ROW EXECUTE FUNCTION \
14842                 {schema}.block_execution_hash_insert()"
14843            ),
14844        ] {
14845            sqlx::query(sqlx::AssertSqlSafe(statement))
14846                .execute(&admin_pool)
14847                .await
14848                .unwrap();
14849        }
14850        let mut lock_transaction = admin_pool.begin().await.unwrap();
14851        sqlx::query("SELECT pg_advisory_xact_lock($1)")
14852            .bind(advisory_lock)
14853            .execute(&mut *lock_transaction)
14854            .await
14855            .unwrap();
14856
14857        let value = U256::from(1_000_000_000_000_000u64);
14858        let in_flight = Arc::clone(&client.in_flight);
14859        let mut wrap = Box::pin(client.wrap(value));
14860        tokio::time::timeout(Duration::from_secs(2), async {
14861            loop {
14862                tokio::select! {
14863                    result = &mut wrap => {
14864                        panic!("persistence completed while the table was locked: {result:?}")
14865                    }
14866                    () = tokio::time::sleep(Duration::from_millis(1)) => {}
14867                }
14868
14869                if matches!(*in_flight.lock(), Some(InFlightSlot::AwaitingFinality(_))) {
14870                    break;
14871                }
14872            }
14873        })
14874        .await
14875        .unwrap();
14876        drop(wrap);
14877        lock_transaction.rollback().await.unwrap();
14878
14879        let slot = *client.in_flight.lock();
14880        let second_error = client
14881            .wrap(U256::from(2_000_000_000_000_000u64))
14882            .await
14883            .unwrap_err();
14884        let broadcasts = state
14885            .recorded_requests()
14886            .into_iter()
14887            .filter(|request| request["method"] == "eth_sendRawTransaction")
14888            .count();
14889        let status: String = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
14890            "SELECT status FROM {schema}.execution_intent"
14891        )))
14892        .fetch_one(&admin_pool)
14893        .await
14894        .unwrap();
14895
14896        assert!(matches!(slot, Some(InFlightSlot::AwaitingFinality(_))));
14897        assert_eq!(status, "prepared");
14898        assert!(
14899            second_error.to_string().contains("awaiting finality"),
14900            "was: {second_error}"
14901        );
14902        assert_eq!(broadcasts, 0);
14903
14904        drop_execution_schema(&admin_pool, &schema).await;
14905    }
14906
14907    #[tokio::test]
14908    async fn reservation_failure_releases_preparing_slot() {
14909        let state = signing_rpc_state();
14910        let Some((admin_pool, schema, mut client, state)) =
14911            execution_client_with_database("execution_reservation_fail_test", state).await
14912        else {
14913            return;
14914        };
14915        sqlx::query(sqlx::AssertSqlSafe(format!(
14916            "DROP TABLE {schema}.execution_intent CASCADE"
14917        )))
14918        .execute(&admin_pool)
14919        .await
14920        .unwrap();
14921
14922        let error = client
14923            .wrap(U256::from(1_000_000_000_000_000u64))
14924            .await
14925            .unwrap_err();
14926        let retry_error = client
14927            .wrap(U256::from(2_000_000_000_000_000u64))
14928            .await
14929            .unwrap_err();
14930        let broadcasts = state
14931            .recorded_requests()
14932            .into_iter()
14933            .filter(|request| request["method"] == "eth_sendRawTransaction")
14934            .count();
14935
14936        assert_eq!(
14937            error.to_string(),
14938            "Execution intent reservation failed before commit"
14939        );
14940        assert!(reservation_failure_proven_not_committed(&error));
14941        assert_eq!(
14942            retry_error.to_string(),
14943            "Execution intent reservation failed before commit"
14944        );
14945        assert_eq!(broadcasts, 0);
14946        assert!(client.in_flight.lock().is_none());
14947
14948        drop_execution_schema(&admin_pool, &schema).await;
14949    }
14950
14951    #[tokio::test]
14952    async fn reservation_commit_failure_keeps_preparing_slot() {
14953        let state = signing_rpc_state();
14954        let Some((admin_pool, schema, mut client, state)) =
14955            execution_client_with_database("execution_reservation_commit_fail_test", state).await
14956        else {
14957            return;
14958        };
14959        install_reservation_commit_rejection(&admin_pool, &schema).await;
14960
14961        let error = client
14962            .wrap(U256::from(1_000_000_000_000_000u64))
14963            .await
14964            .unwrap_err();
14965        let slot = *client.in_flight.lock();
14966        let second_error = client
14967            .wrap(U256::from(2_000_000_000_000_000u64))
14968            .await
14969            .unwrap_err();
14970        let requests = state.recorded_requests();
14971        let intent_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
14972            "SELECT COUNT(*) FROM {schema}.execution_intent"
14973        )))
14974        .fetch_one(&admin_pool)
14975        .await
14976        .unwrap();
14977        let signed_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
14978            "SELECT COUNT(*) FROM {schema}.execution_transaction_hash"
14979        )))
14980        .fetch_one(&admin_pool)
14981        .await
14982        .unwrap();
14983
14984        assert_eq!(
14985            error.to_string(),
14986            "Execution intent reservation commit outcome is unknown; reconciliation is required"
14987        );
14988        assert!(!reservation_failure_proven_not_committed(&error));
14989        assert!(matches!(
14990            slot,
14991            Some(InFlightSlot::Preparing(TransactionPurpose::Wrap))
14992        ));
14993        assert!(
14994            second_error.to_string().contains("being prepared"),
14995            "was: {second_error}"
14996        );
14997        assert_eq!(intent_count, 0);
14998        assert_eq!(signed_count, 0);
14999        assert!(
15000            requests
15001                .iter()
15002                .all(|request| request["method"] != "eth_getTransactionCount")
15003        );
15004        assert!(
15005            requests
15006                .iter()
15007                .all(|request| request["method"] != "eth_sendRawTransaction")
15008        );
15009
15010        drop_execution_schema(&admin_pool, &schema).await;
15011    }
15012
15013    #[tokio::test]
15014    async fn persistence_failure_keeps_unbroadcast_slot() {
15015        let state = signing_rpc_state();
15016        let Some((admin_pool, schema, mut client, state)) =
15017            execution_client_with_database("execution_persist_fail_test", state).await
15018        else {
15019            return;
15020        };
15021        sqlx::query(sqlx::AssertSqlSafe(format!(
15022            "ALTER TABLE {schema}.execution_transaction_hash \
15023             ADD CONSTRAINT execution_hash_reject CHECK (FALSE)"
15024        )))
15025        .execute(&admin_pool)
15026        .await
15027        .unwrap();
15028
15029        let value = U256::from(1_000_000_000_000_000u64);
15030        let error = client.wrap(value).await.unwrap_err();
15031        let in_flight = awaiting_in_flight(&client);
15032        let second_error = client
15033            .wrap(U256::from(2_000_000_000_000_000u64))
15034            .await
15035            .unwrap_err();
15036        let broadcasts = state
15037            .recorded_requests()
15038            .into_iter()
15039            .filter(|request| request["method"] == "eth_sendRawTransaction")
15040            .count();
15041        let expected_hash = expected_wrap_tx_hash(value).await;
15042
15043        let error_message = error.to_string();
15044        assert!(error_message.starts_with(&format!(
15045            "Failed to persist transaction {expected_hash}: Failed to persist signed transaction"
15046        )), "was: {error_message}");
15047        assert!(
15048            error_message.ends_with("the in-flight slot stays occupied"),
15049            "was: {error_message}"
15050        );
15051        assert_eq!(in_flight.nonce, 7);
15052        assert_eq!(in_flight.tx_hash, expected_hash);
15053        assert_eq!(in_flight.purpose, TransactionPurpose::Wrap);
15054        assert!(
15055            second_error.to_string().contains("still awaiting finality"),
15056            "was: {second_error}"
15057        );
15058        assert_eq!(broadcasts, 0);
15059
15060        drop_execution_schema(&admin_pool, &schema).await;
15061    }
15062
15063    #[tokio::test]
15064    async fn cancellation_after_dispatch_keeps_record_and_in_flight_slot() {
15065        let state = signing_rpc_state().with_sleep(
15066            "eth_sendRawTransaction",
15067            Duration::from_secs(EXECUTION_RPC_TIMEOUT_SECS + 2),
15068        );
15069        let Some((admin_pool, schema, mut client, state)) =
15070            execution_client_with_database("execution_cancel_test", state).await
15071        else {
15072            return;
15073        };
15074
15075        let mut wrap = Box::pin(client.wrap(U256::from(1_000_000_000_000_000u64)));
15076        tokio::select! {
15077            result = &mut wrap => panic!("broadcast completed before cancellation: {result:?}"),
15078            () = await_recorded_requests(&state, "eth_sendRawTransaction", 1) => {}
15079        }
15080        drop(wrap);
15081
15082        let in_flight = awaiting_in_flight(&client);
15083        let error = client
15084            .wrap(U256::from(2_000_000_000_000_000u64))
15085            .await
15086            .unwrap_err();
15087        let record = client
15088            .cache
15089            .get_execution_transaction(42161, &in_flight.tx_hash.to_string())
15090            .await
15091            .unwrap()
15092            .unwrap();
15093        let broadcasts = state
15094            .recorded_requests()
15095            .into_iter()
15096            .filter(|request| request["method"] == "eth_sendRawTransaction")
15097            .count();
15098
15099        assert!(
15100            error.to_string().contains("still awaiting finality"),
15101            "was: {error}"
15102        );
15103        assert_eq!(record.nonce, 7);
15104        assert_eq!(record.purpose, "wrap");
15105        assert_eq!(record.status, "broadcast");
15106        assert_eq!(broadcasts, 1);
15107
15108        drop_execution_schema(&admin_pool, &schema).await;
15109    }
15110
15111    #[tokio::test]
15112    async fn cancellation_during_receipt_polling_keeps_record_and_in_flight_slot() {
15113        let state = signing_rpc_state()
15114            .with_send_raw_transaction_echo()
15115            .with_response("eth_getTransactionReceipt", RECEIPT_SUCCESS)
15116            .with_sleep(
15117                "eth_getTransactionReceipt",
15118                Duration::from_secs(EXECUTION_RPC_TIMEOUT_SECS + 2),
15119            );
15120        let Some((admin_pool, schema, mut client, state)) =
15121            execution_client_with_database("execution_receipt_cancel_test", state).await
15122        else {
15123            return;
15124        };
15125
15126        let mut wrap = Box::pin(client.wrap(U256::from(1_000_000_000_000_000u64)));
15127        tokio::select! {
15128            result = &mut wrap => panic!("receipt polling completed before cancellation: {result:?}"),
15129            () = await_recorded_requests(&state, "eth_getTransactionReceipt", 3) => {}
15130        }
15131        drop(wrap);
15132
15133        let in_flight = awaiting_in_flight(&client);
15134        let second_error = client
15135            .wrap(U256::from(2_000_000_000_000_000u64))
15136            .await
15137            .unwrap_err();
15138        let record = client
15139            .cache
15140            .get_execution_transaction(42161, &in_flight.tx_hash.to_string())
15141            .await
15142            .unwrap()
15143            .unwrap();
15144        let requests = state.recorded_requests();
15145
15146        assert_eq!(in_flight.nonce, 7);
15147        assert_eq!(in_flight.purpose, TransactionPurpose::Wrap);
15148        assert!(
15149            second_error.to_string().contains("still awaiting finality"),
15150            "was: {second_error}"
15151        );
15152        assert_eq!(record.nonce, 7);
15153        assert_eq!(record.transaction_hash, in_flight.tx_hash.to_string());
15154        assert_eq!(record.purpose, "wrap");
15155        assert_eq!(record.status, "broadcast");
15156        assert_eq!(
15157            requests
15158                .iter()
15159                .filter(|request| request["method"] == "eth_sendRawTransaction")
15160                .count(),
15161            1
15162        );
15163        assert_eq!(
15164            requests
15165                .iter()
15166                .filter(|request| request["method"] == "eth_getTransactionReceipt")
15167                .count(),
15168            3
15169        );
15170
15171        drop_execution_schema(&admin_pool, &schema).await;
15172    }
15173
15174    #[tokio::test]
15175    async fn rejected_broadcast_stays_dropped_and_occupied() {
15176        let state = signing_rpc_state()
15177            .with_response("eth_sendRawTransaction", SEND_RAW_TRANSACTION_REJECTED)
15178            .with_response("eth_getTransactionReceipt", RECEIPT_NULL);
15179        let Some((admin_pool, schema, mut client, state)) =
15180            execution_client_with_database("execution_rejected_test", state).await
15181        else {
15182            return;
15183        };
15184
15185        let error = client
15186            .wrap(U256::from(1_000_000_000_000_000u64))
15187            .await
15188            .unwrap_err();
15189        let (purpose, status): (String, String) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
15190            "SELECT purpose, status FROM {schema}.execution_intent"
15191        )))
15192        .fetch_one(&admin_pool)
15193        .await
15194        .unwrap();
15195        let broadcasts = state
15196            .recorded_requests()
15197            .into_iter()
15198            .filter(|request| request["method"] == "eth_sendRawTransaction")
15199            .count();
15200
15201        assert!(
15202            error.to_string().contains("Timed out awaiting finality"),
15203            "was: {error}"
15204        );
15205        assert!(client.in_flight.lock().is_some());
15206        assert_eq!(purpose, "wrap");
15207        assert_eq!(status, "dropped");
15208        assert_eq!(broadcasts, 1);
15209
15210        drop_execution_schema(&admin_pool, &schema).await;
15211    }
15212
15213    #[tokio::test]
15214    async fn legacy_table_loss_does_not_release_rejected_broadcast() {
15215        let state = signing_rpc_state()
15216            .with_response("eth_sendRawTransaction", SEND_RAW_TRANSACTION_REJECTED)
15217            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
15218            .with_sleep("eth_sendRawTransaction", Duration::from_secs(1));
15219        let Some((admin_pool, schema, mut client, state)) =
15220            execution_client_with_database("execution_rejected_update_test", state).await
15221        else {
15222            return;
15223        };
15224
15225        let mut wrap = Box::pin(client.wrap(U256::from(1_000_000_000_000_000u64)));
15226        tokio::select! {
15227            result = &mut wrap => panic!("broadcast completed before database failure: {result:?}"),
15228            () = await_recorded_requests(&state, "eth_sendRawTransaction", 1) => {}
15229        }
15230        sqlx::query(sqlx::AssertSqlSafe(format!(
15231            "DROP TABLE {schema}.execution_transaction"
15232        )))
15233        .execute(&admin_pool)
15234        .await
15235        .unwrap();
15236
15237        let error = wrap.await.unwrap_err();
15238
15239        assert!(
15240            error.to_string().contains("Timed out awaiting finality"),
15241            "was: {error}"
15242        );
15243        let in_flight = awaiting_in_flight(&client);
15244        assert_eq!(in_flight.nonce, 7);
15245        assert_eq!(in_flight.purpose, TransactionPurpose::Wrap);
15246
15247        drop_execution_schema(&admin_pool, &schema).await;
15248    }
15249
15250    #[tokio::test]
15251    async fn finalized_receipt_keeps_slot_when_status_update_fails() {
15252        let expected_hash = expected_wrap_tx_hash(U256::from(1_000_000_000_000_000u64)).await;
15253        let block = finalized_wrap_block(expected_hash);
15254        let receipt = receipt_with_transaction_hash(RECEIPT_SUCCESS, expected_hash);
15255        let state = with_finalized_identity(
15256            signing_rpc_state()
15257                .with_send_raw_transaction_echo()
15258                .with_response("eth_getTransactionReceipt", &receipt)
15259                .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block)
15260                .with_call_response_sequence(
15261                    BALANCE_OF_SELECTOR,
15262                    &[
15263                        CALL_BALANCE,
15264                        CALL_BALANCE,
15265                        CALL_BALANCE,
15266                        CALL_BALANCE,
15267                        CALL_BALANCE,
15268                        CALL_BALANCE,
15269                        CALL_BALANCE_AFTER_WRAP,
15270                        CALL_BALANCE_AFTER_WRAP,
15271                        CALL_BALANCE_AFTER_WRAP,
15272                    ],
15273                ),
15274            &block,
15275            &receipt,
15276        );
15277        let Some((admin_pool, schema, mut client, state)) =
15278            execution_client_with_database("execution_finalized_update_test", state).await
15279        else {
15280            return;
15281        };
15282
15283        for statement in [
15284            format!(
15285                "CREATE FUNCTION {schema}.reject_finalized_status() RETURNS trigger \
15286                 LANGUAGE plpgsql AS 'BEGIN IF NEW.status = ''finalized'' THEN \
15287                 RAISE EXCEPTION ''test finalized status rejection''; END IF; \
15288                 RETURN NEW; END'"
15289            ),
15290            format!(
15291                "CREATE TRIGGER reject_finalized_status BEFORE UPDATE ON \
15292                 {schema}.execution_transaction_hash FOR EACH ROW \
15293                 EXECUTE FUNCTION {schema}.reject_finalized_status()"
15294            ),
15295        ] {
15296            sqlx::query(sqlx::AssertSqlSafe(statement))
15297                .execute(&admin_pool)
15298                .await
15299                .unwrap();
15300        }
15301
15302        let error = client
15303            .wrap(U256::from(1_000_000_000_000_000u64))
15304            .await
15305            .unwrap_err();
15306        let in_flight = awaiting_in_flight(&client);
15307        let requests = state.recorded_requests();
15308
15309        assert!(
15310            error
15311                .to_string()
15312                .contains("test finalized status rejection"),
15313            "was: {error}"
15314        );
15315        assert_eq!(in_flight.nonce, 7);
15316        assert_eq!(in_flight.purpose, TransactionPurpose::Wrap);
15317        assert_eq!(
15318            requests
15319                .iter()
15320                .filter(|request| request["method"] == "eth_sendRawTransaction")
15321                .count(),
15322            1
15323        );
15324        assert_eq!(
15325            requests
15326                .iter()
15327                .filter(|request| request["method"] == "eth_getTransactionReceipt")
15328                .count(),
15329            3
15330        );
15331
15332        drop_execution_schema(&admin_pool, &schema).await;
15333    }
15334
15335    #[tokio::test]
15336    async fn broadcast_timeout_marks_dropped_and_keeps_ownership() {
15337        let state = signing_rpc_state()
15338            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
15339            .with_sleep(
15340                "eth_sendRawTransaction",
15341                Duration::from_secs(EXECUTION_RPC_TIMEOUT_SECS + 2),
15342            );
15343        let Some((admin_pool, schema, mut client, _)) =
15344            execution_client_with_database("execution_timeout_test", state).await
15345        else {
15346            return;
15347        };
15348
15349        let error = client
15350            .wrap(U256::from(1_000_000_000_000_000u64))
15351            .await
15352            .unwrap_err();
15353
15354        assert!(
15355            error.to_string().contains("Timed out awaiting finality"),
15356            "was: {error}"
15357        );
15358        let in_flight = awaiting_in_flight(&client);
15359        assert_eq!(in_flight.purpose, TransactionPurpose::Wrap);
15360        assert_eq!(in_flight.nonce, 7);
15361
15362        // The persisted record stays active for reconciliation instead of rebroadcasting.
15363        let record = client
15364            .cache
15365            .get_execution_transaction(42161, &in_flight.tx_hash.to_string())
15366            .await
15367            .unwrap()
15368            .unwrap();
15369        assert_eq!(record.status, "dropped");
15370
15371        drop_execution_schema(&admin_pool, &schema).await;
15372    }
15373
15374    #[tokio::test]
15375    async fn restart_reconciles_dropped_transaction_without_rebroadcast() {
15376        let initial_state = broadcast_rpc_state()
15377            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
15378            .with_call_response(BALANCE_OF_SELECTOR, CALL_BALANCE);
15379        let Some((admin_pool, schema, mut first_client, _)) =
15380            execution_client_with_database("execution_restart_test", initial_state).await
15381        else {
15382            return;
15383        };
15384        let expected_hash = expected_wrap_tx_hash(U256::from(1_000_000_000_000_000_u64)).await;
15385        let error = first_client
15386            .wrap(U256::from(1_000_000_000_000_000_u64))
15387            .await
15388            .unwrap_err();
15389        assert!(
15390            error.to_string().contains("Timed out awaiting finality"),
15391            "was: {error}"
15392        );
15393        let database = first_client.cache.database.as_ref().unwrap().clone();
15394        let payload_keys = first_client.payload_keys.clone();
15395        drop(first_client);
15396
15397        let block = finalized_wrap_block(expected_hash);
15398        let receipt = receipt_with_transaction_hash(RECEIPT_SUCCESS, expected_hash);
15399        let restart_state = with_finalized_identity(
15400            execution_rpc_state()
15401                .with_response("eth_getTransactionReceipt", &receipt)
15402                .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block)
15403                .with_response_sequence(
15404                    "eth_call",
15405                    &[
15406                        CALL_BALANCE,
15407                        CALL_BALANCE,
15408                        CALL_BALANCE,
15409                        CALL_BALANCE_AFTER_WRAP,
15410                        CALL_BALANCE_AFTER_WRAP,
15411                        CALL_BALANCE_AFTER_WRAP,
15412                    ],
15413                ),
15414            &block,
15415            &receipt,
15416        );
15417        let addr = start_mock_rpc_server(restart_state.clone()).await;
15418        let mut restarted = test_client(format!("http://{addr}"));
15419        restarted.cache.database = Some(database);
15420        restarted.payload_keys = payload_keys;
15421        restarted.signer = Some(Arc::new(
15422            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
15423        ));
15424
15425        restarted.reconcile_unresolved_execution().await.unwrap();
15426        restarted.reconcile_unresolved_execution().await.unwrap();
15427
15428        let record = restarted
15429            .cache
15430            .get_execution_transaction(42161, &expected_hash.to_string())
15431            .await
15432            .unwrap()
15433            .unwrap();
15434        let requests = restart_state.recorded_requests();
15435        assert_eq!(record.status, "finalized");
15436        assert!(restarted.in_flight.lock().is_none());
15437        assert_eq!(
15438            requests
15439                .iter()
15440                .filter(|request| request["method"] == "eth_getTransactionReceipt")
15441                .count(),
15442            6
15443        );
15444        assert_eq!(
15445            requests
15446                .iter()
15447                .filter(|request| {
15448                    request["method"] == "eth_call"
15449                        && request["params"][0]["data"]
15450                            .as_str()
15451                            .is_some_and(|data| data.starts_with(BALANCE_OF_SELECTOR))
15452                })
15453                .count(),
15454            6
15455        );
15456        assert_eq!(
15457            requests
15458                .iter()
15459                .filter(|request| request["method"] == "eth_sendRawTransaction")
15460                .count(),
15461            0
15462        );
15463
15464        drop_execution_schema(&admin_pool, &schema).await;
15465    }
15466
15467    fn migration_test_intent(id: i64) -> ExecutionIntentRow {
15468        ExecutionIntentRow {
15469            id,
15470            schema_version: crate::execution::transaction::EXECUTION_SCHEMA_VERSION,
15471            chain_id: 42_161,
15472            wallet_address: WALLET.to_string(),
15473            nonce: None,
15474            purpose: "wrap".to_string(),
15475            status: "prepared".to_string(),
15476            client_order_id: None,
15477            trader_id: None,
15478            strategy_id: None,
15479            account_id: None,
15480            instrument_id: None,
15481            pool_address: None,
15482            transaction_to: WETH.to_string(),
15483            transaction_input: "0xd0e30db0".to_string(),
15484            transaction_value: "1".to_string(),
15485            amount_in: None,
15486            created_block: FIXTURE_BLOCK,
15487            acknowledgement_emitted: false,
15488            fill_emitted: false,
15489            terminal_emitted: false,
15490            active: true,
15491        }
15492    }
15493
15494    fn migration_nonce_verification() -> Verified<u64> {
15495        Verified {
15496            value: 7,
15497            read: crate::rpc::verification::VerificationRead::TransactionCount,
15498            provider_ids: [
15499                "authoritative".to_string(),
15500                "verifier-a".to_string(),
15501                "verifier-b".to_string(),
15502            ],
15503            normalized_value_digest: keccak256(7_u64.to_be_bytes()),
15504        }
15505    }
15506
15507    fn migration_finalized_header() -> VerifiedBlockHeader {
15508        VerifiedBlockHeader {
15509            number: FIXTURE_BLOCK,
15510            hash: B256::from_str(FIXTURE_BLOCK_HASH).unwrap(),
15511            parent_hash: B256::from_str(
15512                "0x0000000000000000000000000000000000000000000000000000000000000001",
15513            )
15514            .unwrap(),
15515            timestamp: FIXTURE_BLOCK_TIMESTAMP,
15516            base_fee_per_gas: Some(100_000_000),
15517        }
15518    }
15519
15520    #[tokio::test]
15521    async fn verification_migration_recovers_prepared_unassigned_intent() {
15522        let client = test_client("http://127.0.0.1:1".to_string());
15523        let snapshot = ExecutionVerificationMigrationSnapshot {
15524            intents: vec![migration_test_intent(1)],
15525            hashes: Vec::new(),
15526        };
15527        let finalized = migration_finalized_header();
15528        let migration = client
15529            .build_execution_verification_migration(
15530                snapshot,
15531                finalized,
15532                &[finalized],
15533                &migration_nonce_verification(),
15534            )
15535            .await
15536            .unwrap();
15537
15538        assert_eq!(migration.records.len(), 1);
15539        let record = &migration.records[0];
15540        assert_eq!(record.intent_id, 1);
15541        assert_eq!(record.nonce, None);
15542        assert_eq!(record.transaction_hash, None);
15543        assert!(record.recover_prepared);
15544        assert_eq!(record.decisions.len(), 1);
15545    }
15546
15547    #[tokio::test]
15548    async fn verification_migration_rejects_inconsistent_released_history() {
15549        let client = test_client("http://127.0.0.1:1".to_string());
15550        let finalized = migration_finalized_header();
15551        let nonce_verification = migration_nonce_verification();
15552        let mut missing_marker = migration_test_intent(1);
15553        missing_marker.active = false;
15554        missing_marker.status = "finalized".to_string();
15555        missing_marker.nonce = Some(6);
15556        let error = client
15557            .build_execution_verification_migration(
15558                ExecutionVerificationMigrationSnapshot {
15559                    intents: vec![missing_marker],
15560                    hashes: Vec::new(),
15561                },
15562                finalized,
15563                &[finalized],
15564                &nonce_verification,
15565            )
15566            .await
15567            .err()
15568            .unwrap();
15569        assert!(
15570            error.to_string().contains("has no durable event marker"),
15571            "was: {error}"
15572        );
15573
15574        let mut first = migration_test_intent(1);
15575        first.active = false;
15576        first.status = "recoverable".to_string();
15577        first.nonce = Some(6);
15578        let mut second = first.clone();
15579        second.id = 2;
15580        let error = client
15581            .build_execution_verification_migration(
15582                ExecutionVerificationMigrationSnapshot {
15583                    intents: vec![first, second],
15584                    hashes: Vec::new(),
15585                },
15586                finalized,
15587                &[finalized],
15588                &nonce_verification,
15589            )
15590            .await
15591            .err()
15592            .unwrap();
15593        assert!(
15594            error
15595                .to_string()
15596                .contains("duplicate signer nonce ownership"),
15597            "was: {error}"
15598        );
15599    }
15600
15601    #[tokio::test]
15602    async fn verification_migration_reconstructs_consumed_active_intent() {
15603        let expected_hash = expected_wrap_tx_hash(U256::from(1u64)).await;
15604        let mut block_value: serde_json::Value =
15605            serde_json::from_str(&finalized_wrap_block(expected_hash)).unwrap();
15606        block_value["result"]["transactions"][0]["value"] = serde_json::json!("0x1");
15607        let block = block_value.to_string();
15608        let receipt = receipt_with_transaction_hash(RECEIPT_SUCCESS, expected_hash);
15609        let state = with_finalized_identity(
15610            execution_rpc_state()
15611                .with_response("eth_getTransactionCount", TRANSACTION_COUNT_NEXT)
15612                .with_response("eth_getTransactionReceipt", &receipt)
15613                .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block),
15614            &block,
15615            &receipt,
15616        );
15617        let Some((admin_pool, pg_config)) =
15618            connect_test_postgres("verification_migration_reconstructs_consumed_active_intent")
15619                .await
15620        else {
15621            return;
15622        };
15623        let schema = format!(
15624            "verification_migration_reconstructs_consumed_active_intent_{}",
15625            std::process::id()
15626        );
15627        setup_execution_schema(&admin_pool, &schema).await;
15628        let options: sqlx::postgres::PgConnectOptions = pg_config.into();
15629        let database = connect_test_database(options.options([("search_path", schema.clone())]))
15630            .await
15631            .unwrap();
15632        let addr = start_mock_rpc_server(state).await;
15633        let (mut client, _) = swap_client_with_cache(test_config(format!("http://{addr}")));
15634        client.cache.database = Some(database.clone());
15635        client
15636            .cache
15637            .ensure_execution_transaction_schema()
15638            .await
15639            .unwrap();
15640        let (intent, persisted_hash, _) = persist_test_wrap_broadcast(&database, None).await;
15641        protect_test_storage(&mut client, &schema).await;
15642        assert_eq!(persisted_hash, expected_hash);
15643
15644        let snapshot = database
15645            .load_execution_verification_migration_snapshot(42_161, WALLET)
15646            .await
15647            .unwrap();
15648        let nonce_verification = required_verification(
15649            client
15650                .verification
15651                .verify_transaction_count(&Address::from_str(WALLET).unwrap(), FIXTURE_BLOCK + 1)
15652                .await,
15653            "test migration nonce",
15654        )
15655        .unwrap();
15656        let headers = [
15657            VerifiedBlockHeader {
15658                number: FIXTURE_BLOCK,
15659                hash: B256::from_str(FIXTURE_BLOCK_HASH).unwrap(),
15660                parent_hash: B256::from_str(
15661                    "0x0000000000000000000000000000000000000000000000000000000000000001",
15662                )
15663                .unwrap(),
15664                timestamp: FIXTURE_BLOCK_TIMESTAMP,
15665                base_fee_per_gas: Some(100_000_000),
15666            },
15667            VerifiedBlockHeader {
15668                number: FIXTURE_BLOCK + 1,
15669                hash: B256::from([0x22; 32]),
15670                parent_hash: B256::from_str(FIXTURE_BLOCK_HASH).unwrap(),
15671                timestamp: FIXTURE_BLOCK_TIMESTAMP + 1,
15672                base_fee_per_gas: Some(100_000_000),
15673            },
15674        ];
15675        let migration = client
15676            .build_execution_verification_migration(
15677                snapshot,
15678                headers[1],
15679                &headers,
15680                &nonce_verification,
15681            )
15682            .await
15683            .unwrap();
15684        let finalized_headers = headers
15685            .iter()
15686            .map(|header| ExecutionVerifiedHeader {
15687                number: header.number,
15688                hash: header.hash.to_string(),
15689                parent_hash: header.parent_hash.to_string(),
15690                timestamp: header.timestamp,
15691                base_fee_per_gas: header.base_fee_per_gas,
15692            })
15693            .collect::<Vec<_>>();
15694        let decisions = [verification_decision(
15695            &nonce_verification,
15696            Some(FIXTURE_BLOCK + 1),
15697            Some(FIXTURE_BLOCK + 1),
15698        )];
15699
15700        initialize_test_verification_migration(
15701            &client,
15702            &finalized_headers,
15703            8,
15704            &decisions,
15705            &migration,
15706        )
15707        .await;
15708
15709        let migrated = database.get_execution_intent(intent.id).await.unwrap();
15710        let resume = database
15711            .load_execution_verification_resume(
15712                42_161,
15713                WALLET,
15714                &client
15715                    .config
15716                    .verification
15717                    .as_ref()
15718                    .unwrap()
15719                    .manifest_version,
15720                &client.config.verification.as_ref().unwrap().manifest_digest,
15721            )
15722            .await
15723            .unwrap()
15724            .unwrap();
15725        let evidence: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
15726            "SELECT COUNT(*) FROM {schema}.execution_verification_decision \
15727             WHERE intent_id = $1 AND decision_class = 'migration'"
15728        )))
15729        .bind(intent.id)
15730        .fetch_one(&admin_pool)
15731        .await
15732        .unwrap();
15733
15734        assert_eq!(migrated.status, "finalized");
15735        assert!(migrated.active);
15736        assert_eq!(resume.next_canonical_nonce, 8);
15737        assert!(evidence >= 5);
15738
15739        drop_execution_schema(&admin_pool, &schema).await;
15740    }
15741
15742    #[tokio::test]
15743    async fn verification_resume_accepts_one_consumed_owned_nonce() {
15744        let Some((admin_pool, schema, client, _)) = execution_client_with_database(
15745            "verification_resume_owned_nonce_test",
15746            ready_rpc_state(),
15747        )
15748        .await
15749        else {
15750            return;
15751        };
15752        let database = client.cache.database.as_ref().unwrap();
15753        persist_test_wrap_broadcast(database, client.payload_keys.as_deref()).await;
15754        let resume = database
15755            .load_execution_verification_resume(
15756                42_161,
15757                WALLET,
15758                &client
15759                    .config
15760                    .verification
15761                    .as_ref()
15762                    .unwrap()
15763                    .manifest_version,
15764                &client.config.verification.as_ref().unwrap().manifest_digest,
15765            )
15766            .await
15767            .unwrap()
15768            .unwrap();
15769
15770        ensure_test_verification_ledger(&client, &resume.finalized_headers, 7, 8)
15771            .await
15772            .unwrap();
15773
15774        let nonce_state = sqlx::query_as::<_, (i64, i64)>(sqlx::AssertSqlSafe(format!(
15775            "SELECT next_canonical_nonce, revision FROM {schema}.execution_verification_nonce"
15776        )))
15777        .fetch_one(&admin_pool)
15778        .await
15779        .unwrap();
15780        assert_eq!(nonce_state, (7, 0));
15781
15782        drop_execution_schema(&admin_pool, &schema).await;
15783    }
15784
15785    #[tokio::test]
15786    async fn verification_resume_rejects_unowned_nonce_advance() {
15787        for (test_name, mutation, observed_canonical_nonce) in [
15788            ("verification_resume_no_active_test", "no_active", 8),
15789            ("verification_resume_excess_test", "unchanged", 9),
15790            ("verification_resume_signed_test", "signed", 8),
15791            ("verification_resume_prepared_test", "prepared", 8),
15792            (
15793                "verification_resume_missing_payload_test",
15794                "missing_payload",
15795                8,
15796            ),
15797        ] {
15798            let Some((admin_pool, schema, client, _)) =
15799                execution_client_with_database(test_name, ready_rpc_state()).await
15800            else {
15801                return;
15802            };
15803            let database = client.cache.database.as_ref().unwrap();
15804            let (intent, _, _) =
15805                persist_test_wrap_broadcast(database, client.payload_keys.as_deref()).await;
15806
15807            match mutation {
15808                "no_active" => {
15809                    sqlx::query(sqlx::AssertSqlSafe(format!(
15810                        "UPDATE {schema}.execution_intent SET active = FALSE WHERE id = $1"
15811                    )))
15812                    .bind(intent.id)
15813                    .execute(&admin_pool)
15814                    .await
15815                    .unwrap();
15816                }
15817                "signed" => {
15818                    sqlx::query(sqlx::AssertSqlSafe(format!(
15819                        "UPDATE {schema}.execution_intent SET status = 'signed' WHERE id = $1"
15820                    )))
15821                    .bind(intent.id)
15822                    .execute(&admin_pool)
15823                    .await
15824                    .unwrap();
15825                    sqlx::query(sqlx::AssertSqlSafe(format!(
15826                        "UPDATE {schema}.execution_transaction_hash SET status = 'signed' \
15827                         WHERE intent_id = $1"
15828                    )))
15829                    .bind(intent.id)
15830                    .execute(&admin_pool)
15831                    .await
15832                    .unwrap();
15833                }
15834                "prepared" => {
15835                    sqlx::query(sqlx::AssertSqlSafe(format!(
15836                        "UPDATE {schema}.execution_intent \
15837                         SET nonce = NULL, status = 'prepared' WHERE id = $1"
15838                    )))
15839                    .bind(intent.id)
15840                    .execute(&admin_pool)
15841                    .await
15842                    .unwrap();
15843                    sqlx::query(sqlx::AssertSqlSafe(format!(
15844                        "UPDATE {schema}.execution_transaction_hash SET current = FALSE \
15845                         WHERE intent_id = $1"
15846                    )))
15847                    .bind(intent.id)
15848                    .execute(&admin_pool)
15849                    .await
15850                    .unwrap();
15851                }
15852                "missing_payload" => {
15853                    sqlx::query(sqlx::AssertSqlSafe(format!(
15854                        "UPDATE {schema}.execution_transaction_hash \
15855                         SET payload_expected = FALSE, sealed_transaction = NULL \
15856                         WHERE intent_id = $1"
15857                    )))
15858                    .bind(intent.id)
15859                    .execute(&admin_pool)
15860                    .await
15861                    .unwrap();
15862                }
15863                "unchanged" => {}
15864                _ => unreachable!(),
15865            }
15866            let resume = database
15867                .load_execution_verification_resume(
15868                    42_161,
15869                    WALLET,
15870                    &client
15871                        .config
15872                        .verification
15873                        .as_ref()
15874                        .unwrap()
15875                        .manifest_version,
15876                    &client.config.verification.as_ref().unwrap().manifest_digest,
15877                )
15878                .await
15879                .unwrap()
15880                .unwrap();
15881
15882            let error = ensure_test_verification_ledger(
15883                &client,
15884                &resume.finalized_headers,
15885                7,
15886                observed_canonical_nonce,
15887            )
15888            .await
15889            .unwrap_err();
15890
15891            assert!(
15892                error
15893                    .to_string()
15894                    .contains(if observed_canonical_nonce == 9 {
15895                        "outside the owned recovery range"
15896                    } else {
15897                        "without"
15898                    }),
15899                "{mutation}: {error}"
15900            );
15901            let nonce_state = sqlx::query_as::<_, (i64, i64)>(sqlx::AssertSqlSafe(format!(
15902                "SELECT next_canonical_nonce, revision FROM {schema}.execution_verification_nonce"
15903            )))
15904            .fetch_one(&admin_pool)
15905            .await
15906            .unwrap();
15907            assert_eq!(nonce_state, (7, 0), "{mutation}");
15908
15909            drop_execution_schema(&admin_pool, &schema).await;
15910        }
15911    }
15912
15913    #[tokio::test]
15914    async fn restart_marks_prepared_intent_recoverable_without_broadcast() {
15915        let Some((admin_pool, schema, client, state)) =
15916            execution_client_with_database("execution_prepared_restart_test", ready_rpc_state())
15917                .await
15918        else {
15919            return;
15920        };
15921        let database = client.cache.database.as_ref().unwrap();
15922        let intent = reserve_test_wrap_intent(database).await;
15923        *client.in_flight.lock() = Some(InFlightSlot::Preparing(TransactionPurpose::Wrap));
15924
15925        client.reconcile_unresolved_execution().await.unwrap();
15926
15927        let (status, active): (String, bool) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
15928            "SELECT status, active FROM {schema}.execution_intent WHERE id = {}",
15929            intent.id
15930        )))
15931        .fetch_one(&admin_pool)
15932        .await
15933        .unwrap();
15934        let transitions: Vec<String> = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
15935            "SELECT to_status FROM {schema}.execution_transaction_transition ORDER BY id"
15936        )))
15937        .fetch_all(&admin_pool)
15938        .await
15939        .unwrap();
15940
15941        assert_eq!(status, "recoverable");
15942        assert!(!active);
15943        assert_eq!(transitions, ["prepared", "recoverable"]);
15944        assert!(client.in_flight.lock().is_none());
15945        assert!(
15946            state
15947                .recorded_requests()
15948                .iter()
15949                .all(|request| request["method"] != "eth_sendRawTransaction")
15950        );
15951
15952        drop_execution_schema(&admin_pool, &schema).await;
15953    }
15954
15955    #[tokio::test]
15956    async fn protected_restart_authenticates_envelope_before_recovery() {
15957        let initial_state = broadcast_rpc_state()
15958            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
15959            .with_call_response(BALANCE_OF_SELECTOR, CALL_BALANCE);
15960        let Some((admin_pool, schema, mut first_client, _)) =
15961            execution_client_with_database("protected_restart_test", initial_state).await
15962        else {
15963            return;
15964        };
15965        let database = first_client.cache.database.as_ref().unwrap().clone();
15966        let value = U256::from(1_000_000_000_000_000_u64);
15967        let expected_hash = expected_wrap_tx_hash(value).await;
15968
15969        let error = first_client.wrap(value).await.unwrap_err();
15970
15971        assert!(error.to_string().contains("Timed out awaiting finality"));
15972        let intent = database
15973            .get_active_execution_intent(42161, WALLET)
15974            .await
15975            .unwrap()
15976            .unwrap();
15977        let payload = database
15978            .get_execution_transaction_hashes(intent.id)
15979            .await
15980            .unwrap()
15981            .pop()
15982            .unwrap();
15983        assert!(payload.raw_transaction.is_none());
15984        assert!(payload.sealed_transaction.is_some());
15985        let keys = first_client.payload_keys.take().unwrap();
15986        drop(first_client);
15987
15988        let block = finalized_wrap_block(expected_hash);
15989        let receipt = receipt_with_transaction_hash(RECEIPT_SUCCESS, expected_hash);
15990        let restart_state = with_finalized_identity(
15991            execution_rpc_state()
15992                .with_response("eth_getTransactionReceipt", &receipt)
15993                .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block)
15994                .with_response_sequence(
15995                    "eth_call",
15996                    &[
15997                        CALL_BALANCE,
15998                        CALL_BALANCE,
15999                        CALL_BALANCE,
16000                        CALL_BALANCE_AFTER_WRAP,
16001                        CALL_BALANCE_AFTER_WRAP,
16002                        CALL_BALANCE_AFTER_WRAP,
16003                    ],
16004                ),
16005            &block,
16006            &receipt,
16007        );
16008        let addr = start_mock_rpc_server(restart_state.clone()).await;
16009        let mut restarted = test_client(format!("http://{addr}"));
16010        restarted.cache.database = Some(database);
16011        restarted.payload_keys = Some(keys);
16012        restarted.signer = Some(Arc::new(
16013            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16014        ));
16015
16016        restarted.reconcile_unresolved_execution().await.unwrap();
16017
16018        let record = restarted
16019            .cache
16020            .get_execution_transaction(42161, &expected_hash.to_string())
16021            .await
16022            .unwrap()
16023            .unwrap();
16024        let requests = restart_state.recorded_requests();
16025        assert_eq!(record.status, "finalized");
16026        assert!(restarted.in_flight.lock().is_none());
16027        assert_eq!(
16028            requests
16029                .iter()
16030                .filter(|request| request["method"] == "eth_sendRawTransaction")
16031                .count(),
16032            0
16033        );
16034
16035        drop_execution_schema(&admin_pool, &schema).await;
16036    }
16037
16038    #[tokio::test]
16039    async fn active_intent_reconciliation_waits_for_reservation_fence() {
16040        let Some((admin_pool, schema, client, _)) = execution_client_with_database(
16041            "execution_active_intent_reservation_fence_test",
16042            ready_rpc_state(),
16043        )
16044        .await
16045        else {
16046            return;
16047        };
16048        let database = client.cache.database.as_ref().unwrap().clone();
16049        let lock = PgAdvisoryLock::new(format!(
16050            "nautilus:blockchain:execution:42161:{}",
16051            WALLET.to_ascii_lowercase()
16052        ));
16053        let PgAdvisoryLockKey::BigInt(lock_key) = lock.key() else {
16054            unreachable!("string advisory locks use the 64-bit key space");
16055        };
16056        let mut reservation = admin_pool.begin().await.unwrap();
16057        sqlx::query("SELECT pg_advisory_xact_lock($1)")
16058            .bind(*lock_key)
16059            .execute(&mut *reservation)
16060            .await
16061            .unwrap();
16062        sqlx::query(sqlx::AssertSqlSafe(format!(
16063            "INSERT INTO {schema}.execution_intent (\
16064                schema_version, chain_id, wallet_address, purpose, status, transaction_to, \
16065                transaction_input, transaction_value, created_block\
16066             ) VALUES (2, $1, $2, 'wrap', 'prepared', $3, '0xd0e30db0', '1', $4)"
16067        )))
16068        .bind(42161_i32)
16069        .bind(WALLET)
16070        .bind(WETH_ADDRESS.to_string())
16071        .bind(i64::try_from(FIXTURE_BLOCK).unwrap())
16072        .execute(&mut *reservation)
16073        .await
16074        .unwrap();
16075
16076        let mut reconciliation = Box::pin(database.get_active_execution_intent(42161, WALLET));
16077        assert!(
16078            tokio::time::timeout(Duration::from_millis(100), &mut reconciliation)
16079                .await
16080                .is_err(),
16081            "reconciliation did not wait for the reservation fence"
16082        );
16083
16084        reservation.commit().await.unwrap();
16085        let intent = tokio::time::timeout(Duration::from_secs(2), reconciliation)
16086            .await
16087            .unwrap()
16088            .unwrap()
16089            .unwrap();
16090
16091        assert_eq!(intent.chain_id, 42161);
16092        assert_eq!(intent.wallet_address, WALLET);
16093        assert_eq!(intent.purpose, "wrap");
16094        assert_eq!(intent.status, "prepared");
16095        assert!(intent.active);
16096
16097        drop_execution_schema(&admin_pool, &schema).await;
16098    }
16099
16100    #[tokio::test]
16101    async fn restart_keeps_unbroadcast_signed_intent_reserved() {
16102        let Some((admin_pool, schema, client, state)) =
16103            execution_client_with_database("execution_signed_restart_test", ready_rpc_state())
16104                .await
16105        else {
16106            return;
16107        };
16108        let database = client.cache.database.as_ref().unwrap();
16109        let intent = reserve_test_wrap_intent(database).await;
16110        let transaction = build_eip1559_transaction(
16111            42161,
16112            7,
16113            78_000,
16114            130_000_000,
16115            10_000_000,
16116            WETH_ADDRESS,
16117            U256::from(1u64),
16118            Bytes::from(nautilus_core::hex::decode("d0e30db0").unwrap()),
16119        );
16120        let (tx_hash, raw_transaction) = sign_eip1559_transaction(
16121            transaction,
16122            &PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16123        )
16124        .await
16125        .unwrap();
16126        database
16127            .assign_execution_intent_nonce(intent.id, 7)
16128            .await
16129            .unwrap();
16130        let intent = database.get_execution_intent(intent.id).await.unwrap();
16131        persist_test_payload(
16132            database,
16133            client.payload_keys.as_deref(),
16134            &intent,
16135            tx_hash,
16136            &raw_transaction,
16137        )
16138        .await;
16139
16140        let error = client.reconcile_unresolved_execution().await.unwrap_err();
16141
16142        assert!(
16143            error
16144                .to_string()
16145                .contains("was not authorized for broadcast"),
16146            "was: {error}"
16147        );
16148        let recovery = recovering_in_flight(&client);
16149        assert_eq!(recovery.intent_id, intent.id);
16150        assert_eq!(recovery.nonce, 7);
16151        assert_eq!(recovery.purpose, TransactionPurpose::Wrap);
16152        assert_eq!(
16153            execution_intent_markers(&admin_pool, &schema).await,
16154            vec![("wrap".into(), "signed".into(), false, true)]
16155        );
16156        assert!(
16157            state
16158                .recorded_requests()
16159                .iter()
16160                .all(|request| request["method"] != "eth_sendRawTransaction")
16161        );
16162
16163        drop_execution_schema(&admin_pool, &schema).await;
16164    }
16165
16166    #[tokio::test]
16167    async fn restart_quarantines_legacy_recoverable_signed_intent() {
16168        let Some((admin_pool, schema, client, state)) = execution_client_with_database(
16169            "execution_legacy_recoverable_restart_test",
16170            ready_rpc_state(),
16171        )
16172        .await
16173        else {
16174            return;
16175        };
16176        let database = client.cache.database.as_ref().unwrap();
16177        let intent = reserve_test_wrap_intent(database).await;
16178        let tx_hash = B256::from([0x55; 32]);
16179        database
16180            .assign_execution_intent_nonce(intent.id, 7)
16181            .await
16182            .unwrap();
16183        let intent = database.get_execution_intent(intent.id).await.unwrap();
16184        persist_test_payload(
16185            database,
16186            client.payload_keys.as_deref(),
16187            &intent,
16188            tx_hash,
16189            &[0x01, 0x02, 0x03],
16190        )
16191        .await;
16192        sqlx::query(sqlx::AssertSqlSafe(format!(
16193            "UPDATE {schema}.execution_intent SET status = 'recoverable', active = FALSE WHERE id = {}",
16194            intent.id
16195        )))
16196        .execute(&admin_pool)
16197        .await
16198        .unwrap();
16199
16200        let error = client.reconcile_unresolved_execution().await.unwrap_err();
16201
16202        assert!(
16203            error
16204                .to_string()
16205                .contains("retains signed transaction bytes"),
16206            "was: {error}"
16207        );
16208        assert!(client.in_flight.lock().is_none());
16209        assert!(
16210            state
16211                .recorded_requests()
16212                .iter()
16213                .all(|request| request["method"] != "eth_sendRawTransaction")
16214        );
16215
16216        drop_execution_schema(&admin_pool, &schema).await;
16217    }
16218
16219    #[rstest]
16220    #[case::current("execution_invalid_signed_restart_test", false, "broadcast")]
16221    #[case::historical("execution_invalid_historical_restart_test", true, "replaced")]
16222    #[tokio::test]
16223    async fn restart_rejects_invalid_signed_bytes_before_recovery_effects(
16224        #[case] test_name: &str,
16225        #[case] historical: bool,
16226        #[case] expected_status: &str,
16227    ) {
16228        let Some((admin_pool, schema, first_client, state, _)) =
16229            swap_client_with_database(test_name, ready_rpc_state()).await
16230        else {
16231            return;
16232        };
16233        let database = first_client.cache.database.as_ref().unwrap().clone();
16234        let (intent, _) =
16235            persist_invalid_test_swap(&database, first_client.payload_keys.as_deref()).await;
16236
16237        if historical {
16238            sqlx::query(sqlx::AssertSqlSafe(format!(
16239                "UPDATE {schema}.execution_transaction_hash \
16240                 SET current = FALSE, status = 'replaced' WHERE intent_id = $1"
16241            )))
16242            .bind(intent.id)
16243            .execute(&admin_pool)
16244            .await
16245            .unwrap();
16246            sqlx::query(sqlx::AssertSqlSafe(format!(
16247                "INSERT INTO {schema}.execution_transaction_hash (\
16248                     intent_id, chain_id, transaction_hash, payload_expected, status, current\
16249                 ) VALUES ($1, 42161, $2, FALSE, 'replaced', TRUE)"
16250            )))
16251            .bind(intent.id)
16252            .bind(B256::from([0x44; 32]).to_string())
16253            .execute(&admin_pool)
16254            .await
16255            .unwrap();
16256            sqlx::query(sqlx::AssertSqlSafe(format!(
16257                "UPDATE {schema}.execution_intent SET status = 'replaced' WHERE id = $1"
16258            )))
16259            .bind(intent.id)
16260            .execute(&admin_pool)
16261            .await
16262            .unwrap();
16263        }
16264        let transitions_before: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
16265            "SELECT COUNT(*) FROM {schema}.execution_transaction_transition"
16266        )))
16267        .fetch_one(&admin_pool)
16268        .await
16269        .unwrap();
16270        let config = first_client.config.clone();
16271        let payload_keys = first_client.payload_keys.clone();
16272        drop(first_client);
16273
16274        let (mut restarted, _) = swap_client_with_cache(config);
16275        restarted.cache.database = Some(database);
16276        restarted.payload_keys = payload_keys;
16277        restarted.signer = Some(Arc::new(
16278            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16279        ));
16280        let mut receiver = start_with_events(&mut restarted);
16281
16282        let error = restarted
16283            .reconcile_unresolved_execution()
16284            .await
16285            .unwrap_err();
16286
16287        let recovery = recovering_in_flight(&restarted);
16288        let (status, acknowledgement_emitted, active): (String, bool, bool) =
16289            sqlx::query_as(sqlx::AssertSqlSafe(format!(
16290                "SELECT status, acknowledgement_emitted, active FROM {schema}.execution_intent"
16291            )))
16292            .fetch_one(&admin_pool)
16293            .await
16294            .unwrap();
16295        let transitions_after: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
16296            "SELECT COUNT(*) FROM {schema}.execution_transaction_transition"
16297        )))
16298        .fetch_one(&admin_pool)
16299        .await
16300        .unwrap();
16301
16302        assert!(
16303            error
16304                .to_string()
16305                .contains("is not a complete EIP-2718 envelope"),
16306            "was: {error}"
16307        );
16308        assert_eq!(recovery.intent_id, intent.id);
16309        assert_eq!(recovery.nonce, 7);
16310        assert_eq!(recovery.purpose, TransactionPurpose::Swap);
16311        assert_eq!(status, expected_status);
16312        assert!(!acknowledgement_emitted);
16313        assert!(active);
16314        assert_eq!(transitions_after, transitions_before);
16315        assert!(collect_order_events(&mut receiver).is_empty());
16316        assert!(
16317            state
16318                .recorded_requests()
16319                .iter()
16320                .all(|request| request["method"] != "eth_sendRawTransaction")
16321        );
16322
16323        drop_execution_schema(&admin_pool, &schema).await;
16324    }
16325
16326    #[tokio::test]
16327    async fn restart_rebroadcasts_only_durably_authorized_bytes() {
16328        let Some((admin_pool, schema, first_client, _)) =
16329            execution_client_with_database("execution_broadcast_restart_test", ready_rpc_state())
16330                .await
16331        else {
16332            return;
16333        };
16334        let database = first_client.cache.database.as_ref().unwrap().clone();
16335        let (_, _, raw_tx) =
16336            persist_test_wrap_broadcast(&database, first_client.payload_keys.as_deref()).await;
16337        let payload_keys = first_client.payload_keys.clone();
16338        drop(first_client);
16339
16340        let restart_state = execution_rpc_state()
16341            .with_response("eth_getTransactionCount", TRANSACTION_COUNT)
16342            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
16343            .with_response("eth_call", CALL_EMPTY)
16344            .with_send_raw_transaction_echo();
16345        let addr = start_mock_rpc_server(restart_state.clone()).await;
16346        let mut restarted = test_client(format!("http://{addr}"));
16347        restarted.cache.database = Some(database);
16348        restarted.payload_keys = payload_keys;
16349        restarted.signer = Some(Arc::new(
16350            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16351        ));
16352
16353        restarted.reconcile_unresolved_execution().await.unwrap();
16354
16355        let broadcasts = restart_state
16356            .recorded_requests()
16357            .into_iter()
16358            .filter(|request| request["method"] == "eth_sendRawTransaction")
16359            .collect::<Vec<_>>();
16360        assert_eq!(broadcasts.len(), 1);
16361        assert_eq!(broadcasts[0]["params"][0], hex::encode_prefixed(&raw_tx));
16362        assert_eq!(
16363            execution_intent_markers(&admin_pool, &schema).await,
16364            vec![("wrap".into(), "dropped".into(), false, true)]
16365        );
16366
16367        drop_execution_schema(&admin_pool, &schema).await;
16368    }
16369
16370    #[tokio::test]
16371    async fn restart_suppresses_rebroadcast_when_canonical_nonce_advanced() {
16372        let Some((admin_pool, schema, first_client, _)) = execution_client_with_database(
16373            "execution_rebroadcast_nonce_advanced_test",
16374            ready_rpc_state(),
16375        )
16376        .await
16377        else {
16378            return;
16379        };
16380        let database = first_client.cache.database.as_ref().unwrap().clone();
16381        let (intent, _, _) =
16382            persist_test_wrap_broadcast(&database, first_client.payload_keys.as_deref()).await;
16383        let payload_keys = first_client.payload_keys.clone();
16384        drop(first_client);
16385
16386        let mut empty_block: serde_json::Value =
16387            serde_json::from_str(&replacement_head_block(B256::from([0x44; 32]))).unwrap();
16388        empty_block["result"]["transactions"] = serde_json::json!([]);
16389        let empty_block = empty_block.to_string();
16390        let restart_state = execution_rpc_state()
16391            .with_response("eth_getTransactionCount", TRANSACTION_COUNT_NEXT)
16392            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
16393            .with_response("eth_call", CALL_EMPTY)
16394            .with_parameter_response("eth_getBlockByNumber", "0x1cf0d40", &empty_block)
16395            .with_send_raw_transaction_echo();
16396        let addr = start_mock_rpc_server(restart_state.clone()).await;
16397        let mut restarted = test_client(format!("http://{addr}"));
16398        restarted.cache.database = Some(database);
16399        restarted.payload_keys = payload_keys;
16400        restarted.signer = Some(Arc::new(
16401            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16402        ));
16403
16404        let error = restarted
16405            .reconcile_unresolved_execution()
16406            .await
16407            .unwrap_err();
16408
16409        let requests = restart_state.recorded_requests();
16410        assert!(
16411            requests
16412                .iter()
16413                .all(|request| request["method"] != "eth_sendRawTransaction")
16414        );
16415        assert!(requests.iter().all(|request| {
16416            request["method"] != "eth_call" || request["params"][0]["data"] != "0xd0e30db0"
16417        }));
16418        let decision_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
16419            "SELECT COUNT(*) FROM {schema}.execution_verification_decision \
16420             WHERE intent_id = $1 AND decision_class = 'rebroadcast'"
16421        )))
16422        .bind(intent.id)
16423        .fetch_one(&admin_pool)
16424        .await
16425        .unwrap();
16426        assert!(
16427            error
16428                .to_string()
16429                .contains("without an authenticated signer transaction"),
16430            "was: {error}"
16431        );
16432        assert_eq!(decision_count, 6);
16433        let replacement_decision_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
16434            "SELECT COUNT(*) FROM {schema}.execution_verification_decision \
16435                 WHERE intent_id = $1 AND decision_class = 'replacement_scan'"
16436        )))
16437        .bind(intent.id)
16438        .fetch_one(&admin_pool)
16439        .await
16440        .unwrap();
16441        assert_eq!(replacement_decision_count, 2);
16442        let nonce_state = sqlx::query_as::<_, (i64, i64)>(sqlx::AssertSqlSafe(format!(
16443            "SELECT next_canonical_nonce, revision FROM {schema}.execution_verification_nonce"
16444        )))
16445        .fetch_one(&admin_pool)
16446        .await
16447        .unwrap();
16448        assert_eq!(nonce_state, (7, 0));
16449
16450        drop_execution_schema(&admin_pool, &schema).await;
16451    }
16452
16453    #[tokio::test]
16454    async fn restart_suppresses_rebroadcast_when_receipt_exists() {
16455        let Some((admin_pool, schema, first_client, _)) = execution_client_with_database(
16456            "execution_rebroadcast_receipt_present_test",
16457            ready_rpc_state(),
16458        )
16459        .await
16460        else {
16461            return;
16462        };
16463        let database = first_client.cache.database.as_ref().unwrap().clone();
16464        let (intent, _, _) =
16465            persist_test_wrap_broadcast(&database, first_client.payload_keys.as_deref()).await;
16466        let payload_keys = first_client.payload_keys.clone();
16467        drop(first_client);
16468
16469        let restart_state = execution_rpc_state()
16470            .with_response("eth_getTransactionCount", TRANSACTION_COUNT)
16471            .with_response("eth_getTransactionReceipt", RECEIPT_SUCCESS)
16472            .with_response("eth_call", CALL_EMPTY)
16473            .with_send_raw_transaction_echo();
16474        let addr = start_mock_rpc_server(restart_state.clone()).await;
16475        let mut restarted = test_client(format!("http://{addr}"));
16476        restarted.cache.database = Some(database);
16477        restarted.payload_keys = payload_keys;
16478        restarted.signer = Some(Arc::new(
16479            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16480        ));
16481
16482        let error = restarted
16483            .reconcile_unresolved_execution()
16484            .await
16485            .unwrap_err();
16486
16487        assert!(
16488            error
16489                .to_string()
16490                .contains("finalized transaction verification is locally invalid"),
16491            "was: {error}"
16492        );
16493        let requests = restart_state.recorded_requests();
16494        assert!(
16495            requests
16496                .iter()
16497                .all(|request| request["method"] != "eth_sendRawTransaction")
16498        );
16499        assert!(requests.iter().all(|request| {
16500            request["method"] != "eth_call" || request["params"][0]["data"] != "0xd0e30db0"
16501        }));
16502        let decision_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
16503            "SELECT COUNT(*) FROM {schema}.execution_verification_decision \
16504             WHERE intent_id = $1 AND decision_class = 'rebroadcast'"
16505        )))
16506        .bind(intent.id)
16507        .fetch_one(&admin_pool)
16508        .await
16509        .unwrap();
16510        assert_eq!(decision_count, 6);
16511
16512        drop_execution_schema(&admin_pool, &schema).await;
16513    }
16514
16515    #[rstest]
16516    #[case("execution_rebroadcast_false_test", CALL_ZERO)]
16517    #[case("execution_rebroadcast_revert_test", CALL_REVERTED)]
16518    #[tokio::test]
16519    async fn restart_suppresses_rebroadcast_when_simulation_denies(
16520        #[case] test_name: &str,
16521        #[case] simulation_response: &str,
16522    ) {
16523        let Some((admin_pool, schema, first_client, _)) =
16524            execution_client_with_database(test_name, ready_rpc_state()).await
16525        else {
16526            return;
16527        };
16528        let database = first_client.cache.database.as_ref().unwrap().clone();
16529        let (intent, _, _) =
16530            persist_test_wrap_broadcast(&database, first_client.payload_keys.as_deref()).await;
16531        let payload_keys = first_client.payload_keys.clone();
16532        drop(first_client);
16533
16534        let restart_state = execution_rpc_state()
16535            .with_response("eth_getTransactionCount", TRANSACTION_COUNT)
16536            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
16537            .with_response("eth_call", simulation_response)
16538            .with_send_raw_transaction_echo();
16539        let addr = start_mock_rpc_server(restart_state.clone()).await;
16540        let mut restarted = test_client(format!("http://{addr}"));
16541        restarted.cache.database = Some(database);
16542        restarted.payload_keys = payload_keys;
16543        restarted.signer = Some(Arc::new(
16544            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16545        ));
16546
16547        restarted.reconcile_unresolved_execution().await.unwrap();
16548
16549        let requests = restart_state.recorded_requests();
16550        assert!(
16551            requests
16552                .iter()
16553                .all(|request| request["method"] != "eth_sendRawTransaction")
16554        );
16555        let simulation_calls = requests
16556            .iter()
16557            .filter(|request| {
16558                request["method"] == "eth_call"
16559                    && request["params"][0]["data"] == "0xd0e30db0"
16560                    && request["params"][1] == FIXTURE_BLOCK_PARAM
16561            })
16562            .count();
16563        assert_eq!(simulation_calls, 3);
16564        let decision_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
16565            "SELECT COUNT(*) FROM {schema}.execution_verification_decision \
16566             WHERE intent_id = $1 AND decision_class = 'rebroadcast'"
16567        )))
16568        .bind(intent.id)
16569        .fetch_one(&admin_pool)
16570        .await
16571        .unwrap();
16572        assert_eq!(decision_count, 7);
16573
16574        drop_execution_schema(&admin_pool, &schema).await;
16575    }
16576
16577    #[tokio::test]
16578    async fn restart_wrap_identity_mismatch_keeps_signer_ownership() {
16579        let initial_state = broadcast_rpc_state()
16580            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
16581            .with_call_response(BALANCE_OF_SELECTOR, CALL_BALANCE);
16582        let Some((admin_pool, schema, mut first_client, _)) =
16583            execution_client_with_database("execution_restart_mismatch_test", initial_state).await
16584        else {
16585            return;
16586        };
16587        let expected_hash = expected_wrap_tx_hash(U256::from(1_000_000_000_000_000_u64)).await;
16588        let error = first_client
16589            .wrap(U256::from(1_000_000_000_000_000_u64))
16590            .await
16591            .unwrap_err();
16592        assert!(
16593            error.to_string().contains("Timed out awaiting finality"),
16594            "was: {error}"
16595        );
16596        let database = first_client.cache.database.as_ref().unwrap().clone();
16597        let payload_keys = first_client.payload_keys.clone();
16598        drop(first_client);
16599
16600        // The finalized transaction carries no value, so it cannot be the persisted wrap
16601        let mismatched_block = serde_json::json!({
16602            "jsonrpc": "2.0",
16603            "id": 1,
16604            "result": {
16605                "number": "0x1cf0d41",
16606                "hash": "0x2222222222222222222222222222222222222222222222222222222222222222",
16607                "parentHash": FIXTURE_BLOCK_HASH,
16608                "timestamp": "0x69044a21",
16609                "baseFeePerGas": "0x5f5e100",
16610                "transactions": [{
16611                    "hash": expected_hash.to_string(),
16612                    "from": WALLET,
16613                    "nonce": "0x7",
16614                    "chainId": "0xa4b1",
16615                    "type": "0x2",
16616                    "to": WETH,
16617                    "input": "0xd0e30db0",
16618                    "value": "0x0",
16619                    "gas": "0x130b0",
16620                    "maxFeePerGas": "0x7bfa480",
16621                    "maxPriorityFeePerGas": "0x989680"
16622                }]
16623            }
16624        })
16625        .to_string();
16626        let receipt = receipt_with_transaction_hash(RECEIPT_SUCCESS, expected_hash);
16627        let restart_state = with_finalized_identity(
16628            execution_rpc_state()
16629                .with_response("eth_getTransactionReceipt", &receipt)
16630                .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &mismatched_block),
16631            &mismatched_block,
16632            &receipt,
16633        );
16634        let addr = start_mock_rpc_server(restart_state.clone()).await;
16635        let mut restarted = test_client(format!("http://{addr}"));
16636        restarted.cache.database = Some(database);
16637        restarted.payload_keys = payload_keys;
16638        restarted.signer = Some(Arc::new(
16639            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16640        ));
16641
16642        let error = restarted
16643            .reconcile_unresolved_execution()
16644            .await
16645            .unwrap_err();
16646
16647        assert!(
16648            error
16649                .to_string()
16650                .contains("finalized transaction identity mismatch"),
16651            "was: {error}"
16652        );
16653        let in_flight = awaiting_in_flight(&restarted);
16654        assert_eq!(in_flight.nonce, 7);
16655        assert_eq!(in_flight.purpose, TransactionPurpose::Wrap);
16656        assert_eq!(in_flight.tx_hash, expected_hash);
16657        let requests = restart_state.recorded_requests();
16658        assert!(
16659            requests.iter().all(|request| {
16660                request["method"] != "eth_call"
16661                    || !request["params"][0]["data"]
16662                        .as_str()
16663                        .is_some_and(|data| data.starts_with(BALANCE_OF_SELECTOR))
16664            }),
16665            "the postcondition must not run when call identity is unproven"
16666        );
16667        assert!(
16668            requests
16669                .iter()
16670                .all(|request| request["method"] != "eth_sendRawTransaction")
16671        );
16672        let (status, active): (String, bool) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
16673            "SELECT status, active FROM {schema}.execution_intent"
16674        )))
16675        .fetch_one(&admin_pool)
16676        .await
16677        .unwrap();
16678        assert_eq!(status, "dropped");
16679        assert!(active);
16680
16681        let database = restarted.cache.database.as_ref().unwrap().clone();
16682        let payload_keys = restarted.payload_keys.clone();
16683        drop(restarted);
16684        let mut second = test_client(format!("http://{addr}"));
16685        second.cache.database = Some(database);
16686        second.payload_keys = payload_keys;
16687        second.signer = Some(Arc::new(
16688            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16689        ));
16690        let error = second.reconcile_unresolved_execution().await.unwrap_err();
16691        assert!(
16692            error
16693                .to_string()
16694                .contains("finalized transaction identity mismatch"),
16695            "was: {error}"
16696        );
16697        let active: bool = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
16698            "SELECT active FROM {schema}.execution_intent"
16699        )))
16700        .fetch_one(&admin_pool)
16701        .await
16702        .unwrap();
16703        assert!(active);
16704
16705        drop_execution_schema(&admin_pool, &schema).await;
16706    }
16707
16708    #[tokio::test]
16709    async fn restart_wrap_postcondition_failure_keeps_signer_ownership() {
16710        let initial_state = broadcast_rpc_state()
16711            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
16712            .with_call_response(BALANCE_OF_SELECTOR, CALL_BALANCE);
16713        let Some((admin_pool, schema, mut first_client, _)) =
16714            execution_client_with_database("execution_restart_postcondition_test", initial_state)
16715                .await
16716        else {
16717            return;
16718        };
16719        let expected_hash = expected_wrap_tx_hash(U256::from(1_000_000_000_000_000_u64)).await;
16720        let error = first_client
16721            .wrap(U256::from(1_000_000_000_000_000_u64))
16722            .await
16723            .unwrap_err();
16724        assert!(
16725            error.to_string().contains("Timed out awaiting finality"),
16726            "was: {error}"
16727        );
16728        let database = first_client.cache.database.as_ref().unwrap().clone();
16729        let payload_keys = first_client.payload_keys.clone();
16730        drop(first_client);
16731
16732        // Call identity matches, but the wrapped balance does not increase
16733        let failing_wrap_state = || {
16734            let block = finalized_wrap_block(expected_hash);
16735            let receipt = receipt_with_transaction_hash(RECEIPT_SUCCESS, expected_hash);
16736            with_finalized_identity(
16737                execution_rpc_state()
16738                    .with_response("eth_getTransactionReceipt", &receipt)
16739                    .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block)
16740                    .with_response_sequence("eth_call", &[CALL_BALANCE; 6]),
16741                &block,
16742                &receipt,
16743            )
16744        };
16745        let restart_state = failing_wrap_state();
16746        let addr = start_mock_rpc_server(restart_state.clone()).await;
16747        let mut restarted = test_client(format!("http://{addr}"));
16748        restarted.cache.database = Some(database);
16749        restarted.payload_keys = payload_keys;
16750        restarted.signer = Some(Arc::new(
16751            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16752        ));
16753
16754        let error = restarted
16755            .reconcile_unresolved_execution()
16756            .await
16757            .unwrap_err();
16758
16759        assert!(
16760            error.to_string().contains("did not increase by"),
16761            "was: {error}"
16762        );
16763        let in_flight = awaiting_in_flight(&restarted);
16764        assert_eq!(in_flight.nonce, 7);
16765        assert_eq!(in_flight.purpose, TransactionPurpose::Wrap);
16766        assert!(
16767            restart_state
16768                .recorded_requests()
16769                .iter()
16770                .all(|request| request["method"] != "eth_sendRawTransaction")
16771        );
16772        assert_eq!(
16773            execution_intent_markers(&admin_pool, &schema).await,
16774            vec![("wrap".into(), "dropped".into(), false, true)]
16775        );
16776
16777        let addr = start_mock_rpc_server(failing_wrap_state()).await;
16778        let error = later_reconnect(restarted, format!("http://{addr}")).await;
16779        assert!(
16780            error.to_string().contains("did not increase by"),
16781            "was: {error}"
16782        );
16783        assert_eq!(
16784            execution_intent_markers(&admin_pool, &schema).await,
16785            vec![("wrap".into(), "dropped".into(), false, true)]
16786        );
16787
16788        drop_execution_schema(&admin_pool, &schema).await;
16789    }
16790
16791    #[tokio::test]
16792    async fn restart_wrap_revert_marks_terminal_and_releases() {
16793        let initial_state = broadcast_rpc_state()
16794            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
16795            .with_call_response(BALANCE_OF_SELECTOR, CALL_BALANCE);
16796        let Some((admin_pool, schema, mut first_client, _)) =
16797            execution_client_with_database("execution_restart_wrap_revert_test", initial_state)
16798                .await
16799        else {
16800            return;
16801        };
16802        let expected_hash = expected_wrap_tx_hash(U256::from(1_000_000_000_000_000_u64)).await;
16803        let error = first_client
16804            .wrap(U256::from(1_000_000_000_000_000_u64))
16805            .await
16806            .unwrap_err();
16807        assert!(
16808            error.to_string().contains("Timed out awaiting finality"),
16809            "was: {error}"
16810        );
16811        let database = first_client.cache.database.as_ref().unwrap().clone();
16812        let payload_keys = first_client.payload_keys.clone();
16813        drop(first_client);
16814
16815        let block = finalized_wrap_block(expected_hash);
16816        let receipt = receipt_with_transaction_hash(RECEIPT_REVERTED, expected_hash);
16817        let restart_state = with_finalized_identity(
16818            execution_rpc_state()
16819                .with_response("eth_getTransactionReceipt", &receipt)
16820                .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block),
16821            &block,
16822            &receipt,
16823        );
16824        let addr = start_mock_rpc_server(restart_state.clone()).await;
16825        let mut restarted = test_client(format!("http://{addr}"));
16826        restarted.cache.database = Some(database);
16827        restarted.payload_keys = payload_keys;
16828        restarted.signer = Some(Arc::new(
16829            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16830        ));
16831
16832        restarted.reconcile_unresolved_execution().await.unwrap();
16833
16834        assert!(restarted.in_flight.lock().is_none());
16835        assert!(
16836            restart_state
16837                .recorded_requests()
16838                .iter()
16839                .all(|request| request["method"] != "eth_sendRawTransaction")
16840        );
16841        assert_eq!(
16842            execution_intent_markers(&admin_pool, &schema).await,
16843            vec![("wrap".into(), "reverted".into(), true, false)]
16844        );
16845
16846        drop_execution_schema(&admin_pool, &schema).await;
16847    }
16848
16849    #[tokio::test]
16850    async fn restart_quarantines_unretained_same_nonce_wrap_replacement() {
16851        let initial_state = broadcast_rpc_state()
16852            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
16853            .with_call_response(BALANCE_OF_SELECTOR, CALL_BALANCE);
16854        let Some((admin_pool, schema, mut first_client, _)) =
16855            execution_client_with_database("execution_restart_replacement_test", initial_state)
16856                .await
16857        else {
16858            return;
16859        };
16860        let original_hash = expected_wrap_tx_hash(U256::from(1_000_000_000_000_000_u64)).await;
16861        let error = first_client
16862            .wrap(U256::from(1_000_000_000_000_000_u64))
16863            .await
16864            .unwrap_err();
16865        assert!(
16866            error.to_string().contains("Timed out awaiting finality"),
16867            "was: {error}"
16868        );
16869        let database = first_client.cache.database.as_ref().unwrap().clone();
16870        let payload_keys = first_client.payload_keys.clone();
16871        drop(first_client);
16872
16873        // Matching call fields do not authenticate an unknown same-nonce payload.
16874        let replacement_hash = B256::from([0x44; 32]);
16875        let restart_state = execution_rpc_state()
16876            .with_response("eth_getTransactionCount", TRANSACTION_COUNT_NEXT)
16877            .with_response_sequence("eth_call", &[CALL_BALANCE, CALL_BALANCE_AFTER_WRAP])
16878            .with_response_sequence(
16879                "eth_getTransactionReceipt",
16880                &[RECEIPT_NULL, RECEIPT_NULL, RECEIPT_NULL],
16881            )
16882            .with_parameter_response(
16883                "eth_getBlockByNumber",
16884                "0x1cf0d40",
16885                &replacement_head_block(replacement_hash),
16886            )
16887            .with_parameter_response(
16888                "eth_getBlockByNumber",
16889                "0x1cf0d41",
16890                &finalized_wrap_block(replacement_hash),
16891            );
16892        let addr = start_mock_rpc_server(restart_state.clone()).await;
16893        let mut restarted = test_client(format!("http://{addr}"));
16894        restarted.cache.database = Some(database);
16895        restarted.payload_keys = payload_keys;
16896        restarted.signer = Some(Arc::new(
16897            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16898        ));
16899        restarted.transaction_limits.receipt_timeout_secs = 2;
16900
16901        let error = restarted
16902            .reconcile_unresolved_execution()
16903            .await
16904            .unwrap_err();
16905
16906        let hashes: Vec<(String, String, bool)> = sqlx::query_as(sqlx::AssertSqlSafe(format!(
16907            "SELECT transaction_hash, status, current FROM \
16908                 {schema}.execution_transaction_hash ORDER BY id"
16909        )))
16910        .fetch_all(&admin_pool)
16911        .await
16912        .unwrap();
16913        let requests = restart_state.recorded_requests();
16914
16915        assert!(
16916            error
16917                .to_string()
16918                .contains("has no authenticated retained payload"),
16919            "was: {error}"
16920        );
16921        assert_eq!(
16922            hashes,
16923            [(original_hash.to_string(), "dropped".to_string(), true)]
16924        );
16925        assert!(restarted.in_flight.lock().is_some());
16926        assert_eq!(
16927            execution_intent_markers(&admin_pool, &schema).await,
16928            vec![("wrap".into(), "dropped".into(), false, true)]
16929        );
16930        assert_eq!(
16931            requests
16932                .iter()
16933                .filter(|request| request["method"] == "eth_getTransactionReceipt")
16934                .count(),
16935            3
16936        );
16937        assert!(requests.iter().all(|request| {
16938            request["method"] != "eth_call" || request["params"][0]["data"] != "0xd0e30db0"
16939        }));
16940        assert!(
16941            requests
16942                .iter()
16943                .all(|request| request["method"] != "eth_sendRawTransaction")
16944        );
16945
16946        drop_execution_schema(&admin_pool, &schema).await;
16947    }
16948
16949    #[tokio::test]
16950    async fn replacement_scan_accepts_only_an_authenticated_retained_payload() {
16951        let expected_hash = expected_wrap_tx_hash(U256::from(1_u64)).await;
16952        let mut replacement_block: serde_json::Value =
16953            serde_json::from_str(&replacement_head_block(expected_hash)).unwrap();
16954        replacement_block["result"]["transactions"][1]["value"] = serde_json::json!("0x1");
16955        let replacement_block = replacement_block.to_string();
16956        let state = execution_rpc_state().with_parameter_response(
16957            "eth_getBlockByNumber",
16958            "0x1cf0d40",
16959            &replacement_block,
16960        );
16961        let Some((admin_pool, schema, client, _)) =
16962            execution_client_with_database("replacement_scan_authenticated", state).await
16963        else {
16964            return;
16965        };
16966        let database = client.cache.database.as_ref().unwrap();
16967        let (intent, persisted_hash, persisted_raw) =
16968            persist_test_wrap_broadcast(database, client.payload_keys.as_deref()).await;
16969        assert_eq!(persisted_hash, expected_hash);
16970        let authenticated_payloads = HashMap::from([(expected_hash, persisted_raw.clone())]);
16971        let head = VerifiedBlockHeader {
16972            number: FIXTURE_BLOCK,
16973            hash: B256::from_str(FIXTURE_BLOCK_HASH).unwrap(),
16974            parent_hash: B256::from_str(
16975                "0x0000000000000000000000000000000000000000000000000000000000000001",
16976            )
16977            .unwrap(),
16978            timestamp: FIXTURE_BLOCK_TIMESTAMP,
16979            base_fee_per_gas: Some(100_000_000),
16980        };
16981
16982        let matched = client
16983            .transaction_executor()
16984            .unwrap()
16985            .scan_canonical_replacement(&intent, 7, head, &authenticated_payloads)
16986            .await
16987            .unwrap()
16988            .unwrap();
16989
16990        assert_eq!(matched, (expected_hash, persisted_raw));
16991        let cursor: (i64, String) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
16992            "SELECT finalized_cursor_number, finalized_cursor_hash \
16993             FROM {schema}.execution_replacement_scan WHERE intent_id = $1"
16994        )))
16995        .bind(intent.id)
16996        .fetch_one(&admin_pool)
16997        .await
16998        .unwrap();
16999        assert_eq!(
17000            cursor,
17001            (FIXTURE_BLOCK as i64, FIXTURE_BLOCK_HASH.to_string())
17002        );
17003        let evidence_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
17004            "SELECT COUNT(*) FROM {schema}.execution_verification_decision \
17005             WHERE intent_id = $1 AND decision_class = 'replacement_scan'"
17006        )))
17007        .bind(intent.id)
17008        .fetch_one(&admin_pool)
17009        .await
17010        .unwrap();
17011        assert_eq!(evidence_count, 2);
17012
17013        drop_execution_schema(&admin_pool, &schema).await;
17014    }
17015
17016    #[tokio::test]
17017    async fn restart_reconciles_finalized_approve_after_validation() {
17018        let initial_state = broadcast_rpc_state()
17019            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
17020            .with_response("eth_call", CALL_BOOL_TRUE)
17021            .with_call_response_sequence(ALLOWANCE_SELECTOR, &[CALL_ZERO; 3])
17022            .with_call_response(ALLOWANCE_SELECTOR, CALL_ZERO);
17023        let Some((admin_pool, schema, mut first_client, _)) =
17024            execution_client_with_database("execution_restart_approve_test", initial_state).await
17025        else {
17026            return;
17027        };
17028        let expected_hash = expected_approve_tx_hash(U256::from(1_000u64)).await;
17029        let error = first_client
17030            .approve(WETH_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
17031            .await
17032            .unwrap_err();
17033        assert!(
17034            error.to_string().contains("Timed out awaiting finality"),
17035            "was: {error}"
17036        );
17037        let database = first_client.cache.database.as_ref().unwrap().clone();
17038        let payload_keys = first_client.payload_keys.clone();
17039        drop(first_client);
17040
17041        let block = finalized_approve_block(expected_hash, U256::from(1_000u64));
17042        let receipt = receipt_with_transaction_hash(RECEIPT_SUCCESS, expected_hash);
17043        let restart_state = with_finalized_identity(
17044            execution_rpc_state()
17045                .with_response("eth_getTransactionReceipt", &receipt)
17046                .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block)
17047                .with_call_response(ALLOWANCE_SELECTOR, CALL_ALLOWANCE_1000),
17048            &block,
17049            &receipt,
17050        );
17051        let addr = start_mock_rpc_server(restart_state.clone()).await;
17052        let mut restarted = test_client(format!("http://{addr}"));
17053        restarted.cache.database = Some(database);
17054        restarted.payload_keys = payload_keys;
17055        restarted.signer = Some(Arc::new(
17056            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
17057        ));
17058
17059        restarted.reconcile_unresolved_execution().await.unwrap();
17060
17061        let record = restarted
17062            .cache
17063            .get_execution_transaction(42161, &expected_hash.to_string())
17064            .await
17065            .unwrap()
17066            .unwrap();
17067        let requests = restart_state.recorded_requests();
17068        assert_eq!(record.status, "finalized");
17069        assert_eq!(record.purpose, "approve");
17070        assert!(restarted.in_flight.lock().is_none());
17071        assert_eq!(
17072            execution_intent_markers(&admin_pool, &schema).await,
17073            vec![("approve".into(), "finalized".into(), true, false)]
17074        );
17075        assert_eq!(
17076            requests
17077                .iter()
17078                .filter(|request| {
17079                    request["method"] == "eth_call"
17080                        && request["params"][0]["data"]
17081                            .as_str()
17082                            .is_some_and(|data| data.starts_with(ALLOWANCE_SELECTOR))
17083                })
17084                .count(),
17085            3
17086        );
17087        assert_eq!(
17088            requests
17089                .iter()
17090                .filter(|request| request["method"] == "eth_sendRawTransaction")
17091                .count(),
17092            0
17093        );
17094
17095        drop_execution_schema(&admin_pool, &schema).await;
17096    }
17097
17098    #[tokio::test]
17099    async fn restart_approve_postcondition_failure_keeps_signer_ownership() {
17100        let initial_state = broadcast_rpc_state()
17101            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
17102            .with_response("eth_call", CALL_BOOL_TRUE)
17103            .with_call_response_sequence(ALLOWANCE_SELECTOR, &[CALL_ZERO; 3])
17104            .with_call_response(ALLOWANCE_SELECTOR, CALL_ZERO);
17105        let Some((admin_pool, schema, mut first_client, _)) =
17106            execution_client_with_database("execution_restart_approve_post_test", initial_state)
17107                .await
17108        else {
17109            return;
17110        };
17111        let expected_hash = expected_approve_tx_hash(U256::from(1_000u64)).await;
17112        let error = first_client
17113            .approve(WETH_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
17114            .await
17115            .unwrap_err();
17116        assert!(
17117            error.to_string().contains("Timed out awaiting finality"),
17118            "was: {error}"
17119        );
17120        let database = first_client.cache.database.as_ref().unwrap().clone();
17121        let payload_keys = first_client.payload_keys.clone();
17122        drop(first_client);
17123
17124        // Call identity matches, but the allowance does not equal the approved amount
17125        let block = finalized_approve_block(expected_hash, U256::from(1_000u64));
17126        let receipt = receipt_with_transaction_hash(RECEIPT_SUCCESS, expected_hash);
17127        let restart_state = with_finalized_identity(
17128            execution_rpc_state()
17129                .with_response("eth_getTransactionReceipt", &receipt)
17130                .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block)
17131                .with_call_response(ALLOWANCE_SELECTOR, CALL_ZERO),
17132            &block,
17133            &receipt,
17134        );
17135        let addr = start_mock_rpc_server(restart_state.clone()).await;
17136        let mut restarted = test_client(format!("http://{addr}"));
17137        restarted.cache.database = Some(database);
17138        restarted.payload_keys = payload_keys;
17139        restarted.signer = Some(Arc::new(
17140            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
17141        ));
17142
17143        let error = restarted
17144            .reconcile_unresolved_execution()
17145            .await
17146            .unwrap_err();
17147
17148        assert!(
17149            error
17150                .to_string()
17151                .contains("does not equal the requested amount"),
17152            "was: {error}"
17153        );
17154        let in_flight = awaiting_in_flight(&restarted);
17155        assert_eq!(in_flight.nonce, 7);
17156        assert_eq!(in_flight.purpose, TransactionPurpose::Approve);
17157        assert!(
17158            restart_state
17159                .recorded_requests()
17160                .iter()
17161                .all(|request| request["method"] != "eth_sendRawTransaction")
17162        );
17163        assert_eq!(
17164            execution_intent_markers(&admin_pool, &schema).await,
17165            vec![("approve".into(), "dropped".into(), false, true)]
17166        );
17167
17168        let error = later_reconnect(restarted, format!("http://{addr}")).await;
17169        assert!(
17170            error
17171                .to_string()
17172                .contains("does not equal the requested amount"),
17173            "was: {error}"
17174        );
17175        assert_eq!(
17176            execution_intent_markers(&admin_pool, &schema).await,
17177            vec![("approve".into(), "dropped".into(), false, true)]
17178        );
17179
17180        drop_execution_schema(&admin_pool, &schema).await;
17181    }
17182
17183    #[tokio::test]
17184    async fn disappearing_unfinalized_receipt_drops_without_committing_inclusion() {
17185        let state = execution_rpc_state()
17186            .with_response("eth_getTransactionCount", TRANSACTION_COUNT)
17187            .with_response("eth_estimateGas", ESTIMATE_GAS)
17188            .with_response("eth_call", CALL_BALANCE)
17189            .with_response_sequence(
17190                "eth_getTransactionReceipt",
17191                &[
17192                    RECEIPT_SUCCESS,
17193                    RECEIPT_SUCCESS,
17194                    RECEIPT_SUCCESS,
17195                    RECEIPT_NULL,
17196                    RECEIPT_NULL,
17197                    RECEIPT_NULL,
17198                ],
17199            )
17200            .with_parameter_response("eth_getBlockByNumber", "finalized", BLOCK_BY_NUMBER)
17201            .with_send_raw_transaction_echo();
17202        let Some((admin_pool, schema, mut client, _)) =
17203            execution_client_with_database("execution_receipt_disappeared_test", state).await
17204        else {
17205            return;
17206        };
17207        client.transaction_limits.receipt_timeout_secs = 2;
17208
17209        let error = client
17210            .wrap(U256::from(1_000_000_000_000_000_u64))
17211            .await
17212            .unwrap_err();
17213        let transitions: Vec<String> = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
17214            "SELECT to_status FROM {schema}.execution_transaction_transition ORDER BY id"
17215        )))
17216        .fetch_all(&admin_pool)
17217        .await
17218        .unwrap();
17219
17220        assert!(
17221            error.to_string().contains("Timed out awaiting finality"),
17222            "was: {error}"
17223        );
17224        assert_eq!(transitions, ["prepared", "signed", "broadcast", "dropped"]);
17225        assert!(client.in_flight.lock().is_some());
17226
17227        drop_execution_schema(&admin_pool, &schema).await;
17228    }
17229
17230    #[tokio::test]
17231    async fn changed_unfinalized_block_drops_without_committing_inclusion() {
17232        let changed_block = serde_json::json!({
17233            "jsonrpc": "2.0",
17234            "id": 1,
17235            "result": {
17236                "number": "0x1cf0d41",
17237                "hash": "0x4444444444444444444444444444444444444444444444444444444444444444",
17238                "parentHash": FIXTURE_BLOCK_HASH,
17239                "timestamp": "0x69044a21",
17240                "baseFeePerGas": "0x5f5e100",
17241                "transactions": []
17242            }
17243        })
17244        .to_string();
17245        let state = execution_rpc_state()
17246            .with_response("eth_getTransactionCount", TRANSACTION_COUNT)
17247            .with_response("eth_estimateGas", ESTIMATE_GAS)
17248            .with_response("eth_call", CALL_BALANCE)
17249            .with_response_sequence(
17250                "eth_getTransactionReceipt",
17251                &[
17252                    RECEIPT_SUCCESS,
17253                    RECEIPT_SUCCESS,
17254                    RECEIPT_SUCCESS,
17255                    RECEIPT_SUCCESS,
17256                    RECEIPT_SUCCESS,
17257                    RECEIPT_SUCCESS,
17258                ],
17259            )
17260            .with_parameter_response_sequence(
17261                "eth_getBlockByNumber",
17262                "0x1cf0d41",
17263                &[
17264                    BLOCK_CANONICAL,
17265                    BLOCK_CANONICAL,
17266                    BLOCK_CANONICAL,
17267                    &changed_block,
17268                    &changed_block,
17269                    &changed_block,
17270                ],
17271            )
17272            .with_parameter_response("eth_getBlockByNumber", "finalized", BLOCK_BY_NUMBER)
17273            .with_send_raw_transaction_echo();
17274        let Some((admin_pool, schema, mut client, _)) =
17275            execution_client_with_database("execution_reorg_test", state).await
17276        else {
17277            return;
17278        };
17279        client.transaction_limits.receipt_timeout_secs = 2;
17280
17281        let error = client
17282            .wrap(U256::from(1_000_000_000_000_000_u64))
17283            .await
17284            .unwrap_err();
17285        let transitions: Vec<String> = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
17286            "SELECT to_status FROM {schema}.execution_transaction_transition ORDER BY id"
17287        )))
17288        .fetch_all(&admin_pool)
17289        .await
17290        .unwrap();
17291
17292        assert!(
17293            error.to_string().contains("Timed out awaiting finality"),
17294            "was: {error}"
17295        );
17296        assert_eq!(transitions, ["prepared", "signed", "broadcast", "dropped"]);
17297        assert!(client.in_flight.lock().is_some());
17298
17299        drop_execution_schema(&admin_pool, &schema).await;
17300    }
17301
17302    #[tokio::test]
17303    async fn pre_sign_pending_nonce_drift_blocks_wrap_before_signature() {
17304        let state = execution_rpc_state()
17305            .with_response_sequence(
17306                "eth_getTransactionCount",
17307                &[
17308                    TRANSACTION_COUNT,
17309                    TRANSACTION_COUNT,
17310                    TRANSACTION_COUNT,
17311                    TRANSACTION_COUNT_NEXT,
17312                    TRANSACTION_COUNT_NEXT,
17313                    TRANSACTION_COUNT_NEXT,
17314                ],
17315            )
17316            .with_response("eth_estimateGas", ESTIMATE_GAS)
17317            .with_call_response(BALANCE_OF_SELECTOR, CALL_BALANCE)
17318            .with_send_raw_transaction_echo();
17319        let Some((admin_pool, schema, mut client, state)) =
17320            execution_client_with_database("pre_sign_pending_nonce_drift", state).await
17321        else {
17322            return;
17323        };
17324
17325        let error = client
17326            .wrap(U256::from(1_000_000_000_000_000_u64))
17327            .await
17328            .unwrap_err();
17329        let signed_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
17330            "SELECT COUNT(*) FROM {schema}.execution_transaction_hash"
17331        )))
17332        .fetch_one(&admin_pool)
17333        .await
17334        .unwrap();
17335
17336        assert!(
17337            error
17338                .to_string()
17339                .contains("Pending nonce does not match the verified canonical nonce"),
17340            "was: {error}"
17341        );
17342        assert_eq!(signed_count, 0);
17343        assert!(
17344            state
17345                .recorded_requests()
17346                .iter()
17347                .all(|request| request["method"] != "eth_sendRawTransaction")
17348        );
17349        assert!(client.in_flight.lock().is_none());
17350
17351        drop_execution_schema(&admin_pool, &schema).await;
17352    }
17353
17354    #[tokio::test]
17355    async fn unknown_same_nonce_swap_replacement_emits_no_rejection() {
17356        let replacement_hash = B256::from([0x44; 32]);
17357        let replacement_block = replacement_head_block(replacement_hash);
17358        let state = execution_rpc_state()
17359            .with_parameter_response("eth_getBlockByNumber", "0x1cf0d40", &replacement_block)
17360            .with_send_raw_transaction_echo();
17361        let Some((admin_pool, schema, mut client, _, _)) =
17362            swap_client_with_database("unknown_same_nonce_swap_replacement", state).await
17363        else {
17364            return;
17365        };
17366        let mut receiver = start_with_events(&mut client);
17367        let database = client.cache.database.as_ref().unwrap();
17368        let (intent, original_hash, original_payload) =
17369            persist_test_swap_broadcast(database, client.payload_keys.as_deref()).await;
17370        let authenticated_payloads = HashMap::from([(original_hash, original_payload)]);
17371        let head = VerifiedBlockHeader {
17372            number: FIXTURE_BLOCK,
17373            hash: B256::from_str(FIXTURE_BLOCK_HASH).unwrap(),
17374            parent_hash: B256::from_str(
17375                "0x0000000000000000000000000000000000000000000000000000000000000001",
17376            )
17377            .unwrap(),
17378            timestamp: FIXTURE_BLOCK_TIMESTAMP,
17379            base_fee_per_gas: Some(100_000_000),
17380        };
17381
17382        let error = client
17383            .transaction_executor()
17384            .unwrap()
17385            .scan_canonical_replacement(&intent, 7, head, &authenticated_payloads)
17386            .await
17387            .unwrap_err();
17388        let events = collect_order_events(&mut receiver);
17389        let (status, active): (String, bool) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
17390            "SELECT status, active FROM {schema}.execution_intent"
17391        )))
17392        .fetch_one(&admin_pool)
17393        .await
17394        .unwrap();
17395
17396        assert!(
17397            error
17398                .to_string()
17399                .contains("has no authenticated retained payload"),
17400            "was: {error}"
17401        );
17402        assert!(events.is_empty(), "was: {events:?}");
17403        assert_eq!(status, "broadcast");
17404        assert!(active);
17405
17406        drop_execution_schema(&admin_pool, &schema).await;
17407    }
17408
17409    #[tokio::test]
17410    async fn replacement_scan_rejects_rpc_fields_that_conflict_with_the_payload() {
17411        let expected_hash = expected_wrap_tx_hash(U256::from(1_u64)).await;
17412        let mut replacement_block: serde_json::Value =
17413            serde_json::from_str(&replacement_head_block(expected_hash)).unwrap();
17414        replacement_block["result"]["transactions"][1]["value"] = serde_json::json!("0x2");
17415        let replacement_block = replacement_block.to_string();
17416        let state = execution_rpc_state().with_parameter_response(
17417            "eth_getBlockByNumber",
17418            "0x1cf0d40",
17419            &replacement_block,
17420        );
17421        let Some((admin_pool, schema, client, _)) =
17422            execution_client_with_database("replacement_scan_payload_mismatch", state).await
17423        else {
17424            return;
17425        };
17426        let database = client.cache.database.as_ref().unwrap();
17427        let (intent, persisted_hash, persisted_payload) =
17428            persist_test_wrap_broadcast(database, client.payload_keys.as_deref()).await;
17429        assert_eq!(persisted_hash, expected_hash);
17430        let authenticated_payloads = HashMap::from([(persisted_hash, persisted_payload)]);
17431        let head = VerifiedBlockHeader {
17432            number: FIXTURE_BLOCK,
17433            hash: B256::from_str(FIXTURE_BLOCK_HASH).unwrap(),
17434            parent_hash: B256::from_str(
17435                "0x0000000000000000000000000000000000000000000000000000000000000001",
17436            )
17437            .unwrap(),
17438            timestamp: FIXTURE_BLOCK_TIMESTAMP,
17439            base_fee_per_gas: Some(100_000_000),
17440        };
17441
17442        let error = client
17443            .transaction_executor()
17444            .unwrap()
17445            .scan_canonical_replacement(&intent, 7, head, &authenticated_payloads)
17446            .await
17447            .unwrap_err();
17448        let (status, active): (String, bool) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
17449            "SELECT status, active FROM {schema}.execution_intent"
17450        )))
17451        .fetch_one(&admin_pool)
17452        .await
17453        .unwrap();
17454
17455        assert!(
17456            error
17457                .to_string()
17458                .contains("failed authenticated payload validation"),
17459            "was: {error}"
17460        );
17461        assert_eq!(status, "broadcast");
17462        assert!(active);
17463
17464        drop_execution_schema(&admin_pool, &schema).await;
17465    }
17466
17467    #[tokio::test]
17468    async fn execution_transaction_constraints_reject_conflicting_identity() {
17469        const TRANSACTION_HASH: &str = "0xduplicate-transaction-hash";
17470        const OTHER_WALLET: &str = "0x0000000000000000000000000000000000000001";
17471        let Some((admin_pool, schema, client, _)) = execution_client_with_unprotected_database(
17472            "execution_duplicate_record_test",
17473            ready_rpc_state(),
17474        )
17475        .await
17476        else {
17477            return;
17478        };
17479        let database = client.cache.database.as_ref().unwrap();
17480        let operator = database
17481            .reserve_execution_intent(&ExecutionIntentInsert {
17482                chain_id: 42161,
17483                wallet_address: WALLET.to_string(),
17484                purpose: "wrap".to_string(),
17485                client_order_id: None,
17486                trader_id: None,
17487                strategy_id: None,
17488                account_id: None,
17489                instrument_id: None,
17490                pool_address: None,
17491                transaction_to: WETH_ADDRESS.to_string(),
17492                transaction_input: "0xd0e30db0".to_string(),
17493                transaction_value: "1".to_string(),
17494                amount_in: None,
17495                created_block: FIXTURE_BLOCK,
17496            })
17497            .await
17498            .unwrap();
17499        database
17500            .assign_execution_intent_nonce(operator.id, 7)
17501            .await
17502            .unwrap();
17503        database
17504            .add_execution_transaction_hash(operator.id, 42161, TRANSACTION_HASH, &[1, 2, 3])
17505            .await
17506            .unwrap();
17507        database
17508            .add_execution_transaction_hash(operator.id, 42161, TRANSACTION_HASH, &[1, 2, 3])
17509            .await
17510            .unwrap();
17511
17512        let signer_conflict = database
17513            .reserve_execution_intent(&ExecutionIntentInsert {
17514                chain_id: 42161,
17515                wallet_address: WALLET.to_string(),
17516                purpose: "approve".to_string(),
17517                client_order_id: None,
17518                trader_id: None,
17519                strategy_id: None,
17520                account_id: None,
17521                instrument_id: None,
17522                pool_address: None,
17523                transaction_to: ROUTER_ADDRESS.to_string(),
17524                transaction_input: "0x01".to_string(),
17525                transaction_value: "0".to_string(),
17526                amount_in: None,
17527                created_block: FIXTURE_BLOCK,
17528            })
17529            .await
17530            .unwrap_err();
17531        let other = database
17532            .reserve_execution_intent(&ExecutionIntentInsert {
17533                chain_id: 42161,
17534                wallet_address: OTHER_WALLET.to_string(),
17535                purpose: "approve".to_string(),
17536                client_order_id: None,
17537                trader_id: None,
17538                strategy_id: None,
17539                account_id: None,
17540                instrument_id: None,
17541                pool_address: None,
17542                transaction_to: ROUTER_ADDRESS.to_string(),
17543                transaction_input: "0x01".to_string(),
17544                transaction_value: "0".to_string(),
17545                amount_in: None,
17546                created_block: FIXTURE_BLOCK,
17547            })
17548            .await
17549            .unwrap();
17550        database
17551            .assign_execution_intent_nonce(other.id, 7)
17552            .await
17553            .unwrap();
17554        let hash_conflict = database
17555            .add_execution_transaction_hash(other.id, 42161, TRANSACTION_HASH, &[4, 5, 6])
17556            .await
17557            .unwrap_err();
17558
17559        let record = database
17560            .get_execution_transaction(42161, TRANSACTION_HASH)
17561            .await
17562            .unwrap()
17563            .unwrap();
17564        let count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
17565            "SELECT COUNT(*) FROM {schema}.execution_intent"
17566        )))
17567        .fetch_one(&admin_pool)
17568        .await
17569        .unwrap();
17570
17571        assert_eq!(record.wallet_address.as_deref(), Some(WALLET));
17572        assert_eq!(record.nonce, 7);
17573        assert_eq!(record.transaction_hash, TRANSACTION_HASH);
17574        assert_eq!(record.purpose, "wrap");
17575        assert_eq!(record.status, "signed");
17576        assert_eq!(record.client_order_id, None);
17577        assert!(
17578            hash_conflict
17579                .to_string()
17580                .contains("conflicts with its persisted identity"),
17581            "was: {hash_conflict}"
17582        );
17583        assert_eq!(
17584            signer_conflict.to_string(),
17585            "Execution intent reservation failed before commit"
17586        );
17587        assert!(
17588            signer_conflict.chain().any(|cause| cause
17589                .to_string()
17590                .contains("execution_intent_active_signer_key")),
17591            "was: {signer_conflict:#}"
17592        );
17593        assert_eq!(count, 2);
17594
17595        drop_execution_schema(&admin_pool, &schema).await;
17596    }
17597
17598    #[tokio::test]
17599    async fn execution_status_transitions_are_idempotent() {
17600        const TRANSACTION_HASH: &str =
17601            "0x5555555555555555555555555555555555555555555555555555555555555555";
17602        let Some((admin_pool, schema, client, _)) = execution_client_with_unprotected_database(
17603            "execution_transition_test",
17604            ready_rpc_state(),
17605        )
17606        .await
17607        else {
17608            return;
17609        };
17610        let database = client.cache.database.as_ref().unwrap();
17611        let intent = database
17612            .reserve_execution_intent(&ExecutionIntentInsert {
17613                chain_id: 42161,
17614                wallet_address: WALLET.to_string(),
17615                purpose: "wrap".to_string(),
17616                client_order_id: None,
17617                trader_id: None,
17618                strategy_id: None,
17619                account_id: None,
17620                instrument_id: None,
17621                pool_address: None,
17622                transaction_to: WETH_ADDRESS.to_string(),
17623                transaction_input: "0xd0e30db0".to_string(),
17624                transaction_value: "1".to_string(),
17625                amount_in: None,
17626                created_block: FIXTURE_BLOCK,
17627            })
17628            .await
17629            .unwrap();
17630        database
17631            .assign_execution_intent_nonce(intent.id, 7)
17632            .await
17633            .unwrap();
17634        database
17635            .add_execution_transaction_hash(intent.id, 42161, TRANSACTION_HASH, &[1, 2, 3])
17636            .await
17637            .unwrap();
17638
17639        for _ in 0..2 {
17640            database
17641                .record_execution_status(
17642                    intent.id,
17643                    TRANSACTION_HASH,
17644                    TransactionStatus::Broadcast,
17645                    None,
17646                    None,
17647                    None,
17648                    None,
17649                    None,
17650                )
17651                .await
17652                .unwrap();
17653        }
17654
17655        for status in [TransactionStatus::Included, TransactionStatus::Included] {
17656            database
17657                .record_execution_status(
17658                    intent.id,
17659                    TRANSACTION_HASH,
17660                    status,
17661                    Some(FIXTURE_BLOCK + 1),
17662                    Some("0x2222222222222222222222222222222222222222222222222222222222222222"),
17663                    Some(true),
17664                    Some(50_112),
17665                    Some("100000000"),
17666                )
17667                .await
17668                .unwrap();
17669        }
17670
17671        for _ in 0..2 {
17672            database
17673                .record_execution_status(
17674                    intent.id,
17675                    TRANSACTION_HASH,
17676                    TransactionStatus::Finalized,
17677                    Some(FIXTURE_BLOCK + 1),
17678                    Some("0x2222222222222222222222222222222222222222222222222222222222222222"),
17679                    Some(true),
17680                    Some(50_112),
17681                    Some("100000000"),
17682                )
17683                .await
17684                .unwrap();
17685        }
17686
17687        let transitions: Vec<String> = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
17688            "SELECT to_status FROM {schema}.execution_transaction_transition ORDER BY id"
17689        )))
17690        .fetch_all(&admin_pool)
17691        .await
17692        .unwrap();
17693        let (status, active): (String, bool) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
17694            "SELECT status, active FROM {schema}.execution_intent"
17695        )))
17696        .fetch_one(&admin_pool)
17697        .await
17698        .unwrap();
17699        let append_only_error = sqlx::query(sqlx::AssertSqlSafe(format!(
17700            "DELETE FROM {schema}.execution_transaction_transition WHERE intent_id = {}",
17701            intent.id
17702        )))
17703        .execute(&admin_pool)
17704        .await
17705        .unwrap_err();
17706
17707        assert_eq!(
17708            transitions,
17709            ["prepared", "signed", "broadcast", "included", "finalized"]
17710        );
17711        assert_eq!(status, "finalized");
17712        assert!(active);
17713        assert!(
17714            append_only_error
17715                .to_string()
17716                .contains("Execution transitions are append-only"),
17717            "was: {append_only_error}"
17718        );
17719
17720        drop_execution_schema(&admin_pool, &schema).await;
17721    }
17722
17723    #[tokio::test]
17724    async fn wrap_then_approve_persists_records_and_clears_in_flight() {
17725        let state = execution_rpc_state()
17726            .with_parameter_response_sequence(
17727                "eth_getBlockByNumber",
17728                "latest",
17729                &[
17730                    BLOCK_BY_NUMBER,
17731                    BLOCK_BY_NUMBER,
17732                    BLOCK_BY_NUMBER,
17733                    BLOCK_FINALIZED,
17734                    BLOCK_FINALIZED,
17735                    BLOCK_FINALIZED,
17736                ],
17737            )
17738            .with_response_sequence(
17739                "eth_call",
17740                &[
17741                    CALL_BALANCE,
17742                    CALL_BALANCE,
17743                    CALL_BALANCE,
17744                    CALL_BALANCE,
17745                    CALL_BALANCE,
17746                    CALL_BALANCE,
17747                    CALL_BALANCE_AFTER_WRAP,
17748                    CALL_BALANCE_AFTER_WRAP,
17749                    CALL_BALANCE_AFTER_WRAP,
17750                    CALL_BOOL_TRUE,
17751                    CALL_BOOL_TRUE,
17752                    CALL_BOOL_TRUE,
17753                ],
17754            )
17755            .with_call_response_sequence(
17756                ALLOWANCE_SELECTOR,
17757                &[
17758                    CALL_ZERO,
17759                    CALL_ZERO,
17760                    CALL_ZERO,
17761                    CALL_ALLOWANCE_MAX,
17762                    CALL_ALLOWANCE_MAX,
17763                    CALL_ALLOWANCE_MAX,
17764                ],
17765            )
17766            .with_response_sequence(
17767                "eth_getTransactionCount",
17768                &[
17769                    TRANSACTION_COUNT,
17770                    TRANSACTION_COUNT,
17771                    TRANSACTION_COUNT,
17772                    TRANSACTION_COUNT,
17773                    TRANSACTION_COUNT,
17774                    TRANSACTION_COUNT,
17775                    TRANSACTION_COUNT_NEXT,
17776                    TRANSACTION_COUNT_NEXT,
17777                    TRANSACTION_COUNT_NEXT,
17778                    TRANSACTION_COUNT_NEXT,
17779                    TRANSACTION_COUNT_NEXT,
17780                    TRANSACTION_COUNT_NEXT,
17781                ],
17782            )
17783            .with_response("eth_estimateGas", ESTIMATE_GAS)
17784            .with_response_sequence(
17785                "eth_getTransactionReceipt",
17786                &[
17787                    RECEIPT_SUCCESS,
17788                    RECEIPT_SUCCESS,
17789                    RECEIPT_SUCCESS,
17790                    RECEIPT_SUCCESS,
17791                    RECEIPT_SUCCESS,
17792                    RECEIPT_SUCCESS,
17793                ],
17794            )
17795            .with_send_raw_transaction_echo();
17796        let Some((admin_pool, schema, mut client, state)) =
17797            execution_client_with_database("execution_client_test", state).await
17798        else {
17799            return;
17800        };
17801        client.config.unlimited_approval = true;
17802        client.transaction_limits.receipt_timeout_secs = 3;
17803
17804        let wrap_hash = client
17805            .wrap(U256::from(1_000_000_000_000_000u64))
17806            .await
17807            .unwrap();
17808
17809        let record = client
17810            .cache
17811            .get_execution_transaction(42161, &wrap_hash.to_string())
17812            .await
17813            .unwrap()
17814            .unwrap();
17815        assert_eq!(record.nonce, 7);
17816        assert_eq!(record.purpose, "wrap");
17817        assert_eq!(record.status, "finalized");
17818        assert_eq!(
17819            execution_intent_markers(&admin_pool, &schema).await,
17820            vec![("wrap".into(), "finalized".into(), true, false)]
17821        );
17822
17823        let approve_hash = client
17824            .approve(WETH_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
17825            .await
17826            .unwrap();
17827
17828        let record = client
17829            .cache
17830            .get_execution_transaction(42161, &approve_hash.to_string())
17831            .await
17832            .unwrap()
17833            .unwrap();
17834        assert_eq!(record.nonce, 8);
17835        assert_eq!(record.purpose, "approve");
17836        assert_eq!(record.status, "finalized");
17837        assert_eq!(
17838            execution_intent_markers(&admin_pool, &schema).await,
17839            vec![
17840                ("wrap".into(), "finalized".into(), true, false),
17841                ("approve".into(), "finalized".into(), true, false),
17842            ]
17843        );
17844
17845        let requests = state.recorded_requests();
17846        let broadcasts: Vec<_> = requests
17847            .iter()
17848            .filter(|request| request["method"] == "eth_sendRawTransaction")
17849            .collect();
17850        assert_eq!(broadcasts.len(), 2);
17851        for broadcast in &broadcasts {
17852            let payload = broadcast["params"][0].as_str().unwrap();
17853            assert!(payload.starts_with("0x02"), "was: {payload}");
17854        }
17855        let broadcast_indexes = requests
17856            .iter()
17857            .enumerate()
17858            .filter_map(|(index, request)| {
17859                (request["method"] == "eth_sendRawTransaction").then_some(index)
17860            })
17861            .collect::<Vec<_>>();
17862
17863        for (index, expected_block) in broadcast_indexes
17864            .into_iter()
17865            .zip([FIXTURE_BLOCK_PARAM, "0x1cf0d42"])
17866        {
17867            let nonce_fence = &requests[index - 6..index];
17868            assert!(nonce_fence[..3].iter().all(|request| {
17869                request["method"] == "eth_getTransactionCount"
17870                    && request["params"][1] == expected_block
17871            }));
17872            assert!(nonce_fence[3..].iter().all(|request| {
17873                request["method"] == "eth_getTransactionCount" && request["params"][1] == "pending"
17874            }));
17875        }
17876        let receipt_polls = requests
17877            .iter()
17878            .filter(|request| request["method"] == "eth_getTransactionReceipt")
17879            .count();
17880        assert_eq!(receipt_polls, 6);
17881        let allowance_calls: Vec<_> = requests
17882            .iter()
17883            .filter(|request| {
17884                request["method"] == "eth_call"
17885                    && request["params"][0]["data"]
17886                        .as_str()
17887                        .is_some_and(|data| data.starts_with(ALLOWANCE_SELECTOR))
17888            })
17889            .collect();
17890        assert_eq!(allowance_calls.len(), 6);
17891        assert!(
17892            allowance_calls[..3]
17893                .iter()
17894                .all(|request| request["params"][1] == "0x1cf0d42")
17895        );
17896        assert!(
17897            allowance_calls[3..]
17898                .iter()
17899                .all(|request| request["params"][1] == "0x1cf0d41")
17900        );
17901
17902        // Unlimited approval policy encoded U256::MAX in the approve calldata
17903        let estimates: Vec<_> = requests
17904            .iter()
17905            .filter(|request| request["method"] == "eth_estimateGas")
17906            .collect();
17907        assert_eq!(estimates.len(), 6);
17908        assert!(estimates[..3].iter().all(|request| {
17909            request["params"].as_array().unwrap().len() == 2
17910                && request["params"][1] == FIXTURE_BLOCK_PARAM
17911        }));
17912        assert!(estimates[3..].iter().all(|request| {
17913            request["params"].as_array().unwrap().len() == 2 && request["params"][1] == "0x1cf0d42"
17914        }));
17915        let approve_data = estimates[3]["params"][0]["data"].as_str().unwrap();
17916        assert!(
17917            approve_data.starts_with("0x095ea7b3"),
17918            "was: {approve_data}"
17919        );
17920        assert!(
17921            approve_data.ends_with(&"f".repeat(64)),
17922            "was: {approve_data}"
17923        );
17924
17925        for selector in [FACTORY_SELECTOR, WETH9_SELECTOR] {
17926            let calls: Vec<_> = requests
17927                .iter()
17928                .filter(|request| {
17929                    request["method"] == "eth_call"
17930                        && request["params"][0]["data"]
17931                            .as_str()
17932                            .is_some_and(|data| data.starts_with(selector))
17933                })
17934                .collect();
17935            assert!(!calls.is_empty(), "selector {selector}");
17936            assert!(
17937                calls.iter().all(|request| request["params"][1]
17938                    .as_str()
17939                    .is_some_and(|block| block.starts_with("0x"))),
17940                "selector {selector}: {calls:?}"
17941            );
17942        }
17943        let latest_blocks = requests
17944            .iter()
17945            .filter(|request| {
17946                request["method"] == "eth_getBlockByNumber"
17947                    && request["params"] == serde_json::json!(["latest", false])
17948            })
17949            .count();
17950        assert_eq!(latest_blocks, 6);
17951
17952        drop_execution_schema(&admin_pool, &schema).await;
17953    }
17954
17955    #[tokio::test]
17956    async fn reverted_receipt_marks_record_reverted_and_errors() {
17957        let state = signing_rpc_state()
17958            .with_response("eth_getTransactionReceipt", RECEIPT_REVERTED)
17959            .with_send_raw_transaction_echo();
17960        let Some((admin_pool, schema, mut client, _)) =
17961            execution_client_with_database("execution_reverted_test", state).await
17962        else {
17963            return;
17964        };
17965
17966        let error = client
17967            .wrap(U256::from(1_000_000_000_000_000u64))
17968            .await
17969            .unwrap_err();
17970
17971        assert!(
17972            error.to_string().contains("reverted on-chain"),
17973            "was: {error}"
17974        );
17975        assert!(client.in_flight.lock().is_none());
17976
17977        let expected_hash = expected_wrap_tx_hash(U256::from(1_000_000_000_000_000u64)).await;
17978
17979        let record = client
17980            .cache
17981            .get_execution_transaction(42161, &expected_hash.to_string())
17982            .await
17983            .unwrap()
17984            .unwrap();
17985        assert_eq!(record.status, "reverted");
17986        assert_eq!(
17987            execution_intent_markers(&admin_pool, &schema).await,
17988            vec![("wrap".into(), "reverted".into(), true, false)]
17989        );
17990
17991        drop_execution_schema(&admin_pool, &schema).await;
17992    }
17993
17994    fn market_sell_order_with_id(instrument_id: InstrumentId, client_order_id: &str) -> OrderAny {
17995        OrderTestBuilder::new(OrderType::Market)
17996            .trader_id(TraderId::from("TRADER-001"))
17997            .strategy_id(StrategyId::from("S-001"))
17998            .instrument_id(instrument_id)
17999            .client_order_id(ClientOrderId::from(client_order_id))
18000            .side(OrderSide::Sell)
18001            .quantity(Quantity::from("0.001"))
18002            .build()
18003    }
18004
18005    fn market_buy_order_with_id(instrument_id: InstrumentId, client_order_id: &str) -> OrderAny {
18006        OrderTestBuilder::new(OrderType::Market)
18007            .trader_id(TraderId::from("TRADER-001"))
18008            .strategy_id(StrategyId::from("S-001"))
18009            .instrument_id(instrument_id)
18010            .client_order_id(ClientOrderId::from(client_order_id))
18011            .side(OrderSide::Buy)
18012            .quantity(Quantity::from("0.001"))
18013            .build()
18014    }
18015
18016    fn submit_order_list_cmd(orders: &[OrderAny]) -> SubmitOrderList {
18017        let order_list = OrderList::new(
18018            OrderListId::from("OL-001"),
18019            orders[0].instrument_id(),
18020            orders[0].strategy_id(),
18021            orders.iter().map(|order| order.client_order_id()).collect(),
18022            UnixNanos::default(),
18023        );
18024        SubmitOrderList::new(
18025            TraderId::from("TRADER-001"),
18026            Some(ClientId::from("BLOCKCHAIN-001")),
18027            orders[0].strategy_id(),
18028            order_list,
18029            orders
18030                .iter()
18031                .map(|order| order.init_event().clone())
18032                .collect(),
18033            None,
18034            None,
18035            None,
18036            UUID4::new(),
18037            UnixNanos::default(),
18038            None,
18039        )
18040    }
18041
18042    fn modify_order_cmd(
18043        instrument_id: InstrumentId,
18044        client_order_id: ClientOrderId,
18045    ) -> ModifyOrder {
18046        ModifyOrder::new(
18047            TraderId::from("TRADER-001"),
18048            Some(ClientId::from("BLOCKCHAIN-001")),
18049            StrategyId::from("S-001"),
18050            instrument_id,
18051            client_order_id,
18052            None,
18053            Some(Quantity::from("0.002")),
18054            None,
18055            None,
18056            UUID4::new(),
18057            UnixNanos::default(),
18058            None,
18059            None,
18060        )
18061    }
18062
18063    fn cancel_order_cmd(
18064        instrument_id: InstrumentId,
18065        client_order_id: ClientOrderId,
18066    ) -> CancelOrder {
18067        CancelOrder::new(
18068            TraderId::from("TRADER-001"),
18069            Some(ClientId::from("BLOCKCHAIN-001")),
18070            StrategyId::from("S-001"),
18071            instrument_id,
18072            client_order_id,
18073            None,
18074            UUID4::new(),
18075            UnixNanos::default(),
18076            None,
18077            None,
18078        )
18079    }
18080
18081    fn cancel_all_orders_cmd(instrument_id: InstrumentId) -> CancelAllOrders {
18082        CancelAllOrders::new(
18083            TraderId::from("TRADER-001"),
18084            Some(ClientId::from("BLOCKCHAIN-001")),
18085            StrategyId::from("S-001"),
18086            instrument_id,
18087            Some(OrderSide::Sell),
18088            UUID4::new(),
18089            UnixNanos::default(),
18090            None,
18091            None,
18092        )
18093    }
18094
18095    fn batch_cancel_orders_cmd(cancels: Vec<CancelOrder>) -> BatchCancelOrders {
18096        BatchCancelOrders::new(
18097            TraderId::from("TRADER-001"),
18098            Some(ClientId::from("BLOCKCHAIN-001")),
18099            StrategyId::from("S-001"),
18100            cancels[0].instrument_id,
18101            cancels,
18102            UUID4::new(),
18103            UnixNanos::default(),
18104            None,
18105            None,
18106        )
18107    }
18108
18109    fn query_order_cmd(instrument_id: InstrumentId, client_order_id: ClientOrderId) -> QueryOrder {
18110        QueryOrder::new(
18111            TraderId::from("TRADER-001"),
18112            Some(ClientId::from("BLOCKCHAIN-001")),
18113            StrategyId::from("S-001"),
18114            instrument_id,
18115            client_order_id,
18116            None,
18117            UUID4::new(),
18118            UnixNanos::default(),
18119            None,
18120            None,
18121        )
18122    }
18123
18124    async fn unsupported_client_with_mock_rpc()
18125    -> (BlockchainExecutionClient, MockRpcState, Rc<RefCell<Cache>>) {
18126        let state = ready_rpc_state();
18127        let addr = start_mock_rpc_server(state.clone()).await;
18128        let (client, cache) = swap_client_with_cache(test_config(format!("http://{addr}")));
18129        (client, state, cache)
18130    }
18131
18132    #[tokio::test]
18133    async fn submit_order_denies_reduce_only_without_side_effects() {
18134        let (mut client, state, cache) = unsupported_client_with_mock_rpc().await;
18135        let order = OrderTestBuilder::new(OrderType::Market)
18136            .trader_id(TraderId::from("TRADER-001"))
18137            .strategy_id(StrategyId::from("S-001"))
18138            .instrument_id(test_pool().instrument_id)
18139            .client_order_id(ClientOrderId::from("O-REDUCE-ONLY"))
18140            .side(OrderSide::Sell)
18141            .quantity(Quantity::from("0.001"))
18142            .reduce_only(true)
18143            .build();
18144        cache
18145            .borrow_mut()
18146            .add_order(order.clone(), None, None, false)
18147            .unwrap();
18148        let mut receiver = start_with_events(&mut client);
18149
18150        client.submit_order(submit_order_cmd(&order)).unwrap();
18151
18152        let events = collect_order_events(&mut receiver);
18153        assert_eq!(events.len(), 1, "was: {events:?}");
18154        let OrderEventAny::Denied(denied) = &events[0] else {
18155            panic!("expected OrderDenied, was {:?}", events[0]);
18156        };
18157        assert_eq!(denied.client_order_id, order.client_order_id());
18158        assert_eq!(denied.reason, "UNSUPPORTED_REDUCE_ONLY");
18159        assert!(state.recorded_requests().is_empty());
18160        assert!(client.in_flight.lock().is_none());
18161    }
18162
18163    #[tokio::test]
18164    async fn submit_order_list_denies_every_order_without_side_effects() {
18165        let (mut client, state, cache) = unsupported_client_with_mock_rpc().await;
18166        let pool = test_pool();
18167        let first = test_market_sell_order(pool.instrument_id);
18168        let second = market_sell_order_with_id(pool.instrument_id, "O-SWAP-002");
18169        cache
18170            .borrow_mut()
18171            .add_order(second.clone(), None, None, false)
18172            .unwrap();
18173        let orders = [first.clone(), second.clone()];
18174        let mut receiver = start_with_events(&mut client);
18175
18176        client
18177            .submit_order_list(submit_order_list_cmd(&orders))
18178            .unwrap();
18179
18180        let events = collect_order_events(&mut receiver);
18181        let mut denied_ids = Vec::new();
18182
18183        for event in &events {
18184            let OrderEventAny::Denied(denied) = event else {
18185                panic!("expected OrderDenied, was {event:?}");
18186            };
18187            assert_eq!(denied.reason, ORDER_LIST_UNSUPPORTED);
18188            denied_ids.push(denied.client_order_id);
18189        }
18190        denied_ids.sort();
18191        assert_eq!(
18192            denied_ids,
18193            [first.client_order_id(), second.client_order_id()]
18194        );
18195        assert!(state.recorded_requests().is_empty());
18196        assert!(client.in_flight.lock().is_none());
18197
18198        for order in &orders {
18199            let cache_ref = cache.borrow();
18200            let cached = cache_ref.order(&order.client_order_id()).unwrap();
18201            assert_eq!(cached.status(), OrderStatus::Initialized);
18202        }
18203    }
18204
18205    #[tokio::test]
18206    async fn modify_order_rejects_without_side_effects() {
18207        let (mut client, state, cache) = unsupported_client_with_mock_rpc().await;
18208        let order = test_market_sell_order(test_pool().instrument_id);
18209        let mut receiver = start_with_events(&mut client);
18210
18211        client
18212            .modify_order(modify_order_cmd(
18213                order.instrument_id(),
18214                order.client_order_id(),
18215            ))
18216            .unwrap();
18217
18218        let events = collect_order_events(&mut receiver);
18219        assert_eq!(events.len(), 1, "was: {events:?}");
18220        let OrderEventAny::ModifyRejected(rejected) = &events[0] else {
18221            panic!("expected OrderModifyRejected, was {:?}", events[0]);
18222        };
18223        assert_eq!(rejected.client_order_id, order.client_order_id());
18224        assert_eq!(rejected.reason, ORDER_MODIFY_UNSUPPORTED);
18225        assert!(state.recorded_requests().is_empty());
18226        assert!(client.in_flight.lock().is_none());
18227        let cache_ref = cache.borrow();
18228        let cached = cache_ref.order(&order.client_order_id()).unwrap();
18229        assert_eq!(cached.status(), OrderStatus::Initialized);
18230    }
18231
18232    #[tokio::test]
18233    async fn cancel_order_rejects_without_side_effects() {
18234        let (mut client, state, cache) = unsupported_client_with_mock_rpc().await;
18235        let order = test_market_sell_order(test_pool().instrument_id);
18236        let mut receiver = start_with_events(&mut client);
18237
18238        client
18239            .cancel_order(cancel_order_cmd(
18240                order.instrument_id(),
18241                order.client_order_id(),
18242            ))
18243            .unwrap();
18244
18245        let events = collect_order_events(&mut receiver);
18246        assert_eq!(events.len(), 1, "was: {events:?}");
18247        let OrderEventAny::CancelRejected(rejected) = &events[0] else {
18248            panic!("expected OrderCancelRejected, was {:?}", events[0]);
18249        };
18250        assert_eq!(rejected.client_order_id, order.client_order_id());
18251        assert_eq!(rejected.reason, ORDER_CANCEL_UNSUPPORTED);
18252        assert!(state.recorded_requests().is_empty());
18253        assert!(client.in_flight.lock().is_none());
18254        let cache_ref = cache.borrow();
18255        let cached = cache_ref.order(&order.client_order_id()).unwrap();
18256        assert_eq!(cached.status(), OrderStatus::Initialized);
18257    }
18258
18259    #[tokio::test]
18260    async fn batch_cancel_orders_rejects_each_order_without_side_effects() {
18261        let (mut client, state, cache) = unsupported_client_with_mock_rpc().await;
18262        let pool = test_pool();
18263        let first = test_market_sell_order(pool.instrument_id);
18264        let second = market_sell_order_with_id(pool.instrument_id, "O-SWAP-002");
18265        cache
18266            .borrow_mut()
18267            .add_order(second.clone(), None, None, false)
18268            .unwrap();
18269        let orders = [first.clone(), second.clone()];
18270        let cancels = orders
18271            .iter()
18272            .map(|order| cancel_order_cmd(order.instrument_id(), order.client_order_id()))
18273            .collect();
18274        let mut receiver = start_with_events(&mut client);
18275
18276        client
18277            .batch_cancel_orders(batch_cancel_orders_cmd(cancels))
18278            .unwrap();
18279
18280        let events = collect_order_events(&mut receiver);
18281        let mut rejected_ids = Vec::new();
18282
18283        for event in &events {
18284            let OrderEventAny::CancelRejected(rejected) = event else {
18285                panic!("expected OrderCancelRejected, was {event:?}");
18286            };
18287            assert_eq!(rejected.reason, ORDER_CANCEL_UNSUPPORTED);
18288            rejected_ids.push(rejected.client_order_id);
18289        }
18290        rejected_ids.sort();
18291        assert_eq!(
18292            rejected_ids,
18293            [first.client_order_id(), second.client_order_id()]
18294        );
18295        assert!(state.recorded_requests().is_empty());
18296        assert!(client.in_flight.lock().is_none());
18297    }
18298
18299    #[tokio::test]
18300    async fn cancel_all_and_query_order_log_unsupported_without_side_effects() {
18301        let (mut client, state, cache) = unsupported_client_with_mock_rpc().await;
18302        let order = test_market_sell_order(test_pool().instrument_id);
18303        let mut receiver = start_with_events(&mut client);
18304
18305        client
18306            .cancel_all_orders(cancel_all_orders_cmd(order.instrument_id()))
18307            .unwrap();
18308        client
18309            .query_order(query_order_cmd(
18310                order.instrument_id(),
18311                order.client_order_id(),
18312            ))
18313            .unwrap();
18314
18315        let events = collect_order_events(&mut receiver);
18316        assert!(events.is_empty(), "was: {events:?}");
18317        assert!(state.recorded_requests().is_empty());
18318        assert!(client.in_flight.lock().is_none());
18319        let cache_ref = cache.borrow();
18320        let cached = cache_ref.order(&order.client_order_id()).unwrap();
18321        assert_eq!(cached.status(), OrderStatus::Initialized);
18322    }
18323
18324    #[tokio::test]
18325    async fn unsupported_commands_handle_unknown_orders_without_panic() {
18326        let (mut client, state, _) = unsupported_client_with_mock_rpc().await;
18327        let instrument_id = test_pool().instrument_id;
18328        let unknown = ClientOrderId::from("O-UNKNOWN");
18329        let mut receiver = start_with_events(&mut client);
18330
18331        client
18332            .modify_order(modify_order_cmd(instrument_id, unknown))
18333            .unwrap();
18334        client
18335            .cancel_order(cancel_order_cmd(instrument_id, unknown))
18336            .unwrap();
18337        client
18338            .batch_cancel_orders(batch_cancel_orders_cmd(vec![cancel_order_cmd(
18339                instrument_id,
18340                unknown,
18341            )]))
18342            .unwrap();
18343        client
18344            .query_order(query_order_cmd(instrument_id, unknown))
18345            .unwrap();
18346
18347        let events = collect_order_events(&mut receiver);
18348        assert!(events.is_empty(), "was: {events:?}");
18349        assert!(state.recorded_requests().is_empty());
18350        assert!(client.in_flight.lock().is_none());
18351    }
18352
18353    #[tokio::test]
18354    async fn report_generators_error_except_mass_status_without_side_effects() {
18355        let (client, state, _) = unsupported_client_with_mock_rpc().await;
18356
18357        let report = client
18358            .generate_order_status_report(&GenerateOrderStatusReport::new(
18359                UUID4::new(),
18360                UnixNanos::default(),
18361                None,
18362                None,
18363                None,
18364                None,
18365                None,
18366            ))
18367            .await
18368            .unwrap_err();
18369        assert_eq!(report.to_string(), VENUE_EXECUTION_REPORTS_UNSUPPORTED);
18370
18371        let reports = client
18372            .generate_order_status_reports(&GenerateOrderStatusReports::new(
18373                UUID4::new(),
18374                UnixNanos::default(),
18375                false,
18376                None,
18377                None,
18378                None,
18379                None,
18380                None,
18381            ))
18382            .await
18383            .unwrap_err();
18384        assert_eq!(reports.to_string(), VENUE_EXECUTION_REPORTS_UNSUPPORTED);
18385
18386        let fills = client
18387            .generate_fill_reports(GenerateFillReports::new(
18388                UUID4::new(),
18389                UnixNanos::default(),
18390                None,
18391                None,
18392                None,
18393                None,
18394                None,
18395                None,
18396            ))
18397            .await
18398            .unwrap_err();
18399        assert_eq!(fills.to_string(), VENUE_EXECUTION_REPORTS_UNSUPPORTED);
18400
18401        let positions = client
18402            .generate_position_status_reports(&GeneratePositionStatusReports::new(
18403                UUID4::new(),
18404                UnixNanos::default(),
18405                None,
18406                None,
18407                None,
18408                None,
18409                None,
18410            ))
18411            .await
18412            .unwrap_err();
18413        assert_eq!(positions.to_string(), VENUE_EXECUTION_REPORTS_UNSUPPORTED);
18414
18415        let mass_status = client.generate_mass_status(None).await.unwrap();
18416        assert!(mass_status.is_none());
18417
18418        let mass_status = client.generate_mass_status(Some(60)).await.unwrap();
18419        assert!(mass_status.is_none());
18420
18421        assert!(state.recorded_requests().is_empty());
18422        assert!(client.in_flight.lock().is_none());
18423    }
18424
18425    async fn connect_test_postgres(
18426        test_name: &str,
18427    ) -> Option<(sqlx::PgPool, PostgresConnectOptions)> {
18428        let pg_config = get_postgres_connect_options(None, None, None, None, None);
18429        let admin_options: sqlx::postgres::PgConnectOptions = pg_config.clone().into();
18430        let admin_pool = match PgPoolOptions::new()
18431            .max_connections(1)
18432            .connect_with(admin_options)
18433            .await
18434        {
18435            Ok(pool) => pool,
18436            Err(e) => {
18437                eprintln!("Postgres unavailable; skipping {test_name} test: {e}");
18438                return None;
18439            }
18440        };
18441
18442        Some((admin_pool, pg_config))
18443    }
18444
18445    async fn execution_client_with_database(
18446        test_name: &str,
18447        state: MockRpcState,
18448    ) -> Option<(
18449        sqlx::PgPool,
18450        String,
18451        BlockchainExecutionClient,
18452        MockRpcState,
18453    )> {
18454        let (admin_pool, schema, mut client, state) =
18455            execution_client_with_unprotected_database(test_name, state).await?;
18456        protect_test_storage(&mut client, &schema).await;
18457        Some((admin_pool, schema, client, state))
18458    }
18459
18460    async fn execution_client_with_unprotected_database(
18461        test_name: &str,
18462        state: MockRpcState,
18463    ) -> Option<(
18464        sqlx::PgPool,
18465        String,
18466        BlockchainExecutionClient,
18467        MockRpcState,
18468    )> {
18469        let (admin_pool, pg_config) = connect_test_postgres(test_name).await?;
18470        let schema = format!("{test_name}_{}", std::process::id());
18471        setup_execution_schema(&admin_pool, &schema).await;
18472
18473        let db_options: sqlx::postgres::PgConnectOptions = pg_config.into();
18474        let db_options = db_options.options([("search_path", schema.clone())]);
18475        let database = connect_test_database(db_options).await.unwrap();
18476        let addr = start_mock_rpc_server(state.clone()).await;
18477        let mut client = test_client(format!("http://{addr}"));
18478        client.cache.database = Some(database);
18479        // Mirror the connect-time migration: tests create the pre-submission table shape
18480        client
18481            .cache
18482            .ensure_execution_transaction_schema()
18483            .await
18484            .unwrap();
18485        initialize_test_verification_ledger(&client).await;
18486        client.signer = Some(Arc::new(
18487            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
18488        ));
18489        client.core.set_connected();
18490
18491        Some((admin_pool, schema, client, state))
18492    }
18493
18494    async fn install_reservation_commit_rejection(admin_pool: &sqlx::PgPool, schema: &str) {
18495        for statement in [
18496            format!(
18497                "CREATE FUNCTION {schema}.reject_execution_reservation_commit() RETURNS trigger \
18498                 LANGUAGE plpgsql AS 'BEGIN RAISE EXCEPTION ''test reservation commit rejection''; \
18499                 RETURN NEW; END'"
18500            ),
18501            format!(
18502                "CREATE CONSTRAINT TRIGGER reject_execution_reservation_commit AFTER INSERT ON \
18503                 {schema}.execution_transaction_transition DEFERRABLE INITIALLY DEFERRED \
18504                 FOR EACH ROW EXECUTE FUNCTION {schema}.reject_execution_reservation_commit()"
18505            ),
18506        ] {
18507            sqlx::query(sqlx::AssertSqlSafe(statement))
18508                .execute(admin_pool)
18509                .await
18510                .unwrap();
18511        }
18512    }
18513
18514    async fn install_recoverable_commit_rejection(admin_pool: &sqlx::PgPool, schema: &str) {
18515        for statement in [
18516            format!(
18517                "CREATE FUNCTION {schema}.reject_recoverable_commit() RETURNS trigger \
18518                 LANGUAGE plpgsql AS 'BEGIN IF NEW.transition_key = ''recoverable'' THEN \
18519                 RAISE EXCEPTION ''test recoverable commit rejection''; END IF; \
18520                 RETURN NEW; END'"
18521            ),
18522            format!(
18523                "CREATE CONSTRAINT TRIGGER reject_recoverable_commit AFTER INSERT ON \
18524                 {schema}.execution_transaction_transition DEFERRABLE INITIALLY DEFERRED \
18525                 FOR EACH ROW EXECUTE FUNCTION {schema}.reject_recoverable_commit()"
18526            ),
18527        ] {
18528            sqlx::query(sqlx::AssertSqlSafe(statement))
18529                .execute(admin_pool)
18530                .await
18531                .unwrap();
18532        }
18533    }
18534
18535    async fn drop_execution_schema(admin_pool: &sqlx::PgPool, schema: &str) {
18536        sqlx::query(sqlx::AssertSqlSafe(format!("DROP SCHEMA {schema} CASCADE")))
18537            .execute(admin_pool)
18538            .await
18539            .unwrap();
18540    }
18541
18542    async fn setup_execution_schema(admin_pool: &sqlx::PgPool, schema: &str) {
18543        for statement in [
18544            format!("CREATE SCHEMA {schema}"),
18545            format!(
18546                r#"CREATE TABLE {schema}."chain" (chain_id INTEGER PRIMARY KEY, name TEXT NOT NULL)"#
18547            ),
18548            format!(r#"INSERT INTO {schema}."chain" (chain_id, name) VALUES (42161, 'Arbitrum')"#),
18549            format!(
18550                r#"CREATE TABLE {schema}."execution_transaction" (
18551                    id BIGSERIAL PRIMARY KEY,
18552                    chain_id INTEGER NOT NULL REFERENCES {schema}."chain"(chain_id) ON DELETE CASCADE,
18553                    nonce BIGINT NOT NULL,
18554                    transaction_hash TEXT NOT NULL,
18555                    purpose TEXT NOT NULL,
18556                    status TEXT NOT NULL,
18557                    UNIQUE (chain_id, transaction_hash)
18558                )"#
18559            ),
18560        ] {
18561            sqlx::query(sqlx::AssertSqlSafe(statement))
18562                .execute(admin_pool)
18563                .await
18564                .unwrap();
18565        }
18566    }
18567
18568    fn execution_transaction_create_sql() -> &'static str {
18569        const TABLES_SQL: &str = include_str!("../../../../../schema/sql/tables.sql");
18570        const START: &str = "CREATE TABLE IF NOT EXISTS \"execution_transaction\"";
18571        let start = TABLES_SQL
18572            .find(START)
18573            .expect("execution_transaction table is missing from tables.sql");
18574        let statement = &TABLES_SQL[start..];
18575        let end = statement
18576            .find(";\n")
18577            .expect("execution_transaction CREATE TABLE is unterminated")
18578            + 1;
18579        &statement[..end]
18580    }
18581}