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(chain.clone());
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            http_rpc_client.clone(),
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            http_rpc_client.clone(),
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                self.chain.clone(),
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 for inconsistent storage state, missing keys, or any payload which cannot
853    /// be 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: self.http_rpc_client.clone(),
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.
3711async fn execute_swap(
3712    mut plan: SwapPlan,
3713    executor: TransactionExecutor,
3714    emitter: ExecutionEventEmitter,
3715    max_quote_age_blocks: u64,
3716    deadline_seconds: u64,
3717) -> anyhow::Result<()> {
3718    let order = &plan.order;
3719
3720    if let Err(e) = executor.claim_slot(TransactionPurpose::Swap) {
3721        emitter.emit_order_denied(order, &e.to_string());
3722        return Ok(());
3723    }
3724
3725    let Some(profiler_position) = plan.profiler_position.as_ref() else {
3726        release_preparing_slot(&executor.in_flight);
3727        emitter.emit_order_denied(order, "Pool profiler has no quote provenance");
3728        return Ok(());
3729    };
3730    let mut swap_anchors = match validate_swap_quote(
3731        profiler_position,
3732        &plan,
3733        max_quote_age_blocks,
3734        &executor.verification,
3735        &executor.deployment_manifest,
3736    )
3737    .await
3738    {
3739        Ok(anchors) => anchors,
3740        Err(e) => {
3741            release_preparing_slot(&executor.in_flight);
3742            emitter.emit_order_denied(order, &e.to_string());
3743            return Ok(());
3744        }
3745    };
3746    let (amount_in, min_amount_out) = match verified_swap_amounts(&plan, swap_anchors.quote) {
3747        Ok(amounts) => amounts,
3748        Err(e) => {
3749            release_preparing_slot(&executor.in_flight);
3750            emitter.emit_order_denied(order, &e.to_string());
3751            return Ok(());
3752        }
3753    };
3754    plan.amount_in = amount_in;
3755    plan.min_amount_out = min_amount_out;
3756    let deadline = match swap_anchors.state.timestamp.checked_add(deadline_seconds) {
3757        Some(deadline) => deadline,
3758        None => {
3759            release_preparing_slot(&executor.in_flight);
3760            emitter.emit_order_denied(
3761                order,
3762                &format!(
3763                    "Swap deadline overflow: anchor timestamp {} plus `deadline_seconds` {deadline_seconds} exceeds u64",
3764                    swap_anchors.state.timestamp
3765                ),
3766            );
3767            return Ok(());
3768        }
3769    };
3770
3771    swap_anchors.precondition_decisions =
3772        match check_swap_preconditions(&plan, swap_anchors.state.number, &executor).await {
3773            Ok(decisions) => decisions,
3774            Err(e) => {
3775                release_preparing_slot(&executor.in_flight);
3776                emitter.emit_order_denied(order, &e.to_string());
3777                return Ok(());
3778            }
3779        };
3780
3781    let calldata = UniswapV3SwapRouter::exactInputSingleCall {
3782        params: UniswapV3SwapRouter::ExactInputSingleParams {
3783            tokenIn: plan.token_in,
3784            tokenOut: plan.token_out,
3785            fee: plan.fee,
3786            recipient: executor.wallet_address,
3787            deadline: U256::from(deadline),
3788            amountIn: plan.amount_in,
3789            amountOutMinimum: plan.min_amount_out,
3790            sqrtPriceLimitX96: U160::ZERO,
3791        },
3792    }
3793    .abi_encode();
3794
3795    let calldata = Bytes::from(calldata);
3796    let intent = ExecutionIntentInsert {
3797        chain_id: executor.chain_id,
3798        wallet_address: executor.wallet_address.to_string(),
3799        purpose: TransactionPurpose::Swap.as_str().to_string(),
3800        client_order_id: Some(order.client_order_id().to_string()),
3801        trader_id: Some(order.trader_id().to_string()),
3802        strategy_id: Some(order.strategy_id().to_string()),
3803        account_id: Some(executor.account_id.to_string()),
3804        instrument_id: Some(plan.instrument_id.to_string()),
3805        pool_address: Some(plan.pool_address.to_string()),
3806        transaction_to: plan.router.to_string(),
3807        transaction_input: hex::encode_prefixed(&calldata),
3808        transaction_value: U256::ZERO.to_string(),
3809        amount_in: Some(plan.amount_in.to_string()),
3810        created_block: swap_anchors.state.number,
3811    };
3812    let intent = match executor.database.reserve_execution_intent(&intent).await {
3813        Ok(intent) => intent,
3814        Err(e) => {
3815            release_preparing_if_reservation_not_committed(&executor.in_flight, &e);
3816            emitter.emit_order_denied(order, &e.to_string());
3817            return Ok(());
3818        }
3819    };
3820    let prepared = match executor
3821        .prepare_and_sign_swap(
3822            intent.id,
3823            intent.created_block,
3824            plan.router,
3825            U256::ZERO,
3826            calldata,
3827            &swap_anchors,
3828        )
3829        .await
3830    {
3831        Ok(prepared) => prepared,
3832        Err(e) => {
3833            if executor
3834                .database
3835                .mark_execution_intent_recoverable(intent.id)
3836                .await
3837                .is_ok()
3838            {
3839                release_preparing_slot(&executor.in_flight);
3840            }
3841            emitter.emit_order_denied(order, &e.to_string());
3842            return Ok(());
3843        }
3844    };
3845
3846    if let Err(e) = executor
3847        .fill_and_persist(&prepared, TransactionPurpose::Swap)
3848        .await
3849    {
3850        emitter.emit_order_denied(order, &e.to_string());
3851        return Ok(());
3852    }
3853
3854    let broadcast = executor.broadcast(&prepared).await?;
3855    emitter.emit_order_submitted(order);
3856    executor
3857        .database
3858        .mark_execution_event_emitted(intent.id, "acknowledgement")
3859        .await?;
3860
3861    if let BroadcastOutcome::Ambiguous(message) = broadcast {
3862        log::warn!("{message}");
3863    }
3864
3865    let trace_purpose = match plan.order.order_side() {
3866        OrderSide::Sell => "swap_sell",
3867        OrderSide::Buy => "swap_buy",
3868    };
3869
3870    match executor.await_finality(&prepared).await? {
3871        InclusionOutcome::Finalized(mut included) => {
3872            included.finality.decisions.extend(
3873                verify_finalized_transaction(
3874                    &included,
3875                    &intent,
3876                    prepared.nonce,
3877                    &prepared.raw_tx,
3878                    &executor,
3879                    trace_purpose,
3880                )
3881                .await?,
3882            );
3883            let fill = validate_finalized_swap_fill(&plan, &included)?;
3884            let wallet = load_verified_wallet_after_fill(&plan, &included, &executor).await?;
3885            executor
3886                .commit_verified_finality(
3887                    &included,
3888                    TransactionStatus::Finalized,
3889                    &wallet.decisions,
3890                )
3891                .await?;
3892            complete_finalized_swap(
3893                &plan,
3894                intent.id,
3895                included.tx_hash,
3896                fill,
3897                wallet,
3898                &executor,
3899                &emitter,
3900            )
3901            .await?;
3902            executor.release_slot();
3903            Ok(())
3904        }
3905        InclusionOutcome::Reverted(mut included) => {
3906            included.finality.decisions.extend(
3907                verify_finalized_transaction(
3908                    &included,
3909                    &intent,
3910                    prepared.nonce,
3911                    &prepared.raw_tx,
3912                    &executor,
3913                    trace_purpose,
3914                )
3915                .await?,
3916            );
3917            executor
3918                .commit_verified_finality(&included, TransactionStatus::Reverted, &[])
3919                .await?;
3920            send_reverted_order(&emitter, order, &included)?;
3921            executor
3922                .database
3923                .mark_execution_event_emitted(intent.id, "terminal")
3924                .await?;
3925            executor.release_slot();
3926            Ok(())
3927        }
3928        InclusionOutcome::Pending(message) => anyhow::bail!(message),
3929    }
3930}
3931
3932async fn validate_swap_quote(
3933    position: &BlockPosition,
3934    plan: &SwapPlan,
3935    max_age_blocks: u64,
3936    verification: &VerificationCoordinator,
3937    manifest: &BlockchainDeploymentManifest,
3938) -> anyhow::Result<SwapQuoteAnchors> {
3939    let block_hash = position.block_hash.as_deref().ok_or_else(|| {
3940        anyhow::anyhow!(
3941            "Pool state at block {} has no ingestion-time block hash; refresh the profiler before execution",
3942            position.number
3943        )
3944    })?;
3945    let expected_block_hash = B256::from_str(block_hash)
3946        .with_context(|| format!("Invalid profiler block hash {block_hash}"))?;
3947    let now_unix_secs = current_unix_secs()?;
3948    let head = verified_value(
3949        verification.verify_decision_header(now_unix_secs).await,
3950        "swap decision header",
3951    )?;
3952    validate_quote_age(position.number, head.number, max_age_blocks)?;
3953    let canonical_block = verified_value(
3954        verification.verify_block(position.number).await,
3955        "profiler watermark header",
3956    )?;
3957    anyhow::ensure!(
3958        canonical_block.hash == expected_block_hash,
3959        "Pool state block {} changed from {} to {}; refresh the profiler before execution",
3960        position.number,
3961        expected_block_hash,
3962        canonical_block.hash
3963    );
3964
3965    let snapshot_transaction = position.transaction_index == BLOCK_SCOPED_SNAPSHOT_INDEX;
3966    let snapshot_log = position.log_index == BLOCK_SCOPED_SNAPSHOT_INDEX;
3967    anyhow::ensure!(
3968        snapshot_transaction == snapshot_log,
3969        "Pool state at block {} has an invalid partial snapshot watermark",
3970        position.number
3971    );
3972
3973    if snapshot_transaction {
3974        let snapshot_hash = B256::from_str(&position.transaction_hash)
3975            .with_context(|| "Invalid block-scoped snapshot hash")?;
3976        anyhow::ensure!(
3977            snapshot_hash == expected_block_hash,
3978            "Block-scoped snapshot hash {snapshot_hash} does not match ingestion hash {expected_block_hash}"
3979        );
3980    } else {
3981        validate_profiler_event_verified(
3982            position,
3983            expected_block_hash,
3984            plan.pool_address,
3985            &plan.pool,
3986            verification,
3987        )
3988        .await?;
3989    }
3990
3991    let ancestry = verified_value(
3992        verification
3993            .verify_header_window(canonical_block, head.number)
3994            .await,
3995        "profiler-to-decision ancestry",
3996    )?;
3997
3998    if let Some(last) = ancestry.last() {
3999        anyhow::ensure!(
4000            *last == head,
4001            "Swap decision header conflicts with profiler ancestry"
4002        );
4003    } else {
4004        anyhow::ensure!(
4005            canonical_block == head,
4006            "Empty profiler ancestry does not end at the decision header"
4007        );
4008    }
4009    verified_value(
4010        verification
4011            .verify_deployment_manifest(manifest, head.number)
4012            .await,
4013        "swap deployment manifest",
4014    )?;
4015    let quote_contract = validate_manifest_pool(plan, manifest)?;
4016    let quote_kind = match plan.order.order_side() {
4017        OrderSide::Sell => SwapQuoteKind::ExactInput(quantity_to_raw_amount(
4018            plan.order.quantity(),
4019            plan.pool.get_base_token().decimals,
4020        )?),
4021        OrderSide::Buy => SwapQuoteKind::ExactOutput(quantity_to_raw_amount(
4022            plan.order.quantity(),
4023            plan.pool.get_base_token().decimals,
4024        )?),
4025    };
4026    let quote = verified_value(
4027        verify_swap_quote(verification, quote_contract, plan, quote_kind, head.number).await,
4028        "independent swap quote",
4029    )?;
4030
4031    Ok(SwapQuoteAnchors {
4032        watermark: canonical_block,
4033        state: head,
4034        quote_contract,
4035        token_in: plan.token_in,
4036        token_out: plan.token_out,
4037        fee: plan.fee,
4038        quote_kind,
4039        quote,
4040        precondition_decisions: Vec::new(),
4041    })
4042}
4043
4044async fn verify_swap_quote(
4045    verification: &VerificationCoordinator,
4046    quote_contract: Address,
4047    plan: &SwapPlan,
4048    kind: SwapQuoteKind,
4049    block: u64,
4050) -> VerificationOutcome<UniswapV3Quote> {
4051    match kind {
4052        SwapQuoteKind::ExactInput(amount_in) => {
4053            verification
4054                .verify_quote_exact_input_single(
4055                    &quote_contract,
4056                    plan.token_in,
4057                    plan.token_out,
4058                    amount_in,
4059                    plan.fee,
4060                    block,
4061                )
4062                .await
4063        }
4064        SwapQuoteKind::ExactOutput(amount_out) => {
4065            verification
4066                .verify_quote_exact_output_single(
4067                    &quote_contract,
4068                    plan.token_in,
4069                    plan.token_out,
4070                    amount_out,
4071                    plan.fee,
4072                    block,
4073                )
4074                .await
4075        }
4076    }
4077}
4078
4079fn verified_swap_amounts(plan: &SwapPlan, quote: UniswapV3Quote) -> anyhow::Result<(U256, U256)> {
4080    anyhow::ensure!(
4081        !quote.amount.is_zero(),
4082        "Independent swap quote returned zero"
4083    );
4084    let base_amount =
4085        quantity_to_raw_amount(plan.order.quantity(), plan.pool.get_base_token().decimals)?;
4086    let slippage_bps = plan.slippage_bps;
4087    match plan.order.order_side() {
4088        OrderSide::Sell => Ok((
4089            base_amount,
4090            derive_min_amount_out(quote.amount, slippage_bps)?,
4091        )),
4092        OrderSide::Buy => {
4093            let ceiling = plan.quote_spend_ceiling.ok_or_else(|| {
4094                anyhow::anyhow!(
4095                    "No quote spend ceiling for BUY token pair {} -> {}",
4096                    plan.token_in,
4097                    plan.token_out
4098                )
4099            })?;
4100            anyhow::ensure!(
4101                quote.amount <= ceiling.max_amount,
4102                "BUY quote amount {} exceeds the configured quote-spend maximum {} for {} -> {}",
4103                quote.amount,
4104                ceiling.max_amount,
4105                plan.token_in,
4106                plan.token_out
4107            );
4108            Ok((
4109                quote.amount,
4110                derive_min_amount_out(base_amount, slippage_bps)?,
4111            ))
4112        }
4113    }
4114}
4115
4116fn validate_manifest_pool(
4117    plan: &SwapPlan,
4118    manifest: &BlockchainDeploymentManifest,
4119) -> anyhow::Result<Address> {
4120    let matching = manifest
4121        .pools
4122        .iter()
4123        .filter(|pool| Address::from_str(&pool.address).ok() == Some(plan.pool_address))
4124        .collect::<Vec<_>>();
4125    anyhow::ensure!(
4126        matching.len() == 1,
4127        "Pool {} does not have exactly one deployment manifest definition",
4128        plan.pool_address
4129    );
4130    let pool = matching[0];
4131    let token0 = Address::from_str(&pool.token0)?;
4132    let token1 = Address::from_str(&pool.token1)?;
4133    let factory = Address::from_str(&pool.factory)?;
4134    let quote_contract = Address::from_str(&pool.quote_contract)?;
4135    anyhow::ensure!(
4136        token0 == plan.pool.token0.address
4137            && token1 == plan.pool.token1.address
4138            && pool.fee == plan.pool.fee.expect("validated pool fee")
4139            && factory == plan.factory,
4140        "Cached pool {} does not match its deployment manifest identity",
4141        plan.pool_address
4142    );
4143
4144    for token in [&plan.pool.token0, &plan.pool.token1] {
4145        let identities = manifest
4146            .tokens
4147            .iter()
4148            .filter(|identity| Address::from_str(&identity.address).ok() == Some(token.address))
4149            .collect::<Vec<_>>();
4150        anyhow::ensure!(
4151            identities.len() == 1,
4152            "Token {} does not have exactly one deployment manifest identity",
4153            token.address
4154        );
4155        let identity = identities[0];
4156        anyhow::ensure!(
4157            identity.name == token.name
4158                && identity.symbol == token.symbol
4159                && identity.decimals == token.decimals,
4160            "Cached token {} does not match its deployment manifest identity",
4161            token.address
4162        );
4163        let expected_role = if token.address == plan.pool.get_base_token().address {
4164            "base"
4165        } else {
4166            "quote"
4167        };
4168        anyhow::ensure!(
4169            matches!(identity.asset_role.as_str(), "both") || identity.asset_role == expected_role,
4170            "Token {} is not permitted as the pool {expected_role} asset",
4171            token.address
4172        );
4173    }
4174    Ok(quote_contract)
4175}
4176
4177async fn validate_profiler_event_verified(
4178    position: &BlockPosition,
4179    expected_block_hash: B256,
4180    pool_address: Address,
4181    pool: &Pool,
4182    verification: &VerificationCoordinator,
4183) -> anyhow::Result<()> {
4184    let transaction_hash = B256::from_str(&position.transaction_hash).with_context(|| {
4185        format!(
4186            "Invalid profiler transaction hash {}",
4187            position.transaction_hash
4188        )
4189    })?;
4190    let receipt = verified_value(
4191        verification.verify_receipt(&transaction_hash).await,
4192        "profiler watermark receipt",
4193    )?;
4194    anyhow::ensure!(
4195        receipt.status,
4196        "Profiler transaction did not execute successfully"
4197    );
4198    anyhow::ensure!(
4199        receipt.transaction_hash == transaction_hash,
4200        "Profiler receipt transaction hash does not match its ingestion watermark"
4201    );
4202    anyhow::ensure!(
4203        receipt.block_number == position.number
4204            && receipt.block_hash == expected_block_hash
4205            && receipt.transaction_index == u64::from(position.transaction_index),
4206        "Profiler receipt position does not match its ingestion watermark"
4207    );
4208    let matching_logs = receipt
4209        .logs
4210        .iter()
4211        .filter(|log| rpc_log::extract_log_index(log).ok() == Some(position.log_index))
4212        .collect::<Vec<_>>();
4213    anyhow::ensure!(
4214        matching_logs.len() == 1,
4215        "Profiler receipt contains {} logs at global index {}; expected exactly one",
4216        matching_logs.len(),
4217        position.log_index
4218    );
4219    let log = matching_logs[0];
4220    let log_transaction_hash = B256::from_str(&rpc_log::extract_transaction_hash(log)?)
4221        .with_context(|| "Invalid profiler log transaction hash")?;
4222    let log_block_hash = log
4223        .block_hash
4224        .as_deref()
4225        .ok_or_else(|| anyhow::anyhow!("Profiler log has no block hash"))?;
4226    anyhow::ensure!(
4227        !log.removed
4228            && log_transaction_hash == transaction_hash
4229            && rpc_log::extract_block_number(log)? == position.number
4230            && rpc_log::extract_transaction_index(log)? == position.transaction_index
4231            && B256::from_str(log_block_hash)? == expected_block_hash,
4232        "Profiler log position does not match its ingestion watermark"
4233    );
4234    anyhow::ensure!(
4235        rpc_log::extract_address(log)? == pool_address,
4236        "Profiler watermark log did not come from expected pool {pool_address}"
4237    );
4238    let signature = log
4239        .topics
4240        .first()
4241        .ok_or_else(|| anyhow::anyhow!("Profiler watermark log has no event signature"))?;
4242    let supported =
4243        profiler_event_signatures(pool).any(|expected| expected.eq_ignore_ascii_case(signature));
4244    anyhow::ensure!(
4245        supported,
4246        "Profiler watermark log has an unsupported event signature"
4247    );
4248    Ok(())
4249}
4250
4251fn profiler_event_signatures(pool: &Pool) -> impl Iterator<Item = &str> {
4252    [
4253        Some(pool.dex.swap_created_event.as_ref()),
4254        Some(pool.dex.mint_created_event.as_ref()),
4255        Some(pool.dex.burn_created_event.as_ref()),
4256        Some(pool.dex.collect_created_event.as_ref()),
4257        pool.dex.flash_created_event.as_deref(),
4258        pool.dex.fee_protocol_update_event.as_deref(),
4259        pool.dex.fee_protocol_collect_event.as_deref(),
4260    ]
4261    .into_iter()
4262    .flatten()
4263}
4264
4265fn validate_quote_age(
4266    profiler_block: u64,
4267    latest_block: u64,
4268    max_age_blocks: u64,
4269) -> anyhow::Result<()> {
4270    anyhow::ensure!(
4271        profiler_block <= latest_block,
4272        "Pool state at block {profiler_block} is ahead of the latest block {latest_block}; the execution RPC endpoint lags the data feed"
4273    );
4274    let quote_age = latest_block - profiler_block;
4275    anyhow::ensure!(
4276        quote_age <= max_age_blocks,
4277        "Stale quote: pool state at block {profiler_block}, latest block {latest_block}, exceeds `max_quote_age_blocks` {max_age_blocks}"
4278    );
4279    Ok(())
4280}
4281
4282fn validate_rpc_transaction_matches_payload(
4283    transaction: &RpcTransaction,
4284    raw_transaction: &[u8],
4285) -> anyhow::Result<()> {
4286    let signed = decode_signed_transaction(raw_transaction)?;
4287    anyhow::ensure!(
4288        transaction.hash == signed.hash
4289            && transaction.from == signed.signer
4290            && transaction.nonce == signed.nonce
4291            && transaction.chain_id == Some(signed.chain_id)
4292            && transaction.transaction_type == Some(2)
4293            && transaction.to == Some(signed.to)
4294            && transaction.input == signed.input
4295            && transaction.value == signed.value
4296            && transaction.gas == Some(signed.gas_limit)
4297            && transaction.max_fee_per_gas == Some(U256::from(signed.max_fee_per_gas))
4298            && transaction.max_priority_fee_per_gas
4299                == Some(U256::from(signed.max_priority_fee_per_gas)),
4300        "Verified transaction fields differ from the authenticated signed payload"
4301    );
4302    Ok(())
4303}
4304
4305async fn verify_finalized_transaction(
4306    included: &IncludedTransaction,
4307    intent: &ExecutionIntentRow,
4308    nonce: u64,
4309    raw_transaction: &[u8],
4310    executor: &TransactionExecutor,
4311    trace_purpose: &str,
4312) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
4313    verify_finalized_transaction_identity(
4314        included,
4315        intent,
4316        nonce,
4317        raw_transaction,
4318        &executor.verification,
4319        executor.wallet_address,
4320        executor.chain_id,
4321        &executor.deployment_manifest,
4322        trace_purpose,
4323    )
4324    .await
4325}
4326
4327#[expect(clippy::too_many_arguments)]
4328async fn verify_finalized_transaction_identity(
4329    included: &IncludedTransaction,
4330    intent: &ExecutionIntentRow,
4331    nonce: u64,
4332    raw_transaction: &[u8],
4333    verification: &VerificationCoordinator,
4334    wallet_address: Address,
4335    chain_id: u32,
4336    deployment_manifest: &BlockchainDeploymentManifest,
4337    trace_purpose: &str,
4338) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
4339    let transaction_verification = required_verification(
4340        verification.verify_transaction(&included.tx_hash).await,
4341        "finalized transaction",
4342    )?;
4343    let transaction = &transaction_verification.value;
4344    let signed = decode_signed_transaction(raw_transaction)?;
4345    let (expected_to, expected_input, expected_value) = persisted_call_fields(intent)?;
4346    anyhow::ensure!(
4347        included.receipt.transaction_hash == included.tx_hash
4348            && signed.hash == included.tx_hash
4349            && signed.signer == wallet_address
4350            && signed.chain_id == u64::from(chain_id)
4351            && signed.nonce == nonce
4352            && signed.to == expected_to
4353            && signed.input == expected_input
4354            && signed.value == expected_value,
4355        "Finalized transaction does not match the authenticated signed payload and persisted intent"
4356    );
4357    validate_rpc_transaction_matches_payload(transaction, raw_transaction)
4358        .context("finalized transaction identity mismatch")?;
4359
4360    let trace_verification = required_verification(
4361        verification.verify_call_trace(&included.tx_hash).await,
4362        "finalized call trace",
4363    )?;
4364    validate_call_trace(
4365        &trace_verification.value,
4366        &signed,
4367        included.receipt.status,
4368        trace_purpose,
4369        deployment_manifest,
4370    )?;
4371    let deployment_verification = required_verification(
4372        verification
4373            .verify_deployment_manifest(deployment_manifest, included.block_number)
4374            .await,
4375        "inclusion deployment manifest",
4376    )?;
4377
4378    Ok(vec![
4379        verification_decision(
4380            &transaction_verification,
4381            Some(included.block_number),
4382            Some(included.block_number),
4383        ),
4384        verification_decision(
4385            &trace_verification,
4386            Some(included.block_number),
4387            Some(included.block_number),
4388        ),
4389        verification_decision(
4390            &deployment_verification,
4391            Some(included.block_number),
4392            Some(included.block_number),
4393        ),
4394    ])
4395}
4396
4397fn validate_call_trace(
4398    trace: &VerifiedCallTrace,
4399    signed: &crate::execution::transaction::DecodedSignedTransaction,
4400    receipt_success: bool,
4401    purpose: &str,
4402    manifest: &BlockchainDeploymentManifest,
4403) -> anyhow::Result<()> {
4404    anyhow::ensure!(
4405        trace.call_type == RpcCallType::Call
4406            && trace.from == signed.signer
4407            && trace.to == Some(signed.to)
4408            && trace.value == signed.value
4409            && trace.input_digest == keccak256(&signed.input)
4410            && trace.success == receipt_success,
4411        "Verified call-trace root differs from the authenticated transaction"
4412    );
4413    validate_internal_calls(&trace.calls, signed.to, purpose, manifest)
4414}
4415
4416fn validate_internal_calls(
4417    calls: &[VerifiedCallTrace],
4418    caller_context: Address,
4419    purpose: &str,
4420    manifest: &BlockchainDeploymentManifest,
4421) -> anyhow::Result<()> {
4422    for call in calls {
4423        anyhow::ensure!(
4424            call.from == caller_context,
4425            "Verified call trace child has an invalid caller context"
4426        );
4427        let target = call.to.ok_or_else(|| {
4428            anyhow::anyhow!("Verified call trace contains an operation without a target")
4429        })?;
4430        let call_type = match call.call_type {
4431            RpcCallType::Call => "call",
4432            RpcCallType::Callcode => "callcode",
4433            RpcCallType::Delegatecall => "delegatecall",
4434            RpcCallType::Staticcall => "staticcall",
4435            RpcCallType::Create | RpcCallType::Create2 | RpcCallType::Selfdestruct => {
4436                anyhow::bail!("Verified call trace contains a forbidden state-changing operation")
4437            }
4438        };
4439        let permitted = manifest.call_edges.iter().any(|edge| {
4440            edge.purpose == purpose
4441                && edge.call_type.eq_ignore_ascii_case(call_type)
4442                && Address::from_str(&edge.caller).ok() == Some(call.from)
4443                && Address::from_str(&edge.target).ok() == Some(target)
4444        });
4445        anyhow::ensure!(
4446            permitted,
4447            "Verified call trace contains an unreviewed {call_type} edge {} -> {target} for {purpose}",
4448            call.from
4449        );
4450        let child_context = match call.call_type {
4451            RpcCallType::Call | RpcCallType::Staticcall => target,
4452            RpcCallType::Callcode | RpcCallType::Delegatecall => caller_context,
4453            RpcCallType::Create | RpcCallType::Create2 | RpcCallType::Selfdestruct => {
4454                unreachable!("forbidden operations return before child traversal")
4455            }
4456        };
4457        validate_internal_calls(&call.calls, child_context, purpose, manifest)?;
4458    }
4459    Ok(())
4460}
4461
4462async fn verify_wrap_balance_increase(
4463    executor: &TransactionExecutor,
4464    weth_address: &Address,
4465    amount_wei: U256,
4466    included: &IncludedTransaction,
4467) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
4468    let previous_block = included.block_number.checked_sub(1).ok_or_else(|| {
4469        anyhow::anyhow!(
4470            "Included wrap transaction {} has invalid block number 0",
4471            included.tx_hash
4472        )
4473    })?;
4474    let call = ERC20::balanceOfCall {
4475        account: executor.wallet_address,
4476    }
4477    .abi_encode();
4478    let balance_before = required_verification(
4479        executor
4480            .verification
4481            .verify_decoded_call(
4482                None,
4483                weth_address,
4484                U256::ZERO,
4485                &call,
4486                previous_block,
4487                |result| ERC20::balanceOfCall::abi_decode_returns(result).map_err(Into::into),
4488            )
4489            .await,
4490        "wrapped balance before finality",
4491    )
4492    .with_context(|| {
4493        format!(
4494            "failed to verify WETH balance before included transaction {} at block {previous_block}",
4495            included.tx_hash
4496        )
4497    })?;
4498    let balance_after = required_verification(
4499        executor
4500            .verification
4501            .verify_decoded_call(
4502                None,
4503                weth_address,
4504                U256::ZERO,
4505                &call,
4506                included.block_number,
4507                |result| ERC20::balanceOfCall::abi_decode_returns(result).map_err(Into::into),
4508            )
4509            .await,
4510        "wrapped balance after finality",
4511    )
4512    .with_context(|| {
4513        format!(
4514            "failed to verify WETH balance after included transaction {} at block {}",
4515            included.tx_hash, included.block_number
4516        )
4517    })?;
4518    let expected_balance = balance_before
4519        .value
4520        .checked_add(amount_wei)
4521        .ok_or_else(|| {
4522            anyhow::anyhow!(
4523                "WETH balance overflow for included transaction {} at block {}",
4524                included.tx_hash,
4525                included.block_number
4526            )
4527        })?;
4528    anyhow::ensure!(
4529        balance_after.value == expected_balance,
4530        "WETH balance after transaction {} did not increase by {amount_wei}: expected {expected_balance}, was {}",
4531        included.tx_hash,
4532        balance_after.value
4533    );
4534
4535    Ok(vec![
4536        verification_decision(&balance_before, Some(previous_block), Some(previous_block)),
4537        verification_decision(
4538            &balance_after,
4539            Some(included.block_number),
4540            Some(included.block_number),
4541        ),
4542    ])
4543}
4544
4545async fn verify_approve_allowance(
4546    executor: &TransactionExecutor,
4547    token: &Address,
4548    router: &Address,
4549    amount: U256,
4550    included: &IncludedTransaction,
4551) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
4552    let call = ERC20::allowanceCall {
4553        owner: executor.wallet_address,
4554        spender: *router,
4555    }
4556    .abi_encode();
4557    let allowance = required_verification(
4558        executor
4559            .verification
4560            .verify_decoded_call(
4561                None,
4562                token,
4563                U256::ZERO,
4564                &call,
4565                included.block_number,
4566                |result| ERC20::allowanceCall::abi_decode_returns(result).map_err(Into::into),
4567            )
4568            .await,
4569        "router allowance after finality",
4570    )
4571    .with_context(|| {
4572        format!(
4573            "failed to verify router allowance after included transaction {} at block {}",
4574            included.tx_hash, included.block_number
4575        )
4576    })?;
4577    anyhow::ensure!(
4578        allowance.value == amount,
4579        "Router allowance after transaction {} does not equal the requested amount {amount}: was {}",
4580        included.tx_hash,
4581        allowance.value
4582    );
4583
4584    Ok(vec![verification_decision(
4585        &allowance,
4586        Some(included.block_number),
4587        Some(included.block_number),
4588    )])
4589}
4590
4591async fn complete_finalized_swap(
4592    plan: &SwapPlan,
4593    intent_id: i64,
4594    tx_hash: B256,
4595    fill: Option<FinalizedSwapFill>,
4596    wallet: VerifiedWalletRefresh,
4597    executor: &TransactionExecutor,
4598    emitter: &ExecutionEventEmitter,
4599) -> anyhow::Result<()> {
4600    if let Some(fill) = fill {
4601        let filled = OrderFilled::new(
4602            emitter.trader_id(),
4603            plan.order.strategy_id(),
4604            plan.order.instrument_id(),
4605            plan.order.client_order_id(),
4606            fill.venue_order_id,
4607            emitter.account_id(),
4608            fill.trade_id,
4609            plan.order.order_side(),
4610            plan.order.order_type(),
4611            fill.last_qty,
4612            fill.last_px,
4613            plan.quote_currency,
4614            LiquiditySide::Taker,
4615            execution_event_id(tx_hash, b"fill"),
4616            fill.ts_event,
4617            fill.ts_event,
4618            false,
4619            None,
4620            Some(fill.commission),
4621            None,
4622        );
4623        emitter.try_send_order_event(OrderEventAny::Filled(filled))?;
4624
4625        if fill.last_qty < plan.order.quantity() {
4626            let canceled = OrderCanceled::new(
4627                emitter.trader_id(),
4628                plan.order.strategy_id(),
4629                plan.order.instrument_id(),
4630                plan.order.client_order_id(),
4631                execution_event_id(tx_hash, b"partial_cancel"),
4632                fill.ts_event,
4633                fill.ts_event,
4634                false,
4635                Some(fill.venue_order_id),
4636                Some(emitter.account_id()),
4637                None,
4638            );
4639            emitter.try_send_order_event(OrderEventAny::Canceled(canceled))?;
4640        }
4641    }
4642
4643    *executor.wallet_balance.lock() = wallet.wallet_balance;
4644    emitter.try_emit_account_state(
4645        wallet.balances,
4646        vec![],
4647        true,
4648        get_atomic_clock_realtime().get_time_ns(),
4649        None,
4650    )?;
4651    executor
4652        .database
4653        .mark_execution_event_emitted(intent_id, "fill")
4654        .await
4655}
4656
4657fn send_reverted_order(
4658    emitter: &ExecutionEventEmitter,
4659    order: &OrderAny,
4660    included: &IncludedTransaction,
4661) -> anyhow::Result<()> {
4662    let ts_event = finalized_inclusion_time(included)?;
4663    let rejected = OrderRejected::new(
4664        emitter.trader_id(),
4665        order.strategy_id(),
4666        order.instrument_id(),
4667        order.client_order_id(),
4668        emitter.account_id(),
4669        format!("Transaction {} reverted on-chain", included.tx_hash).into(),
4670        execution_event_id(included.tx_hash, b"reverted"),
4671        ts_event,
4672        ts_event,
4673        false,
4674        false,
4675    );
4676    emitter.try_send_order_event(OrderEventAny::Rejected(rejected))
4677}
4678
4679fn finalized_inclusion_time(included: &IncludedTransaction) -> anyhow::Result<UnixNanos> {
4680    let inclusion = &included.finality.inclusion_header;
4681    anyhow::ensure!(
4682        inclusion.number == included.block_number
4683            && inclusion.hash == included.receipt.block_hash.to_string(),
4684        "Verified inclusion header does not match the finalized receipt"
4685    );
4686    let timestamp = inclusion.timestamp;
4687    let nanos = timestamp
4688        .checked_mul(NANOSECONDS_IN_SECOND)
4689        .ok_or_else(|| anyhow::anyhow!("Verified inclusion timestamp exceeds nanoseconds"))?;
4690    Ok(UnixNanos::from(nanos))
4691}
4692
4693fn execution_event_id(tx_hash: B256, event: &[u8]) -> UUID4 {
4694    let mut identity = Vec::with_capacity(tx_hash.len() + event.len());
4695    identity.extend_from_slice(tx_hash.as_slice());
4696    identity.extend_from_slice(event);
4697    let digest = keccak256(identity);
4698    let mut bytes = [0u8; 16];
4699    bytes.copy_from_slice(&digest[..16]);
4700    UUID4::from_bytes(bytes)
4701}
4702
4703struct FinalizedSwapFill {
4704    venue_order_id: VenueOrderId,
4705    trade_id: TradeId,
4706    last_qty: Quantity,
4707    last_px: Price,
4708    commission: Money,
4709    ts_event: UnixNanos,
4710}
4711
4712struct VerifiedWalletRefresh {
4713    wallet_balance: WalletBalance,
4714    balances: Vec<AccountBalance>,
4715    decisions: Vec<ExecutionVerificationDecision>,
4716}
4717
4718fn validate_finalized_swap_fill(
4719    plan: &SwapPlan,
4720    included: &IncludedTransaction,
4721) -> anyhow::Result<Option<FinalizedSwapFill>> {
4722    let signature =
4723        keccak256("Swap(address,address,int256,int256,uint160,uint128,int24)").to_string();
4724    let swap_logs = included
4725        .receipt
4726        .logs
4727        .iter()
4728        .filter(|log| {
4729            !log.removed
4730                && log.topics.first().is_some_and(|topic| topic == &signature)
4731                && Address::from_str(&log.address).ok() == Some(plan.pool_address)
4732        })
4733        .collect::<Vec<_>>();
4734    anyhow::ensure!(
4735        swap_logs.len() == 1,
4736        "Finalized transaction {} emitted {} Swap logs from expected pool {}; expected exactly one",
4737        included.tx_hash,
4738        swap_logs.len(),
4739        plan.pool_address
4740    );
4741    let log = swap_logs[0];
4742    let log_transaction_hash = B256::from_str(&rpc_log::extract_transaction_hash(log)?)
4743        .with_context(|| "Invalid finalized Swap log transaction hash")?;
4744    let log_block_hash = log
4745        .block_hash
4746        .as_deref()
4747        .ok_or_else(|| anyhow::anyhow!("Finalized Swap log has no block hash"))?;
4748    anyhow::ensure!(
4749        log_transaction_hash == included.tx_hash
4750            && rpc_log::extract_block_number(log)? == included.block_number
4751            && u64::from(rpc_log::extract_transaction_index(log)?)
4752                == included.receipt.transaction_index
4753            && B256::from_str(log_block_hash)
4754                .with_context(|| "Invalid finalized Swap log block hash")?
4755                == included.receipt.block_hash,
4756        "Finalized Swap log position does not match transaction {}",
4757        included.tx_hash
4758    );
4759
4760    let dex = crate::exchanges::get_dex_extended(plan.pool.chain.name, &plan.pool.dex.name)
4761        .ok_or_else(|| {
4762            anyhow::anyhow!(
4763                "No RPC Swap decoder for {}:{}",
4764                plan.pool.chain.name,
4765                plan.pool.dex.name
4766            )
4767        })?;
4768    let event = dex.parse_swap_event_rpc(log)?;
4769    let (base_amount, quote_amount) =
4770        if plan.pool.get_base_token().address == plan.pool.token0.address {
4771            (event.amount0, event.amount1)
4772        } else {
4773            (event.amount1, event.amount0)
4774        };
4775    let last_qty = match plan.order.order_side() {
4776        OrderSide::Sell => {
4777            anyhow::ensure!(
4778                base_amount.is_positive() && base_amount.unsigned_abs() == plan.amount_in,
4779                "Finalized Swap input {base_amount} does not match the persisted amount {}",
4780                plan.amount_in
4781            );
4782            plan.order.quantity()
4783        }
4784        OrderSide::Buy => {
4785            anyhow::ensure!(
4786                quote_amount.is_positive() && quote_amount.unsigned_abs() == plan.amount_in,
4787                "Finalized Swap input {quote_amount} does not match the persisted amount {}",
4788                plan.amount_in
4789            );
4790            anyhow::ensure!(
4791                base_amount.is_negative(),
4792                "Finalized Swap base amount {base_amount} is not a BUY output"
4793            );
4794            raw_amount_to_quantity(
4795                base_amount.unsigned_abs(),
4796                plan.pool.get_base_token().decimals,
4797            )?
4798        }
4799    };
4800
4801    let block = &included.finality.inclusion_header;
4802    anyhow::ensure!(
4803        block.number == included.block_number
4804            && block.hash == included.receipt.block_hash.to_string(),
4805        "Verified inclusion header {} does not match receipt hash {}",
4806        included.block_number,
4807        included.receipt.block_hash
4808    );
4809    let timestamp_ns = block
4810        .timestamp
4811        .checked_mul(NANOSECONDS_IN_SECOND)
4812        .ok_or_else(|| anyhow::anyhow!("Finalized block timestamp overflows nanoseconds"))?;
4813    let mut swap = event.to_pool_swap(
4814        plan.pool.chain.clone(),
4815        plan.instrument_id,
4816        plan.pool.pool_identifier,
4817        UnixNanos::from(timestamp_ns),
4818    );
4819    swap.calculate_trade_info(&plan.pool.token0, &plan.pool.token1, None)?;
4820    let trade = swap
4821        .trade_info
4822        .as_ref()
4823        .ok_or_else(|| anyhow::anyhow!("Finalized Swap has no calculated trade information"))?;
4824    anyhow::ensure!(
4825        trade.order_side == plan.order.order_side(),
4826        "Finalized Swap side {} does not match {} order",
4827        trade.order_side,
4828        plan.order.order_side()
4829    );
4830    let gas_cost = included
4831        .receipt
4832        .effective_gas_price
4833        .checked_mul(U256::from(included.receipt.gas_used))
4834        .ok_or_else(|| anyhow::anyhow!("Finalized transaction gas commission overflow"))?;
4835    let commission = Money::from_u256(gas_cost, plan.pool.chain.native_currency())?;
4836    let trade_digest = keccak256(format!("{}:{}", included.tx_hash, swap.log_index));
4837    let trade_digest = trade_digest.to_string();
4838    let trade_id = TradeId::new_checked(&trade_digest[2..38])?;
4839
4840    if plan
4841        .order
4842        .trade_ids()
4843        .iter()
4844        .any(|existing| **existing == trade_id)
4845    {
4846        return Ok(None);
4847    }
4848
4849    let venue_order_id = VenueOrderId::new_checked(included.tx_hash.to_string())?;
4850    let ts_event = UnixNanos::from(timestamp_ns);
4851    let last_px = match plan.order.order_side() {
4852        OrderSide::Buy => fill_price_from_quote(last_qty, plan.amount_in, plan.quote_currency)?,
4853        _ => trade.execution_price,
4854    };
4855    Ok(Some(FinalizedSwapFill {
4856        venue_order_id,
4857        trade_id,
4858        last_qty,
4859        last_px,
4860        commission,
4861        ts_event,
4862    }))
4863}
4864
4865async fn load_verified_wallet_after_fill(
4866    plan: &SwapPlan,
4867    included: &IncludedTransaction,
4868    executor: &TransactionExecutor,
4869) -> anyhow::Result<VerifiedWalletRefresh> {
4870    let mut token_universe = executor.wallet_balance.lock().token_universe.clone();
4871    token_universe.insert(plan.pool.token0.address);
4872    token_universe.insert(plan.pool.token1.address);
4873
4874    let native_amount = required_verification(
4875        executor
4876            .verification
4877            .verify_balance(&executor.wallet_address, included.block_number)
4878            .await,
4879        "finalized native balance",
4880    )?;
4881    let native_balance = Money::from_u256(native_amount.value, plan.pool.chain.native_currency())?;
4882    let mut decisions = vec![verification_decision(
4883        &native_amount,
4884        Some(included.block_number),
4885        Some(included.block_number),
4886    )];
4887    let mut token_addresses = token_universe.iter().copied().collect::<Vec<_>>();
4888    token_addresses.sort_unstable();
4889    let mut token_balances = Vec::with_capacity(token_addresses.len());
4890    for address in token_addresses {
4891        let identities = executor
4892            .deployment_manifest
4893            .tokens
4894            .iter()
4895            .filter(|identity| Address::from_str(&identity.address).ok() == Some(address))
4896            .collect::<Vec<_>>();
4897        anyhow::ensure!(
4898            identities.len() == 1,
4899            "Wallet token {address} does not have exactly one deployment manifest identity"
4900        );
4901        let identity = identities[0];
4902        let token = Token::new(
4903            plan.pool.chain.clone(),
4904            address,
4905            identity.name.clone(),
4906            identity.symbol.clone(),
4907            identity.decimals,
4908        );
4909        let call = ERC20::balanceOfCall {
4910            account: executor.wallet_address,
4911        }
4912        .abi_encode();
4913        let amount = required_verification(
4914            executor
4915                .verification
4916                .verify_decoded_call(
4917                    None,
4918                    &address,
4919                    U256::ZERO,
4920                    &call,
4921                    included.block_number,
4922                    |result| ERC20::balanceOfCall::abi_decode_returns(result).map_err(Into::into),
4923                )
4924                .await,
4925            "finalized token balance",
4926        )?;
4927        decisions.push(verification_decision(
4928            &amount,
4929            Some(included.block_number),
4930            Some(included.block_number),
4931        ));
4932        token_balances.push(TokenBalance::new(amount.value, token));
4933    }
4934
4935    let mut wallet_balance = WalletBalance::new(token_universe);
4936    let balances = wallet_balance.replace_balances(native_balance, token_balances)?;
4937    Ok(VerifiedWalletRefresh {
4938        wallet_balance,
4939        balances,
4940        decisions,
4941    })
4942}
4943
4944async fn verify_connect_capabilities(
4945    verification: &VerificationCoordinator,
4946    manifest: &BlockchainDeploymentManifest,
4947    wallet: Address,
4948    weth: Address,
4949    block: u64,
4950) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
4951    let contract = manifest
4952        .contracts
4953        .first()
4954        .ok_or_else(|| anyhow::anyhow!("Deployment manifest has no capability probe target"))?;
4955    let contract_address = Address::from_str(&contract.address)
4956        .with_context(|| "Deployment manifest capability probe target is invalid")?;
4957    let storage = required_verification(
4958        verification
4959            .verify_storage(&contract_address, &B256::ZERO, block)
4960            .await,
4961        "Blockchain explicit-height storage capability",
4962    )?;
4963
4964    let balance_call = ERC20::balanceOfCall { account: wallet }.abi_encode();
4965    let gas = required_verification(
4966        verification
4967            .verify_gas_estimate(&wallet, &weth, U256::ZERO, &balance_call, block)
4968            .await,
4969        "Blockchain explicit-height gas capability",
4970    )?;
4971
4972    let mut decisions = vec![
4973        verification_decision(&storage, Some(block), Some(block)),
4974        verification_decision(&gas, Some(block), Some(block)),
4975    ];
4976
4977    for pool in &manifest.pools {
4978        let quote_contract = Address::from_str(&pool.quote_contract)
4979            .with_context(|| "Deployment manifest quote capability target is invalid")?;
4980        let token_in = Address::from_str(&pool.token0)
4981            .with_context(|| "Deployment manifest quote input token is invalid")?;
4982        let token_out = Address::from_str(&pool.token1)
4983            .with_context(|| "Deployment manifest quote output token is invalid")?;
4984        let fee = U24::try_from(pool.fee)
4985            .map_err(|_| anyhow::anyhow!("Deployment manifest pool fee is invalid"))?;
4986        let quote = required_verification(
4987            verification
4988                .verify_quote_exact_input_single(
4989                    &quote_contract,
4990                    token_in,
4991                    token_out,
4992                    U256::from(1u64),
4993                    fee,
4994                    block,
4995                )
4996                .await,
4997            "Blockchain explicit-height quote capability",
4998        )?;
4999        decisions.push(verification_decision(&quote, Some(block), Some(block)));
5000    }
5001
5002    let trace = required_verification(
5003        verification.verify_call_trace_capability().await,
5004        "Blockchain call trace capability",
5005    )?;
5006    decisions.push(verification_decision(&trace, None, None));
5007    Ok(decisions)
5008}
5009
5010/// Runs the read-only pre-trade checks for a swap: deployed bytecode at the pool, router,
5011/// and token addresses, and an operator-prepared router allowance and input-token balance
5012/// covering the amount. The shared signing pipeline checks the exact maximum native cost.
5013/// Never wraps or approves.
5014async fn check_swap_preconditions(
5015    plan: &SwapPlan,
5016    block: u64,
5017    executor: &TransactionExecutor,
5018) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
5019    let mut decisions = Vec::new();
5020    let factory_call = UniswapV3RouterState::factoryCall.abi_encode();
5021    let router_factory = required_verification(
5022        executor
5023            .verification
5024            .verify_decoded_call(
5025                None,
5026                &plan.router,
5027                U256::ZERO,
5028                &factory_call,
5029                block,
5030                |result| {
5031                    UniswapV3RouterState::factoryCall::abi_decode_returns(result)
5032                        .map_err(Into::into)
5033                },
5034            )
5035            .await,
5036        "swap router factory",
5037    )?;
5038    anyhow::ensure!(
5039        router_factory.value == plan.factory,
5040        "Router {} reports an unexpected factory",
5041        plan.router,
5042    );
5043    decisions.push(verification_decision(
5044        &router_factory,
5045        Some(block),
5046        Some(block),
5047    ));
5048
5049    let weth_call = UniswapV3RouterState::WETH9Call.abi_encode();
5050    let router_weth = required_verification(
5051        executor
5052            .verification
5053            .verify_decoded_call(
5054                None,
5055                &plan.router,
5056                U256::ZERO,
5057                &weth_call,
5058                block,
5059                |result| {
5060                    UniswapV3RouterState::WETH9Call::abi_decode_returns(result).map_err(Into::into)
5061                },
5062            )
5063            .await,
5064        "swap router wrapped native",
5065    )?;
5066    anyhow::ensure!(
5067        router_weth.value == plan.weth,
5068        "Router {} reports an unexpected wrapped native contract",
5069        plan.router,
5070    );
5071    decisions.push(verification_decision(
5072        &router_weth,
5073        Some(block),
5074        Some(block),
5075    ));
5076
5077    let pool_call = UniswapV3Factory::getPoolCall {
5078        tokenA: plan.token_in,
5079        tokenB: plan.token_out,
5080        fee: plan.fee,
5081    }
5082    .abi_encode();
5083    let registered_pool = required_verification(
5084        executor
5085            .verification
5086            .verify_decoded_call(
5087                None,
5088                &plan.factory,
5089                U256::ZERO,
5090                &pool_call,
5091                block,
5092                |result| {
5093                    UniswapV3Factory::getPoolCall::abi_decode_returns(result).map_err(Into::into)
5094                },
5095            )
5096            .await,
5097        "swap factory pool",
5098    )?;
5099    anyhow::ensure!(
5100        registered_pool.value == plan.pool_address,
5101        "Factory resolves an unexpected pool for the swap token pair and fee"
5102    );
5103    decisions.push(verification_decision(
5104        &registered_pool,
5105        Some(block),
5106        Some(block),
5107    ));
5108
5109    for token in [&plan.pool.token0, &plan.pool.token1] {
5110        let decimals_call = ERC20::decimalsCall.abi_encode();
5111        let decimals = required_verification(
5112            executor
5113                .verification
5114                .verify_decoded_call(
5115                    None,
5116                    &token.address,
5117                    U256::ZERO,
5118                    &decimals_call,
5119                    block,
5120                    |result| ERC20::decimalsCall::abi_decode_returns(result).map_err(Into::into),
5121                )
5122                .await,
5123            "swap token decimals",
5124        )?;
5125        anyhow::ensure!(
5126            decimals.value == token.decimals,
5127            "Token {} reports unexpected decimals",
5128            token.address,
5129        );
5130        decisions.push(verification_decision(&decimals, Some(block), Some(block)));
5131    }
5132
5133    let allowance_call = ERC20::allowanceCall {
5134        owner: executor.wallet_address,
5135        spender: plan.router,
5136    }
5137    .abi_encode();
5138    let allowance = required_verification(
5139        executor
5140            .verification
5141            .verify_decoded_call(
5142                None,
5143                &plan.token_in,
5144                U256::ZERO,
5145                &allowance_call,
5146                block,
5147                |result| ERC20::allowanceCall::abi_decode_returns(result).map_err(Into::into),
5148            )
5149            .await,
5150        "swap input allowance",
5151    )?;
5152
5153    if allowance.value < plan.amount_in {
5154        anyhow::bail!(
5155            "Router allowance {} is below the swap amount {} for input token {}; approve the router explicitly before submitting",
5156            allowance.value,
5157            plan.amount_in,
5158            plan.token_in
5159        );
5160    }
5161    decisions.push(verification_decision(&allowance, Some(block), Some(block)));
5162
5163    let balance_call = ERC20::balanceOfCall {
5164        account: executor.wallet_address,
5165    }
5166    .abi_encode();
5167    let balance = required_verification(
5168        executor
5169            .verification
5170            .verify_decoded_call(
5171                None,
5172                &plan.token_in,
5173                U256::ZERO,
5174                &balance_call,
5175                block,
5176                |result| ERC20::balanceOfCall::abi_decode_returns(result).map_err(Into::into),
5177            )
5178            .await,
5179        "swap input balance",
5180    )?;
5181
5182    if balance.value < plan.amount_in {
5183        anyhow::bail!(
5184            "Input token {} balance {} is below the swap amount {}",
5185            plan.token_in,
5186            balance.value,
5187            plan.amount_in
5188        );
5189    }
5190    decisions.push(verification_decision(&balance, Some(block), Some(block)));
5191
5192    Ok(decisions)
5193}
5194
5195/// Converts a base-denominated order quantity into raw token units with exact integer
5196/// scaling by the base token decimals.
5197///
5198/// `Quantity::raw` is scaled to the greater of its declared precision and `FIXED_PRECISION`;
5199/// token amounts are scaled to the token's decimals. Quantities not exactly representable in
5200/// token units are rejected rather than rounded.
5201fn quantity_to_raw_amount(quantity: Quantity, decimals: u8) -> anyhow::Result<U256> {
5202    if quantity.is_zero() {
5203        anyhow::bail!("Order quantity must be positive");
5204    }
5205
5206    let raw = U256::from(quantity.raw());
5207    let raw_precision = quantity.precision.max(FIXED_PRECISION);
5208    if decimals >= raw_precision {
5209        let scale = U256::from(10u64)
5210            .checked_pow(U256::from(decimals - raw_precision))
5211            .ok_or_else(|| anyhow::anyhow!("Order amount scaling overflow"))?;
5212        raw.checked_mul(scale).ok_or_else(|| {
5213            anyhow::anyhow!("Order amount overflow scaling quantity to raw token units")
5214        })
5215    } else {
5216        let divisor = U256::from(10u64)
5217            .checked_pow(U256::from(raw_precision - decimals))
5218            .ok_or_else(|| anyhow::anyhow!("Order amount scaling overflow"))?;
5219        if !(raw % divisor).is_zero() {
5220            anyhow::bail!(
5221                "Order quantity {quantity} is not exactly representable in {decimals} base token decimals"
5222            );
5223        }
5224        Ok(raw / divisor)
5225    }
5226}
5227
5228fn swap_token_pair(
5229    side: OrderSide,
5230    base: Address,
5231    quote: Address,
5232) -> anyhow::Result<(Address, Address)> {
5233    match side {
5234        OrderSide::Sell => Ok((base, quote)),
5235        OrderSide::Buy => Ok((quote, base)),
5236    }
5237}
5238
5239fn fill_price_from_quote(
5240    last_qty: Quantity,
5241    quote_amount: U256,
5242    quote_currency: Currency,
5243) -> anyhow::Result<Price> {
5244    let quote = Money::from_u256(quote_amount, quote_currency)?;
5245    Price::from_decimal_dp(quote.as_decimal() / last_qty.as_decimal(), FIXED_PRECISION)
5246        .map_err(anyhow::Error::from)
5247}
5248
5249fn raw_amount_to_quantity(amount: U256, decimals: u8) -> anyhow::Result<Quantity> {
5250    if amount.is_zero() {
5251        anyhow::bail!("Executed amount must be positive");
5252    }
5253    let quantity = if decimals >= FIXED_PRECISION {
5254        let scale = U256::from(10u64)
5255            .checked_pow(U256::from(decimals - FIXED_PRECISION))
5256            .ok_or_else(|| anyhow::anyhow!("Executed amount scaling overflow"))?;
5257        Quantity::from_u256(amount / scale, FIXED_PRECISION).map_err(anyhow::Error::from)?
5258    } else {
5259        Quantity::from_u256(amount, decimals).map_err(anyhow::Error::from)?
5260    };
5261
5262    if quantity.is_zero() {
5263        anyhow::bail!(
5264            "Executed amount {amount} is below representable quantity precision {FIXED_PRECISION}"
5265        );
5266    }
5267    Ok(quantity)
5268}
5269
5270fn exact_output_amount(quote: &SwapQuote, zero_for_one: bool) -> anyhow::Result<U256> {
5271    let amount = if zero_for_one {
5272        quote.amount1
5273    } else {
5274        quote.amount0
5275    };
5276
5277    if !amount.is_negative() {
5278        anyhow::bail!("Swap quote output amount {amount} is not a positive output");
5279    }
5280    Ok(amount.unsigned_abs())
5281}
5282
5283/// Derives the minimum output accepted for the swap: the quoted output reduced by
5284/// `slippage_bps`, in exact integer arithmetic. Rejects a zero minimum, which would leave
5285/// the swap without slippage protection.
5286fn derive_min_amount_out(quoted_amount_out: U256, slippage_bps: u32) -> anyhow::Result<U256> {
5287    if slippage_bps >= BPS_DENOMINATOR {
5288        anyhow::bail!("Slippage {slippage_bps} bps must be below {BPS_DENOMINATOR}");
5289    }
5290    let min_amount_out = quoted_amount_out
5291        .checked_mul(U256::from(BPS_DENOMINATOR - slippage_bps))
5292        .and_then(|scaled| scaled.checked_div(U256::from(BPS_DENOMINATOR)))
5293        .ok_or_else(|| anyhow::anyhow!("Minimum output derivation overflow"))?;
5294    if min_amount_out.is_zero() {
5295        anyhow::bail!(
5296            "Derived minimum output is zero for quoted output {quoted_amount_out} at {slippage_bps} bps slippage"
5297        );
5298    }
5299    Ok(min_amount_out)
5300}
5301
5302impl BlockchainExecutionClient {
5303    async fn build_execution_verification_migration(
5304        &self,
5305        snapshot: ExecutionVerificationMigrationSnapshot,
5306        finalized: VerifiedBlockHeader,
5307        finalized_headers: &[VerifiedBlockHeader],
5308        nonce_verification: &Verified<u64>,
5309    ) -> anyhow::Result<ExecutionVerificationMigration> {
5310        let next_canonical_nonce = nonce_verification.value;
5311        let mut hashes_by_intent: HashMap<i64, Vec<&ExecutionTransactionHashRow>> = HashMap::new();
5312        for hash in &snapshot.hashes {
5313            hashes_by_intent
5314                .entry(hash.intent_id)
5315                .or_default()
5316                .push(hash);
5317        }
5318        let active_count = snapshot
5319            .intents
5320            .iter()
5321            .filter(|intent| intent.active)
5322            .count();
5323        anyhow::ensure!(
5324            active_count <= 1,
5325            "Retained execution history has multiple active signer owners"
5326        );
5327
5328        let mut nonce_owners = HashMap::new();
5329        let mut records = Vec::with_capacity(snapshot.intents.len());
5330        let finalized_headers = finalized_headers
5331            .iter()
5332            .map(durable_verified_header)
5333            .collect::<Vec<_>>();
5334
5335        for intent in &snapshot.intents {
5336            anyhow::ensure!(
5337                intent.chain_id == self.chain.chain_id
5338                    && intent.wallet_address == self.config.wallet_address,
5339                "Retained execution intent belongs to another signer"
5340            );
5341            let purpose = TransactionPurpose::parse(&intent.purpose).ok_or_else(|| {
5342                anyhow::anyhow!("Retained execution intent has an unsupported purpose")
5343            })?;
5344            let hashes = hashes_by_intent
5345                .get(&intent.id)
5346                .map(Vec::as_slice)
5347                .unwrap_or_default();
5348            let current = hashes
5349                .iter()
5350                .copied()
5351                .filter(|hash| hash.current)
5352                .collect::<Vec<_>>();
5353            anyhow::ensure!(
5354                current.len() <= 1,
5355                "Retained execution intent {} has multiple current hashes",
5356                intent.id
5357            );
5358            let current = current.first().copied();
5359            let mut authenticated = HashMap::new();
5360
5361            for hash in hashes {
5362                if hash.payload_expected {
5363                    let raw = open_execution_payload(
5364                        self.payload_keys
5365                            .as_deref()
5366                            .expect("Postgres execution requires payload keys"),
5367                        self.payload_policy(),
5368                        intent,
5369                        hash,
5370                        "verification migration",
5371                    )?;
5372                    authenticated.insert(hash.id, raw);
5373                } else {
5374                    anyhow::ensure!(
5375                        hash.raw_transaction.is_none() && hash.sealed_transaction.is_none(),
5376                        "Unowned replacement hash retains signed bytes"
5377                    );
5378                }
5379            }
5380
5381            if let Some(nonce) = intent.nonce {
5382                anyhow::ensure!(
5383                    nonce_owners.insert(nonce, intent.id).is_none(),
5384                    "Retained execution history has duplicate signer nonce ownership"
5385                );
5386            }
5387
5388            let base_decision = verification_decision(
5389                nonce_verification,
5390                Some(finalized.number),
5391                Some(finalized.number),
5392            );
5393
5394            if !intent.active {
5395                if matches!(intent.status.as_str(), "finalized" | "reverted") {
5396                    let expected_marker =
5397                        if purpose == TransactionPurpose::Swap && intent.status == "finalized" {
5398                            intent.fill_emitted
5399                        } else {
5400                            intent.terminal_emitted
5401                        };
5402                    anyhow::ensure!(
5403                        expected_marker,
5404                        "Released terminal intent {} has no durable event marker",
5405                        intent.id
5406                    );
5407                } else {
5408                    anyhow::ensure!(
5409                        matches!(intent.status.as_str(), "dropped" | "recoverable")
5410                            && authenticated.is_empty(),
5411                        "Released nonterminal intent {} retains signed ownership",
5412                        intent.id
5413                    );
5414                    records.push(ExecutionVerificationMigrationRecord {
5415                        intent_id: intent.id,
5416                        nonce: intent.nonce,
5417                        transaction_hash: None,
5418                        terminal_status: None,
5419                        block_number: None,
5420                        block_hash: None,
5421                        receipt_success: None,
5422                        gas_used: None,
5423                        effective_gas_price: None,
5424                        recover_prepared: false,
5425                        decisions: vec![base_decision],
5426                    });
5427                    continue;
5428                }
5429            }
5430
5431            if intent.active && intent.nonce.is_none() {
5432                anyhow::ensure!(
5433                    intent.status == "prepared" && hashes.is_empty(),
5434                    "Unassigned active intent {} is not an unsigned preparation",
5435                    intent.id
5436                );
5437                records.push(ExecutionVerificationMigrationRecord {
5438                    intent_id: intent.id,
5439                    nonce: None,
5440                    transaction_hash: None,
5441                    terminal_status: None,
5442                    block_number: None,
5443                    block_hash: None,
5444                    receipt_success: None,
5445                    gas_used: None,
5446                    effective_gas_price: None,
5447                    recover_prepared: true,
5448                    decisions: vec![base_decision],
5449                });
5450                continue;
5451            }
5452
5453            let nonce = intent.nonce.ok_or_else(|| {
5454                anyhow::anyhow!("Retained signed intent {} has no nonce", intent.id)
5455            })?;
5456            let current = current.ok_or_else(|| {
5457                anyhow::anyhow!("Retained signed intent {} has no current hash", intent.id)
5458            })?;
5459            let raw_transaction = authenticated.get(&current.id).ok_or_else(|| {
5460                anyhow::anyhow!(
5461                    "Retained signed intent {} has no authenticated current payload",
5462                    intent.id
5463                )
5464            })?;
5465
5466            if intent.active && nonce == next_canonical_nonce {
5467                anyhow::ensure!(
5468                    !matches!(intent.status.as_str(), "finalized" | "reverted"),
5469                    "Active terminal intent conflicts with the canonical nonce ledger"
5470                );
5471                records.push(ExecutionVerificationMigrationRecord {
5472                    intent_id: intent.id,
5473                    nonce: Some(nonce),
5474                    transaction_hash: Some(current.transaction_hash.clone()),
5475                    terminal_status: None,
5476                    block_number: None,
5477                    block_hash: None,
5478                    receipt_success: None,
5479                    gas_used: None,
5480                    effective_gas_price: None,
5481                    recover_prepared: false,
5482                    decisions: vec![base_decision],
5483                });
5484                continue;
5485            }
5486            anyhow::ensure!(
5487                nonce < next_canonical_nonce,
5488                "Retained active nonce {nonce} is above canonical nonce {next_canonical_nonce}"
5489            );
5490
5491            let receipt_verification = required_verification(
5492                self.verification
5493                    .verify_receipt(&B256::from_str(&current.transaction_hash).with_context(
5494                        || {
5495                            format!(
5496                                "Retained transaction hash {} is invalid",
5497                                current.transaction_hash
5498                            )
5499                        },
5500                    )?)
5501                    .await,
5502                "migration receipt",
5503            )?;
5504            let receipt = receipt_verification.value.clone();
5505            anyhow::ensure!(
5506                receipt.block_number <= finalized.number,
5507                "Retained terminal receipt is above the verified finalized boundary"
5508            );
5509            let inclusion_verification = required_verification(
5510                self.verification.verify_block(receipt.block_number).await,
5511                "migration inclusion header",
5512            )?;
5513            anyhow::ensure!(
5514                inclusion_verification.value.hash == receipt.block_hash
5515                    && finalized_headers.iter().any(|header| {
5516                        header.number == receipt.block_number
5517                            && header.hash == receipt.block_hash.to_string()
5518                    }),
5519                "Retained terminal receipt is not on the verified finalized ancestry"
5520            );
5521            let tx_hash = B256::from_str(&current.transaction_hash)
5522                .context("Retained transaction hash is invalid")?;
5523            let included = IncludedTransaction {
5524                intent_id: intent.id,
5525                nonce,
5526                tx_hash,
5527                block_number: receipt.block_number,
5528                receipt: receipt.clone(),
5529                finality: StableFinality {
5530                    decisions: Vec::new(),
5531                    inclusion_header: durable_verified_header(&inclusion_verification.value),
5532                    finalized_headers: finalized_headers.clone(),
5533                },
5534            };
5535            let trace_purpose = match purpose {
5536                TransactionPurpose::Wrap => "wrap",
5537                TransactionPurpose::Approve => "approve",
5538                TransactionPurpose::Swap => {
5539                    match self.restore_swap_plan(intent)?.order.order_side() {
5540                        OrderSide::Sell => "swap_sell",
5541                        OrderSide::Buy => "swap_buy",
5542                    }
5543                }
5544            };
5545            let mut decisions = vec![
5546                verification_decision(
5547                    &receipt_verification,
5548                    Some(receipt.block_number),
5549                    Some(receipt.block_number),
5550                ),
5551                verification_decision(
5552                    &inclusion_verification,
5553                    Some(receipt.block_number),
5554                    Some(receipt.block_number),
5555                ),
5556            ];
5557            decisions.extend(
5558                verify_finalized_transaction_identity(
5559                    &included,
5560                    intent,
5561                    nonce,
5562                    raw_transaction,
5563                    &self.verification,
5564                    self.wallet_address,
5565                    self.chain.chain_id,
5566                    &self
5567                        .config
5568                        .verification
5569                        .as_ref()
5570                        .expect("verification config validated")
5571                        .deployment_manifest,
5572                    trace_purpose,
5573                )
5574                .await?,
5575            );
5576            let terminal_status = if receipt.status {
5577                TransactionStatus::Finalized
5578            } else {
5579                TransactionStatus::Reverted
5580            };
5581
5582            if !intent.active {
5583                anyhow::ensure!(
5584                    intent.status == terminal_status.as_str(),
5585                    "Released terminal intent status conflicts with its verified receipt"
5586                );
5587            }
5588            records.push(ExecutionVerificationMigrationRecord {
5589                intent_id: intent.id,
5590                nonce: Some(nonce),
5591                transaction_hash: Some(current.transaction_hash.clone()),
5592                terminal_status: Some(terminal_status),
5593                block_number: Some(receipt.block_number),
5594                block_hash: Some(receipt.block_hash.to_string()),
5595                receipt_success: Some(receipt.status),
5596                gas_used: Some(receipt.gas_used),
5597                effective_gas_price: Some(receipt.effective_gas_price.to_string()),
5598                recover_prepared: false,
5599                decisions,
5600            });
5601        }
5602
5603        Ok(ExecutionVerificationMigration { snapshot, records })
5604    }
5605}
5606
5607#[async_trait(?Send)]
5608impl ExecutionClient for BlockchainExecutionClient {
5609    fn is_connected(&self) -> bool {
5610        self.core.is_connected()
5611    }
5612
5613    fn client_id(&self) -> ClientId {
5614        self.core.client_id
5615    }
5616
5617    fn account_id(&self) -> AccountId {
5618        self.core.account_id
5619    }
5620
5621    fn venue(&self) -> Venue {
5622        self.core.venue
5623    }
5624
5625    fn handles_order_venue(&self, venue: Venue) -> bool {
5626        venue.parse_dex().is_ok_and(|(blockchain, dex_type)| {
5627            blockchain == self.chain.name && dex_type == DexType::UniswapV3
5628        })
5629    }
5630
5631    fn oms_type(&self) -> OmsType {
5632        self.core.oms_type
5633    }
5634
5635    fn get_account(&self) -> Option<AccountAny> {
5636        self.core.cache().account_owned(&self.core.account_id)
5637    }
5638
5639    fn generate_account_state(
5640        &self,
5641        balances: Vec<AccountBalance>,
5642        margins: Vec<MarginBalance>,
5643        reported: bool,
5644        ts_event: UnixNanos,
5645        info: Option<Params>,
5646    ) -> anyhow::Result<()> {
5647        self.emitter
5648            .try_emit_account_state(balances, margins, reported, ts_event, info)
5649    }
5650
5651    fn start(&mut self) -> anyhow::Result<()> {
5652        if self.core.is_started() {
5653            return Ok(());
5654        }
5655
5656        self.emitter.set_sender(get_exec_event_sender());
5657        self.core.set_started();
5658        log::info!(
5659            "Started: client_id={}, account_id={}",
5660            self.core.client_id,
5661            self.core.account_id
5662        );
5663        Ok(())
5664    }
5665
5666    fn stop(&mut self) -> anyhow::Result<()> {
5667        if self.core.is_stopped() {
5668            return Ok(());
5669        }
5670
5671        self.pending_tasks.begin_shutdown();
5672        self.signer = None;
5673        self.core.set_stopped();
5674        self.core.set_disconnected();
5675        log::info!("Stopped: client_id={}", self.core.client_id);
5676        Ok(())
5677    }
5678
5679    fn submit_order(&self, cmd: SubmitOrder) -> anyhow::Result<()> {
5680        let order = self.core.cache().try_order_owned(&cmd.client_order_id)?;
5681
5682        if order.is_closed() {
5683            log::warn!("Cannot submit closed order {}", order.client_order_id());
5684            return Ok(());
5685        }
5686
5687        if let Err(reason) = validate_order(&order) {
5688            self.emitter.emit_order_denied(&order, &reason.to_string());
5689            return Ok(());
5690        }
5691
5692        if !self.pending_tasks.is_open() {
5693            self.emitter
5694                .emit_order_denied(&order, "Blockchain execution client is shutting down");
5695            return Ok(());
5696        }
5697
5698        let plan = match self.prepare_swap(&cmd, &order) {
5699            Ok(plan) => plan,
5700            Err(e) => {
5701                self.emitter.emit_order_denied(&order, &e.to_string());
5702                return Ok(());
5703            }
5704        };
5705
5706        let executor = match self.transaction_executor() {
5707            Ok(executor) => executor,
5708            Err(e) => {
5709                self.emitter.emit_order_denied(&order, &e.to_string());
5710                return Ok(());
5711            }
5712        };
5713
5714        let emitter = self.emitter.clone();
5715        let max_quote_age_blocks = self.transaction_limits.max_quote_age_blocks;
5716        let deadline_seconds = self.transaction_limits.deadline_seconds;
5717        let client_order_id = order.client_order_id();
5718
5719        let future = async move {
5720            if let Err(e) = execute_swap(
5721                plan,
5722                executor,
5723                emitter,
5724                max_quote_age_blocks,
5725                deadline_seconds,
5726            )
5727            .await
5728            {
5729                log::warn!("Swap execution for order {client_order_id} failed: {e:?}");
5730            }
5731        };
5732
5733        if let Err(e) = self.pending_tasks.spawn(future) {
5734            release_preparing_slot(&self.in_flight);
5735            log::warn!("Skipping blockchain swap after shutdown began: {e}");
5736        }
5737
5738        Ok(())
5739    }
5740
5741    fn submit_order_list(&self, cmd: SubmitOrderList) -> anyhow::Result<()> {
5742        let orders = self
5743            .core
5744            .cache()
5745            .orders_for_ids(&cmd.order_list.client_order_ids, &cmd);
5746
5747        for order in &orders {
5748            if order.is_closed() {
5749                log::warn!("Cannot submit closed order {}", order.client_order_id());
5750                continue;
5751            }
5752
5753            self.emitter
5754                .emit_order_denied(order, ORDER_LIST_UNSUPPORTED);
5755        }
5756
5757        Ok(())
5758    }
5759
5760    fn modify_order(&self, cmd: ModifyOrder) -> anyhow::Result<()> {
5761        let Ok(order) = self.core.cache().try_order_owned(&cmd.client_order_id) else {
5762            log::warn!("Cannot modify unknown order {}", cmd.client_order_id);
5763            return Ok(());
5764        };
5765
5766        self.emitter.emit_order_modify_rejected(
5767            &order,
5768            cmd.venue_order_id,
5769            ORDER_MODIFY_UNSUPPORTED,
5770            get_atomic_clock_realtime().get_time_ns(),
5771        );
5772        Ok(())
5773    }
5774
5775    fn cancel_order(&self, cmd: CancelOrder) -> anyhow::Result<()> {
5776        let Ok(order) = self.core.cache().try_order_owned(&cmd.client_order_id) else {
5777            log::warn!("Cannot cancel unknown order {}", cmd.client_order_id);
5778            return Ok(());
5779        };
5780
5781        self.emitter.emit_order_cancel_rejected(
5782            &order,
5783            cmd.venue_order_id,
5784            ORDER_CANCEL_UNSUPPORTED,
5785            get_atomic_clock_realtime().get_time_ns(),
5786        );
5787        Ok(())
5788    }
5789
5790    fn cancel_all_orders(&self, cmd: CancelAllOrders) -> anyhow::Result<()> {
5791        log::warn!(
5792            "Cancel-all for {} is not supported on the blockchain execution client",
5793            cmd.instrument_id
5794        );
5795        Ok(())
5796    }
5797
5798    fn batch_cancel_orders(&self, cmd: BatchCancelOrders) -> anyhow::Result<()> {
5799        for cancel in cmd.cancels {
5800            self.cancel_order(cancel)?;
5801        }
5802        Ok(())
5803    }
5804
5805    fn query_account(&self, cmd: QueryAccount) -> anyhow::Result<()> {
5806        anyhow::ensure!(
5807            cmd.account_id == self.core.account_id,
5808            "Query account ID {} does not match client account ID {}",
5809            cmd.account_id,
5810            self.core.account_id
5811        );
5812        anyhow::ensure!(self.core.is_started(), "Execution client is not started");
5813
5814        let balances = self.wallet_balance.lock().as_account_balances()?;
5815        self.generate_account_state(
5816            balances,
5817            vec![],
5818            true,
5819            get_atomic_clock_realtime().get_time_ns(),
5820            None,
5821        )
5822    }
5823
5824    fn query_order(&self, cmd: QueryOrder) -> anyhow::Result<()> {
5825        log::warn!(
5826            "Order queries are not supported on the blockchain execution client; cannot query {}",
5827            cmd.client_order_id
5828        );
5829        Ok(())
5830    }
5831
5832    async fn connect(&mut self) -> anyhow::Result<()> {
5833        if self.core.is_connected() {
5834            log::warn!("Blockchain execution client already connected");
5835            return Ok(());
5836        }
5837
5838        log::info!(
5839            "Connecting to blockchain execution client on chain {}",
5840            self.chain.name
5841        );
5842
5843        if !self.pending_tasks.is_open() || !self.pending_tasks.is_empty() {
5844            self.pending_tasks.begin_shutdown();
5845            self.pending_tasks
5846                .finish_shutdown(Duration::from_secs(5), Duration::from_secs(2))
5847                .await
5848                .map_err(|e| anyhow::anyhow!("Failed to terminate blockchain submissions: {e}"))?;
5849            self.signer = None;
5850            self.pending_tasks
5851                .start_generation()
5852                .map_err(|e| anyhow::anyhow!("Failed to start blockchain task generation: {e}"))?;
5853        }
5854        release_preparing_slot(&self.in_flight);
5855
5856        let setup_guard = TaskGroupGuard::new(&[&self.pending_tasks], || {});
5857
5858        let payload_keys = PayloadKeySet::load(
5859            self.config.payload_key_env.as_deref(),
5860            &self.config.payload_key_retired_env,
5861            self.config.payload_deployment_id.as_deref(),
5862        )?
5863        .map(Arc::new);
5864
5865        if self.cache.database.is_some() || self.config.postgres_cache_database_config.is_some() {
5866            let keys = payload_keys.as_deref().ok_or_else(|| {
5867                anyhow::anyhow!(
5868                    "Postgres execution requires an active payload key and deployment identity"
5869                )
5870            })?;
5871
5872            if self.cache.database.is_none() {
5873                let pg_options = self
5874                    .config
5875                    .postgres_cache_database_config
5876                    .as_ref()
5877                    .expect("Postgres configuration checked above");
5878                let database = crate::cache::database::BlockchainCacheDatabase::connect(
5879                    pg_options.clone().into(),
5880                )
5881                .await
5882                .map_err(|e| {
5883                    anyhow::anyhow!("Failed to connect to the Postgres cache database: {e}")
5884                })?;
5885                self.cache.database = Some(database);
5886            }
5887            self.cache
5888                .database
5889                .as_ref()
5890                .expect("database was attached")
5891                .require_execution_payload_storage_ready(keys)
5892                .await?;
5893            self.cache.initialize_chain().await;
5894            self.cache.ensure_execution_transaction_schema().await?;
5895            let check = self
5896                .cache
5897                .database
5898                .as_ref()
5899                .expect("database was attached")
5900                .check_execution_payload_storage(
5901                    Some(keys),
5902                    Some(PayloadPolicy {
5903                        chain_id: self.chain.chain_id,
5904                        signer: self.wallet_address,
5905                        gas_limit: self.config.gas_limit,
5906                        max_fee_per_gas: self.config.max_fee_per_gas_wei,
5907                    }),
5908                    100,
5909                )
5910                .await?;
5911            anyhow::ensure!(
5912                check.protected,
5913                "Postgres execution requires protected payload storage"
5914            );
5915        } else {
5916            log::warn!(
5917                "No Postgres cache database configured; transactions will be refused (no durable store)"
5918            );
5919        }
5920        self.payload_keys = payload_keys;
5921
5922        let verification = self
5923            .config
5924            .verification
5925            .as_ref()
5926            .expect("verification config validated at construction");
5927        let position = if let Some(database) = self.cache.database.as_ref() {
5928            database
5929                .load_execution_verification_position(
5930                    self.chain.chain_id,
5931                    &self.config.wallet_address,
5932                    &verification.manifest_version,
5933                    &verification.manifest_digest,
5934                )
5935                .await?
5936        } else {
5937            None
5938        };
5939        let migration_snapshot = if position.is_none() {
5940            if let Some(database) = self.cache.database.as_ref() {
5941                let snapshot = database
5942                    .load_execution_verification_migration_snapshot(
5943                        self.chain.chain_id,
5944                        &self.config.wallet_address,
5945                    )
5946                    .await?;
5947
5948                if snapshot.intents.is_empty() {
5949                    None
5950                } else {
5951                    Some(snapshot)
5952                }
5953            } else {
5954                None
5955            }
5956        } else {
5957            None
5958        };
5959
5960        let chain_id_verification = required_verification(
5961            self.verification.verify_chain_id().await,
5962            "Blockchain chain ID",
5963        )?;
5964        let checkpoint_verification = required_verification(
5965            self.verification.verify_checkpoint().await,
5966            "Blockchain checkpoint",
5967        )?;
5968        let checkpoint = checkpoint_verification.value;
5969        let finalized_verification = required_verification(
5970            self.verification.verify_finalized_header().await,
5971            "Blockchain finalized header",
5972        )?;
5973        let finalized = finalized_verification.value;
5974        let mut connect_decisions = vec![
5975            verification_decision(&chain_id_verification, None, None),
5976            verification_decision(
5977                &checkpoint_verification,
5978                Some(checkpoint.number),
5979                Some(checkpoint.number),
5980            ),
5981            verification_decision(
5982                &finalized_verification,
5983                Some(finalized.number),
5984                Some(finalized.number),
5985            ),
5986        ];
5987        let mut finalized_headers = if let Some(position) = position.as_ref() {
5988            let durable_tip = parse_verified_header(&position.finalized_tip)?;
5989            anyhow::ensure!(
5990                durable_tip.number >= checkpoint.number,
5991                "Durable finalized header tip precedes the trusted checkpoint"
5992            );
5993            let durable_tip_verification = required_verification(
5994                self.verification.verify_block(durable_tip.number).await,
5995                "Blockchain durable finalized tip",
5996            )?;
5997            anyhow::ensure!(
5998                durable_tip_verification.value == durable_tip,
5999                "Durable finalized header tip conflicts with independent sources"
6000            );
6001            connect_decisions.push(verification_decision(
6002                &durable_tip_verification,
6003                Some(durable_tip.number),
6004                Some(durable_tip.number),
6005            ));
6006            vec![durable_tip]
6007        } else {
6008            vec![checkpoint]
6009        };
6010        let mut ancestry_cursor = *finalized_headers
6011            .last()
6012            .expect("finalized header ledger is nonempty");
6013        anyhow::ensure!(
6014            finalized.number >= ancestry_cursor.number,
6015            "Verified finalized height regressed below the durable finalized header tip"
6016        );
6017
6018        while ancestry_cursor.number < finalized.number {
6019            let end = ancestry_cursor
6020                .number
6021                .saturating_add(4_096)
6022                .min(finalized.number);
6023            let start = ancestry_cursor.number.saturating_add(1);
6024            let headers_verification = required_verification(
6025                self.verification
6026                    .verify_header_window(ancestry_cursor, end)
6027                    .await,
6028                "Blockchain finalized ancestry",
6029            )?;
6030            let headers = &headers_verification.value;
6031            ancestry_cursor = *headers
6032                .last()
6033                .expect("nonempty ancestry window advances the cursor");
6034            finalized_headers.extend(headers.iter().copied());
6035            connect_decisions.push(verification_decision(
6036                &headers_verification,
6037                Some(start),
6038                Some(end),
6039            ));
6040        }
6041        anyhow::ensure!(
6042            ancestry_cursor == finalized,
6043            "Verified finalized header conflicts with its ancestry window"
6044        );
6045        let nonce_verification = required_verification(
6046            self.verification
6047                .verify_transaction_count(&self.wallet_address, finalized.number)
6048                .await,
6049            "Blockchain finalized transaction count",
6050        )?;
6051        let observed_canonical_nonce = nonce_verification.value;
6052        let next_canonical_nonce = position
6053            .as_ref()
6054            .map_or(observed_canonical_nonce, |position| {
6055                position.next_canonical_nonce
6056            });
6057
6058        if let Some(position) = position.as_ref() {
6059            log::debug!(
6060                "Resumed execution verification ledger at nonce revision {} with observed finalized nonce {}",
6061                position.revision,
6062                observed_canonical_nonce,
6063            );
6064        }
6065        connect_decisions.push(verification_decision(
6066            &nonce_verification,
6067            Some(finalized.number),
6068            Some(finalized.number),
6069        ));
6070        let deployment_verification = required_verification(
6071            self.verification
6072                .verify_deployment_manifest(&verification.deployment_manifest, finalized.number)
6073                .await,
6074            "Blockchain deployment manifest",
6075        )?;
6076        connect_decisions.push(verification_decision(
6077            &deployment_verification,
6078            Some(finalized.number),
6079            Some(finalized.number),
6080        ));
6081        connect_decisions.extend(
6082            verify_connect_capabilities(
6083                &self.verification,
6084                &verification.deployment_manifest,
6085                self.wallet_address,
6086                self.weth_address,
6087                finalized.number,
6088            )
6089            .await?,
6090        );
6091        let migration = if let Some(snapshot) = migration_snapshot {
6092            Some(
6093                self.build_execution_verification_migration(
6094                    snapshot,
6095                    finalized,
6096                    &finalized_headers,
6097                    &nonce_verification,
6098                )
6099                .await?,
6100            )
6101        } else {
6102            None
6103        };
6104
6105        if let Some(database) = self.cache.database.as_ref() {
6106            let identities = std::iter::once(&verification.authoritative)
6107                .chain(
6108                    verification
6109                        .verifiers
6110                        .iter()
6111                        .map(|provider| &provider.identity),
6112                )
6113                .collect::<Vec<_>>();
6114            let provider_ids = identities
6115                .iter()
6116                .map(|identity| identity.provider_id.clone())
6117                .collect::<Vec<_>>();
6118            let operator_ids = identities
6119                .iter()
6120                .map(|identity| identity.operator_id.clone())
6121                .collect::<Vec<_>>();
6122            let failure_domain_ids = identities
6123                .iter()
6124                .flat_map(|identity| identity.failure_domain_ids.iter().cloned())
6125                .collect::<Vec<_>>();
6126            let finalized_headers = finalized_headers
6127                .iter()
6128                .map(durable_verified_header)
6129                .collect::<Vec<_>>();
6130            database
6131                .ensure_execution_verification_schema(&ExecutionVerificationBootstrap {
6132                    chain_id: self.chain.chain_id,
6133                    wallet_address: &self.config.wallet_address,
6134                    manifest_version: &verification.manifest_version,
6135                    manifest_digest: &verification.manifest_digest,
6136                    checkpoint_number: checkpoint.number,
6137                    checkpoint_hash: &checkpoint.hash.to_string(),
6138                    checkpoint_parent_hash: &checkpoint.parent_hash.to_string(),
6139                    checkpoint_timestamp: checkpoint.timestamp,
6140                    checkpoint_base_fee_per_gas: checkpoint.base_fee_per_gas,
6141                    finalized_headers: &finalized_headers,
6142                    next_canonical_nonce,
6143                    observed_canonical_nonce,
6144                    provider_ids: &provider_ids,
6145                    operator_ids: &operator_ids,
6146                    failure_domain_ids: &failure_domain_ids,
6147                    decisions: &connect_decisions,
6148                    migration: migration.as_ref(),
6149                })
6150                .await?;
6151        }
6152
6153        let payload_connect_lease = if let (Some(database), Some(keys)) =
6154            (self.cache.database.as_ref(), self.payload_keys.as_deref())
6155        {
6156            Some(
6157                database
6158                    .require_execution_payload_storage(keys, self.payload_policy(), 100)
6159                    .await?,
6160            )
6161        } else {
6162            None
6163        };
6164
6165        // Load the signer key from the configured environment variable; the key is never
6166        // logged, serialized, or stored in configuration
6167        let private_key = Zeroizing::new(
6168            std::env::var(&self.config.signer_private_key_env).map_err(|_| {
6169                anyhow::anyhow!(
6170                    "Signer private key environment variable '{}' is not set",
6171                    self.config.signer_private_key_env
6172                )
6173            })?,
6174        );
6175        let encoded_key = private_key.trim();
6176        let encoded_key = encoded_key.strip_prefix("0x").unwrap_or(encoded_key);
6177        let key_bytes = Zeroizing::new(hex::decode_array::<32>(encoded_key).map_err(|_| {
6178            anyhow::anyhow!(
6179                "Signer private key in '{}' is not a valid hex private key",
6180                self.config.signer_private_key_env
6181            )
6182        })?);
6183        let signer = PrivateKeySigner::from_slice(&key_bytes[..]).map_err(|_| {
6184            anyhow::anyhow!(
6185                "Signer private key in '{}' is not a valid secp256k1 private key",
6186                self.config.signer_private_key_env
6187            )
6188        })?;
6189
6190        if signer.address() != self.wallet_address {
6191            anyhow::bail!(
6192                "Signer address {} derived from '{}' does not match configured wallet address {}",
6193                signer.address(),
6194                self.config.signer_private_key_env,
6195                self.wallet_address
6196            );
6197        }
6198
6199        self.signer = Some(Arc::new(signer));
6200        drop(payload_connect_lease);
6201
6202        if self.cache.has_database()
6203            && let Err(e) = self.reconcile_unresolved_execution().await
6204        {
6205            self.signer = None;
6206            return Err(e);
6207        }
6208
6209        if let Err(e) = self.refresh_wallet_balances().await {
6210            self.signer = None;
6211            return Err(e);
6212        }
6213        self.core.set_connected();
6214        setup_guard.disarm();
6215        log::info!(
6216            "Blockchain execution client connected on chain {}",
6217            self.chain.name
6218        );
6219        Ok(())
6220    }
6221
6222    async fn disconnect(&mut self) -> anyhow::Result<()> {
6223        self.pending_tasks.begin_shutdown();
6224        let tasks_result = self
6225            .pending_tasks
6226            .finish_shutdown(Duration::from_secs(5), Duration::from_secs(2))
6227            .await;
6228        self.signer = None;
6229        self.core.set_disconnected();
6230        tasks_result
6231            .map(|_| ())
6232            .map_err(|e| anyhow::anyhow!("Failed to terminate blockchain submissions: {e}"))?;
6233        Ok(())
6234    }
6235
6236    async fn generate_order_status_report(
6237        &self,
6238        _cmd: &GenerateOrderStatusReport,
6239    ) -> anyhow::Result<Option<OrderStatusReport>> {
6240        anyhow::bail!("{VENUE_EXECUTION_REPORTS_UNSUPPORTED}");
6241    }
6242
6243    async fn generate_order_status_reports(
6244        &self,
6245        _cmd: &GenerateOrderStatusReports,
6246    ) -> anyhow::Result<Vec<OrderStatusReport>> {
6247        anyhow::bail!("{VENUE_EXECUTION_REPORTS_UNSUPPORTED}");
6248    }
6249
6250    async fn generate_fill_reports(
6251        &self,
6252        _cmd: GenerateFillReports,
6253    ) -> anyhow::Result<Vec<FillReport>> {
6254        anyhow::bail!("{VENUE_EXECUTION_REPORTS_UNSUPPORTED}");
6255    }
6256
6257    async fn generate_position_status_reports(
6258        &self,
6259        _cmd: &GeneratePositionStatusReports,
6260    ) -> anyhow::Result<Vec<PositionStatusReport>> {
6261        anyhow::bail!("{VENUE_EXECUTION_REPORTS_UNSUPPORTED}");
6262    }
6263
6264    async fn generate_mass_status(
6265        &self,
6266        _lookback_mins: Option<u64>,
6267    ) -> anyhow::Result<Option<ExecutionMassStatus>> {
6268        // Venue mass status is unsupported; durable intent reconciliation at connect
6269        // covers restart recovery, and Ok(None) keeps LiveNode startup safe
6270        log::warn!(
6271            "Mass status is not supported on the blockchain execution client; skipping venue reconciliation"
6272        );
6273        Ok(None)
6274    }
6275}
6276
6277fn validate_order(order: &impl Order) -> Result<(), OrderDeniedReason> {
6278    if order.is_reduce_only() {
6279        return Err(OrderDeniedReason::UnsupportedReduceOnly);
6280    }
6281
6282    Ok(())
6283}
6284
6285#[cfg(test)]
6286mod tests {
6287    use std::{
6288        cell::RefCell,
6289        rc::Rc,
6290        sync::atomic::{AtomicU64, Ordering},
6291    };
6292
6293    use alloy::{
6294        primitives::{address, aliases::I24},
6295        sol_types::SolValue,
6296    };
6297    use nautilus_common::{
6298        cache::Cache, live::runner::replace_exec_event_sender, messages::ExecutionEvent,
6299        testing::wait_until_async,
6300    };
6301    use nautilus_core::UUID4;
6302    use nautilus_infrastructure::sql::pg::{PostgresConnectOptions, get_postgres_connect_options};
6303    use nautilus_model::{
6304        defi::{
6305            PoolProfiler,
6306            chain::chains,
6307            data::block::BlockPosition,
6308            pool_analysis::{
6309                position::PoolPosition,
6310                snapshot::{PoolAnalytics, PoolSnapshot, PoolState},
6311            },
6312            tick_map::{tick::PoolTick, tick_math::get_tick_at_sqrt_ratio},
6313        },
6314        enums::AccountType,
6315        events::OrderEventAny,
6316        identifiers::{OrderListId, StrategyId, TraderId},
6317        orders::{OrderList, OrderTestBuilder},
6318        types::Price,
6319    };
6320    use rstest::rstest;
6321    use sqlx::postgres::{PgAdvisoryLock, PgAdvisoryLockKey, PgPoolOptions};
6322
6323    use super::*;
6324    use crate::{
6325        cache::database::tests::connect_test_database,
6326        config::{
6327            BlockchainCallEdgeManifest, BlockchainChainAnchorConfig, BlockchainContractManifest,
6328            BlockchainContractProbe, BlockchainContractRole, BlockchainDeploymentManifest,
6329            BlockchainPoolManifest, BlockchainProviderIdentity, BlockchainTokenManifest,
6330            BlockchainVerificationConfig, BlockchainVerificationProviderConfig, QuoteSpendLimit,
6331        },
6332        constants::BLOCKCHAIN_VENUE,
6333        exchanges::arbitrum::UNISWAP_V3,
6334        rpc::http::{
6335            EXECUTION_RPC_TIMEOUT_SECS,
6336            tests::mock::{MockRpcState, start_mock_rpc_server},
6337        },
6338    };
6339
6340    /// Polls for the receipt of a broadcast transaction until it exists or the poll bound
6341    /// is exhausted. A `null` receipt result is a legitimate pending response.
6342    async fn poll_for_receipt(
6343        http_rpc_client: &BlockchainHttpRpcClient,
6344        tx_hash: &B256,
6345        max_polls: u32,
6346        interval: Duration,
6347    ) -> anyhow::Result<Option<RpcTransactionReceipt>> {
6348        let mut last_error = None;
6349        let mut observed_pending = false;
6350
6351        for attempt in 0..max_polls {
6352            if attempt > 0 {
6353                tokio::time::sleep(interval).await;
6354            }
6355
6356            match http_rpc_client.get_transaction_receipt(tx_hash).await {
6357                Ok(Some(receipt)) => return Ok(Some(receipt)),
6358                Ok(None) => observed_pending = true,
6359                Err(e) => {
6360                    log::warn!(
6361                        "Receipt poll {}/{} for transaction {tx_hash} failed: {e}",
6362                        attempt + 1,
6363                        max_polls
6364                    );
6365                    last_error = Some(e);
6366                }
6367            }
6368        }
6369
6370        if !observed_pending && let Some(e) = last_error {
6371            return Err(e);
6372        }
6373
6374        Ok(None)
6375    }
6376
6377    const CHAIN_ID_ARBITRUM: &str =
6378        include_str!("../../test_data/execution/rpc_eth_chain_id_arbitrum.json");
6379    const CHAIN_ID_ETHEREUM: &str =
6380        include_str!("../../test_data/execution/rpc_eth_chain_id_ethereum.json");
6381    const GET_CODE_DEPLOYED: &str =
6382        include_str!("../../test_data/execution/rpc_eth_get_code_deployed.json");
6383    const GET_CODE_EMPTY: &str =
6384        include_str!("../../test_data/execution/rpc_eth_get_code_empty.json");
6385    const GET_BALANCE: &str = include_str!("../../test_data/execution/rpc_eth_get_balance.json");
6386    const GET_BALANCE_ZERO: &str =
6387        include_str!("../../test_data/execution/rpc_eth_get_balance_zero.json");
6388    const GET_BALANCE_INSUFFICIENT: &str =
6389        include_str!("../../test_data/execution/rpc_eth_get_balance_insufficient.json");
6390    const CALL_BALANCE: &str = include_str!("../../test_data/execution/rpc_eth_call_balance.json");
6391    const CALL_BALANCE_AFTER_WRAP: &str =
6392        include_str!("../../test_data/execution/rpc_eth_call_balance_after_wrap.json");
6393    const CALL_BALANCE_WETH: &str =
6394        include_str!("../../test_data/execution/rpc_eth_call_balance_weth.json");
6395    const CALL_BALANCE_USDC: &str =
6396        include_str!("../../test_data/execution/rpc_eth_call_balance_usdc.json");
6397    const CALL_BALANCE_WETH_UPDATED: &str =
6398        include_str!("../../test_data/execution/rpc_eth_call_balance_weth_updated.json");
6399    const CALL_BALANCE_USDC_UPDATED: &str =
6400        include_str!("../../test_data/execution/rpc_eth_call_balance_usdc_updated.json");
6401    const CALL_BOOL_TRUE: &str =
6402        include_str!("../../test_data/execution/rpc_eth_call_bool_true.json");
6403    const CALL_EMPTY: &str = include_str!("../../test_data/execution/rpc_eth_call_empty.json");
6404    const CALL_ZERO: &str = include_str!("../../test_data/execution/rpc_eth_call_zero.json");
6405    const CALL_ALLOWANCE: &str =
6406        include_str!("../../test_data/execution/rpc_eth_call_allowance.json");
6407    const CALL_ALLOWANCE_1000: &str = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0x00000000000000000000000000000000000000000000000000000000000003e8\"}";
6408    const CALL_ALLOWANCE_MAX: &str = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\"}";
6409    const CALL_FACTORY: &str = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0x0000000000000000000000001f98431c8ad98523631ae4a59f267346ea31f984\"}";
6410    const CALL_WETH: &str = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0x00000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab1\"}";
6411    const CALL_USDC: &str = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0x000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e5831\"}";
6412    const CALL_FEE_500: &str = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0x00000000000000000000000000000000000000000000000000000000000001f4\"}";
6413    const CALL_POOL: &str = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0x000000000000000000000000c6962004f452be9203591991d15f6b388e09e8d0\"}";
6414    const CALL_REVERTED: &str =
6415        r#"{"jsonrpc":"2.0","id":1,"error":{"code":3,"message":"execution reverted"}}"#;
6416    const STORAGE_ZERO: &str = r#"{"jsonrpc":"2.0","id":1,"result":"0x0000000000000000000000000000000000000000000000000000000000000000"}"#;
6417    const TRACE_UNKNOWN_TRANSACTION: &str =
6418        r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32000,"message":"transaction not found"}}"#;
6419    const CALL_DECIMALS_18: &str = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0x0000000000000000000000000000000000000000000000000000000000000012\"}";
6420    const CALL_DECIMALS_6: &str = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0x0000000000000000000000000000000000000000000000000000000000000006\"}";
6421    const TRANSACTION_COUNT: &str =
6422        include_str!("../../test_data/execution/rpc_eth_get_transaction_count.json");
6423    const TRANSACTION_COUNT_NEXT: &str =
6424        include_str!("../../test_data/execution/rpc_eth_get_transaction_count_next.json");
6425    const ESTIMATE_GAS: &str = include_str!("../../test_data/execution/rpc_eth_estimate_gas.json");
6426    const MAX_PRIORITY_FEE: &str =
6427        include_str!("../../test_data/execution/rpc_eth_max_priority_fee_per_gas.json");
6428    const BLOCK_BY_NUMBER: &str =
6429        include_str!("../../test_data/execution/rpc_eth_get_block_by_number.json");
6430    const BLOCK_CANONICAL: &str =
6431        include_str!("../../test_data/execution/rpc_eth_get_block_canonical.json");
6432    const BLOCK_FINALIZED: &str =
6433        include_str!("../../test_data/execution/rpc_eth_get_block_finalized.json");
6434    const RECEIPT_SUCCESS: &str =
6435        include_str!("../../test_data/execution/rpc_eth_get_transaction_receipt_success.json");
6436    const RECEIPT_REVERTED: &str =
6437        include_str!("../../test_data/execution/rpc_eth_get_transaction_receipt_reverted.json");
6438    const RECEIPT_NULL: &str =
6439        include_str!("../../test_data/execution/rpc_eth_get_transaction_receipt_null.json");
6440    const SEND_RAW_TRANSACTION: &str =
6441        include_str!("../../test_data/execution/rpc_eth_send_raw_transaction.json");
6442    const SEND_RAW_TRANSACTION_REJECTED: &str =
6443        include_str!("../../test_data/execution/rpc_eth_send_raw_transaction_rejected.json");
6444    const SEND_RAW_TRANSACTION_NONCE_TOO_LOW: &str =
6445        include_str!("../../test_data/execution/rpc_eth_send_raw_transaction_nonce_too_low.json");
6446    const RPC_METHOD_NOT_FOUND: &str =
6447        include_str!("../../test_data/execution/rpc_error_method_not_found.json");
6448
6449    const WALLET: &str = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266";
6450    const ROUTER: &str = "0xE592427A0AEce92De3Edee1F18E0157C05861564";
6451    const WETH: &str = "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1";
6452    const USDC: &str = "0xaf88d065e77c8cC2239327C5EDb3A432268e5831";
6453    const TEST_TIMEOUT: Duration = Duration::from_secs(10);
6454
6455    const WETH_ADDRESS: Address = address!("82aF49447D8a07e3bd95BD0d56f35241523fBab1");
6456    const USDC_ADDRESS: Address = address!("af88d065e77c8cC2239327C5EDb3A432268e5831");
6457    const ROUTER_ADDRESS: Address = address!("E592427A0AEce92De3Edee1F18E0157C05861564");
6458
6459    // Anvil development key for WALLET (public, test-only)
6460    const TEST_PRIVATE_KEY: &str =
6461        "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
6462
6463    const BALANCE_OF_SELECTOR: &str = "0x70a08231";
6464    const ALLOWANCE_SELECTOR: &str = "0xdd62ed3e";
6465    const POOL_TOKEN0_SELECTOR: &str = "0x0dfe1681";
6466    const POOL_TOKEN1_SELECTOR: &str = "0xd21220a7";
6467    const POOL_FEE_SELECTOR: &str = "0xddca3f43";
6468    const DECIMALS_SELECTOR: &str = "0x313ce567";
6469    const FACTORY_SELECTOR: &str = "0xc45a0155";
6470    const WETH9_SELECTOR: &str = "0x4aa4a4fc";
6471    const GET_POOL_SELECTOR: &str = "0x1698ee82";
6472    const QUOTE_EXACT_INPUT_SELECTOR: &str = "0xc6a5026a";
6473    const QUOTE_EXACT_OUTPUT_SELECTOR: &str = "0xbd21704a";
6474
6475    fn test_pool() -> Pool {
6476        let chain = Arc::new(chains::ARBITRUM.clone());
6477        let dex = UNISWAP_V3.dex.clone();
6478        let weth = Token::new(
6479            chain.clone(),
6480            address!("82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
6481            "Wrapped Ether".to_string(),
6482            "WETH".to_string(),
6483            18,
6484        );
6485        let usdc = Token::new(
6486            chain.clone(),
6487            address!("af88d065e77c8cC2239327C5EDb3A432268e5831"),
6488            "USD Coin".to_string(),
6489            "USDC".to_string(),
6490            6,
6491        );
6492
6493        Pool::new(
6494            chain,
6495            dex,
6496            address!("C6962004f452bE9203591991D15f6b388e09E8D0"),
6497            PoolIdentifier::from_address(address!("C6962004f452bE9203591991D15f6b388e09E8D0")),
6498            55_000_000,
6499            weth,
6500            usdc,
6501            Some(500),
6502            Some(10),
6503            UnixNanos::default(),
6504        )
6505    }
6506
6507    fn test_config(http_rpc_url: String) -> BlockchainExecutionClientConfig {
6508        test_config_with_signer_env(http_rpc_url, "BLOCKCHAIN_TEST_PRIVATE_KEY")
6509    }
6510
6511    fn test_config_with_signer_env(
6512        http_rpc_url: String,
6513        signer_env: &str,
6514    ) -> BlockchainExecutionClientConfig {
6515        let verifier_separator = if http_rpc_url.contains('?') { '&' } else { '?' };
6516        let code_hash = keccak256(
6517            hex::decode("6080604052348015600e575f5ffd5b5060").expect("valid test bytecode"),
6518        )
6519        .to_string();
6520        let result = |response: &str| {
6521            serde_json::from_str::<serde_json::Value>(response).unwrap()["result"]
6522                .as_str()
6523                .unwrap()
6524                .to_string()
6525        };
6526        let probe = |call_data: &str, expected_output: String| BlockchainContractProbe {
6527            call_data: call_data.to_string(),
6528            expected_output,
6529        };
6530        let contract = |address: &str, role| {
6531            let probes = match role {
6532                BlockchainContractRole::Router => vec![
6533                    probe(FACTORY_SELECTOR, result(CALL_FACTORY)),
6534                    probe(WETH9_SELECTOR, result(CALL_WETH)),
6535                ],
6536                BlockchainContractRole::Factory => vec![probe(
6537                    &hex::encode_prefixed(
6538                        UniswapV3Factory::getPoolCall {
6539                            tokenA: WETH_ADDRESS,
6540                            tokenB: USDC_ADDRESS,
6541                            fee: U24::try_from(500u32).unwrap(),
6542                        }
6543                        .abi_encode(),
6544                    ),
6545                    result(CALL_POOL),
6546                )],
6547                BlockchainContractRole::WrappedNative => {
6548                    vec![probe(DECIMALS_SELECTOR, result(CALL_DECIMALS_18))]
6549                }
6550                BlockchainContractRole::Quote => {
6551                    vec![probe(FACTORY_SELECTOR, result(CALL_FACTORY))]
6552                }
6553                BlockchainContractRole::Token => {
6554                    vec![probe(DECIMALS_SELECTOR, result(CALL_DECIMALS_6))]
6555                }
6556                BlockchainContractRole::Pool => vec![
6557                    probe(POOL_TOKEN0_SELECTOR, result(CALL_WETH)),
6558                    probe(POOL_TOKEN1_SELECTOR, result(CALL_USDC)),
6559                    probe(POOL_FEE_SELECTOR, result(CALL_FEE_500)),
6560                ],
6561                BlockchainContractRole::Implementation => Vec::new(),
6562            };
6563            BlockchainContractManifest {
6564                address: address.to_string(),
6565                role,
6566                runtime_code_hash: code_hash.clone(),
6567                proxy: None,
6568                probes,
6569            }
6570        };
6571        let deployment_manifest = BlockchainDeploymentManifest {
6572            version: "test-v1".to_string(),
6573            chain_id: chains::ARBITRUM.chain_id,
6574            chain_name: chains::ARBITRUM.name.to_string(),
6575            contracts: vec![
6576                contract(ROUTER, BlockchainContractRole::Router),
6577                contract(
6578                    "0x1F98431c8aD98523631AE4a59f267346ea31F984",
6579                    BlockchainContractRole::Factory,
6580                ),
6581                contract(WETH, BlockchainContractRole::WrappedNative),
6582                contract(
6583                    "0x61fFE014bA17989E743c5F6cB21bF9697530B21e",
6584                    BlockchainContractRole::Quote,
6585                ),
6586                contract(USDC, BlockchainContractRole::Token),
6587                contract(
6588                    "0xC6962004f452bE9203591991D15f6b388e09E8D0",
6589                    BlockchainContractRole::Pool,
6590                ),
6591            ],
6592            tokens: vec![
6593                BlockchainTokenManifest {
6594                    address: WETH.to_string(),
6595                    name: "Wrapped Ether".to_string(),
6596                    symbol: "WETH".to_string(),
6597                    decimals: 18,
6598                    asset_role: "both".to_string(),
6599                },
6600                BlockchainTokenManifest {
6601                    address: USDC.to_string(),
6602                    name: "USD Coin".to_string(),
6603                    symbol: "USDC".to_string(),
6604                    decimals: 6,
6605                    asset_role: "both".to_string(),
6606                },
6607            ],
6608            pools: vec![BlockchainPoolManifest {
6609                address: "0xC6962004f452bE9203591991D15f6b388e09E8D0".to_string(),
6610                token0: WETH.to_string(),
6611                token1: USDC.to_string(),
6612                fee: 500,
6613                factory: "0x1F98431c8aD98523631AE4a59f267346ea31F984".to_string(),
6614                quote_contract: "0x61fFE014bA17989E743c5F6cB21bF9697530B21e".to_string(),
6615            }],
6616            call_edges: ["swap_sell", "swap_buy"]
6617                .into_iter()
6618                .map(|purpose| BlockchainCallEdgeManifest {
6619                    purpose: purpose.to_string(),
6620                    caller: ROUTER.to_string(),
6621                    target: "0xC6962004f452bE9203591991D15f6b388e09E8D0".to_string(),
6622                    call_type: "call".to_string(),
6623                })
6624                .collect(),
6625        };
6626        let manifest_digest =
6627            keccak256(serde_json::to_vec(&deployment_manifest).unwrap()).to_string();
6628        let verification = BlockchainVerificationConfig {
6629            authoritative: BlockchainProviderIdentity {
6630                provider_id: "authoritative".to_string(),
6631                operator_id: "operator-a".to_string(),
6632                failure_domain_ids: vec!["domain-a".to_string()],
6633            },
6634            verifiers: vec![
6635                BlockchainVerificationProviderConfig {
6636                    identity: BlockchainProviderIdentity {
6637                        provider_id: "verifier-a".to_string(),
6638                        operator_id: "operator-b".to_string(),
6639                        failure_domain_ids: vec!["domain-b".to_string()],
6640                    },
6641                    http_rpc_url: format!("{http_rpc_url}{verifier_separator}source=verifier-a")
6642                        .into(),
6643                },
6644                BlockchainVerificationProviderConfig {
6645                    identity: BlockchainProviderIdentity {
6646                        provider_id: "verifier-b".to_string(),
6647                        operator_id: "operator-c".to_string(),
6648                        failure_domain_ids: vec!["domain-c".to_string()],
6649                    },
6650                    http_rpc_url: format!("{http_rpc_url}{verifier_separator}source=verifier-b")
6651                        .into(),
6652                },
6653            ],
6654            chain_anchor: BlockchainChainAnchorConfig {
6655                chain_id: chains::ARBITRUM.chain_id,
6656                chain_name: chains::ARBITRUM.name.to_string(),
6657                checkpoint_height: 30_346_560,
6658                checkpoint_hash:
6659                    "0x1111111111111111111111111111111111111111111111111111111111111111".to_string(),
6660                checkpoint_timestamp: 1_761_888_800,
6661                max_head_skew_blocks: 3,
6662                max_head_age_secs: u64::MAX,
6663                max_future_drift_secs: u64::MAX,
6664            },
6665            manifest_version: "test-v1".to_string(),
6666            manifest_digest,
6667            deployment_manifest,
6668        };
6669        BlockchainExecutionClientConfig::builder()
6670            .client_id(AccountId::from("BLOCKCHAIN-001"))
6671            .chain(chains::ARBITRUM.clone())
6672            .wallet_address(WALLET.to_string())
6673            .http_rpc_url(http_rpc_url.into())
6674            .verification(verification)
6675            .signer_private_key_env(signer_env.to_string())
6676            .router_addresses(vec![ROUTER.to_string()])
6677            .weth_address(WETH.to_string())
6678            .max_fee_per_gas_wei(1_000_000_000)
6679            .base_fee_buffer_bps(2_000)
6680            .gas_limit(1_000_000)
6681            .gas_buffer_bps(2_000)
6682            .allowed_token_pairs(vec![(WETH.to_string(), USDC.to_string())])
6683            .slippage_bps(50)
6684            .max_slippage_bps(200)
6685            .max_order_amount(1_000_000_000_000_000_000)
6686            .deadline_seconds(300)
6687            .max_quote_age_blocks(100)
6688            .receipt_timeout_secs(1)
6689            .build()
6690    }
6691
6692    fn buy_test_config(http_rpc_url: String) -> BlockchainExecutionClientConfig {
6693        let max_amount = expected_buy_amount_in().to_string();
6694        let mut config = test_config(http_rpc_url);
6695        config.allowed_token_pairs = Some(vec![
6696            (WETH.to_string(), USDC.to_string()),
6697            (USDC.to_string(), WETH.to_string()),
6698        ]);
6699        config.quote_spend_limits = Some(vec![quote_spend_limit(USDC, WETH, 6, &max_amount)]);
6700        config
6701    }
6702
6703    fn refresh_test_manifest_digest(config: &mut BlockchainExecutionClientConfig) {
6704        let verification = config.verification.as_mut().unwrap();
6705        verification.manifest_digest =
6706            keccak256(serde_json::to_vec(&verification.deployment_manifest).unwrap()).to_string();
6707    }
6708
6709    fn quote_spend_limit(
6710        token_in: &str,
6711        token_out: &str,
6712        spend_token_decimals: u8,
6713        max_amount: &str,
6714    ) -> QuoteSpendLimit {
6715        QuoteSpendLimit::builder()
6716            .token_in(token_in.to_string())
6717            .token_out(token_out.to_string())
6718            .spend_token(token_in.to_string())
6719            .spend_token_decimals(spend_token_decimals)
6720            .max_amount(max_amount.to_string())
6721            .build()
6722    }
6723
6724    fn test_client_from_config(
6725        config: BlockchainExecutionClientConfig,
6726        pool: Pool,
6727    ) -> BlockchainExecutionClient {
6728        test_client_result(config, pool).unwrap()
6729    }
6730
6731    fn test_client_result(
6732        config: BlockchainExecutionClientConfig,
6733        pool: Pool,
6734    ) -> anyhow::Result<BlockchainExecutionClient> {
6735        test_client_and_cache(config, pool).map(|(client, _)| client)
6736    }
6737
6738    fn test_client_and_cache(
6739        config: BlockchainExecutionClientConfig,
6740        pool: Pool,
6741    ) -> anyhow::Result<(BlockchainExecutionClient, Rc<RefCell<Cache>>)> {
6742        let cache = Rc::new(RefCell::new(Cache::default()));
6743        cache.borrow_mut().add_pool(pool).unwrap();
6744        let core = ExecutionClientCore::new(
6745            TraderId::from("TRADER-001"),
6746            ClientId::from("BLOCKCHAIN-001"),
6747            *BLOCKCHAIN_VENUE,
6748            OmsType::Netting,
6749            AccountId::from("BLOCKCHAIN-001"),
6750            AccountType::Wallet,
6751            None,
6752            cache.clone(),
6753        );
6754
6755        let client = BlockchainExecutionClient::new(core, config)?;
6756        Ok((client, cache))
6757    }
6758
6759    fn test_client(http_rpc_url: String) -> BlockchainExecutionClient {
6760        test_client_from_config(test_config(http_rpc_url), test_pool())
6761    }
6762
6763    async fn client_with_mock_rpc(
6764        state: MockRpcState,
6765    ) -> (BlockchainExecutionClient, MockRpcState) {
6766        let addr = start_mock_rpc_server(state.clone()).await;
6767        (test_client(format!("http://{addr}")), state)
6768    }
6769
6770    async fn client_with_token_mock_rpc(
6771        state: MockRpcState,
6772        signer_env: &str,
6773    ) -> (BlockchainExecutionClient, MockRpcState, Rc<RefCell<Cache>>) {
6774        let state = with_connect_capabilities(state);
6775        let addr = start_mock_rpc_server(state.clone()).await;
6776        let pool = test_pool();
6777        let tokens = [pool.token0.clone(), pool.token1.clone()];
6778        let mut config = test_config_with_signer_env(format!("http://{addr}"), signer_env);
6779        config.tokens = Some(vec![WETH.to_string(), USDC.to_string()]);
6780        let (mut client, cache) = test_client_and_cache(config, pool).unwrap();
6781        for token in tokens {
6782            client.cache.add_token(token).await.unwrap();
6783        }
6784        (client, state, cache)
6785    }
6786
6787    fn execution_rpc_state() -> MockRpcState {
6788        MockRpcState::default()
6789            .with_receipt_hash_from_request()
6790            .with_response("eth_chainId", CHAIN_ID_ARBITRUM)
6791            .with_response("eth_getCode", GET_CODE_DEPLOYED)
6792            .with_response("eth_getBalance", GET_BALANCE)
6793            .with_response("eth_getBlockByNumber", BLOCK_BY_NUMBER)
6794            .with_response("eth_getTransactionCount", TRANSACTION_COUNT)
6795            .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", BLOCK_CANONICAL)
6796            .with_parameter_response("eth_getBlockByNumber", "finalized", BLOCK_FINALIZED)
6797            .with_parameter_response("eth_getBlockByNumber", "0x1cf0d42", BLOCK_FINALIZED)
6798            .with_response("eth_maxPriorityFeePerGas", MAX_PRIORITY_FEE)
6799            .with_call_response(FACTORY_SELECTOR, CALL_FACTORY)
6800            .with_call_response(WETH9_SELECTOR, CALL_WETH)
6801            .with_call_response(GET_POOL_SELECTOR, CALL_POOL)
6802            .with_call_response(POOL_TOKEN0_SELECTOR, CALL_WETH)
6803            .with_call_response(POOL_TOKEN1_SELECTOR, CALL_USDC)
6804            .with_call_response(POOL_FEE_SELECTOR, CALL_FEE_500)
6805            .with_contract_call_response(WETH, DECIMALS_SELECTOR, CALL_DECIMALS_18)
6806            .with_contract_call_response(USDC, DECIMALS_SELECTOR, CALL_DECIMALS_6)
6807            .with_call_response(
6808                QUOTE_EXACT_INPUT_SELECTOR,
6809                &quote_response(expected_sell_quote_amount()),
6810            )
6811            .with_call_response(
6812                QUOTE_EXACT_OUTPUT_SELECTOR,
6813                &quote_response(expected_buy_amount_in()),
6814            )
6815    }
6816
6817    fn ready_rpc_state() -> MockRpcState {
6818        with_connect_capabilities(execution_rpc_state())
6819            .with_call_response(BALANCE_OF_SELECTOR, CALL_BALANCE)
6820            .with_call_response(ALLOWANCE_SELECTOR, CALL_ALLOWANCE)
6821    }
6822
6823    fn with_connect_capabilities(state: MockRpcState) -> MockRpcState {
6824        state
6825            .with_response("eth_getStorageAt", STORAGE_ZERO)
6826            .with_response("eth_estimateGas", ESTIMATE_GAS)
6827            .with_parameter_response(
6828                "debug_traceTransaction",
6829                &B256::ZERO.to_string(),
6830                TRACE_UNKNOWN_TRANSACTION,
6831            )
6832    }
6833
6834    fn signing_rpc_state() -> MockRpcState {
6835        ready_rpc_state()
6836            .with_response("eth_getTransactionCount", TRANSACTION_COUNT)
6837            .with_response("eth_estimateGas", ESTIMATE_GAS)
6838    }
6839
6840    fn broadcast_rpc_state() -> MockRpcState {
6841        execution_rpc_state()
6842            .with_response("eth_getTransactionCount", TRANSACTION_COUNT)
6843            .with_response("eth_estimateGas", ESTIMATE_GAS)
6844            .with_response("eth_getTransactionReceipt", RECEIPT_SUCCESS)
6845            .with_send_raw_transaction_echo()
6846            .with_call_response_sequence(ALLOWANCE_SELECTOR, &[CALL_ZERO, CALL_ALLOWANCE_1000])
6847    }
6848
6849    async fn expected_wrap_tx_hash(value: U256) -> B256 {
6850        expected_tx_hash(
6851            WETH_ADDRESS,
6852            value,
6853            Bytes::from(nautilus_core::hex::decode("d0e30db0").unwrap()),
6854        )
6855        .await
6856    }
6857
6858    async fn expected_approve_tx_hash(amount: U256) -> B256 {
6859        let calldata = ERC20::approveCall {
6860            spender: ROUTER_ADDRESS,
6861            amount,
6862        }
6863        .abi_encode();
6864        expected_tx_hash(WETH_ADDRESS, U256::ZERO, Bytes::from(calldata)).await
6865    }
6866
6867    async fn expected_tx_hash(to: Address, value: U256, input: Bytes) -> B256 {
6868        // The orchestration's policy math is deterministic: nonce 7 from the fixture,
6869        // buffered gas 78000, buffered max fee 130000000, priority fee 10000000
6870        let expected_tx =
6871            build_eip1559_transaction(42161, 7, 78_000, 130_000_000, 10_000_000, to, value, input);
6872        let (expected_hash, _) = sign_eip1559_transaction(
6873            expected_tx,
6874            &PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
6875        )
6876        .await
6877        .unwrap();
6878        expected_hash
6879    }
6880
6881    async fn await_recorded_requests(state: &MockRpcState, method: &str, expected: usize) {
6882        wait_until_async(
6883            || async {
6884                state
6885                    .recorded_requests()
6886                    .iter()
6887                    .filter(|request| request["method"] == method)
6888                    .count()
6889                    >= expected
6890            },
6891            TEST_TIMEOUT,
6892        )
6893        .await;
6894    }
6895
6896    /// The block number served by the `eth_getBlockByNumber` fixture; swap quotes pin their
6897    /// profiler state to it so the freshness check passes.
6898    const FIXTURE_BLOCK: u64 = 30_346_560;
6899    const FIXTURE_BLOCK_PARAM: &str = "0x1cf0d40";
6900    const FIXTURE_BLOCK_HASH: &str =
6901        "0x1111111111111111111111111111111111111111111111111111111111111111";
6902    /// The timestamp served by the `eth_getBlockByNumber` fixture.
6903    const FIXTURE_BLOCK_TIMESTAMP: u64 = 1_761_888_800;
6904    /// Synthetic full-range liquidity for the test pool profiler.
6905    const TEST_LIQUIDITY: u128 = 1_000_000_000_000_000_000_000;
6906
6907    fn test_profiler(pool: &Pool, block_number: u64) -> PoolProfiler {
6908        test_profiler_at_block(pool, block_number, FIXTURE_BLOCK_HASH)
6909    }
6910
6911    fn test_profiler_at_block(pool: &Pool, block_number: u64, block_hash: &str) -> PoolProfiler {
6912        test_profiler_with_range(
6913            pool,
6914            block_number,
6915            block_hash,
6916            U160::from(1u128 << 96),
6917            -887_220,
6918            887_220,
6919            TEST_LIQUIDITY,
6920        )
6921    }
6922
6923    fn test_profiler_with_state(
6924        pool: &Pool,
6925        block_number: u64,
6926        sqrt_price_x96: U160,
6927        liquidity: u128,
6928    ) -> PoolProfiler {
6929        test_profiler_with_range(
6930            pool,
6931            block_number,
6932            FIXTURE_BLOCK_HASH,
6933            sqrt_price_x96,
6934            -887_220,
6935            887_220,
6936            liquidity,
6937        )
6938    }
6939
6940    /// Builds a profiler from a synthetic snapshot with a single position over
6941    /// `[tick_lower, tick_upper]`: exact-input quotes within the range match the
6942    /// constant-product curve, and a tiny test swap never crosses a tick.
6943    fn test_profiler_with_range(
6944        pool: &Pool,
6945        block_number: u64,
6946        block_hash: &str,
6947        sqrt_price_x96: U160,
6948        tick_lower: i32,
6949        tick_upper: i32,
6950        liquidity: u128,
6951    ) -> PoolProfiler {
6952        let snapshot = PoolSnapshot::new(
6953            pool.instrument_id,
6954            PoolState {
6955                current_tick: get_tick_at_sqrt_ratio(sqrt_price_x96),
6956                price_sqrt_ratio_x96: sqrt_price_x96,
6957                liquidity,
6958                protocol_fees_token0: U256::ZERO,
6959                protocol_fees_token1: U256::ZERO,
6960                fee_protocol: 0,
6961                fee_protocol0_basis_points: None,
6962                fee_protocol1_basis_points: None,
6963                fee_growth_global_0: U256::ZERO,
6964                fee_growth_global_1: U256::ZERO,
6965            },
6966            vec![PoolPosition::new(
6967                address!("DeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF"),
6968                tick_lower,
6969                tick_upper,
6970                liquidity as i128,
6971            )],
6972            vec![
6973                PoolTick::new(
6974                    tick_lower,
6975                    liquidity,
6976                    liquidity as i128,
6977                    U256::ZERO,
6978                    U256::ZERO,
6979                    true,
6980                    0,
6981                ),
6982                PoolTick::new(
6983                    tick_upper,
6984                    liquidity,
6985                    -(liquidity as i128),
6986                    U256::ZERO,
6987                    U256::ZERO,
6988                    true,
6989                    0,
6990                ),
6991            ],
6992            PoolAnalytics::default(),
6993            BlockPosition::new(
6994                block_number,
6995                block_hash.to_string(),
6996                BLOCK_SCOPED_SNAPSHOT_INDEX,
6997                BLOCK_SCOPED_SNAPSHOT_INDEX,
6998            )
6999            .with_block_hash(Some(block_hash.to_string())),
7000            UnixNanos::default(),
7001            UnixNanos::default(),
7002        );
7003        let mut profiler = PoolProfiler::new(Arc::new(pool.clone()));
7004        profiler.restore_from_snapshot(snapshot).unwrap();
7005        profiler
7006    }
7007
7008    fn test_market_sell_order(instrument_id: InstrumentId) -> OrderAny {
7009        market_sell_order_with_id(instrument_id, "O-SWAP-001")
7010    }
7011
7012    fn test_market_buy_order(instrument_id: InstrumentId) -> OrderAny {
7013        market_buy_order_with_id(instrument_id, "O-SWAP-BUY-001")
7014    }
7015
7016    fn submit_order_cmd(order: &OrderAny) -> SubmitOrder {
7017        SubmitOrder::new(
7018            TraderId::from("TRADER-001"),
7019            Some(ClientId::from("BLOCKCHAIN-001")),
7020            order.strategy_id(),
7021            order.instrument_id(),
7022            order.client_order_id(),
7023            order.init_event().clone(),
7024            None,
7025            None,
7026            None,
7027            UUID4::new(),
7028            UnixNanos::default(),
7029            None,
7030        )
7031    }
7032
7033    /// Builds a client whose shared cache holds the test pool, a live profiler at
7034    /// `FIXTURE_BLOCK`, and a SELL market order, without a durable store.
7035    fn swap_client_with_cache(
7036        config: BlockchainExecutionClientConfig,
7037    ) -> (BlockchainExecutionClient, Rc<RefCell<Cache>>) {
7038        let cache = Rc::new(RefCell::new(Cache::default()));
7039        let pool = test_pool();
7040        cache.borrow_mut().add_pool(pool.clone()).unwrap();
7041        cache
7042            .borrow_mut()
7043            .add_order(
7044                test_market_sell_order(pool.instrument_id),
7045                None,
7046                None,
7047                false,
7048            )
7049            .unwrap();
7050        cache
7051            .borrow_mut()
7052            .add_pool_profiler(test_profiler_at_block(
7053                &pool,
7054                FIXTURE_BLOCK,
7055                FIXTURE_BLOCK_HASH,
7056            ))
7057            .unwrap();
7058        let core = ExecutionClientCore::new(
7059            TraderId::from("TRADER-001"),
7060            ClientId::from("BLOCKCHAIN-001"),
7061            *BLOCKCHAIN_VENUE,
7062            OmsType::Netting,
7063            AccountId::from("BLOCKCHAIN-001"),
7064            AccountType::Wallet,
7065            None,
7066            cache.clone(),
7067        );
7068
7069        let client = BlockchainExecutionClient::new(core, config).unwrap();
7070        (client, cache)
7071    }
7072
7073    /// Builds a connected, signer-equipped swap client backed by a fresh Postgres schema.
7074    async fn swap_client_with_database(
7075        test_name: &str,
7076        state: MockRpcState,
7077    ) -> Option<(
7078        sqlx::PgPool,
7079        String,
7080        BlockchainExecutionClient,
7081        MockRpcState,
7082        Rc<RefCell<Cache>>,
7083    )> {
7084        swap_client_with_database_config(test_name, state, test_config).await
7085    }
7086
7087    async fn swap_client_with_buy_database(
7088        test_name: &str,
7089        state: MockRpcState,
7090    ) -> Option<(
7091        sqlx::PgPool,
7092        String,
7093        BlockchainExecutionClient,
7094        MockRpcState,
7095        Rc<RefCell<Cache>>,
7096    )> {
7097        swap_client_with_database_config(test_name, state, buy_test_config).await
7098    }
7099
7100    async fn swap_client_with_database_config<F>(
7101        test_name: &str,
7102        state: MockRpcState,
7103        config: F,
7104    ) -> Option<(
7105        sqlx::PgPool,
7106        String,
7107        BlockchainExecutionClient,
7108        MockRpcState,
7109        Rc<RefCell<Cache>>,
7110    )>
7111    where
7112        F: FnOnce(String) -> BlockchainExecutionClientConfig,
7113    {
7114        let (admin_pool, pg_config) = connect_test_postgres(test_name).await?;
7115        let schema = format!("{test_name}_{}", std::process::id());
7116        setup_execution_schema(&admin_pool, &schema).await;
7117
7118        let db_options: sqlx::postgres::PgConnectOptions = pg_config.into();
7119        let db_options = db_options.options([("search_path", schema.clone())]);
7120        let database = connect_test_database(db_options).await.unwrap();
7121        let addr = start_mock_rpc_server(state.clone()).await;
7122        let (mut client, cache) = swap_client_with_cache(config(format!("http://{addr}")));
7123        client.cache.database = Some(database);
7124        // Mirror the connect-time migration: tests create the pre-submission table shape
7125        client
7126            .cache
7127            .ensure_execution_transaction_schema()
7128            .await
7129            .unwrap();
7130        protect_test_storage(&mut client, &schema).await;
7131        initialize_test_verification_ledger(&client).await;
7132        client.signer = Some(Arc::new(
7133            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
7134        ));
7135        client.core.set_connected();
7136
7137        Some((admin_pool, schema, client, state, cache))
7138    }
7139
7140    async fn initialize_test_verification_ledger(client: &BlockchainExecutionClient) {
7141        initialize_test_verification_ledger_with_headers(
7142            client,
7143            &[ExecutionVerifiedHeader {
7144                number: FIXTURE_BLOCK,
7145                hash: FIXTURE_BLOCK_HASH.to_string(),
7146                parent_hash: "0x0000000000000000000000000000000000000000000000000000000000000001"
7147                    .to_string(),
7148                timestamp: FIXTURE_BLOCK_TIMESTAMP,
7149                base_fee_per_gas: Some(100_000_000),
7150            }],
7151        )
7152        .await;
7153    }
7154
7155    async fn initialize_test_verification_ledger_with_headers(
7156        client: &BlockchainExecutionClient,
7157        finalized_headers: &[ExecutionVerifiedHeader],
7158    ) {
7159        ensure_test_verification_ledger(client, finalized_headers, 7, 7)
7160            .await
7161            .unwrap();
7162    }
7163
7164    async fn ensure_test_verification_ledger(
7165        client: &BlockchainExecutionClient,
7166        finalized_headers: &[ExecutionVerifiedHeader],
7167        next_canonical_nonce: u64,
7168        observed_canonical_nonce: u64,
7169    ) -> anyhow::Result<()> {
7170        let verification = client.config.verification.as_ref().unwrap();
7171        let provider_ids = vec![
7172            "authoritative".to_string(),
7173            "verifier-a".to_string(),
7174            "verifier-b".to_string(),
7175        ];
7176        let operator_ids = vec![
7177            "operator-a".to_string(),
7178            "operator-b".to_string(),
7179            "operator-c".to_string(),
7180        ];
7181        let failure_domain_ids = vec![
7182            "domain-a".to_string(),
7183            "domain-b".to_string(),
7184            "domain-c".to_string(),
7185        ];
7186        let decisions = [ExecutionVerificationDecision {
7187            read_class: "numbered_block",
7188            height_start: Some(FIXTURE_BLOCK),
7189            height_end: Some(FIXTURE_BLOCK),
7190            normalized_value_digest: B256::ZERO.to_string(),
7191        }];
7192        client
7193            .cache
7194            .database
7195            .as_ref()
7196            .unwrap()
7197            .ensure_execution_verification_schema(&ExecutionVerificationBootstrap {
7198                chain_id: 42_161,
7199                wallet_address: WALLET,
7200                manifest_version: &verification.manifest_version,
7201                manifest_digest: &verification.manifest_digest,
7202                checkpoint_number: FIXTURE_BLOCK,
7203                checkpoint_hash: FIXTURE_BLOCK_HASH,
7204                checkpoint_parent_hash:
7205                    "0x0000000000000000000000000000000000000000000000000000000000000001",
7206                checkpoint_timestamp: FIXTURE_BLOCK_TIMESTAMP,
7207                checkpoint_base_fee_per_gas: Some(100_000_000),
7208                finalized_headers,
7209                next_canonical_nonce,
7210                observed_canonical_nonce,
7211                provider_ids: &provider_ids,
7212                operator_ids: &operator_ids,
7213                failure_domain_ids: &failure_domain_ids,
7214                decisions: &decisions,
7215                migration: None,
7216            })
7217            .await
7218    }
7219
7220    async fn initialize_test_verification_migration(
7221        client: &BlockchainExecutionClient,
7222        finalized_headers: &[ExecutionVerifiedHeader],
7223        next_canonical_nonce: u64,
7224        decisions: &[ExecutionVerificationDecision],
7225        migration: &ExecutionVerificationMigration,
7226    ) {
7227        let verification = client.config.verification.as_ref().unwrap();
7228        let provider_ids = vec![
7229            "authoritative".to_string(),
7230            "verifier-a".to_string(),
7231            "verifier-b".to_string(),
7232        ];
7233        let operator_ids = vec![
7234            "operator-a".to_string(),
7235            "operator-b".to_string(),
7236            "operator-c".to_string(),
7237        ];
7238        let failure_domain_ids = vec![
7239            "domain-a".to_string(),
7240            "domain-b".to_string(),
7241            "domain-c".to_string(),
7242        ];
7243        client
7244            .cache
7245            .database
7246            .as_ref()
7247            .unwrap()
7248            .ensure_execution_verification_schema(&ExecutionVerificationBootstrap {
7249                chain_id: 42_161,
7250                wallet_address: WALLET,
7251                manifest_version: &verification.manifest_version,
7252                manifest_digest: &verification.manifest_digest,
7253                checkpoint_number: FIXTURE_BLOCK,
7254                checkpoint_hash: FIXTURE_BLOCK_HASH,
7255                checkpoint_parent_hash:
7256                    "0x0000000000000000000000000000000000000000000000000000000000000001",
7257                checkpoint_timestamp: FIXTURE_BLOCK_TIMESTAMP,
7258                checkpoint_base_fee_per_gas: Some(100_000_000),
7259                finalized_headers,
7260                next_canonical_nonce,
7261                observed_canonical_nonce: next_canonical_nonce,
7262                provider_ids: &provider_ids,
7263                operator_ids: &operator_ids,
7264                failure_domain_ids: &failure_domain_ids,
7265                decisions,
7266                migration: Some(migration),
7267            })
7268            .await
7269            .unwrap();
7270    }
7271
7272    async fn swap_rpc_state() -> MockRpcState {
7273        let min_amount_out = expected_min_amount_out(50);
7274        swap_rpc_state_with_min_amount_out(min_amount_out).await
7275    }
7276
7277    async fn swap_rpc_state_with_min_amount_out(min_amount_out: U256) -> MockRpcState {
7278        let (tx_hash, _) = expected_swap_tx(min_amount_out).await;
7279        finalized_swap_rpc_state(tx_hash, min_amount_out)
7280    }
7281
7282    async fn swap_rpc_state_for_mismatch() -> MockRpcState {
7283        swap_rpc_state()
7284            .await
7285            .with_response("eth_sendRawTransaction", SEND_RAW_TRANSACTION)
7286    }
7287
7288    fn awaiting_in_flight(client: &BlockchainExecutionClient) -> InFlightTransaction {
7289        let slot = *client.in_flight.lock();
7290        let Some(InFlightSlot::AwaitingFinality(in_flight)) = slot else {
7291            panic!("expected an awaiting-finality transaction, was {slot:?}");
7292        };
7293        in_flight
7294    }
7295
7296    fn recovering_in_flight(client: &BlockchainExecutionClient) -> RecoveryTransaction {
7297        let slot = *client.in_flight.lock();
7298        let Some(InFlightSlot::Recovering(recovery)) = slot else {
7299            panic!("expected a recovery transaction, was {slot:?}");
7300        };
7301        recovery
7302    }
7303
7304    async fn execution_intent_markers(
7305        admin_pool: &sqlx::PgPool,
7306        schema: &str,
7307    ) -> Vec<(String, String, bool, bool)> {
7308        sqlx::query_as(sqlx::AssertSqlSafe(format!(
7309            "SELECT purpose, status, terminal_emitted, active \
7310             FROM {schema}.execution_intent ORDER BY id"
7311        )))
7312        .fetch_all(admin_pool)
7313        .await
7314        .unwrap()
7315    }
7316
7317    #[allow(unsafe_code)] // env-var mutation in tests; unique names avoid cross-test races
7318    fn payload_test_keys(
7319        active: [u8; 32],
7320        retired: Vec<[u8; 32]>,
7321        deployment_id: &str,
7322    ) -> PayloadKeySet {
7323        static NEXT_ENV_ID: AtomicU64 = AtomicU64::new(0);
7324
7325        let env_id = NEXT_ENV_ID.fetch_add(1, Ordering::Relaxed);
7326        let active_env = format!("BLOCKCHAIN_TEST_PAYLOAD_KEY_{env_id}_ACTIVE");
7327        let retired_envs = retired
7328            .iter()
7329            .enumerate()
7330            .map(|(index, _)| format!("BLOCKCHAIN_TEST_PAYLOAD_KEY_{env_id}_RETIRED_{index}"))
7331            .collect::<Vec<_>>();
7332        // SAFETY: each invocation uses unique variable names and removes them before returning
7333        unsafe { std::env::set_var(&active_env, hex::encode(active)) };
7334        for (env, key) in retired_envs.iter().zip(&retired) {
7335            // SAFETY: the retired variable name is unique to this invocation
7336            unsafe { std::env::set_var(env, hex::encode(key)) };
7337        }
7338
7339        let keys = PayloadKeySet::load(Some(&active_env), &retired_envs, Some(deployment_id))
7340            .unwrap()
7341            .unwrap();
7342
7343        // SAFETY: no other test or thread uses these unique variable names
7344        unsafe { std::env::remove_var(active_env) };
7345        for env in retired_envs {
7346            // SAFETY: the retired variable name is unique to this invocation
7347            unsafe { std::env::remove_var(env) };
7348        }
7349        keys
7350    }
7351
7352    #[allow(unsafe_code)] // env-var mutation in tests; unique names avoid cross-test races
7353    fn set_test_payload_key(
7354        client: &mut BlockchainExecutionClient,
7355        key: [u8; 32],
7356        deployment_id: &str,
7357    ) -> PayloadKeySet {
7358        static NEXT_ENV_ID: AtomicU64 = AtomicU64::new(0);
7359
7360        let env_id = NEXT_ENV_ID.fetch_add(1, Ordering::Relaxed);
7361        let active_env = format!("BLOCKCHAIN_TEST_EXECUTION_KEY_{env_id}");
7362        // SAFETY: each invocation uses a unique variable name retained for reconnect tests
7363        unsafe { std::env::set_var(&active_env, hex::encode(key)) };
7364        client.config.payload_key_env = Some(active_env);
7365        client.config.payload_deployment_id = Some(deployment_id.to_string());
7366        client.load_payload_keys().unwrap().unwrap()
7367    }
7368
7369    async fn protect_test_storage(client: &mut BlockchainExecutionClient, deployment_id: &str) {
7370        let keys = set_test_payload_key(client, [0xa5; 32], deployment_id);
7371        client
7372            .cache
7373            .database
7374            .as_ref()
7375            .unwrap()
7376            .ensure_execution_payload_storage(&keys)
7377            .await
7378            .unwrap();
7379        client.payload_keys = Some(Arc::new(keys));
7380    }
7381
7382    async fn reserve_test_wrap_intent(database: &BlockchainCacheDatabase) -> ExecutionIntentRow {
7383        reserve_test_wrap_intent_for_wallet(database, WALLET).await
7384    }
7385
7386    async fn reserve_test_wrap_intent_for_wallet(
7387        database: &BlockchainCacheDatabase,
7388        wallet: &str,
7389    ) -> ExecutionIntentRow {
7390        database
7391            .reserve_execution_intent(&ExecutionIntentInsert {
7392                chain_id: 42161,
7393                wallet_address: wallet.to_string(),
7394                purpose: "wrap".to_string(),
7395                client_order_id: None,
7396                trader_id: None,
7397                strategy_id: None,
7398                account_id: None,
7399                instrument_id: None,
7400                pool_address: None,
7401                transaction_to: WETH_ADDRESS.to_string(),
7402                transaction_input: "0xd0e30db0".to_string(),
7403                transaction_value: "1".to_string(),
7404                amount_in: None,
7405                created_block: FIXTURE_BLOCK,
7406            })
7407            .await
7408            .unwrap()
7409    }
7410
7411    async fn persist_test_wrap_broadcast(
7412        database: &BlockchainCacheDatabase,
7413        keys: Option<&PayloadKeySet>,
7414    ) -> (ExecutionIntentRow, B256, Vec<u8>) {
7415        let intent = reserve_test_wrap_intent(database).await;
7416        database
7417            .assign_execution_intent_nonce(intent.id, 7)
7418            .await
7419            .unwrap();
7420        let intent = database.get_execution_intent(intent.id).await.unwrap();
7421        let transaction = build_eip1559_transaction(
7422            42161,
7423            7,
7424            78_000,
7425            130_000_000,
7426            10_000_000,
7427            WETH_ADDRESS,
7428            U256::from(1u64),
7429            Bytes::from(nautilus_core::hex::decode("d0e30db0").unwrap()),
7430        );
7431        let (tx_hash, raw_tx) = sign_eip1559_transaction(
7432            transaction,
7433            &PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
7434        )
7435        .await
7436        .unwrap();
7437        persist_test_payload(database, keys, &intent, tx_hash, &raw_tx).await;
7438        database
7439            .record_execution_status(
7440                intent.id,
7441                &tx_hash.to_string(),
7442                TransactionStatus::Broadcast,
7443                None,
7444                None,
7445                None,
7446                None,
7447                None,
7448            )
7449            .await
7450            .unwrap();
7451        (intent, tx_hash, raw_tx)
7452    }
7453
7454    async fn reserve_test_swap_intent(database: &BlockchainCacheDatabase) -> ExecutionIntentRow {
7455        let pool = test_pool();
7456        let order = test_market_sell_order(pool.instrument_id);
7457        let calldata = expected_swap_calldata(expected_min_amount_out(50));
7458
7459        database
7460            .reserve_execution_intent(&ExecutionIntentInsert {
7461                chain_id: 42161,
7462                wallet_address: WALLET.to_string(),
7463                purpose: "swap".to_string(),
7464                client_order_id: Some(order.client_order_id().to_string()),
7465                trader_id: Some(order.trader_id().to_string()),
7466                strategy_id: Some(order.strategy_id().to_string()),
7467                account_id: Some("BLOCKCHAIN-001".to_string()),
7468                instrument_id: Some(pool.instrument_id.to_string()),
7469                pool_address: Some(pool.address.to_string()),
7470                transaction_to: ROUTER_ADDRESS.to_string(),
7471                transaction_input: hex::encode_prefixed(&calldata),
7472                transaction_value: U256::ZERO.to_string(),
7473                amount_in: Some("1000000000000000".to_string()),
7474                created_block: FIXTURE_BLOCK,
7475            })
7476            .await
7477            .unwrap()
7478    }
7479
7480    async fn persist_invalid_test_swap(
7481        database: &BlockchainCacheDatabase,
7482        keys: Option<&PayloadKeySet>,
7483    ) -> (ExecutionIntentRow, B256) {
7484        let intent = reserve_test_swap_intent(database).await;
7485        database
7486            .assign_execution_intent_nonce(intent.id, 7)
7487            .await
7488            .unwrap();
7489        let intent = database.get_execution_intent(intent.id).await.unwrap();
7490        let (tx_hash, raw_transaction) = expected_swap_tx(expected_min_amount_out(50)).await;
7491        let mut raw_transaction = hex::decode(raw_transaction.strip_prefix("0x").unwrap()).unwrap();
7492        raw_transaction.push(0xff);
7493        persist_test_payload(database, keys, &intent, tx_hash, &raw_transaction).await;
7494        database
7495            .record_execution_status(
7496                intent.id,
7497                &tx_hash.to_string(),
7498                TransactionStatus::Broadcast,
7499                None,
7500                None,
7501                None,
7502                None,
7503                None,
7504            )
7505            .await
7506            .unwrap();
7507
7508        (intent, tx_hash)
7509    }
7510
7511    async fn persist_test_swap_broadcast(
7512        database: &BlockchainCacheDatabase,
7513        keys: Option<&PayloadKeySet>,
7514    ) -> (ExecutionIntentRow, B256, Vec<u8>) {
7515        let intent = reserve_test_swap_intent(database).await;
7516        database
7517            .assign_execution_intent_nonce(intent.id, 7)
7518            .await
7519            .unwrap();
7520        let intent = database.get_execution_intent(intent.id).await.unwrap();
7521        let (tx_hash, raw_transaction) = expected_swap_tx(expected_min_amount_out(50)).await;
7522        let raw_transaction = hex::decode(raw_transaction.strip_prefix("0x").unwrap()).unwrap();
7523        persist_test_payload(database, keys, &intent, tx_hash, &raw_transaction).await;
7524        database
7525            .record_execution_status(
7526                intent.id,
7527                &tx_hash.to_string(),
7528                TransactionStatus::Broadcast,
7529                None,
7530                None,
7531                None,
7532                None,
7533                None,
7534            )
7535            .await
7536            .unwrap();
7537        (intent, tx_hash, raw_transaction)
7538    }
7539
7540    async fn persist_test_payload(
7541        database: &BlockchainCacheDatabase,
7542        keys: Option<&PayloadKeySet>,
7543        intent: &ExecutionIntentRow,
7544        tx_hash: B256,
7545        raw_transaction: &[u8],
7546    ) {
7547        if let Some(keys) = keys {
7548            database.reserve_execution_payload_seal(keys).await.unwrap();
7549            let transaction_hash = tx_hash.to_string();
7550            let context =
7551                payload_context_identity(intent, &transaction_hash, 42_161, keys.deployment_id())
7552                    .unwrap();
7553            let envelope = keys.seal(raw_transaction, &context).unwrap();
7554            database
7555                .add_execution_transaction_envelope(intent.id, 42_161, &transaction_hash, &envelope)
7556                .await
7557                .unwrap();
7558        } else {
7559            database
7560                .add_execution_transaction_hash(
7561                    intent.id,
7562                    42_161,
7563                    &tx_hash.to_string(),
7564                    raw_transaction,
7565                )
7566                .await
7567                .unwrap();
7568        }
7569    }
7570
7571    async fn later_reconnect(
7572        previous: BlockchainExecutionClient,
7573        http_rpc_url: String,
7574    ) -> anyhow::Error {
7575        let database = previous.cache.database.as_ref().unwrap().clone();
7576        let payload_keys = previous.payload_keys.clone();
7577        drop(previous);
7578        let mut next = test_client(http_rpc_url);
7579        next.cache.database = Some(database);
7580        next.payload_keys = payload_keys;
7581        next.signer = Some(Arc::new(
7582            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
7583        ));
7584        next.reconcile_unresolved_execution().await.unwrap_err()
7585    }
7586
7587    fn start_with_events(
7588        client: &mut BlockchainExecutionClient,
7589    ) -> tokio::sync::mpsc::UnboundedReceiver<ExecutionEvent> {
7590        let (sender, receiver) = tokio::sync::mpsc::unbounded_channel();
7591        replace_exec_event_sender(sender);
7592        client.start().unwrap();
7593        receiver
7594    }
7595
7596    async fn await_pending_tasks(client: &BlockchainExecutionClient) {
7597        tokio::time::timeout(TEST_TIMEOUT, async {
7598            while !client.pending_tasks.all_finished() {
7599                tokio::time::sleep(Duration::from_millis(1)).await;
7600            }
7601        })
7602        .await
7603        .unwrap();
7604    }
7605
7606    fn collect_order_events(
7607        receiver: &mut tokio::sync::mpsc::UnboundedReceiver<ExecutionEvent>,
7608    ) -> Vec<OrderEventAny> {
7609        let mut events = Vec::new();
7610
7611        while let Ok(event) = receiver.try_recv() {
7612            if let ExecutionEvent::Order(order_event) = event {
7613                events.push(order_event);
7614            }
7615        }
7616        events
7617    }
7618
7619    fn assert_swap_quarantined_without_terminal_event(events: &[OrderEventAny]) {
7620        assert_eq!(events.len(), 1, "was: {events:?}");
7621        assert!(matches!(&events[0], OrderEventAny::Submitted(_)));
7622    }
7623
7624    fn assert_swap_submitted_and_filled(events: &[OrderEventAny]) {
7625        assert_eq!(events.len(), 2, "was: {events:?}");
7626        assert!(matches!(&events[0], OrderEventAny::Submitted(_)));
7627        assert!(matches!(&events[1], OrderEventAny::Filled(_)));
7628    }
7629
7630    fn expected_swap_calldata(min_amount_out: U256) -> Vec<u8> {
7631        UniswapV3SwapRouter::exactInputSingleCall {
7632            params: UniswapV3SwapRouter::ExactInputSingleParams {
7633                tokenIn: WETH_ADDRESS,
7634                tokenOut: address!("af88d065e77c8cC2239327C5EDb3A432268e5831"),
7635                fee: U24::try_from(500u32).unwrap(),
7636                recipient: address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266"),
7637                deadline: U256::from(FIXTURE_BLOCK_TIMESTAMP + 300),
7638                amountIn: U256::from(1_000_000_000_000_000u64),
7639                amountOutMinimum: min_amount_out,
7640                sqrtPriceLimitX96: U160::ZERO,
7641            },
7642        }
7643        .abi_encode()
7644    }
7645
7646    fn expected_min_amount_out(slippage_bps: u32) -> U256 {
7647        expected_min_amount_out_for(&test_pool(), true, slippage_bps)
7648    }
7649
7650    fn expected_min_amount_out_for(pool: &Pool, zero_for_one: bool, slippage_bps: u32) -> U256 {
7651        let profiler = test_profiler(pool, FIXTURE_BLOCK);
7652        let quote = profiler
7653            .swap_exact_in(U256::from(1_000_000_000_000_000u64), zero_for_one, None)
7654            .unwrap();
7655        let quoted = exact_output_amount(&quote, zero_for_one).unwrap();
7656        derive_min_amount_out(quoted, slippage_bps).unwrap()
7657    }
7658
7659    fn expected_sell_quote_amount() -> U256 {
7660        let profiler = test_profiler(&test_pool(), FIXTURE_BLOCK);
7661        let quote = profiler
7662            .swap_exact_in(U256::from(1_000_000_000_000_000u64), true, None)
7663            .unwrap();
7664        exact_output_amount(&quote, true).unwrap()
7665    }
7666
7667    fn quote_response(amount: U256) -> String {
7668        let result = (amount, U160::from(1u128 << 96), 0u32, U256::from(50_000u64)).abi_encode();
7669        serde_json::json!({
7670            "jsonrpc": "2.0",
7671            "id": 1,
7672            "result": hex::encode_prefixed(result),
7673        })
7674        .to_string()
7675    }
7676
7677    async fn expected_swap_tx(min_amount_out: U256) -> (B256, String) {
7678        // The orchestration's policy math is deterministic: nonce 7 from the fixture,
7679        // buffered gas 78000, buffered max fee 130000000, priority fee 10000000
7680        let expected_tx = build_eip1559_transaction(
7681            42161,
7682            7,
7683            78_000,
7684            130_000_000,
7685            10_000_000,
7686            ROUTER_ADDRESS,
7687            U256::ZERO,
7688            Bytes::from(expected_swap_calldata(min_amount_out)),
7689        );
7690        sign_eip1559_transaction(
7691            expected_tx,
7692            &PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
7693        )
7694        .await
7695        .map(|(hash, raw)| (hash, nautilus_core::hex::encode_prefixed(&raw)))
7696        .unwrap()
7697    }
7698
7699    fn expected_buy_base_amount() -> U256 {
7700        U256::from(1_000_000_000_000_000u64)
7701    }
7702
7703    fn expected_buy_amount_in() -> U256 {
7704        let profiler = test_profiler(&test_pool(), FIXTURE_BLOCK);
7705        profiler
7706            .swap_exact_out(expected_buy_base_amount(), false, None)
7707            .unwrap()
7708            .get_input_amount()
7709    }
7710
7711    fn expected_buy_min_amount_out(slippage_bps: u32) -> U256 {
7712        derive_min_amount_out(expected_buy_base_amount(), slippage_bps).unwrap()
7713    }
7714
7715    fn expected_buy_swap_calldata(min_amount_out: U256, amount_in: U256) -> Vec<u8> {
7716        UniswapV3SwapRouter::exactInputSingleCall {
7717            params: UniswapV3SwapRouter::ExactInputSingleParams {
7718                tokenIn: USDC_ADDRESS,
7719                tokenOut: WETH_ADDRESS,
7720                fee: U24::try_from(500u32).unwrap(),
7721                recipient: address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266"),
7722                deadline: U256::from(FIXTURE_BLOCK_TIMESTAMP + 300),
7723                amountIn: amount_in,
7724                amountOutMinimum: min_amount_out,
7725                sqrtPriceLimitX96: U160::ZERO,
7726            },
7727        }
7728        .abi_encode()
7729    }
7730
7731    async fn expected_buy_swap_tx(min_amount_out: U256, amount_in: U256) -> (B256, String) {
7732        let expected_tx = build_eip1559_transaction(
7733            42161,
7734            7,
7735            78_000,
7736            130_000_000,
7737            10_000_000,
7738            ROUTER_ADDRESS,
7739            U256::ZERO,
7740            Bytes::from(expected_buy_swap_calldata(min_amount_out, amount_in)),
7741        );
7742        sign_eip1559_transaction(
7743            expected_tx,
7744            &PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
7745        )
7746        .await
7747        .map(|(hash, raw)| (hash, nautilus_core::hex::encode_prefixed(&raw)))
7748        .unwrap()
7749    }
7750
7751    fn finalized_buy_swap_receipt(tx_hash: B256, amount_in: U256) -> String {
7752        finalized_buy_swap_receipt_with_base_out(tx_hash, amount_in, expected_buy_base_amount())
7753    }
7754
7755    fn finalized_buy_swap_receipt_with_base_out(
7756        tx_hash: B256,
7757        amount_in: U256,
7758        base_out: U256,
7759    ) -> String {
7760        let data = (
7761            -I256::try_from(u128::try_from(base_out).unwrap()).unwrap(),
7762            I256::try_from(u128::try_from(amount_in).unwrap()).unwrap(),
7763            U160::from(1_u128 << 96),
7764            TEST_LIQUIDITY,
7765            I24::try_from(0).unwrap(),
7766        )
7767            .abi_encode();
7768        serde_json::json!({
7769            "jsonrpc": "2.0",
7770            "id": 1,
7771            "result": {
7772                "transactionHash": tx_hash.to_string(),
7773                "blockHash": "0x2222222222222222222222222222222222222222222222222222222222222222",
7774                "blockNumber": "0x1cf0d41",
7775                "transactionIndex": "0x2",
7776                "gasUsed": "0xc3c0",
7777                "effectiveGasPrice": "0x5f5e100",
7778                "status": "0x1",
7779                "logs": [{
7780                    "removed": false,
7781                    "logIndex": "0x6",
7782                    "transactionIndex": "0x2",
7783                    "transactionHash": tx_hash.to_string(),
7784                    "blockHash": "0x2222222222222222222222222222222222222222222222222222222222222222",
7785                    "blockNumber": "0x1cf0d41",
7786                    "address": test_pool().address.to_string(),
7787                    "data": hex::encode_prefixed(data),
7788                    "topics": [
7789                        "0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67",
7790                        "0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266",
7791                        "0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266"
7792                    ]
7793                }]
7794            }
7795        })
7796        .to_string()
7797    }
7798
7799    fn finalized_buy_swap_block(tx_hash: B256, min_amount_out: U256, amount_in: U256) -> String {
7800        serde_json::json!({
7801            "jsonrpc": "2.0",
7802            "id": 1,
7803            "result": {
7804                "number": "0x1cf0d41",
7805                "hash": "0x2222222222222222222222222222222222222222222222222222222222222222",
7806                "parentHash": FIXTURE_BLOCK_HASH,
7807                "timestamp": "0x69044a21",
7808                "baseFeePerGas": "0x5f5e100",
7809                "transactions": [{
7810                    "hash": tx_hash.to_string(),
7811                    "from": WALLET,
7812                    "nonce": "0x7",
7813                    "chainId": "0xa4b1",
7814                    "type": "0x2",
7815                    "to": ROUTER,
7816                    "input": hex::encode_prefixed(expected_buy_swap_calldata(min_amount_out, amount_in)),
7817                    "value": "0x0",
7818                    "gas": "0x130b0",
7819                    "maxFeePerGas": "0x7bfa480",
7820                    "maxPriorityFeePerGas": "0x989680"
7821                }]
7822            }
7823        })
7824        .to_string()
7825    }
7826
7827    fn finalized_buy_swap_rpc_state(
7828        tx_hash: B256,
7829        min_amount_out: U256,
7830        amount_in: U256,
7831    ) -> MockRpcState {
7832        let receipt = finalized_buy_swap_receipt(tx_hash, amount_in);
7833        let block = finalized_buy_swap_block(tx_hash, min_amount_out, amount_in);
7834        with_finalized_identity(
7835            signing_rpc_state()
7836                .with_response("eth_getTransactionReceipt", &receipt)
7837                .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block)
7838                .with_send_raw_transaction_echo()
7839                .with_call_response(BALANCE_OF_SELECTOR, CALL_BALANCE)
7840                .with_call_response(ALLOWANCE_SELECTOR, CALL_ALLOWANCE),
7841            &block,
7842            &receipt,
7843        )
7844    }
7845
7846    fn finalized_swap_receipt(tx_hash: B256) -> String {
7847        let data = (
7848            I256::try_from(1_000_000_000_000_000_i128).unwrap(),
7849            I256::try_from(-1_000_000_i128).unwrap(),
7850            U160::from(1_u128 << 96),
7851            TEST_LIQUIDITY,
7852            I24::try_from(0).unwrap(),
7853        )
7854            .abi_encode();
7855        serde_json::json!({
7856            "jsonrpc": "2.0",
7857            "id": 1,
7858            "result": {
7859                "transactionHash": tx_hash.to_string(),
7860                "blockHash": "0x2222222222222222222222222222222222222222222222222222222222222222",
7861                "blockNumber": "0x1cf0d41",
7862                "transactionIndex": "0x2",
7863                "gasUsed": "0xc3c0",
7864                "effectiveGasPrice": "0x5f5e100",
7865                "status": "0x1",
7866                "logs": [{
7867                    "removed": false,
7868                    "logIndex": "0x6",
7869                    "transactionIndex": "0x2",
7870                    "transactionHash": tx_hash.to_string(),
7871                    "blockHash": "0x2222222222222222222222222222222222222222222222222222222222222222",
7872                    "blockNumber": "0x1cf0d41",
7873                    "address": test_pool().address.to_string(),
7874                    "data": hex::encode_prefixed(data),
7875                    "topics": [
7876                        "0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67",
7877                        "0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266",
7878                        "0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266"
7879                    ]
7880                }]
7881            }
7882        })
7883        .to_string()
7884    }
7885
7886    fn finalized_swap_receipt_with_unrelated_swap(tx_hash: B256) -> String {
7887        let mut receipt: serde_json::Value =
7888            serde_json::from_str(&finalized_swap_receipt(tx_hash)).unwrap();
7889        let logs = receipt["result"]["logs"].as_array_mut().unwrap();
7890        let mut unrelated = logs[0].clone();
7891        unrelated["address"] = serde_json::json!(ROUTER);
7892        unrelated["logIndex"] = serde_json::json!("0x7");
7893        logs.push(unrelated);
7894        receipt.to_string()
7895    }
7896
7897    fn finalized_swap_block(tx_hash: B256, min_amount_out: U256) -> String {
7898        serde_json::json!({
7899            "jsonrpc": "2.0",
7900            "id": 1,
7901            "result": {
7902                "number": "0x1cf0d41",
7903                "hash": "0x2222222222222222222222222222222222222222222222222222222222222222",
7904                "parentHash": FIXTURE_BLOCK_HASH,
7905                "timestamp": "0x69044a21",
7906                "baseFeePerGas": "0x5f5e100",
7907                "transactions": [{
7908                    "hash": tx_hash.to_string(),
7909                    "from": WALLET,
7910                    "nonce": "0x7",
7911                    "chainId": "0xa4b1",
7912                    "type": "0x2",
7913                    "to": ROUTER,
7914                    "input": hex::encode_prefixed(expected_swap_calldata(min_amount_out)),
7915                    "value": "0x0",
7916                    "gas": "0x130b0",
7917                    "maxFeePerGas": "0x7bfa480",
7918                    "maxPriorityFeePerGas": "0x989680"
7919                }]
7920            }
7921        })
7922        .to_string()
7923    }
7924
7925    fn finalized_swap_rpc_state(tx_hash: B256, min_amount_out: U256) -> MockRpcState {
7926        let receipt = finalized_swap_receipt(tx_hash);
7927        let block = finalized_swap_block(tx_hash, min_amount_out);
7928        with_finalized_identity(
7929            signing_rpc_state()
7930                .with_response("eth_getTransactionReceipt", &receipt)
7931                .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block)
7932                .with_send_raw_transaction_echo()
7933                .with_call_response(BALANCE_OF_SELECTOR, CALL_BALANCE)
7934                .with_call_response(ALLOWANCE_SELECTOR, CALL_ALLOWANCE),
7935            &block,
7936            &receipt,
7937        )
7938    }
7939
7940    fn with_finalized_identity(
7941        state: MockRpcState,
7942        block_response: &str,
7943        receipt_response: &str,
7944    ) -> MockRpcState {
7945        let block: serde_json::Value = serde_json::from_str(block_response).unwrap();
7946        let receipt: serde_json::Value = serde_json::from_str(receipt_response).unwrap();
7947        let transaction = block["result"]["transactions"][0].clone();
7948        let transaction_response = serde_json::json!({
7949            "jsonrpc": "2.0",
7950            "id": 1,
7951            "result": transaction,
7952        })
7953        .to_string();
7954        let success = receipt["result"]["status"] == "0x1";
7955        let mut trace = serde_json::json!({
7956            "type": "CALL",
7957            "from": transaction["from"],
7958            "to": transaction["to"],
7959            "value": transaction["value"],
7960            "gas": transaction["gas"],
7961            "gasUsed": receipt["result"]["gasUsed"],
7962            "input": transaction["input"],
7963            "output": "0x",
7964            "calls": [],
7965        });
7966
7967        if !success {
7968            trace["error"] = serde_json::json!("execution reverted");
7969        }
7970        let trace_response = serde_json::json!({
7971            "jsonrpc": "2.0",
7972            "id": 1,
7973            "result": trace,
7974        })
7975        .to_string();
7976        state
7977            .with_response("eth_getTransactionByHash", &transaction_response)
7978            .with_response("debug_traceTransaction", &trace_response)
7979    }
7980
7981    fn receipt_with_transaction_hash(receipt_response: &str, tx_hash: B256) -> String {
7982        let mut receipt: serde_json::Value = serde_json::from_str(receipt_response).unwrap();
7983        receipt["result"]["transactionHash"] = serde_json::json!(tx_hash.to_string());
7984        receipt.to_string()
7985    }
7986
7987    fn replacement_head_block(tx_hash: B256) -> String {
7988        serde_json::json!({
7989            "jsonrpc": "2.0",
7990            "id": 1,
7991            "result": {
7992                "number": "0x1cf0d40",
7993                "hash": "0x1111111111111111111111111111111111111111111111111111111111111111",
7994                "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000001",
7995                "timestamp": "0x69044a20",
7996                "baseFeePerGas": "0x5f5e100",
7997                "transactions": [{
7998                    "hash": "0x3333333333333333333333333333333333333333333333333333333333333333",
7999                    "from": "0x0000000000000000000000000000000000000001",
8000                    "nonce": "0x1",
8001                    "type": "0x0",
8002                    "to": WETH,
8003                    "input": "0x",
8004                    "value": "0x0",
8005                    "gas": "0x5208"
8006                }, {
8007                    "hash": tx_hash.to_string(),
8008                    "from": WALLET,
8009                    "nonce": "0x7",
8010                    "chainId": "0xa4b1",
8011                    "type": "0x2",
8012                    "to": WETH,
8013                    "input": "0xd0e30db0",
8014                    "value": "0x38d7ea4c68000",
8015                    "gas": "0x130b0",
8016                    "maxFeePerGas": "0x7bfa480",
8017                    "maxPriorityFeePerGas": "0x989680"
8018                }]
8019            }
8020        })
8021        .to_string()
8022    }
8023
8024    fn finalized_wrap_block(tx_hash: B256) -> String {
8025        serde_json::json!({
8026            "jsonrpc": "2.0",
8027            "id": 1,
8028            "result": {
8029                "number": "0x1cf0d41",
8030                "hash": "0x2222222222222222222222222222222222222222222222222222222222222222",
8031                "parentHash": FIXTURE_BLOCK_HASH,
8032                "timestamp": "0x69044a21",
8033                "baseFeePerGas": "0x5f5e100",
8034                "transactions": [{
8035                    "hash": tx_hash.to_string(),
8036                    "from": WALLET,
8037                    "nonce": "0x7",
8038                    "chainId": "0xa4b1",
8039                    "type": "0x2",
8040                    "to": WETH,
8041                    "input": "0xd0e30db0",
8042                    "value": "0x38d7ea4c68000",
8043                    "gas": "0x130b0",
8044                    "maxFeePerGas": "0x7bfa480",
8045                    "maxPriorityFeePerGas": "0x989680"
8046                }]
8047            }
8048        })
8049        .to_string()
8050    }
8051
8052    fn finalized_approve_block(tx_hash: B256, amount: U256) -> String {
8053        let calldata = ERC20::approveCall {
8054            spender: ROUTER_ADDRESS,
8055            amount,
8056        }
8057        .abi_encode();
8058        serde_json::json!({
8059            "jsonrpc": "2.0",
8060            "id": 1,
8061            "result": {
8062                "number": "0x1cf0d41",
8063                "hash": "0x2222222222222222222222222222222222222222222222222222222222222222",
8064                "parentHash": FIXTURE_BLOCK_HASH,
8065                "timestamp": "0x69044a21",
8066                "baseFeePerGas": "0x5f5e100",
8067                "transactions": [{
8068                    "hash": tx_hash.to_string(),
8069                    "from": WALLET,
8070                    "nonce": "0x7",
8071                    "chainId": "0xa4b1",
8072                    "type": "0x2",
8073                    "to": WETH,
8074                    "input": hex::encode_prefixed(calldata),
8075                    "value": "0x0",
8076                    "gas": "0x130b0",
8077                    "maxFeePerGas": "0x7bfa480",
8078                    "maxPriorityFeePerGas": "0x989680"
8079                }]
8080            }
8081        })
8082        .to_string()
8083    }
8084
8085    fn fixture_sell_plan() -> SwapPlan {
8086        let pool = test_pool();
8087        let order = test_market_sell_order(pool.instrument_id);
8088        let quote_token = pool.get_quote_token();
8089        SwapPlan {
8090            order,
8091            quote_currency: Currency::new_checked(
8092                &quote_token.symbol,
8093                quote_token.decimals,
8094                0,
8095                &quote_token.name,
8096                CurrencyType::Crypto,
8097            )
8098            .unwrap(),
8099            instrument_id: pool.instrument_id,
8100            pool_address: pool.address,
8101            router: ROUTER_ADDRESS,
8102            factory: UNISWAP_V3.dex.factory,
8103            weth: WETH_ADDRESS,
8104            token_in: WETH_ADDRESS,
8105            token_out: USDC_ADDRESS,
8106            fee: U24::try_from(500u32).unwrap(),
8107            amount_in: U256::from(1_000_000_000_000_000u64),
8108            min_amount_out: expected_min_amount_out(50),
8109            slippage_bps: 50,
8110            quote_spend_ceiling: None,
8111            profiler_position: Some(profiler_event_position()),
8112            pool,
8113        }
8114    }
8115
8116    fn fixture_block_response(number: u64, hash: B256) -> String {
8117        let parent_hash = if number == FIXTURE_BLOCK {
8118            B256::from_str("0x0000000000000000000000000000000000000000000000000000000000000001")
8119                .unwrap()
8120        } else {
8121            B256::from_str(FIXTURE_BLOCK_HASH).unwrap()
8122        };
8123        serde_json::json!({
8124            "jsonrpc": "2.0",
8125            "id": 1,
8126            "result": {
8127                "number": format!("0x{number:x}"),
8128                "hash": hash.to_string(),
8129                "parentHash": parent_hash.to_string(),
8130                "timestamp": format!("0x{:x}", FIXTURE_BLOCK_TIMESTAMP + number - FIXTURE_BLOCK),
8131                "baseFeePerGas": "0x5f5e100",
8132                "transactions": []
8133            }
8134        })
8135        .to_string()
8136    }
8137
8138    fn profiler_event_receipt(pool_address: Address) -> String {
8139        let transaction_hash = B256::from([0x33; 32]);
8140        serde_json::json!({
8141            "jsonrpc": "2.0",
8142            "id": 1,
8143            "result": {
8144                "transactionHash": transaction_hash.to_string(),
8145                "blockHash": FIXTURE_BLOCK_HASH,
8146                "blockNumber": "0x1cf0d40",
8147                "transactionIndex": "0x2",
8148                "gasUsed": "0xc3c0",
8149                "effectiveGasPrice": "0x5f5e100",
8150                "status": "0x1",
8151                "logs": [{
8152                    "removed": false,
8153                    "logIndex": "0x6",
8154                    "transactionIndex": "0x2",
8155                    "transactionHash": transaction_hash.to_string(),
8156                    "blockHash": FIXTURE_BLOCK_HASH,
8157                    "blockNumber": "0x1cf0d40",
8158                    "address": pool_address.to_string(),
8159                    "data": "0x",
8160                    "topics": [test_pool().dex.swap_created_event.as_ref()]
8161                }]
8162            }
8163        })
8164        .to_string()
8165    }
8166
8167    fn profiler_event_position() -> BlockPosition {
8168        BlockPosition::new(FIXTURE_BLOCK, B256::from([0x33; 32]).to_string(), 2, 6)
8169            .with_block_hash(Some(FIXTURE_BLOCK_HASH.to_string()))
8170    }
8171
8172    #[rstest]
8173    fn quantity_to_raw_amount_scales_by_token_decimals() {
8174        assert_eq!(
8175            quantity_to_raw_amount(Quantity::from("0.001"), 18).unwrap(),
8176            U256::from(1_000_000_000_000_000u64)
8177        );
8178        assert_eq!(
8179            quantity_to_raw_amount(Quantity::from("1.5"), 18).unwrap(),
8180            U256::from(1_500_000_000_000_000_000u128)
8181        );
8182        assert_eq!(
8183            quantity_to_raw_amount(Quantity::from("12.5"), 6).unwrap(),
8184            U256::from(12_500_000u64)
8185        );
8186    }
8187
8188    #[rstest]
8189    fn quantity_to_raw_amount_uses_defi_quantity_precision() {
8190        let amount = U256::from(10_000_000_000_000_000u64);
8191        let quantity = Quantity::from_u256(amount, 18).unwrap();
8192
8193        assert_eq!(quantity_to_raw_amount(quantity, 18).unwrap(), amount);
8194    }
8195
8196    #[rstest]
8197    fn quantity_to_raw_amount_rejects_zero() {
8198        let error = quantity_to_raw_amount(Quantity::from("0.0"), 18).unwrap_err();
8199
8200        assert_eq!(error.to_string(), "Order quantity must be positive");
8201    }
8202
8203    #[rstest]
8204    fn quantity_to_raw_amount_rejects_inexact_token_units() {
8205        let error = quantity_to_raw_amount(Quantity::from("0.0000001"), 6).unwrap_err();
8206
8207        assert!(
8208            error
8209                .to_string()
8210                .contains("is not exactly representable in 6 base token decimals"),
8211            "was: {error}"
8212        );
8213    }
8214
8215    #[rstest]
8216    fn raw_amount_to_quantity_inverts_base_quantity() {
8217        let quantity = Quantity::from("0.001");
8218        let amount = quantity_to_raw_amount(quantity, 18).unwrap();
8219
8220        assert_eq!(raw_amount_to_quantity(amount, 18).unwrap(), quantity);
8221    }
8222
8223    #[rstest]
8224    fn raw_amount_to_quantity_rejects_positive_amount_truncated_to_zero() {
8225        let error = raw_amount_to_quantity(U256::from(99u64), 18).unwrap_err();
8226
8227        assert!(
8228            error
8229                .to_string()
8230                .contains("is below representable quantity precision"),
8231            "was: {error}"
8232        );
8233    }
8234
8235    #[rstest]
8236    fn fill_price_from_quote_recovers_spent_quote_after_quantity_truncation() {
8237        let last_qty = raw_amount_to_quantity(U256::from(1_000_000_403_079_044u64), 18).unwrap();
8238        let quote_amount = U256::from(1_891_348u64);
8239        let quote = Currency::new_checked("USDC", 6, 0, "USD Coin", CurrencyType::Crypto).unwrap();
8240        let last_px = fill_price_from_quote(last_qty, quote_amount, quote).unwrap();
8241
8242        assert_eq!(
8243            Money::from_decimal(last_qty.as_decimal() * last_px.as_decimal(), quote).unwrap(),
8244            Money::from_u256(quote_amount, quote).unwrap()
8245        );
8246    }
8247
8248    #[rstest]
8249    fn swap_token_pair_is_directional() {
8250        assert_eq!(
8251            swap_token_pair(OrderSide::Sell, WETH_ADDRESS, USDC_ADDRESS).unwrap(),
8252            (WETH_ADDRESS, USDC_ADDRESS)
8253        );
8254        assert_eq!(
8255            swap_token_pair(OrderSide::Buy, WETH_ADDRESS, USDC_ADDRESS).unwrap(),
8256            (USDC_ADDRESS, WETH_ADDRESS)
8257        );
8258    }
8259
8260    #[rstest]
8261    fn restore_swap_plan_buy_uses_quote_input() {
8262        let (client, cache) =
8263            swap_client_with_cache(buy_test_config("http://127.0.0.1:1".to_string()));
8264        let order = test_market_buy_order(test_pool().instrument_id);
8265        cache
8266            .borrow_mut()
8267            .add_order(order.clone(), None, None, true)
8268            .unwrap();
8269        let amount_in = U256::from(2_345_678u64);
8270        let intent = ExecutionIntentRow {
8271            id: 7,
8272            schema_version: crate::execution::transaction::EXECUTION_SCHEMA_VERSION,
8273            chain_id: 42161,
8274            wallet_address: WALLET.to_string(),
8275            nonce: Some(7),
8276            purpose: "swap".to_string(),
8277            status: "finalized".to_string(),
8278            client_order_id: Some(order.client_order_id().to_string()),
8279            trader_id: Some(order.trader_id().to_string()),
8280            strategy_id: Some(order.strategy_id().to_string()),
8281            account_id: Some("BLOCKCHAIN-001".to_string()),
8282            instrument_id: Some(order.instrument_id().to_string()),
8283            pool_address: Some(test_pool().address.to_string()),
8284            transaction_to: ROUTER.to_string(),
8285            transaction_input: "0x".to_string(),
8286            transaction_value: "0".to_string(),
8287            amount_in: Some(amount_in.to_string()),
8288            created_block: FIXTURE_BLOCK,
8289            acknowledgement_emitted: true,
8290            fill_emitted: false,
8291            terminal_emitted: false,
8292            active: true,
8293        };
8294
8295        let plan = client.restore_swap_plan(&intent).unwrap();
8296
8297        assert_eq!(plan.token_in, USDC_ADDRESS);
8298        assert_eq!(plan.token_out, WETH_ADDRESS);
8299        assert_eq!(plan.amount_in, amount_in);
8300    }
8301
8302    #[rstest]
8303    #[case(1_000_000, 50, 995_000)]
8304    #[case(1_000_000, 0, 1_000_000)]
8305    #[case(1_000_000, 200, 980_000)]
8306    #[case(10_000, 9_999, 1)]
8307    fn derive_min_amount_out_applies_slippage(
8308        #[case] quoted: u64,
8309        #[case] slippage_bps: u32,
8310        #[case] expected: u64,
8311    ) {
8312        assert_eq!(
8313            derive_min_amount_out(U256::from(quoted), slippage_bps).unwrap(),
8314            U256::from(expected)
8315        );
8316    }
8317
8318    #[rstest]
8319    fn derive_min_amount_out_rejects_zero_result() {
8320        let error = derive_min_amount_out(U256::from(9_999u64), 9_999).unwrap_err();
8321
8322        assert!(
8323            error.to_string().contains("Derived minimum output is zero"),
8324            "was: {error}"
8325        );
8326    }
8327
8328    #[rstest]
8329    fn derive_min_amount_out_rejects_full_slippage() {
8330        let error = derive_min_amount_out(U256::from(1_000_000u64), 10_000).unwrap_err();
8331
8332        assert!(
8333            error.to_string().contains("must be below 10000"),
8334            "was: {error}"
8335        );
8336    }
8337
8338    #[rstest]
8339    fn replacement_scan_range_is_bounded_and_checked() {
8340        assert_eq!(
8341            replacement_scan_range(10, 10).unwrap(),
8342            RangeInclusive::new(10, 10)
8343        );
8344        assert_eq!(
8345            replacement_scan_range(10, 10 + MAX_REPLACEMENT_SCAN_BLOCKS).unwrap(),
8346            RangeInclusive::new(10, 10 + MAX_REPLACEMENT_SCAN_BLOCKS - 1)
8347        );
8348        assert!(
8349            replacement_scan_range(11, 10)
8350                .unwrap_err()
8351                .to_string()
8352                .contains("is behind execution creation block")
8353        );
8354        assert_eq!(
8355            replacement_scan_range(u64::MAX - 1, u64::MAX).unwrap(),
8356            RangeInclusive::new(u64::MAX - 1, u64::MAX)
8357        );
8358    }
8359
8360    #[rstest]
8361    fn terminal_execution_event_ids_are_stable_and_kind_specific() {
8362        let transaction_hash = B256::from([0x42; 32]);
8363
8364        let fill = execution_event_id(transaction_hash, b"fill");
8365        let fill_retry = execution_event_id(transaction_hash, b"fill");
8366        let reverted = execution_event_id(transaction_hash, b"reverted");
8367
8368        assert_eq!(fill, fill_retry);
8369        assert_ne!(fill, reverted);
8370    }
8371
8372    #[rstest]
8373    fn prepared_transaction_debug_redacts_raw_transaction() {
8374        let prepared = PreparedTransaction {
8375            intent_id: 1,
8376            created_block: 2,
8377            nonce: 3,
8378            tx_hash: B256::ZERO,
8379            raw_tx: vec![0xde, 0xad, 0xbe, 0xef],
8380            payload_lease: None,
8381        };
8382
8383        let debug = format!("{prepared:?}");
8384
8385        assert!(debug.contains("raw_tx: \"<redacted>\""));
8386        assert!(!debug.contains("[222, 173, 190, 239]"));
8387    }
8388
8389    #[rstest]
8390    fn call_trace_rejects_unreviewed_internal_edge() {
8391        let signed = crate::execution::transaction::DecodedSignedTransaction {
8392            hash: B256::from([1; 32]),
8393            signer: Address::from_str(WALLET).unwrap(),
8394            chain_id: 42_161,
8395            nonce: 7,
8396            to: ROUTER_ADDRESS,
8397            value: U256::ZERO,
8398            input: Bytes::from(expected_swap_calldata(expected_min_amount_out(50))),
8399            gas_limit: 78_000,
8400            max_fee_per_gas: 130_000_000,
8401            max_priority_fee_per_gas: 10_000_000,
8402        };
8403        let input_digest = keccak256(&signed.input);
8404        let trace = VerifiedCallTrace {
8405            call_type: RpcCallType::Call,
8406            from: signed.signer,
8407            to: Some(signed.to),
8408            value: signed.value,
8409            input_selector: signed
8410                .input
8411                .get(..4)
8412                .map(|selector| selector.try_into().unwrap()),
8413            input_digest,
8414            success: true,
8415            calls: vec![VerifiedCallTrace {
8416                call_type: RpcCallType::Call,
8417                from: ROUTER_ADDRESS,
8418                to: Some(WETH_ADDRESS),
8419                value: U256::ZERO,
8420                input_selector: None,
8421                input_digest: B256::ZERO,
8422                success: true,
8423                calls: Vec::new(),
8424            }],
8425        };
8426        let manifest = &test_config("http://127.0.0.1:1".to_string())
8427            .verification
8428            .unwrap()
8429            .deployment_manifest;
8430
8431        let error = validate_call_trace(&trace, &signed, true, "swap_sell", manifest).unwrap_err();
8432
8433        assert!(
8434            error.to_string().contains("unreviewed call edge"),
8435            "was: {error}"
8436        );
8437    }
8438
8439    #[rstest]
8440    fn call_trace_rejects_unlisted_precompile_target() {
8441        let manifest = test_config("http://127.0.0.1:1".to_string())
8442            .verification
8443            .unwrap()
8444            .deployment_manifest;
8445        let calls = [VerifiedCallTrace {
8446            call_type: RpcCallType::Call,
8447            from: ROUTER_ADDRESS,
8448            to: Some(address!("0000000000000000000000000000000000000007")),
8449            value: U256::ZERO,
8450            input_selector: None,
8451            input_digest: B256::ZERO,
8452            success: true,
8453            calls: Vec::new(),
8454        }];
8455
8456        let error =
8457            validate_internal_calls(&calls, ROUTER_ADDRESS, "swap_sell", &manifest).unwrap_err();
8458
8459        assert_eq!(
8460            error.to_string(),
8461            format!(
8462                "Verified call trace contains an unreviewed call edge {ROUTER_ADDRESS} -> {} for swap_sell",
8463                address!("0000000000000000000000000000000000000007")
8464            )
8465        );
8466    }
8467
8468    #[rstest]
8469    fn call_trace_requires_exact_call_type() {
8470        let manifest = test_config("http://127.0.0.1:1".to_string())
8471            .verification
8472            .unwrap()
8473            .deployment_manifest;
8474        let calls = [VerifiedCallTrace {
8475            call_type: RpcCallType::Staticcall,
8476            from: ROUTER_ADDRESS,
8477            to: Some(test_pool().address),
8478            value: U256::ZERO,
8479            input_selector: None,
8480            input_digest: B256::ZERO,
8481            success: true,
8482            calls: Vec::new(),
8483        }];
8484
8485        let error =
8486            validate_internal_calls(&calls, ROUTER_ADDRESS, "swap_sell", &manifest).unwrap_err();
8487
8488        assert!(
8489            error.to_string().contains("unreviewed staticcall edge"),
8490            "was: {error}"
8491        );
8492    }
8493
8494    #[rstest]
8495    fn call_trace_requires_exact_caller() {
8496        let manifest = test_config("http://127.0.0.1:1".to_string())
8497            .verification
8498            .unwrap()
8499            .deployment_manifest;
8500        let calls = [VerifiedCallTrace {
8501            call_type: RpcCallType::Call,
8502            from: WETH_ADDRESS,
8503            to: Some(test_pool().address),
8504            value: U256::ZERO,
8505            input_selector: None,
8506            input_digest: B256::ZERO,
8507            success: true,
8508            calls: Vec::new(),
8509        }];
8510
8511        let error =
8512            validate_internal_calls(&calls, ROUTER_ADDRESS, "swap_sell", &manifest).unwrap_err();
8513
8514        assert_eq!(
8515            error.to_string(),
8516            "Verified call trace child has an invalid caller context"
8517        );
8518    }
8519
8520    #[rstest]
8521    fn call_trace_rejects_contract_creation() {
8522        let signed = crate::execution::transaction::DecodedSignedTransaction {
8523            hash: B256::from([1; 32]),
8524            signer: Address::from_str(WALLET).unwrap(),
8525            chain_id: 42_161,
8526            nonce: 7,
8527            to: ROUTER_ADDRESS,
8528            value: U256::ZERO,
8529            input: Bytes::from(expected_swap_calldata(expected_min_amount_out(50))),
8530            gas_limit: 78_000,
8531            max_fee_per_gas: 130_000_000,
8532            max_priority_fee_per_gas: 10_000_000,
8533        };
8534        let trace = VerifiedCallTrace {
8535            call_type: RpcCallType::Call,
8536            from: signed.signer,
8537            to: Some(signed.to),
8538            value: signed.value,
8539            input_selector: signed
8540                .input
8541                .get(..4)
8542                .map(|selector| selector.try_into().unwrap()),
8543            input_digest: keccak256(&signed.input),
8544            success: true,
8545            calls: vec![VerifiedCallTrace {
8546                call_type: RpcCallType::Create,
8547                from: ROUTER_ADDRESS,
8548                to: Some(address!("0000000000000000000000000000000000000007")),
8549                value: U256::ZERO,
8550                input_selector: None,
8551                input_digest: B256::ZERO,
8552                success: true,
8553                calls: Vec::new(),
8554            }],
8555        };
8556        let manifest = &test_config("http://127.0.0.1:1".to_string())
8557            .verification
8558            .unwrap()
8559            .deployment_manifest;
8560
8561        let error = validate_call_trace(&trace, &signed, true, "swap_sell", manifest).unwrap_err();
8562
8563        assert!(
8564            error
8565                .to_string()
8566                .contains("forbidden state-changing operation"),
8567            "was: {error}"
8568        );
8569    }
8570
8571    #[tokio::test]
8572    async fn swap_quote_rejects_missing_ingestion_block_hash() {
8573        let (client, state) = client_with_mock_rpc(execution_rpc_state()).await;
8574        let plan = fixture_sell_plan();
8575        let position = BlockPosition::new(
8576            FIXTURE_BLOCK,
8577            FIXTURE_BLOCK_HASH.to_string(),
8578            BLOCK_SCOPED_SNAPSHOT_INDEX,
8579            BLOCK_SCOPED_SNAPSHOT_INDEX,
8580        );
8581
8582        let error = validate_swap_quote(
8583            &position,
8584            &plan,
8585            100,
8586            &client.verification,
8587            &client
8588                .config
8589                .verification
8590                .as_ref()
8591                .unwrap()
8592                .deployment_manifest,
8593        )
8594        .await
8595        .unwrap_err();
8596
8597        assert!(
8598            error.to_string().contains("no ingestion-time block hash"),
8599            "was: {error}"
8600        );
8601        assert!(state.recorded_requests().is_empty());
8602    }
8603
8604    #[tokio::test]
8605    async fn swap_quote_rejects_replaced_ingestion_block() {
8606        let changed = fixture_block_response(FIXTURE_BLOCK, B256::from([0x44; 32]));
8607        let state = execution_rpc_state().with_parameter_response(
8608            "eth_getBlockByNumber",
8609            "0x1cf0d40",
8610            &changed,
8611        );
8612        let (client, _) = client_with_mock_rpc(state).await;
8613        let plan = fixture_sell_plan();
8614        let position = BlockPosition::new(
8615            FIXTURE_BLOCK,
8616            FIXTURE_BLOCK_HASH.to_string(),
8617            BLOCK_SCOPED_SNAPSHOT_INDEX,
8618            BLOCK_SCOPED_SNAPSHOT_INDEX,
8619        )
8620        .with_block_hash(Some(FIXTURE_BLOCK_HASH.to_string()));
8621
8622        let error = validate_swap_quote(
8623            &position,
8624            &plan,
8625            100,
8626            &client.verification,
8627            &client
8628                .config
8629                .verification
8630                .as_ref()
8631                .unwrap()
8632                .deployment_manifest,
8633        )
8634        .await
8635        .unwrap_err();
8636
8637        assert!(error.to_string().contains("changed from"), "was: {error}");
8638    }
8639
8640    #[tokio::test]
8641    async fn swap_quote_accepts_exact_canonical_event_watermark() {
8642        let pool = test_pool();
8643        let receipt = profiler_event_receipt(pool.address);
8644        let state = execution_rpc_state().with_response("eth_getTransactionReceipt", &receipt);
8645        let (client, _) = client_with_mock_rpc(state).await;
8646        let plan = fixture_sell_plan();
8647
8648        let anchors = validate_swap_quote(
8649            &profiler_event_position(),
8650            &plan,
8651            100,
8652            &client.verification,
8653            &client
8654                .config
8655                .verification
8656                .as_ref()
8657                .unwrap()
8658                .deployment_manifest,
8659        )
8660        .await
8661        .unwrap();
8662
8663        assert_eq!(anchors.watermark.number, profiler_event_position().number);
8664        assert_eq!(anchors.state.number, FIXTURE_BLOCK);
8665        assert_eq!(anchors.state.hash, B256::from([0x11; 32]));
8666        assert_eq!(anchors.state.timestamp, FIXTURE_BLOCK_TIMESTAMP);
8667    }
8668
8669    #[tokio::test]
8670    async fn swap_quote_rejects_mismatched_receipt_position() {
8671        let pool = test_pool();
8672        let mut receipt: serde_json::Value =
8673            serde_json::from_str(&profiler_event_receipt(pool.address)).unwrap();
8674        receipt["result"]["transactionIndex"] = serde_json::json!("0x3");
8675        let state =
8676            execution_rpc_state().with_response("eth_getTransactionReceipt", &receipt.to_string());
8677        let (client, _) = client_with_mock_rpc(state).await;
8678        let plan = fixture_sell_plan();
8679
8680        let error = validate_swap_quote(
8681            &profiler_event_position(),
8682            &plan,
8683            100,
8684            &client.verification,
8685            &client
8686                .config
8687                .verification
8688                .as_ref()
8689                .unwrap()
8690                .deployment_manifest,
8691        )
8692        .await
8693        .unwrap_err();
8694
8695        assert_eq!(
8696            error.to_string(),
8697            "Profiler receipt position does not match its ingestion watermark"
8698        );
8699    }
8700
8701    #[tokio::test]
8702    async fn swap_quote_rejects_watermark_from_different_pool() {
8703        let receipt = profiler_event_receipt(ROUTER_ADDRESS);
8704        let state = execution_rpc_state().with_response("eth_getTransactionReceipt", &receipt);
8705        let (client, _) = client_with_mock_rpc(state).await;
8706        let plan = fixture_sell_plan();
8707
8708        let error = validate_swap_quote(
8709            &profiler_event_position(),
8710            &plan,
8711            100,
8712            &client.verification,
8713            &client
8714                .config
8715                .verification
8716                .as_ref()
8717                .unwrap()
8718                .deployment_manifest,
8719        )
8720        .await
8721        .unwrap_err();
8722
8723        assert!(
8724            error
8725                .to_string()
8726                .contains("did not come from expected pool"),
8727            "was: {error}"
8728        );
8729    }
8730
8731    #[rstest]
8732    fn verified_sell_quote_sets_signed_amounts() {
8733        let plan = fixture_sell_plan();
8734        let quote = UniswapV3Quote {
8735            amount: expected_sell_quote_amount(),
8736            sqrt_price_x96_after: U160::from(1u128 << 96),
8737            initialized_ticks_crossed: 0,
8738            gas_estimate: U256::from(50_000u64),
8739        };
8740
8741        let (amount_in, min_amount_out) = verified_swap_amounts(&plan, quote).unwrap();
8742
8743        assert_eq!(amount_in, U256::from(1_000_000_000_000_000u64));
8744        assert_eq!(min_amount_out, expected_min_amount_out(50));
8745    }
8746
8747    #[rstest]
8748    fn exact_output_amount_extracts_negative_leg() {
8749        let pool = test_pool();
8750        let profiler = test_profiler(&pool, FIXTURE_BLOCK);
8751        let quote = profiler
8752            .swap_exact_in(U256::from(1_000_000_000_000_000u64), true, None)
8753            .unwrap();
8754
8755        let amount = exact_output_amount(&quote, true).unwrap();
8756
8757        assert!(amount < U256::from(1_000_000_000_000_000u64));
8758        assert!(amount > U256::from(990_000_000_000_000u64));
8759
8760        let error = exact_output_amount(&quote, false).unwrap_err();
8761        assert!(
8762            error.to_string().contains("is not a positive output"),
8763            "was: {error}"
8764        );
8765    }
8766
8767    #[rstest]
8768    #[case("0xC6962004f452bE9203591991D15f6b388e09E8D0.Arbitrum:UniswapV3", true)]
8769    #[case("0xC6962004f452bE9203591991D15f6b388e09E8D0.Ethereum:UniswapV3", false)]
8770    #[case("0xC6962004f452bE9203591991D15f6b388e09E8D0.Arbitrum:UniswapV4", false)]
8771    #[case("ETHUSDT-PERP.BINANCE", false)]
8772    fn handles_order_venue_matches_chain_and_dex(#[case] instrument: &str, #[case] expected: bool) {
8773        let client = test_client("http://127.0.0.1:1".to_string());
8774        let instrument_id: InstrumentId = instrument.parse().unwrap();
8775
8776        assert_eq!(client.handles_order_venue(instrument_id.venue), expected);
8777    }
8778
8779    #[tokio::test]
8780    async fn submit_order_denies_buy_without_allowlisted_pair() {
8781        let (mut client, cache) =
8782            swap_client_with_cache(test_config("http://127.0.0.1:1".to_string()));
8783        let pool = test_pool();
8784        let order = test_market_buy_order(pool.instrument_id);
8785        cache
8786            .borrow_mut()
8787            .add_order(order.clone(), None, None, true)
8788            .unwrap();
8789        let mut receiver = start_with_events(&mut client);
8790
8791        client.submit_order(submit_order_cmd(&order)).unwrap();
8792
8793        let events = collect_order_events(&mut receiver);
8794        assert_eq!(events.len(), 1);
8795        let OrderEventAny::Denied(denied) = &events[0] else {
8796            panic!("expected OrderDenied, was {:?}", events[0]);
8797        };
8798        assert!(
8799            denied
8800                .reason
8801                .contains("not in the `allowed_token_pairs` allowlist"),
8802            "was: {}",
8803            denied.reason
8804        );
8805        assert!(
8806            denied.reason.contains(&USDC_ADDRESS.to_string()),
8807            "was: {}",
8808            denied.reason
8809        );
8810    }
8811
8812    #[tokio::test]
8813    async fn submit_order_denies_buy_quote_denominated_quantity() {
8814        let (mut client, cache) =
8815            swap_client_with_cache(buy_test_config("http://127.0.0.1:1".to_string()));
8816        let pool = test_pool();
8817        let order = OrderTestBuilder::new(OrderType::Market)
8818            .trader_id(TraderId::from("TRADER-001"))
8819            .strategy_id(StrategyId::from("S-001"))
8820            .instrument_id(pool.instrument_id)
8821            .client_order_id(ClientOrderId::from("O-SWAP-BUY-001"))
8822            .side(OrderSide::Buy)
8823            .quantity(Quantity::from("0.001"))
8824            .quote_quantity(true)
8825            .build();
8826        cache
8827            .borrow_mut()
8828            .add_order(order.clone(), None, None, true)
8829            .unwrap();
8830        let mut receiver = start_with_events(&mut client);
8831
8832        client.submit_order(submit_order_cmd(&order)).unwrap();
8833
8834        let events = collect_order_events(&mut receiver);
8835        assert_eq!(events.len(), 1);
8836        let OrderEventAny::Denied(denied) = &events[0] else {
8837            panic!("expected OrderDenied, was {:?}", events[0]);
8838        };
8839        assert!(
8840            denied.reason.contains("Quote-denominated"),
8841            "was: {}",
8842            denied.reason
8843        );
8844    }
8845
8846    #[tokio::test]
8847    async fn submit_order_denies_buy_amount_above_max_order_amount() {
8848        let mut config = buy_test_config("http://127.0.0.1:1".to_string());
8849        config.max_order_amount = Some(999_999_999_999_999);
8850        let (mut client, cache) = swap_client_with_cache(config);
8851        let order = test_market_buy_order(test_pool().instrument_id);
8852        cache
8853            .borrow_mut()
8854            .add_order(order.clone(), None, None, true)
8855            .unwrap();
8856        let mut receiver = start_with_events(&mut client);
8857
8858        client.submit_order(submit_order_cmd(&order)).unwrap();
8859
8860        let events = collect_order_events(&mut receiver);
8861        assert_eq!(events.len(), 1);
8862        let OrderEventAny::Denied(denied) = &events[0] else {
8863            panic!("expected OrderDenied, was {:?}", events[0]);
8864        };
8865        assert!(
8866            denied
8867                .reason
8868                .contains("exceeds the configured `max_order_amount`"),
8869            "was: {}",
8870            denied.reason
8871        );
8872    }
8873
8874    #[tokio::test]
8875    async fn submit_order_denies_buy_without_quote_spend_limit() {
8876        let mut config = buy_test_config("http://127.0.0.1:1".to_string());
8877        config.quote_spend_limits = None;
8878        let (mut client, cache) = swap_client_with_cache(config);
8879        let order = test_market_buy_order(test_pool().instrument_id);
8880        cache
8881            .borrow_mut()
8882            .add_order(order.clone(), None, None, true)
8883            .unwrap();
8884        let mut receiver = start_with_events(&mut client);
8885
8886        client.submit_order(submit_order_cmd(&order)).unwrap();
8887
8888        let events = collect_order_events(&mut receiver);
8889        assert_eq!(events.len(), 1);
8890        let OrderEventAny::Denied(denied) = &events[0] else {
8891            panic!("expected OrderDenied, was {:?}", events[0]);
8892        };
8893        assert!(
8894            denied
8895                .reason
8896                .contains("No `quote_spend_limits` entry for BUY token pair"),
8897            "was: {}",
8898            denied.reason
8899        );
8900    }
8901
8902    #[tokio::test]
8903    async fn submit_order_uses_pair_specific_quote_spend_limit() {
8904        let mut config = buy_test_config("http://127.0.0.1:1".to_string());
8905        config.quote_spend_limits = Some(vec![quote_spend_limit(
8906            WETH,
8907            USDC,
8908            18,
8909            "1000000000000000000",
8910        )]);
8911        let (mut client, cache) = swap_client_with_cache(config);
8912        let order = test_market_buy_order(test_pool().instrument_id);
8913        cache
8914            .borrow_mut()
8915            .add_order(order.clone(), None, None, true)
8916            .unwrap();
8917        let mut receiver = start_with_events(&mut client);
8918
8919        client.submit_order(submit_order_cmd(&order)).unwrap();
8920
8921        let events = collect_order_events(&mut receiver);
8922        assert_eq!(events.len(), 1);
8923        let OrderEventAny::Denied(denied) = &events[0] else {
8924            panic!("expected OrderDenied, was {:?}", events[0]);
8925        };
8926        assert!(
8927            denied
8928                .reason
8929                .contains("No `quote_spend_limits` entry for BUY token pair"),
8930            "was: {}",
8931            denied.reason
8932        );
8933    }
8934
8935    #[tokio::test]
8936    async fn submit_order_denies_buy_with_quote_spend_precision_mismatch() {
8937        let mut config = buy_test_config("http://127.0.0.1:1".to_string());
8938        config.quote_spend_limits.as_mut().unwrap()[0].spend_token_decimals = 18;
8939        let error = test_client_result(config, test_pool()).unwrap_err();
8940        assert!(
8941            error
8942                .to_string()
8943                .contains("Quote spend limit decimals do not match the deployment manifest"),
8944            "was: {error}"
8945        );
8946    }
8947
8948    #[tokio::test]
8949    async fn submit_order_denies_buy_with_zero_quote_spend_limit() {
8950        let mut config = buy_test_config("http://127.0.0.1:1".to_string());
8951        config.quote_spend_limits.as_mut().unwrap()[0].max_amount = "0".to_string();
8952        let (mut client, cache) = swap_client_with_cache(config);
8953        let order = test_market_buy_order(test_pool().instrument_id);
8954        cache
8955            .borrow_mut()
8956            .add_order(order.clone(), None, None, true)
8957            .unwrap();
8958        let mut receiver = start_with_events(&mut client);
8959
8960        client.submit_order(submit_order_cmd(&order)).unwrap();
8961
8962        let events = collect_order_events(&mut receiver);
8963        assert_eq!(events.len(), 1);
8964        let OrderEventAny::Denied(denied) = &events[0] else {
8965            panic!("expected OrderDenied, was {:?}", events[0]);
8966        };
8967        assert!(
8968            denied
8969                .reason
8970                .contains("exceeds the configured `quote_spend_limits` maximum 0"),
8971            "was: {}",
8972            denied.reason
8973        );
8974    }
8975
8976    #[tokio::test]
8977    async fn submit_order_denies_buy_one_raw_unit_above_quote_spend_limit_before_readiness() {
8978        let amount_in = expected_buy_amount_in();
8979        let mut config = buy_test_config("http://127.0.0.1:1".to_string());
8980        config.quote_spend_limits.as_mut().unwrap()[0].max_amount =
8981            (amount_in - U256::from(1u8)).to_string();
8982        let (mut client, cache) = swap_client_with_cache(config);
8983        let order = test_market_buy_order(test_pool().instrument_id);
8984        cache
8985            .borrow_mut()
8986            .add_order(order.clone(), None, None, true)
8987            .unwrap();
8988        let mut receiver = start_with_events(&mut client);
8989
8990        client.submit_order(submit_order_cmd(&order)).unwrap();
8991
8992        let events = collect_order_events(&mut receiver);
8993        assert_eq!(events.len(), 1);
8994        let OrderEventAny::Denied(denied) = &events[0] else {
8995            panic!("expected OrderDenied, was {:?}", events[0]);
8996        };
8997        assert!(
8998            denied.reason.contains(&format!(
8999                "BUY quote amount {amount_in} exceeds the configured `quote_spend_limits`"
9000            )),
9001            "was: {}",
9002            denied.reason
9003        );
9004        assert!(!client.core.is_connected());
9005        assert!(!client.cache.has_database());
9006        assert!(client.signer.is_none());
9007        assert!(client.pending_tasks.is_empty());
9008    }
9009
9010    #[tokio::test]
9011    async fn submit_order_denies_non_market_order_type() {
9012        let (mut client, cache) =
9013            swap_client_with_cache(test_config("http://127.0.0.1:1".to_string()));
9014        let pool = test_pool();
9015        let order = OrderTestBuilder::new(OrderType::Limit)
9016            .trader_id(TraderId::from("TRADER-001"))
9017            .strategy_id(StrategyId::from("S-001"))
9018            .instrument_id(pool.instrument_id)
9019            .client_order_id(ClientOrderId::from("O-SWAP-001"))
9020            .side(OrderSide::Sell)
9021            .quantity(Quantity::from("0.001"))
9022            .price(Price::from("2000"))
9023            .build();
9024        cache
9025            .borrow_mut()
9026            .add_order(order.clone(), None, None, true)
9027            .unwrap();
9028        let mut receiver = start_with_events(&mut client);
9029
9030        client.submit_order(submit_order_cmd(&order)).unwrap();
9031
9032        let events = collect_order_events(&mut receiver);
9033        assert_eq!(events.len(), 1);
9034        let OrderEventAny::Denied(denied) = &events[0] else {
9035            panic!("expected OrderDenied, was {:?}", events[0]);
9036        };
9037        assert!(
9038            denied.reason.contains("only Market is supported"),
9039            "was: {}",
9040            denied.reason
9041        );
9042    }
9043
9044    #[tokio::test]
9045    async fn submit_order_denies_quote_denominated_quantity() {
9046        let (mut client, cache) =
9047            swap_client_with_cache(test_config("http://127.0.0.1:1".to_string()));
9048        let pool = test_pool();
9049        let order = OrderTestBuilder::new(OrderType::Market)
9050            .trader_id(TraderId::from("TRADER-001"))
9051            .strategy_id(StrategyId::from("S-001"))
9052            .instrument_id(pool.instrument_id)
9053            .client_order_id(ClientOrderId::from("O-SWAP-001"))
9054            .side(OrderSide::Sell)
9055            .quantity(Quantity::from("0.001"))
9056            .quote_quantity(true)
9057            .build();
9058        cache
9059            .borrow_mut()
9060            .add_order(order.clone(), None, None, true)
9061            .unwrap();
9062        let mut receiver = start_with_events(&mut client);
9063
9064        client.submit_order(submit_order_cmd(&order)).unwrap();
9065
9066        let events = collect_order_events(&mut receiver);
9067        assert_eq!(events.len(), 1);
9068        let OrderEventAny::Denied(denied) = &events[0] else {
9069            panic!("expected OrderDenied, was {:?}", events[0]);
9070        };
9071        assert!(
9072            denied.reason.contains("Quote-denominated"),
9073            "was: {}",
9074            denied.reason
9075        );
9076    }
9077
9078    #[tokio::test]
9079    async fn submit_order_denies_unknown_pool() {
9080        let (mut client, cache) =
9081            swap_client_with_cache(test_config("http://127.0.0.1:1".to_string()));
9082        let unknown: InstrumentId = "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45.Arbitrum:UniswapV3"
9083            .parse()
9084            .unwrap();
9085        let order = OrderTestBuilder::new(OrderType::Market)
9086            .trader_id(TraderId::from("TRADER-001"))
9087            .strategy_id(StrategyId::from("S-001"))
9088            .instrument_id(unknown)
9089            .client_order_id(ClientOrderId::from("O-SWAP-001"))
9090            .side(OrderSide::Sell)
9091            .quantity(Quantity::from("0.001"))
9092            .build();
9093        cache
9094            .borrow_mut()
9095            .add_order(order.clone(), None, None, true)
9096            .unwrap();
9097        let mut receiver = start_with_events(&mut client);
9098
9099        client.submit_order(submit_order_cmd(&order)).unwrap();
9100
9101        let events = collect_order_events(&mut receiver);
9102        assert_eq!(events.len(), 1);
9103        let OrderEventAny::Denied(denied) = &events[0] else {
9104            panic!("expected OrderDenied, was {:?}", events[0]);
9105        };
9106        assert!(
9107            denied.reason.contains("Unknown pool"),
9108            "was: {}",
9109            denied.reason
9110        );
9111    }
9112
9113    #[tokio::test]
9114    async fn submit_order_denies_sell_when_only_buy_pair_allowlisted() {
9115        let mut config = test_config("http://127.0.0.1:1".to_string());
9116        config.allowed_token_pairs = Some(vec![(USDC.to_string(), WETH.to_string())]);
9117        let (mut client, _) = swap_client_with_cache(config);
9118        let order = test_market_sell_order(test_pool().instrument_id);
9119        let mut receiver = start_with_events(&mut client);
9120
9121        client.submit_order(submit_order_cmd(&order)).unwrap();
9122
9123        let events = collect_order_events(&mut receiver);
9124        assert_eq!(events.len(), 1);
9125        let OrderEventAny::Denied(denied) = &events[0] else {
9126            panic!("expected OrderDenied, was {:?}", events[0]);
9127        };
9128        assert!(
9129            denied
9130                .reason
9131                .contains("not in the `allowed_token_pairs` allowlist"),
9132            "was: {}",
9133            denied.reason
9134        );
9135        assert!(
9136            denied.reason.contains(&WETH_ADDRESS.to_string()),
9137            "was: {}",
9138            denied.reason
9139        );
9140    }
9141
9142    #[tokio::test]
9143    async fn submit_order_sell_ignores_quote_spend_limits() {
9144        let mut config = test_config("http://127.0.0.1:1".to_string());
9145        config.quote_spend_limits = Some(vec![quote_spend_limit(WETH, USDC, 18, "0")]);
9146        let (mut client, _) = swap_client_with_cache(config);
9147        let order = test_market_sell_order(test_pool().instrument_id);
9148        let mut receiver = start_with_events(&mut client);
9149
9150        client.submit_order(submit_order_cmd(&order)).unwrap();
9151
9152        let events = collect_order_events(&mut receiver);
9153        assert_eq!(events.len(), 1);
9154        let OrderEventAny::Denied(denied) = &events[0] else {
9155            panic!("expected OrderDenied, was {:?}", events[0]);
9156        };
9157        assert!(
9158            denied
9159                .reason
9160                .contains("Blockchain execution client is not connected"),
9161            "was: {}",
9162            denied.reason
9163        );
9164    }
9165
9166    #[tokio::test]
9167    async fn submit_order_denies_token_pair_outside_allowlist() {
9168        let mut config = test_config("http://127.0.0.1:1".to_string());
9169        config.allowed_token_pairs = Some(Vec::new());
9170        let (mut client, _) = swap_client_with_cache(config);
9171        let order = test_market_sell_order(test_pool().instrument_id);
9172        let mut receiver = start_with_events(&mut client);
9173
9174        client.submit_order(submit_order_cmd(&order)).unwrap();
9175
9176        let events = collect_order_events(&mut receiver);
9177        assert_eq!(events.len(), 1);
9178        let OrderEventAny::Denied(denied) = &events[0] else {
9179            panic!("expected OrderDenied, was {:?}", events[0]);
9180        };
9181        assert!(
9182            denied
9183                .reason
9184                .contains("not in the `allowed_token_pairs` allowlist"),
9185            "was: {}",
9186            denied.reason
9187        );
9188    }
9189
9190    #[tokio::test]
9191    async fn submit_order_denies_amount_above_max_order_amount() {
9192        let mut config = test_config("http://127.0.0.1:1".to_string());
9193        config.max_order_amount = Some(999_999_999_999_999); // 0.001 WETH in raw units minus one
9194        let (mut client, _) = swap_client_with_cache(config);
9195        let order = test_market_sell_order(test_pool().instrument_id);
9196        let mut receiver = start_with_events(&mut client);
9197
9198        client.submit_order(submit_order_cmd(&order)).unwrap();
9199
9200        let events = collect_order_events(&mut receiver);
9201        assert_eq!(events.len(), 1);
9202        let OrderEventAny::Denied(denied) = &events[0] else {
9203            panic!("expected OrderDenied, was {:?}", events[0]);
9204        };
9205        assert!(
9206            denied
9207                .reason
9208                .contains("exceeds the configured `max_order_amount`"),
9209            "was: {}",
9210            denied.reason
9211        );
9212    }
9213
9214    #[tokio::test]
9215    async fn submit_order_denies_slippage_param_above_ceiling() {
9216        let (mut client, _) = swap_client_with_cache(test_config("http://127.0.0.1:1".to_string()));
9217        let order = test_market_sell_order(test_pool().instrument_id);
9218        let mut cmd = submit_order_cmd(&order);
9219        cmd.params = Some(serde_json::from_str(r#"{"slippage_bps": 201}"#).unwrap());
9220        let mut receiver = start_with_events(&mut client);
9221
9222        client.submit_order(cmd).unwrap();
9223
9224        let events = collect_order_events(&mut receiver);
9225        assert_eq!(events.len(), 1);
9226        let OrderEventAny::Denied(denied) = &events[0] else {
9227            panic!("expected OrderDenied, was {:?}", events[0]);
9228        };
9229        assert!(
9230            denied
9231                .reason
9232                .contains("exceeds the configured `max_slippage_bps`"),
9233            "was: {}",
9234            denied.reason
9235        );
9236    }
9237
9238    #[tokio::test]
9239    async fn submit_order_denies_pool_without_fee_tier() {
9240        let addr = start_mock_rpc_server(MockRpcState::default()).await;
9241        let mut pool = test_pool();
9242        pool.fee = None;
9243        let cache = Rc::new(RefCell::new(Cache::default()));
9244        cache.borrow_mut().add_pool(pool.clone()).unwrap();
9245        let order = test_market_sell_order(pool.instrument_id);
9246        cache
9247            .borrow_mut()
9248            .add_order(order.clone(), None, None, false)
9249            .unwrap();
9250        let core = ExecutionClientCore::new(
9251            TraderId::from("TRADER-001"),
9252            ClientId::from("BLOCKCHAIN-001"),
9253            *BLOCKCHAIN_VENUE,
9254            OmsType::Netting,
9255            AccountId::from("BLOCKCHAIN-001"),
9256            AccountType::Wallet,
9257            None,
9258            cache,
9259        );
9260        let mut client =
9261            BlockchainExecutionClient::new(core, test_config(format!("http://{addr}"))).unwrap();
9262        let mut receiver = start_with_events(&mut client);
9263
9264        client.submit_order(submit_order_cmd(&order)).unwrap();
9265
9266        let events = collect_order_events(&mut receiver);
9267        assert_eq!(events.len(), 1);
9268        let OrderEventAny::Denied(denied) = &events[0] else {
9269            panic!("expected OrderDenied, was {:?}", events[0]);
9270        };
9271        assert!(
9272            denied.reason.contains("no fee tier"),
9273            "was: {}",
9274            denied.reason
9275        );
9276    }
9277
9278    #[tokio::test]
9279    async fn submit_order_denies_without_live_profiler() {
9280        let addr = start_mock_rpc_server(MockRpcState::default()).await;
9281        let cache = Rc::new(RefCell::new(Cache::default()));
9282        let pool = test_pool();
9283        cache.borrow_mut().add_pool(pool.clone()).unwrap();
9284        let order = test_market_sell_order(pool.instrument_id);
9285        cache
9286            .borrow_mut()
9287            .add_order(order.clone(), None, None, false)
9288            .unwrap();
9289        let core = ExecutionClientCore::new(
9290            TraderId::from("TRADER-001"),
9291            ClientId::from("BLOCKCHAIN-001"),
9292            *BLOCKCHAIN_VENUE,
9293            OmsType::Netting,
9294            AccountId::from("BLOCKCHAIN-001"),
9295            AccountType::Wallet,
9296            None,
9297            cache,
9298        );
9299        let mut client =
9300            BlockchainExecutionClient::new(core, test_config(format!("http://{addr}"))).unwrap();
9301        let mut receiver = start_with_events(&mut client);
9302
9303        client.submit_order(submit_order_cmd(&order)).unwrap();
9304
9305        let events = collect_order_events(&mut receiver);
9306        assert_eq!(events.len(), 1);
9307        let OrderEventAny::Denied(denied) = &events[0] else {
9308            panic!("expected OrderDenied, was {:?}", events[0]);
9309        };
9310        assert!(
9311            denied.reason.contains("No pool profiler"),
9312            "was: {}",
9313            denied.reason
9314        );
9315    }
9316
9317    #[tokio::test]
9318    async fn submit_order_denies_when_not_connected() {
9319        let (mut client, _) = swap_client_with_cache(test_config("http://127.0.0.1:1".to_string()));
9320        let order = test_market_sell_order(test_pool().instrument_id);
9321        let mut receiver = start_with_events(&mut client);
9322
9323        client.submit_order(submit_order_cmd(&order)).unwrap();
9324
9325        let events = collect_order_events(&mut receiver);
9326        assert_eq!(events.len(), 1);
9327        let OrderEventAny::Denied(denied) = &events[0] else {
9328            panic!("expected OrderDenied, was {:?}", events[0]);
9329        };
9330        assert!(
9331            denied.reason.contains("is not connected"),
9332            "was: {}",
9333            denied.reason
9334        );
9335    }
9336
9337    #[tokio::test]
9338    async fn submit_order_denies_without_durable_store() {
9339        let (mut client, _) = swap_client_with_cache(test_config("http://127.0.0.1:1".to_string()));
9340        client.core.set_connected();
9341        let order = test_market_sell_order(test_pool().instrument_id);
9342        let mut receiver = start_with_events(&mut client);
9343
9344        client.submit_order(submit_order_cmd(&order)).unwrap();
9345
9346        let events = collect_order_events(&mut receiver);
9347        assert_eq!(events.len(), 1);
9348        let OrderEventAny::Denied(denied) = &events[0] else {
9349            panic!("expected OrderDenied, was {:?}", events[0]);
9350        };
9351        assert!(
9352            denied.reason.contains("No durable store configured"),
9353            "was: {}",
9354            denied.reason
9355        );
9356    }
9357
9358    #[tokio::test]
9359    async fn submit_order_denies_when_transaction_in_flight() {
9360        let (mut client, _) = swap_client_with_cache(test_config("http://127.0.0.1:1".to_string()));
9361        client.core.set_connected();
9362        *client.in_flight.lock() = Some(InFlightSlot::AwaitingFinality(InFlightTransaction {
9363            intent_id: 1,
9364            nonce: 7,
9365            tx_hash: B256::ZERO,
9366            purpose: TransactionPurpose::Wrap,
9367        }));
9368        let order = test_market_sell_order(test_pool().instrument_id);
9369        let mut receiver = start_with_events(&mut client);
9370
9371        client.submit_order(submit_order_cmd(&order)).unwrap();
9372
9373        let events = collect_order_events(&mut receiver);
9374        assert_eq!(events.len(), 1);
9375        let OrderEventAny::Denied(denied) = &events[0] else {
9376            panic!("expected OrderDenied, was {:?}", events[0]);
9377        };
9378        assert!(
9379            denied.reason.contains("still awaiting finality"),
9380            "was: {}",
9381            denied.reason
9382        );
9383    }
9384
9385    #[tokio::test]
9386    async fn submit_order_denies_without_signer() {
9387        let Some((admin_pool, schema, mut client, _, _)) =
9388            swap_client_with_database("execution_submit_no_signer_test", swap_rpc_state().await)
9389                .await
9390        else {
9391            return;
9392        };
9393        client.signer = None;
9394        let order = test_market_sell_order(test_pool().instrument_id);
9395        let mut receiver = start_with_events(&mut client);
9396
9397        client.submit_order(submit_order_cmd(&order)).unwrap();
9398
9399        let events = collect_order_events(&mut receiver);
9400        assert_eq!(events.len(), 1);
9401        let OrderEventAny::Denied(denied) = &events[0] else {
9402            panic!("expected OrderDenied, was {:?}", events[0]);
9403        };
9404        assert!(
9405            denied.reason.contains("Signer not initialized"),
9406            "was: {}",
9407            denied.reason
9408        );
9409
9410        drop_execution_schema(&admin_pool, &schema).await;
9411    }
9412
9413    #[tokio::test]
9414    async fn submit_order_broadcasts_swap_and_records_client_order_id() {
9415        let Some((admin_pool, schema, mut client, state, _)) =
9416            swap_client_with_database("execution_submit_success_test", swap_rpc_state().await)
9417                .await
9418        else {
9419            return;
9420        };
9421        let order = test_market_sell_order(test_pool().instrument_id);
9422        let mut receiver = start_with_events(&mut client);
9423        let expected_min_out = expected_min_amount_out(50);
9424
9425        client.submit_order(submit_order_cmd(&order)).unwrap();
9426        await_pending_tasks(&client).await;
9427
9428        let events = collect_order_events(&mut receiver);
9429        assert_swap_submitted_and_filled(&events);
9430        let OrderEventAny::Submitted(submitted) = &events[0] else {
9431            panic!("expected OrderSubmitted, was {:?}", events[0]);
9432        };
9433        assert_eq!(submitted.client_order_id, order.client_order_id());
9434
9435        // The recorded broadcast matches the fully signed expected transaction
9436        let (expected_hash, expected_raw) = expected_swap_tx(expected_min_out).await;
9437        let broadcasts: Vec<_> = state
9438            .recorded_requests()
9439            .into_iter()
9440            .filter(|request| request["method"] == "eth_sendRawTransaction")
9441            .collect();
9442        assert_eq!(broadcasts.len(), 1);
9443        assert_eq!(broadcasts[0]["params"][0].as_str().unwrap(), expected_raw);
9444
9445        let record = client
9446            .cache
9447            .get_execution_transaction(42161, &expected_hash.to_string())
9448            .await
9449            .unwrap()
9450            .unwrap();
9451        assert_eq!(record.nonce, 7);
9452        assert_eq!(record.purpose, "swap");
9453        assert_eq!(record.status, "finalized");
9454        assert_eq!(
9455            record.client_order_id.as_deref(),
9456            Some(order.client_order_id().as_str())
9457        );
9458        assert!(client.in_flight.lock().is_none());
9459        let nonce_state = sqlx::query_as::<_, (i64, i64)>(sqlx::AssertSqlSafe(format!(
9460            "SELECT next_canonical_nonce, revision FROM {schema}.execution_verification_nonce"
9461        )))
9462        .fetch_one(&admin_pool)
9463        .await
9464        .unwrap();
9465        let decision_count = sqlx::query_scalar::<_, i64>(sqlx::AssertSqlSafe(format!(
9466            "SELECT COUNT(*) FROM {schema}.execution_verification_decision \
9467             WHERE outcome = 'verified'"
9468        )))
9469        .fetch_one(&admin_pool)
9470        .await
9471        .unwrap();
9472        let connect_decision_count = sqlx::query_scalar::<_, i64>(sqlx::AssertSqlSafe(format!(
9473            "SELECT COUNT(*) FROM {schema}.execution_verification_decision \
9474                 WHERE decision_class = 'connect' AND outcome = 'verified'"
9475        )))
9476        .fetch_one(&admin_pool)
9477        .await
9478        .unwrap();
9479        assert_eq!(nonce_state, (8, 1));
9480        assert_eq!(decision_count, 35);
9481        assert_eq!(connect_decision_count, 1);
9482
9483        drop_execution_schema(&admin_pool, &schema).await;
9484    }
9485
9486    #[tokio::test]
9487    async fn submit_order_pins_every_pre_sign_state_read_to_swap_anchor() {
9488        let Some((admin_pool, schema, mut client, state, _)) =
9489            swap_client_with_database("execution_submit_anchor_reads_test", swap_rpc_state().await)
9490                .await
9491        else {
9492            return;
9493        };
9494        let order = test_market_sell_order(test_pool().instrument_id);
9495        let mut receiver = start_with_events(&mut client);
9496
9497        client.submit_order(submit_order_cmd(&order)).unwrap();
9498        await_pending_tasks(&client).await;
9499
9500        let events = collect_order_events(&mut receiver);
9501        assert_swap_submitted_and_filled(&events);
9502        let requests = state.recorded_requests();
9503        let broadcast_index = requests
9504            .iter()
9505            .position(|request| request["method"] == "eth_sendRawTransaction")
9506            .unwrap();
9507        let pre_broadcast = &requests[..broadcast_index];
9508        let latest_reads: Vec<_> = pre_broadcast
9509            .iter()
9510            .filter(|request| {
9511                request["method"] == "eth_getBlockByNumber" && request["params"][0] == "latest"
9512            })
9513            .collect();
9514        assert_eq!(latest_reads.len(), 3);
9515        assert!(
9516            latest_reads
9517                .iter()
9518                .all(|request| request["params"] == serde_json::json!(["latest", false]))
9519        );
9520
9521        for method in [
9522            "eth_getCode",
9523            "eth_call",
9524            "eth_estimateGas",
9525            "eth_getBalance",
9526        ] {
9527            let pinned: Vec<_> = pre_broadcast
9528                .iter()
9529                .filter(|request| request["method"] == method)
9530                .collect();
9531            assert!(!pinned.is_empty(), "method {method}");
9532            assert_eq!(pinned.len() % 3, 0, "method {method}: {pinned:?}");
9533            assert!(
9534                pinned
9535                    .iter()
9536                    .all(|request| request["params"][1] == FIXTURE_BLOCK_PARAM),
9537                "method {method}: {pinned:?}"
9538            );
9539        }
9540
9541        let numbered_blocks: Vec<_> = pre_broadcast
9542            .iter()
9543            .filter(|request| {
9544                request["method"] == "eth_getBlockByNumber"
9545                    && request["params"][0] == FIXTURE_BLOCK_PARAM
9546            })
9547            .collect();
9548        assert!(!numbered_blocks.is_empty());
9549        assert_eq!(numbered_blocks.len() % 3, 0);
9550        assert!(
9551            numbered_blocks
9552                .iter()
9553                .all(|request| request["params"][1] == false)
9554        );
9555
9556        let chain_ids: Vec<_> = pre_broadcast
9557            .iter()
9558            .filter(|request| request["method"] == "eth_chainId")
9559            .collect();
9560        assert_eq!(chain_ids.len(), 3);
9561        assert!(
9562            chain_ids
9563                .iter()
9564                .all(|request| request["params"] == serde_json::json!([]))
9565        );
9566        let nonces: Vec<_> = pre_broadcast
9567            .iter()
9568            .filter(|request| request["method"] == "eth_getTransactionCount")
9569            .collect();
9570        assert_eq!(nonces.len(), 6);
9571        assert_eq!(
9572            nonces
9573                .iter()
9574                .filter(|request| {
9575                    request["params"] == serde_json::json!([WALLET.to_ascii_lowercase(), "pending"])
9576                })
9577                .count(),
9578            3
9579        );
9580        assert_eq!(
9581            nonces
9582                .iter()
9583                .filter(|request| {
9584                    request["params"]
9585                        == serde_json::json!([WALLET.to_ascii_lowercase(), FIXTURE_BLOCK_PARAM])
9586                })
9587                .count(),
9588            3
9589        );
9590        let priority_fees: Vec<_> = pre_broadcast
9591            .iter()
9592            .filter(|request| request["method"] == "eth_maxPriorityFeePerGas")
9593            .collect();
9594        assert_eq!(priority_fees.len(), 3);
9595        assert!(
9596            priority_fees
9597                .iter()
9598                .all(|request| request["params"] == serde_json::json!([]))
9599        );
9600
9601        let (_, expected_raw) = expected_swap_tx(expected_min_amount_out(50)).await;
9602        assert_eq!(
9603            requests[broadcast_index]["params"][0].as_str().unwrap(),
9604            expected_raw
9605        );
9606        let created_block: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
9607            "SELECT created_block FROM {schema}.execution_intent"
9608        )))
9609        .fetch_one(&admin_pool)
9610        .await
9611        .unwrap();
9612        assert_eq!(created_block, i64::try_from(FIXTURE_BLOCK).unwrap());
9613
9614        drop_execution_schema(&admin_pool, &schema).await;
9615    }
9616
9617    #[tokio::test]
9618    async fn submit_order_denies_changed_swap_anchor_before_signing() {
9619        let canonical = fixture_block_response(FIXTURE_BLOCK, B256::from([0x11; 32]));
9620        let changed = fixture_block_response(FIXTURE_BLOCK, B256::from([0x44; 32]));
9621        let state = swap_rpc_state().await.with_parameter_response_sequence(
9622            "eth_getBlockByNumber",
9623            FIXTURE_BLOCK_PARAM,
9624            &[
9625                &canonical, &canonical, &canonical, &canonical, &canonical, &canonical, &canonical,
9626                &canonical, &canonical, &canonical, &canonical, &canonical, &canonical, &canonical,
9627                &canonical, &changed, &changed, &changed,
9628            ],
9629        );
9630        let Some((admin_pool, schema, mut client, state, _)) =
9631            swap_client_with_database("execution_submit_anchor_change_test", state).await
9632        else {
9633            return;
9634        };
9635        let order = test_market_sell_order(test_pool().instrument_id);
9636        let mut receiver = start_with_events(&mut client);
9637
9638        client.submit_order(submit_order_cmd(&order)).unwrap();
9639        await_pending_tasks(&client).await;
9640
9641        let events = collect_order_events(&mut receiver);
9642        assert_eq!(events.len(), 1, "was: {events:?}");
9643        let OrderEventAny::Denied(denied) = &events[0] else {
9644            panic!("expected OrderDenied, was {:?}", events[0]);
9645        };
9646        assert!(
9647            denied
9648                .reason
9649                .contains("pre-sign checkpoint reread verification disagreed"),
9650            "was: {}",
9651            denied.reason
9652        );
9653        assert_eq!(
9654            execution_intent_markers(&admin_pool, &schema).await,
9655            vec![("swap".into(), "recoverable".into(), false, false)]
9656        );
9657        let signed_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
9658            "SELECT COUNT(*) FROM {schema}.execution_transaction_hash"
9659        )))
9660        .fetch_one(&admin_pool)
9661        .await
9662        .unwrap();
9663        assert_eq!(signed_count, 0);
9664        assert!(
9665            state
9666                .recorded_requests()
9667                .iter()
9668                .all(|request| { request["method"] != "eth_sendRawTransaction" })
9669        );
9670        assert!(client.in_flight.lock().is_none());
9671
9672        drop_execution_schema(&admin_pool, &schema).await;
9673    }
9674
9675    #[tokio::test]
9676    async fn submit_order_keeps_verified_swap_anchor_when_latest_advances() {
9677        let canonical = fixture_block_response(FIXTURE_BLOCK, B256::from([0x11; 32]));
9678        let newer = fixture_block_response(FIXTURE_BLOCK + 1, B256::from([0x44; 32]));
9679        let state = swap_rpc_state().await.with_parameter_response_sequence(
9680            "eth_getBlockByNumber",
9681            "latest",
9682            &[&canonical, &canonical, &canonical, &newer, &newer, &newer],
9683        );
9684        let Some((admin_pool, schema, mut client, state, _)) =
9685            swap_client_with_database("execution_submit_advancing_head_test", state).await
9686        else {
9687            return;
9688        };
9689        let order = test_market_sell_order(test_pool().instrument_id);
9690        let mut receiver = start_with_events(&mut client);
9691
9692        client.submit_order(submit_order_cmd(&order)).unwrap();
9693        await_pending_tasks(&client).await;
9694
9695        let events = collect_order_events(&mut receiver);
9696        assert_swap_submitted_and_filled(&events);
9697        let latest_reads = state
9698            .recorded_requests()
9699            .iter()
9700            .filter(|request| {
9701                request["method"] == "eth_getBlockByNumber" && request["params"][0] == "latest"
9702            })
9703            .count();
9704        assert_eq!(latest_reads, 3);
9705
9706        drop_execution_schema(&admin_pool, &schema).await;
9707    }
9708
9709    #[tokio::test]
9710    async fn submit_order_denies_changed_quote_watermark_before_signing() {
9711        let watermark_number = FIXTURE_BLOCK;
9712        let watermark_hash = B256::from([0x11; 32]);
9713        let canonical = fixture_block_response(watermark_number, watermark_hash);
9714        let changed = fixture_block_response(watermark_number, B256::from([0x66; 32]));
9715        let watermark_param = format!("0x{watermark_number:x}");
9716        let min_amount_out = expected_min_amount_out(50);
9717        let (tx_hash, _) = expected_swap_tx(min_amount_out).await;
9718        let head = finalized_swap_block(tx_hash, min_amount_out);
9719        let state = swap_rpc_state()
9720            .await
9721            .with_response("eth_getBlockByNumber", &head)
9722            .with_parameter_response_sequence(
9723                "eth_getBlockByNumber",
9724                &watermark_param,
9725                &[
9726                    &canonical, &canonical, &canonical, &canonical, &canonical, &canonical,
9727                    &canonical, &canonical, &canonical, &canonical, &canonical, &canonical,
9728                    &changed, &changed, &changed,
9729                ],
9730            );
9731        let Some((admin_pool, schema, mut client, state, cache)) =
9732            swap_client_with_database("execution_submit_watermark_change_test", state).await
9733        else {
9734            return;
9735        };
9736        let pool = test_pool();
9737        cache
9738            .borrow_mut()
9739            .add_pool_profiler(test_profiler_at_block(
9740                &pool,
9741                watermark_number,
9742                &watermark_hash.to_string(),
9743            ))
9744            .unwrap();
9745        let order = test_market_sell_order(pool.instrument_id);
9746        let mut receiver = start_with_events(&mut client);
9747
9748        client.submit_order(submit_order_cmd(&order)).unwrap();
9749        await_pending_tasks(&client).await;
9750
9751        let events = collect_order_events(&mut receiver);
9752        assert_eq!(events.len(), 1, "was: {events:?}");
9753        let OrderEventAny::Denied(denied) = &events[0] else {
9754            panic!("expected OrderDenied, was {:?}", events[0]);
9755        };
9756        assert!(
9757            denied.reason.contains("changed before signing"),
9758            "was: {}",
9759            denied.reason
9760        );
9761        assert_eq!(
9762            execution_intent_markers(&admin_pool, &schema).await,
9763            vec![("swap".into(), "recoverable".into(), false, false)]
9764        );
9765        let signed_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
9766            "SELECT COUNT(*) FROM {schema}.execution_transaction_hash"
9767        )))
9768        .fetch_one(&admin_pool)
9769        .await
9770        .unwrap();
9771        assert_eq!(signed_count, 0);
9772        assert!(
9773            state
9774                .recorded_requests()
9775                .iter()
9776                .all(|request| { request["method"] != "eth_sendRawTransaction" })
9777        );
9778        assert!(client.in_flight.lock().is_none());
9779
9780        drop_execution_schema(&admin_pool, &schema).await;
9781    }
9782
9783    #[tokio::test]
9784    async fn changed_swap_anchor_retains_ownership_when_recovery_commit_fails() {
9785        let canonical = fixture_block_response(FIXTURE_BLOCK, B256::from([0x11; 32]));
9786        let changed = fixture_block_response(FIXTURE_BLOCK, B256::from([0x44; 32]));
9787        let state = swap_rpc_state().await.with_parameter_response_sequence(
9788            "eth_getBlockByNumber",
9789            FIXTURE_BLOCK_PARAM,
9790            &[
9791                &canonical, &canonical, &canonical, &canonical, &canonical, &canonical, &canonical,
9792                &canonical, &canonical, &canonical, &canonical, &canonical, &canonical, &canonical,
9793                &canonical, &changed, &changed, &changed,
9794            ],
9795        );
9796        let Some((admin_pool, schema, mut client, state, _)) =
9797            swap_client_with_database("execution_submit_anchor_recovery_fail_test", state).await
9798        else {
9799            return;
9800        };
9801        install_recoverable_commit_rejection(&admin_pool, &schema).await;
9802        let order = test_market_sell_order(test_pool().instrument_id);
9803        let mut receiver = start_with_events(&mut client);
9804
9805        client.submit_order(submit_order_cmd(&order)).unwrap();
9806        await_pending_tasks(&client).await;
9807
9808        let events = collect_order_events(&mut receiver);
9809        assert_eq!(events.len(), 1, "was: {events:?}");
9810        let OrderEventAny::Denied(denied) = &events[0] else {
9811            panic!("expected OrderDenied, was {:?}", events[0]);
9812        };
9813        assert!(
9814            denied
9815                .reason
9816                .contains("pre-sign checkpoint reread verification disagreed"),
9817            "was: {}",
9818            denied.reason
9819        );
9820        assert_eq!(
9821            execution_intent_markers(&admin_pool, &schema).await,
9822            vec![("swap".into(), "prepared".into(), false, true)]
9823        );
9824        let signed_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
9825            "SELECT COUNT(*) FROM {schema}.execution_transaction_hash"
9826        )))
9827        .fetch_one(&admin_pool)
9828        .await
9829        .unwrap();
9830        assert_eq!(signed_count, 0);
9831        assert!(
9832            state
9833                .recorded_requests()
9834                .iter()
9835                .all(|request| { request["method"] != "eth_sendRawTransaction" })
9836        );
9837        assert!(matches!(
9838            *client.in_flight.lock(),
9839            Some(InFlightSlot::Preparing(TransactionPurpose::Swap))
9840        ));
9841
9842        drop_execution_schema(&admin_pool, &schema).await;
9843    }
9844
9845    #[tokio::test]
9846    async fn finalized_swap_emits_exact_fill_once_and_refreshes_wallet() {
9847        let min_amount_out = expected_min_amount_out(50);
9848        let (expected_hash, _) = expected_swap_tx(min_amount_out).await;
9849        let state = finalized_swap_rpc_state(expected_hash, min_amount_out);
9850        let Some((admin_pool, schema, mut client, state, _)) =
9851            swap_client_with_database("execution_submit_fill_test", state).await
9852        else {
9853            return;
9854        };
9855        let order = test_market_sell_order(test_pool().instrument_id);
9856        let mut receiver = start_with_events(&mut client);
9857
9858        let plan = client
9859            .prepare_swap(&submit_order_cmd(&order), &order)
9860            .unwrap();
9861        execute_swap(
9862            plan,
9863            client.transaction_executor().unwrap(),
9864            client.emitter.clone(),
9865            client.transaction_limits.max_quote_age_blocks,
9866            client.transaction_limits.deadline_seconds,
9867        )
9868        .await
9869        .unwrap();
9870
9871        let (nonce, wallet_address, transaction_to, transaction_input, transaction_value): (
9872            i64,
9873            String,
9874            String,
9875            String,
9876            String,
9877        ) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
9878            "SELECT nonce, wallet_address, transaction_to, transaction_input, transaction_value \
9879             FROM {schema}.execution_intent"
9880        )))
9881        .fetch_one(&admin_pool)
9882        .await
9883        .unwrap();
9884        let finalized_block = client
9885            .http_rpc_client
9886            .block_by_number(FIXTURE_BLOCK + 1, true)
9887            .await
9888            .unwrap();
9889        let finalized_transaction = finalized_block
9890            .transactions
9891            .iter()
9892            .find(|transaction| transaction.hash == expected_hash)
9893            .unwrap();
9894        assert_eq!(finalized_transaction.from.to_string(), wallet_address);
9895        assert_eq!(finalized_transaction.nonce, u64::try_from(nonce).unwrap());
9896        assert_eq!(
9897            finalized_transaction.to,
9898            Some(Address::from_str(&transaction_to).unwrap())
9899        );
9900        assert_eq!(
9901            finalized_transaction.input.as_ref(),
9902            hex::decode(transaction_input.strip_prefix("0x").unwrap()).unwrap()
9903        );
9904        assert_eq!(
9905            finalized_transaction.value,
9906            U256::from_str(&transaction_value).unwrap()
9907        );
9908
9909        let mut order_events = Vec::new();
9910        let mut account_states = Vec::new();
9911
9912        while let Ok(event) = receiver.try_recv() {
9913            match event {
9914                ExecutionEvent::Order(event) => order_events.push(event),
9915                ExecutionEvent::Account(state) => account_states.push(state),
9916                other => panic!("unexpected execution event: {other:?}"),
9917            }
9918        }
9919        assert_eq!(order_events.len(), 2, "was: {order_events:?}");
9920        assert!(matches!(&order_events[0], OrderEventAny::Submitted(_)));
9921        let OrderEventAny::Filled(fill) = &order_events[1] else {
9922            panic!("expected OrderFilled, was {:?}", order_events[1]);
9923        };
9924        let expected_commission = Money::from_u256(
9925            U256::from(50_112_u64) * U256::from(100_000_000_u64),
9926            test_pool().chain.native_currency(),
9927        )
9928        .unwrap();
9929        assert_eq!(fill.client_order_id, order.client_order_id());
9930        assert_eq!(fill.venue_order_id.as_str(), expected_hash.to_string());
9931        assert_eq!(fill.order_side, OrderSide::Sell);
9932        assert_eq!(fill.last_qty, Quantity::from("0.001"));
9933        assert_eq!(fill.last_px, Price::from("1000"));
9934        assert_eq!(fill.currency.code, "USDC");
9935        assert_eq!(fill.commission, Some(expected_commission));
9936        assert_eq!(fill.liquidity_side, LiquiditySide::Taker);
9937        assert_eq!(account_states.len(), 1);
9938        let account_state = &account_states[0];
9939        assert_eq!(account_state.account_id, AccountId::from("BLOCKCHAIN-001"));
9940        assert_eq!(account_state.account_type, AccountType::Wallet);
9941        assert_eq!(account_state.base_currency, None);
9942        assert_eq!(account_state.balances.len(), 3);
9943        assert!(account_state.margins.is_empty());
9944        assert!(account_state.is_reported);
9945        assert_eq!(
9946            account_state.balances,
9947            client.wallet_balance.lock().as_account_balances().unwrap()
9948        );
9949
9950        let (fill_emitted, active): (bool, bool) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
9951            "SELECT fill_emitted, active FROM {schema}.execution_intent"
9952        )))
9953        .fetch_one(&admin_pool)
9954        .await
9955        .unwrap();
9956        assert!(fill_emitted);
9957        assert!(!active);
9958
9959        let database = client.cache.database.as_ref().unwrap().clone();
9960        let payload_keys = client.payload_keys.clone();
9961        let restart_config = client.config.clone();
9962        drop(client);
9963        let (mut restarted, _) = swap_client_with_cache(restart_config);
9964        restarted.cache.database = Some(database);
9965        restarted.payload_keys = payload_keys;
9966        restarted.signer = Some(Arc::new(
9967            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
9968        ));
9969        let mut restart_receiver = start_with_events(&mut restarted);
9970        restarted.reconcile_unresolved_execution().await.unwrap();
9971        restarted.reconcile_unresolved_execution().await.unwrap();
9972        assert!(collect_order_events(&mut restart_receiver).is_empty());
9973        let requests = state.recorded_requests();
9974        assert_eq!(
9975            requests
9976                .iter()
9977                .filter(|request| request["method"] == "eth_sendRawTransaction")
9978                .count(),
9979            1
9980        );
9981        assert_eq!(
9982            requests
9983                .iter()
9984                .filter(|request| {
9985                    request["method"] == "eth_call"
9986                        && request["params"][0]["data"]
9987                            .as_str()
9988                            .is_some_and(|data| data.starts_with(BALANCE_OF_SELECTOR))
9989                })
9990                .count(),
9991            9
9992        );
9993        assert_eq!(
9994            requests
9995                .iter()
9996                .filter(|request| request["method"] == "eth_getBalance")
9997                .count(),
9998            6
9999        );
10000
10001        drop_execution_schema(&admin_pool, &schema).await;
10002    }
10003
10004    #[tokio::test]
10005    async fn restart_emits_committed_swap_from_verified_inclusion_header() {
10006        let min_amount_out = expected_min_amount_out(50);
10007        let (expected_hash, _) = expected_swap_tx(min_amount_out).await;
10008        let state = finalized_swap_rpc_state(expected_hash, min_amount_out);
10009        let Some((admin_pool, schema, mut client, _, _)) =
10010            swap_client_with_database("execution_committed_fill_restart_test", state).await
10011        else {
10012            return;
10013        };
10014        let order = test_market_sell_order(test_pool().instrument_id);
10015        let mut receiver = start_with_events(&mut client);
10016        let plan = client
10017            .prepare_swap(&submit_order_cmd(&order), &order)
10018            .unwrap();
10019
10020        execute_swap(
10021            plan,
10022            client.transaction_executor().unwrap(),
10023            client.emitter.clone(),
10024            client.transaction_limits.max_quote_age_blocks,
10025            client.transaction_limits.deadline_seconds,
10026        )
10027        .await
10028        .unwrap();
10029        assert_swap_submitted_and_filled(&collect_order_events(&mut receiver));
10030        let ancestry_range: (i64, i64) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
10031            "SELECT height_start, height_end FROM {schema}.execution_verification_decision \
10032             WHERE decision_class = 'finality' AND read_class = 'numbered_block' \
10033               AND height_end > height_start"
10034        )))
10035        .fetch_one(&admin_pool)
10036        .await
10037        .unwrap();
10038        assert_eq!(
10039            ancestry_range,
10040            ((FIXTURE_BLOCK + 1) as i64, (FIXTURE_BLOCK + 2) as i64)
10041        );
10042
10043        sqlx::query(sqlx::AssertSqlSafe(format!(
10044            "UPDATE {schema}.execution_intent SET fill_emitted = FALSE, active = TRUE"
10045        )))
10046        .execute(&admin_pool)
10047        .await
10048        .unwrap();
10049        let database = client.cache.database.as_ref().unwrap().clone();
10050        let payload_keys = client.payload_keys.clone();
10051        let restart_config = client.config.clone();
10052        drop(client);
10053        let (mut restarted, _) = swap_client_with_cache(restart_config);
10054        restarted.cache.database = Some(database);
10055        restarted.payload_keys = payload_keys;
10056        restarted.signer = Some(Arc::new(
10057            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
10058        ));
10059        let mut restart_receiver = start_with_events(&mut restarted);
10060
10061        restarted.reconcile_unresolved_execution().await.unwrap();
10062
10063        let events = collect_order_events(&mut restart_receiver);
10064        let (fill_emitted, active): (bool, bool) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
10065            "SELECT fill_emitted, active FROM {schema}.execution_intent"
10066        )))
10067        .fetch_one(&admin_pool)
10068        .await
10069        .unwrap();
10070        assert_eq!(events.len(), 1, "was: {events:?}");
10071        assert!(matches!(&events[0], OrderEventAny::Filled(_)));
10072        assert!(fill_emitted);
10073        assert!(!active);
10074
10075        drop_execution_schema(&admin_pool, &schema).await;
10076    }
10077
10078    #[tokio::test]
10079    async fn finalized_swap_ignores_unrelated_swap_logs() {
10080        let min_amount_out = expected_min_amount_out(50);
10081        let (expected_hash, _) = expected_swap_tx(min_amount_out).await;
10082        let receipt = finalized_swap_receipt_with_unrelated_swap(expected_hash);
10083        let state = finalized_swap_rpc_state(expected_hash, min_amount_out)
10084            .with_response("eth_getTransactionReceipt", &receipt);
10085        let Some((admin_pool, schema, mut client, _, _)) =
10086            swap_client_with_database("execution_submit_unrelated_log_test", state).await
10087        else {
10088            return;
10089        };
10090        let order = test_market_sell_order(test_pool().instrument_id);
10091        let mut receiver = start_with_events(&mut client);
10092
10093        client.submit_order(submit_order_cmd(&order)).unwrap();
10094        await_pending_tasks(&client).await;
10095
10096        let events = collect_order_events(&mut receiver);
10097        assert_swap_submitted_and_filled(&events);
10098
10099        drop_execution_schema(&admin_pool, &schema).await;
10100    }
10101
10102    #[tokio::test]
10103    async fn prepare_swap_buy_accepts_quote_spend_exact_boundary() {
10104        let amount_in = expected_buy_amount_in();
10105        let min_amount_out = expected_buy_min_amount_out(50);
10106        let (expected_hash, _) = expected_buy_swap_tx(min_amount_out, amount_in).await;
10107        let state = finalized_buy_swap_rpc_state(expected_hash, min_amount_out, amount_in);
10108        let max_amount = amount_in.to_string();
10109        let Some((admin_pool, schema, client, _, cache)) = swap_client_with_database_config(
10110            "execution_prepare_buy_test",
10111            state,
10112            move |http_rpc_url| {
10113                let mut config = buy_test_config(http_rpc_url);
10114                config.quote_spend_limits.as_mut().unwrap()[0].max_amount = max_amount;
10115                config
10116            },
10117        )
10118        .await
10119        else {
10120            return;
10121        };
10122        let order = test_market_buy_order(test_pool().instrument_id);
10123        cache
10124            .borrow_mut()
10125            .add_order(order.clone(), None, None, true)
10126            .unwrap();
10127
10128        let plan = client
10129            .prepare_swap(&submit_order_cmd(&order), &order)
10130            .unwrap();
10131
10132        assert_eq!(plan.token_in, USDC_ADDRESS);
10133        assert_eq!(plan.token_out, WETH_ADDRESS);
10134        assert_eq!(plan.amount_in, amount_in);
10135        assert_eq!(plan.min_amount_out, min_amount_out);
10136        assert_ne!(plan.token_in, WETH_ADDRESS);
10137
10138        drop_execution_schema(&admin_pool, &schema).await;
10139    }
10140
10141    #[tokio::test]
10142    async fn submit_order_broadcasts_buy_swap() {
10143        let amount_in = expected_buy_amount_in();
10144        let min_amount_out = expected_buy_min_amount_out(50);
10145        let (expected_hash, expected_raw) = expected_buy_swap_tx(min_amount_out, amount_in).await;
10146        let state = finalized_buy_swap_rpc_state(expected_hash, min_amount_out, amount_in);
10147        let Some((admin_pool, schema, mut client, state, cache)) =
10148            swap_client_with_buy_database("execution_submit_buy_success_test", state).await
10149        else {
10150            return;
10151        };
10152        let order = test_market_buy_order(test_pool().instrument_id);
10153        cache
10154            .borrow_mut()
10155            .add_order(order.clone(), None, None, true)
10156            .unwrap();
10157        let mut receiver = start_with_events(&mut client);
10158
10159        client.submit_order(submit_order_cmd(&order)).unwrap();
10160        await_pending_tasks(&client).await;
10161
10162        let events = collect_order_events(&mut receiver);
10163        assert_swap_submitted_and_filled(&events);
10164        let broadcasts: Vec<_> = state
10165            .recorded_requests()
10166            .into_iter()
10167            .filter(|request| request["method"] == "eth_sendRawTransaction")
10168            .collect();
10169        assert_eq!(broadcasts.len(), 1);
10170        assert_eq!(broadcasts[0]["params"][0].as_str().unwrap(), expected_raw);
10171
10172        drop_execution_schema(&admin_pool, &schema).await;
10173    }
10174
10175    #[tokio::test]
10176    async fn finalized_buy_swap_emits_fill_from_output_leg() {
10177        let amount_in = expected_buy_amount_in();
10178        let min_amount_out = expected_buy_min_amount_out(50);
10179        let (expected_hash, _) = expected_buy_swap_tx(min_amount_out, amount_in).await;
10180        let state = finalized_buy_swap_rpc_state(expected_hash, min_amount_out, amount_in);
10181        let Some((admin_pool, schema, mut client, state, cache)) =
10182            swap_client_with_buy_database("execution_submit_buy_fill_test", state).await
10183        else {
10184            return;
10185        };
10186        let order = test_market_buy_order(test_pool().instrument_id);
10187        cache
10188            .borrow_mut()
10189            .add_order(order.clone(), None, None, true)
10190            .unwrap();
10191        let mut receiver = start_with_events(&mut client);
10192        let plan = client
10193            .prepare_swap(&submit_order_cmd(&order), &order)
10194            .unwrap();
10195
10196        execute_swap(
10197            plan,
10198            client.transaction_executor().unwrap(),
10199            client.emitter.clone(),
10200            client.transaction_limits.max_quote_age_blocks,
10201            client.transaction_limits.deadline_seconds,
10202        )
10203        .await
10204        .unwrap();
10205
10206        let mut order_events = Vec::new();
10207
10208        while let Ok(event) = receiver.try_recv() {
10209            if let ExecutionEvent::Order(event) = event {
10210                order_events.push(event);
10211            }
10212        }
10213        assert_eq!(order_events.len(), 2, "was: {order_events:?}");
10214        assert!(matches!(&order_events[0], OrderEventAny::Submitted(_)));
10215        let OrderEventAny::Filled(fill) = &order_events[1] else {
10216            panic!("expected OrderFilled, was {:?}", order_events[1]);
10217        };
10218        let expected_commission = Money::from_u256(
10219            U256::from(50_112_u64) * U256::from(100_000_000_u64),
10220            test_pool().chain.native_currency(),
10221        )
10222        .unwrap();
10223        assert_eq!(fill.client_order_id, order.client_order_id());
10224        assert_eq!(fill.venue_order_id.as_str(), expected_hash.to_string());
10225        assert_eq!(fill.order_side, OrderSide::Buy);
10226        assert_eq!(fill.last_qty, Quantity::from("0.001"));
10227        assert_eq!(fill.currency.code, "USDC");
10228        assert_eq!(fill.commission, Some(expected_commission));
10229        assert_eq!(fill.liquidity_side, LiquiditySide::Taker);
10230        assert_eq!(
10231            Money::from_decimal(
10232                fill.last_qty.as_decimal() * fill.last_px.as_decimal(),
10233                fill.currency,
10234            )
10235            .unwrap(),
10236            Money::from_u256(amount_in, fill.currency).unwrap()
10237        );
10238
10239        let (fill_emitted, active): (bool, bool) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
10240            "SELECT fill_emitted, active FROM {schema}.execution_intent"
10241        )))
10242        .fetch_one(&admin_pool)
10243        .await
10244        .unwrap();
10245        assert!(fill_emitted);
10246        assert!(!active);
10247
10248        let database = client.cache.database.as_ref().unwrap().clone();
10249        let payload_keys = client.payload_keys.clone();
10250        let restart_config = client.config.clone();
10251        drop(client);
10252        let (mut restarted, _) = swap_client_with_cache(restart_config);
10253        restarted.cache.database = Some(database);
10254        restarted.payload_keys = payload_keys;
10255        restarted.signer = Some(Arc::new(
10256            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
10257        ));
10258        let mut restart_receiver = start_with_events(&mut restarted);
10259        restarted.reconcile_unresolved_execution().await.unwrap();
10260        restarted.reconcile_unresolved_execution().await.unwrap();
10261        assert!(collect_order_events(&mut restart_receiver).is_empty());
10262        assert_eq!(
10263            state
10264                .recorded_requests()
10265                .iter()
10266                .filter(|request| request["method"] == "eth_sendRawTransaction")
10267                .count(),
10268            1
10269        );
10270
10271        drop_execution_schema(&admin_pool, &schema).await;
10272    }
10273
10274    #[tokio::test]
10275    async fn finalized_buy_swap_cancels_remainder_when_output_is_short() {
10276        let amount_in = expected_buy_amount_in();
10277        let min_amount_out = expected_buy_min_amount_out(50);
10278        let (expected_hash, _) = expected_buy_swap_tx(min_amount_out, amount_in).await;
10279        let short_base_out = min_amount_out;
10280        let mut state = finalized_buy_swap_rpc_state(expected_hash, min_amount_out, amount_in);
10281        state = state.with_response(
10282            "eth_getTransactionReceipt",
10283            &finalized_buy_swap_receipt_with_base_out(expected_hash, amount_in, short_base_out),
10284        );
10285        let Some((admin_pool, schema, mut client, _, cache)) =
10286            swap_client_with_buy_database("execution_submit_buy_short_fill_test", state).await
10287        else {
10288            return;
10289        };
10290        let order = test_market_buy_order(test_pool().instrument_id);
10291        cache
10292            .borrow_mut()
10293            .add_order(order.clone(), None, None, true)
10294            .unwrap();
10295        let mut receiver = start_with_events(&mut client);
10296        let plan = client
10297            .prepare_swap(&submit_order_cmd(&order), &order)
10298            .unwrap();
10299
10300        execute_swap(
10301            plan,
10302            client.transaction_executor().unwrap(),
10303            client.emitter.clone(),
10304            client.transaction_limits.max_quote_age_blocks,
10305            client.transaction_limits.deadline_seconds,
10306        )
10307        .await
10308        .unwrap();
10309
10310        let events = collect_order_events(&mut receiver);
10311        assert_eq!(events.len(), 3, "was: {events:?}");
10312        assert!(matches!(&events[0], OrderEventAny::Submitted(_)));
10313        let OrderEventAny::Filled(fill) = &events[1] else {
10314            panic!("expected OrderFilled, was {:?}", events[1]);
10315        };
10316        let expected_qty = raw_amount_to_quantity(short_base_out, 18).unwrap();
10317        assert_eq!(fill.order_side, OrderSide::Buy);
10318        assert_eq!(fill.last_qty, expected_qty);
10319        assert!(fill.last_qty < order.quantity());
10320        let OrderEventAny::Canceled(canceled) = &events[2] else {
10321            panic!("expected OrderCanceled, was {:?}", events[2]);
10322        };
10323        assert_eq!(canceled.client_order_id, order.client_order_id());
10324        assert_eq!(
10325            canceled.venue_order_id.as_ref().map(VenueOrderId::as_str),
10326            Some(expected_hash.to_string().as_str())
10327        );
10328
10329        drop_execution_schema(&admin_pool, &schema).await;
10330    }
10331
10332    #[tokio::test]
10333    async fn finalized_buy_swap_reports_full_output_when_above_order_quantity() {
10334        let amount_in = expected_buy_amount_in();
10335        let min_amount_out = expected_buy_min_amount_out(50);
10336        let (expected_hash, _) = expected_buy_swap_tx(min_amount_out, amount_in).await;
10337        let overshoot_base_out = expected_buy_base_amount() * U256::from(2) + U256::from(44);
10338        let mut state = finalized_buy_swap_rpc_state(expected_hash, min_amount_out, amount_in);
10339        state = state.with_response(
10340            "eth_getTransactionReceipt",
10341            &finalized_buy_swap_receipt_with_base_out(expected_hash, amount_in, overshoot_base_out),
10342        );
10343        let Some((admin_pool, schema, mut client, _, cache)) =
10344            swap_client_with_buy_database("execution_submit_buy_overshoot_fill_test", state).await
10345        else {
10346            return;
10347        };
10348        let order = test_market_buy_order(test_pool().instrument_id);
10349        cache
10350            .borrow_mut()
10351            .add_order(order.clone(), None, None, true)
10352            .unwrap();
10353        let mut receiver = start_with_events(&mut client);
10354        let plan = client
10355            .prepare_swap(&submit_order_cmd(&order), &order)
10356            .unwrap();
10357
10358        execute_swap(
10359            plan,
10360            client.transaction_executor().unwrap(),
10361            client.emitter.clone(),
10362            client.transaction_limits.max_quote_age_blocks,
10363            client.transaction_limits.deadline_seconds,
10364        )
10365        .await
10366        .unwrap();
10367
10368        let events = collect_order_events(&mut receiver);
10369        assert_eq!(events.len(), 2, "was: {events:?}");
10370        assert!(matches!(&events[0], OrderEventAny::Submitted(_)));
10371        let OrderEventAny::Filled(fill) = &events[1] else {
10372            panic!("expected OrderFilled, was {:?}", events[1]);
10373        };
10374        let expected_qty = raw_amount_to_quantity(overshoot_base_out, 18).unwrap();
10375        assert_eq!(fill.order_side, OrderSide::Buy);
10376        assert_eq!(fill.last_qty, expected_qty);
10377        assert!(fill.last_qty > order.quantity());
10378        assert_eq!(
10379            Money::from_decimal(
10380                fill.last_qty.as_decimal() * fill.last_px.as_decimal(),
10381                fill.currency,
10382            )
10383            .unwrap(),
10384            Money::from_u256(amount_in, fill.currency).unwrap()
10385        );
10386
10387        drop_execution_schema(&admin_pool, &schema).await;
10388    }
10389
10390    #[tokio::test]
10391    async fn finalized_buy_swap_quarantines_sell_oriented_log() {
10392        let amount_in = expected_buy_amount_in();
10393        let min_amount_out = expected_buy_min_amount_out(50);
10394        let (expected_hash, _) = expected_buy_swap_tx(min_amount_out, amount_in).await;
10395        let state = finalized_buy_swap_rpc_state(expected_hash, min_amount_out, amount_in)
10396            .with_response(
10397                "eth_getTransactionReceipt",
10398                &finalized_swap_receipt(expected_hash),
10399            );
10400        let Some((admin_pool, schema, mut client, _, cache)) =
10401            swap_client_with_buy_database("execution_submit_buy_sell_log_test", state).await
10402        else {
10403            return;
10404        };
10405        let order = test_market_buy_order(test_pool().instrument_id);
10406        cache
10407            .borrow_mut()
10408            .add_order(order.clone(), None, None, true)
10409            .unwrap();
10410        let mut receiver = start_with_events(&mut client);
10411        let plan = client
10412            .prepare_swap(&submit_order_cmd(&order), &order)
10413            .unwrap();
10414
10415        let error = execute_swap(
10416            plan,
10417            client.transaction_executor().unwrap(),
10418            client.emitter.clone(),
10419            client.transaction_limits.max_quote_age_blocks,
10420            client.transaction_limits.deadline_seconds,
10421        )
10422        .await
10423        .unwrap_err();
10424
10425        let events = collect_order_events(&mut receiver);
10426        assert_swap_quarantined_without_terminal_event(&events);
10427        assert!(
10428            error
10429                .to_string()
10430                .contains("does not match the persisted amount")
10431                || error.to_string().contains("is not a BUY output"),
10432            "was: {error}"
10433        );
10434        assert!(client.in_flight.lock().is_some());
10435
10436        drop_execution_schema(&admin_pool, &schema).await;
10437    }
10438
10439    #[tokio::test]
10440    async fn submit_order_applies_buy_slippage_to_base_output() {
10441        let amount_in = expected_buy_amount_in();
10442        let min_amount_out = expected_buy_min_amount_out(200);
10443        let (expected_hash, expected_raw) = expected_buy_swap_tx(min_amount_out, amount_in).await;
10444        let state = finalized_buy_swap_rpc_state(expected_hash, min_amount_out, amount_in);
10445        let Some((admin_pool, schema, mut client, state, cache)) =
10446            swap_client_with_buy_database("execution_submit_buy_slippage_test", state).await
10447        else {
10448            return;
10449        };
10450        let order = test_market_buy_order(test_pool().instrument_id);
10451        cache
10452            .borrow_mut()
10453            .add_order(order.clone(), None, None, true)
10454            .unwrap();
10455        let mut cmd = submit_order_cmd(&order);
10456        cmd.params = Some(serde_json::from_str(r#"{"slippage_bps": 200}"#).unwrap());
10457        let mut receiver = start_with_events(&mut client);
10458
10459        client.submit_order(cmd).unwrap();
10460        await_pending_tasks(&client).await;
10461
10462        let events = collect_order_events(&mut receiver);
10463        assert_swap_submitted_and_filled(&events);
10464        let broadcasts: Vec<_> = state
10465            .recorded_requests()
10466            .into_iter()
10467            .filter(|request| request["method"] == "eth_sendRawTransaction")
10468            .collect();
10469        assert_eq!(broadcasts.len(), 1);
10470        assert_eq!(broadcasts[0]["params"][0].as_str().unwrap(), expected_raw);
10471
10472        drop_execution_schema(&admin_pool, &schema).await;
10473    }
10474
10475    #[tokio::test]
10476    async fn submit_order_denies_buy_on_insufficient_quote_balance() {
10477        let amount_in = expected_buy_amount_in();
10478        let min_amount_out = expected_buy_min_amount_out(50);
10479        let (expected_hash, _) = expected_buy_swap_tx(min_amount_out, amount_in).await;
10480        let state = finalized_buy_swap_rpc_state(expected_hash, min_amount_out, amount_in)
10481            .with_call_response(BALANCE_OF_SELECTOR, CALL_ZERO);
10482        let Some((admin_pool, schema, mut client, _, cache)) =
10483            swap_client_with_buy_database("execution_submit_buy_balance_test", state).await
10484        else {
10485            return;
10486        };
10487        let order = test_market_buy_order(test_pool().instrument_id);
10488        cache
10489            .borrow_mut()
10490            .add_order(order.clone(), None, None, true)
10491            .unwrap();
10492        let mut receiver = start_with_events(&mut client);
10493
10494        client.submit_order(submit_order_cmd(&order)).unwrap();
10495        await_pending_tasks(&client).await;
10496
10497        let events = collect_order_events(&mut receiver);
10498        assert_eq!(events.len(), 1);
10499        let OrderEventAny::Denied(denied) = &events[0] else {
10500            panic!("expected OrderDenied, was {:?}", events[0]);
10501        };
10502        assert!(
10503            denied.reason.contains("is below the swap amount"),
10504            "was: {}",
10505            denied.reason
10506        );
10507
10508        drop_execution_schema(&admin_pool, &schema).await;
10509    }
10510
10511    #[tokio::test]
10512    async fn finalized_swap_without_log_stays_quarantined() {
10513        let state = swap_rpc_state()
10514            .await
10515            .with_response("eth_getTransactionReceipt", RECEIPT_SUCCESS);
10516        let Some((admin_pool, schema, mut client, _, _)) =
10517            swap_client_with_database("execution_submit_missing_log_test", state).await
10518        else {
10519            return;
10520        };
10521        let order = test_market_sell_order(test_pool().instrument_id);
10522        let mut receiver = start_with_events(&mut client);
10523
10524        client.submit_order(submit_order_cmd(&order)).unwrap();
10525        await_pending_tasks(&client).await;
10526
10527        let events = collect_order_events(&mut receiver);
10528        let (status, terminal_emitted, active): (String, bool, bool) =
10529            sqlx::query_as(sqlx::AssertSqlSafe(format!(
10530                "SELECT status, terminal_emitted, active FROM {schema}.execution_intent"
10531            )))
10532            .fetch_one(&admin_pool)
10533            .await
10534            .unwrap();
10535
10536        assert_swap_quarantined_without_terminal_event(&events);
10537        assert_eq!(status, "broadcast");
10538        assert!(!terminal_emitted);
10539        assert!(active);
10540        assert!(client.in_flight.lock().is_some());
10541
10542        drop_execution_schema(&admin_pool, &schema).await;
10543    }
10544
10545    #[tokio::test]
10546    async fn finalized_swap_refresh_failure_stays_owned_for_reconciliation() {
10547        let min_amount_out = expected_min_amount_out(50);
10548        let (expected_hash, _) = expected_swap_tx(min_amount_out).await;
10549        let state = finalized_swap_rpc_state(expected_hash, min_amount_out).with_response_sequence(
10550            "eth_getBalance",
10551            &[
10552                GET_BALANCE,
10553                GET_BALANCE,
10554                GET_BALANCE,
10555                RPC_METHOD_NOT_FOUND,
10556                RPC_METHOD_NOT_FOUND,
10557                RPC_METHOD_NOT_FOUND,
10558            ],
10559        );
10560        let Some((admin_pool, schema, mut client, _state, _)) =
10561            swap_client_with_database("execution_submit_refresh_fail_test", state).await
10562        else {
10563            return;
10564        };
10565        let order = test_market_sell_order(test_pool().instrument_id);
10566        let mut receiver = start_with_events(&mut client);
10567        let plan = client
10568            .prepare_swap(&submit_order_cmd(&order), &order)
10569            .unwrap();
10570
10571        let error = execute_swap(
10572            plan,
10573            client.transaction_executor().unwrap(),
10574            client.emitter.clone(),
10575            client.transaction_limits.max_quote_age_blocks,
10576            client.transaction_limits.deadline_seconds,
10577        )
10578        .await
10579        .unwrap_err();
10580        let events = collect_order_events(&mut receiver);
10581        let (status, fill_emitted, active): (String, bool, bool) =
10582            sqlx::query_as(sqlx::AssertSqlSafe(format!(
10583                "SELECT status, fill_emitted, active FROM {schema}.execution_intent"
10584            )))
10585            .fetch_one(&admin_pool)
10586            .await
10587            .unwrap();
10588
10589        assert!(
10590            error
10591                .to_string()
10592                .contains("finalized native balance verification is locally invalid"),
10593            "was: {error}"
10594        );
10595        assert_eq!(events.len(), 1, "was: {events:?}");
10596        assert!(matches!(&events[0], OrderEventAny::Submitted(_)));
10597        assert_eq!(status, "broadcast");
10598        assert!(!fill_emitted);
10599        assert!(active);
10600        assert!(client.in_flight.lock().is_some());
10601
10602        drop_execution_schema(&admin_pool, &schema).await;
10603    }
10604
10605    #[tokio::test]
10606    async fn submit_order_denies_router_factory_mismatch_before_signing() {
10607        let state = swap_rpc_state()
10608            .await
10609            .with_call_response(FACTORY_SELECTOR, CALL_ZERO);
10610        let Some((admin_pool, schema, mut client, state, _)) =
10611            swap_client_with_database("execution_submit_factory_test", state).await
10612        else {
10613            return;
10614        };
10615        let order = test_market_sell_order(test_pool().instrument_id);
10616        let mut receiver = start_with_events(&mut client);
10617
10618        client.submit_order(submit_order_cmd(&order)).unwrap();
10619        await_pending_tasks(&client).await;
10620
10621        let events = collect_order_events(&mut receiver);
10622        assert_eq!(events.len(), 1);
10623        let OrderEventAny::Denied(denied) = &events[0] else {
10624            panic!("expected OrderDenied, was {:?}", events[0]);
10625        };
10626        assert!(
10627            denied
10628                .reason
10629                .contains("swap deployment manifest verification disagreed"),
10630            "was: {}",
10631            denied.reason
10632        );
10633        let requests = state.recorded_requests();
10634        assert!(
10635            requests
10636                .iter()
10637                .all(|request| request["method"] != "eth_getTransactionCount")
10638        );
10639        assert!(
10640            requests
10641                .iter()
10642                .all(|request| request["method"] != "eth_sendRawTransaction")
10643        );
10644
10645        drop_execution_schema(&admin_pool, &schema).await;
10646    }
10647
10648    #[tokio::test]
10649    async fn submit_order_denies_router_weth_mismatch_before_signing() {
10650        let state = swap_rpc_state()
10651            .await
10652            .with_call_response(WETH9_SELECTOR, CALL_ZERO);
10653        let Some((admin_pool, schema, mut client, state, _)) =
10654            swap_client_with_database("execution_submit_weth_test", state).await
10655        else {
10656            return;
10657        };
10658        let order = test_market_sell_order(test_pool().instrument_id);
10659        let mut receiver = start_with_events(&mut client);
10660
10661        client.submit_order(submit_order_cmd(&order)).unwrap();
10662        await_pending_tasks(&client).await;
10663
10664        let events = collect_order_events(&mut receiver);
10665        assert_eq!(events.len(), 1);
10666        let OrderEventAny::Denied(denied) = &events[0] else {
10667            panic!("expected OrderDenied, was {:?}", events[0]);
10668        };
10669        assert!(
10670            denied
10671                .reason
10672                .contains("swap deployment manifest verification disagreed"),
10673            "was: {}",
10674            denied.reason
10675        );
10676        let requests = state.recorded_requests();
10677        assert!(
10678            requests
10679                .iter()
10680                .all(|request| request["method"] != "eth_getTransactionCount")
10681        );
10682        assert!(
10683            requests
10684                .iter()
10685                .all(|request| request["method"] != "eth_sendRawTransaction")
10686        );
10687
10688        drop_execution_schema(&admin_pool, &schema).await;
10689    }
10690
10691    #[tokio::test]
10692    async fn submit_order_denies_factory_pool_mismatch_before_signing() {
10693        let state = swap_rpc_state()
10694            .await
10695            .with_call_response(GET_POOL_SELECTOR, CALL_ZERO);
10696        let Some((admin_pool, schema, mut client, state, _)) =
10697            swap_client_with_database("execution_submit_pool_identity_test", state).await
10698        else {
10699            return;
10700        };
10701        let order = test_market_sell_order(test_pool().instrument_id);
10702        let mut receiver = start_with_events(&mut client);
10703
10704        client.submit_order(submit_order_cmd(&order)).unwrap();
10705        await_pending_tasks(&client).await;
10706
10707        let events = collect_order_events(&mut receiver);
10708        assert_eq!(events.len(), 1);
10709        let OrderEventAny::Denied(denied) = &events[0] else {
10710            panic!("expected OrderDenied, was {:?}", events[0]);
10711        };
10712        assert!(
10713            denied
10714                .reason
10715                .contains("swap deployment manifest verification disagreed"),
10716            "was: {}",
10717            denied.reason
10718        );
10719        let requests = state.recorded_requests();
10720        assert!(
10721            requests
10722                .iter()
10723                .all(|request| request["method"] != "eth_getTransactionCount")
10724        );
10725        assert!(
10726            requests
10727                .iter()
10728                .all(|request| request["method"] != "eth_sendRawTransaction")
10729        );
10730
10731        drop_execution_schema(&admin_pool, &schema).await;
10732    }
10733
10734    #[tokio::test]
10735    async fn submit_order_denies_cached_token_decimal_mismatch_before_signing() {
10736        let state = swap_rpc_state().await.with_contract_call_response(
10737            WETH,
10738            DECIMALS_SELECTOR,
10739            CALL_DECIMALS_6,
10740        );
10741        let Some((admin_pool, schema, mut client, state, _)) =
10742            swap_client_with_database("execution_submit_decimals_test", state).await
10743        else {
10744            return;
10745        };
10746        let order = test_market_sell_order(test_pool().instrument_id);
10747        let mut receiver = start_with_events(&mut client);
10748
10749        client.submit_order(submit_order_cmd(&order)).unwrap();
10750        await_pending_tasks(&client).await;
10751
10752        let events = collect_order_events(&mut receiver);
10753        assert_eq!(events.len(), 1);
10754        let OrderEventAny::Denied(denied) = &events[0] else {
10755            panic!("expected OrderDenied, was {:?}", events[0]);
10756        };
10757        assert!(
10758            denied
10759                .reason
10760                .contains("swap deployment manifest verification disagreed"),
10761            "was: {}",
10762            denied.reason
10763        );
10764        let requests = state.recorded_requests();
10765        assert!(
10766            requests
10767                .iter()
10768                .all(|request| request["method"] != "eth_getTransactionCount")
10769        );
10770        assert!(
10771            requests
10772                .iter()
10773                .all(|request| request["method"] != "eth_sendRawTransaction")
10774        );
10775
10776        drop_execution_schema(&admin_pool, &schema).await;
10777    }
10778
10779    #[tokio::test]
10780    async fn submit_order_denies_on_insufficient_router_allowance() {
10781        let state = swap_rpc_state()
10782            .await
10783            .with_call_response(ALLOWANCE_SELECTOR, CALL_ZERO);
10784        let Some((admin_pool, schema, mut client, state, _)) =
10785            swap_client_with_database("execution_submit_allowance_test", state).await
10786        else {
10787            return;
10788        };
10789        let order = test_market_sell_order(test_pool().instrument_id);
10790        let mut receiver = start_with_events(&mut client);
10791
10792        client.submit_order(submit_order_cmd(&order)).unwrap();
10793        await_pending_tasks(&client).await;
10794
10795        let events = collect_order_events(&mut receiver);
10796        assert_eq!(events.len(), 1);
10797        let OrderEventAny::Denied(denied) = &events[0] else {
10798            panic!("expected OrderDenied, was {:?}", events[0]);
10799        };
10800        assert!(
10801            denied.reason.contains("below the swap amount"),
10802            "was: {}",
10803            denied.reason
10804        );
10805        let requests = state.recorded_requests();
10806        assert!(
10807            requests
10808                .iter()
10809                .all(|request| request["method"] != "eth_sendRawTransaction"),
10810            "no broadcast may follow a pre-trade denial"
10811        );
10812        let row_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
10813            "SELECT COUNT(*) FROM {schema}.execution_intent"
10814        )))
10815        .fetch_one(&admin_pool)
10816        .await
10817        .unwrap();
10818        assert_eq!(row_count, 0);
10819        assert!(client.in_flight.lock().is_none());
10820
10821        drop_execution_schema(&admin_pool, &schema).await;
10822    }
10823
10824    #[tokio::test]
10825    async fn submit_order_denies_on_insufficient_input_balance() {
10826        let state = swap_rpc_state()
10827            .await
10828            .with_call_response(BALANCE_OF_SELECTOR, CALL_ZERO);
10829        let Some((admin_pool, schema, mut client, _, _)) =
10830            swap_client_with_database("execution_submit_balance_test", state).await
10831        else {
10832            return;
10833        };
10834        let order = test_market_sell_order(test_pool().instrument_id);
10835        let mut receiver = start_with_events(&mut client);
10836
10837        client.submit_order(submit_order_cmd(&order)).unwrap();
10838        await_pending_tasks(&client).await;
10839
10840        let events = collect_order_events(&mut receiver);
10841        assert_eq!(events.len(), 1);
10842        let OrderEventAny::Denied(denied) = &events[0] else {
10843            panic!("expected OrderDenied, was {:?}", events[0]);
10844        };
10845        assert!(
10846            denied.reason.contains("is below the swap amount"),
10847            "was: {}",
10848            denied.reason
10849        );
10850
10851        drop_execution_schema(&admin_pool, &schema).await;
10852    }
10853
10854    #[tokio::test]
10855    async fn submit_order_denies_on_insufficient_native_balance() {
10856        let state = swap_rpc_state()
10857            .await
10858            .with_response("eth_getBalance", GET_BALANCE_INSUFFICIENT);
10859        let Some((admin_pool, schema, mut client, state, _)) =
10860            swap_client_with_database("execution_submit_native_balance_test", state).await
10861        else {
10862            return;
10863        };
10864        let order = test_market_sell_order(test_pool().instrument_id);
10865        let mut receiver = start_with_events(&mut client);
10866
10867        client.submit_order(submit_order_cmd(&order)).unwrap();
10868        await_pending_tasks(&client).await;
10869
10870        let events = collect_order_events(&mut receiver);
10871        assert_eq!(events.len(), 1);
10872        let OrderEventAny::Denied(denied) = &events[0] else {
10873            panic!("expected OrderDenied, was {:?}", events[0]);
10874        };
10875        assert!(
10876            denied.reason.contains("below maximum transaction cost"),
10877            "was: {}",
10878            denied.reason
10879        );
10880        assert!(
10881            state
10882                .recorded_requests()
10883                .iter()
10884                .all(|request| request["method"] != "eth_sendRawTransaction"),
10885            "no broadcast may follow an insufficient native balance"
10886        );
10887        let (row_count, status): (i64, Option<String>) = sqlx::query_as(sqlx::AssertSqlSafe(
10888            format!("SELECT COUNT(*), MAX(status) FROM {schema}.execution_intent"),
10889        ))
10890        .fetch_one(&admin_pool)
10891        .await
10892        .unwrap();
10893        assert_eq!(row_count, 1);
10894        assert_eq!(status.as_deref(), Some("recoverable"));
10895        assert!(client.in_flight.lock().is_none());
10896
10897        drop_execution_schema(&admin_pool, &schema).await;
10898    }
10899
10900    #[tokio::test]
10901    async fn submit_order_denies_on_stale_quote() {
10902        let Some((admin_pool, schema, mut client, state, cache)) =
10903            swap_client_with_database("execution_submit_stale_quote_test", swap_rpc_state().await)
10904                .await
10905        else {
10906            return;
10907        };
10908        let pool = test_pool();
10909        cache
10910            .borrow_mut()
10911            .add_pool_profiler(test_profiler(&pool, FIXTURE_BLOCK - 101))
10912            .unwrap();
10913        let order = test_market_sell_order(pool.instrument_id);
10914        let mut receiver = start_with_events(&mut client);
10915
10916        client.submit_order(submit_order_cmd(&order)).unwrap();
10917        await_pending_tasks(&client).await;
10918
10919        let events = collect_order_events(&mut receiver);
10920        assert_eq!(events.len(), 1);
10921        let OrderEventAny::Denied(denied) = &events[0] else {
10922            panic!("expected OrderDenied, was {:?}", events[0]);
10923        };
10924        assert!(
10925            denied.reason.contains("Stale quote"),
10926            "was: {}",
10927            denied.reason
10928        );
10929        let requests = state.recorded_requests();
10930        assert!(
10931            requests
10932                .iter()
10933                .all(|request| request["method"] != "eth_sendRawTransaction"),
10934            "no broadcast may follow a pre-trade denial"
10935        );
10936
10937        drop_execution_schema(&admin_pool, &schema).await;
10938    }
10939
10940    #[tokio::test]
10941    async fn submit_order_denies_quote_ahead_of_chain_head() {
10942        let Some((admin_pool, schema, mut client, state, cache)) =
10943            swap_client_with_database("execution_submit_ahead_quote_test", swap_rpc_state().await)
10944                .await
10945        else {
10946            return;
10947        };
10948        let pool = test_pool();
10949        cache
10950            .borrow_mut()
10951            .add_pool_profiler(test_profiler(&pool, FIXTURE_BLOCK + 1))
10952            .unwrap();
10953        let order = test_market_sell_order(pool.instrument_id);
10954        let mut receiver = start_with_events(&mut client);
10955
10956        client.submit_order(submit_order_cmd(&order)).unwrap();
10957        await_pending_tasks(&client).await;
10958
10959        let events = collect_order_events(&mut receiver);
10960        assert_eq!(events.len(), 1);
10961        let OrderEventAny::Denied(denied) = &events[0] else {
10962            panic!("expected OrderDenied, was {:?}", events[0]);
10963        };
10964        assert!(
10965            denied.reason.contains("is ahead of the latest block"),
10966            "was: {}",
10967            denied.reason
10968        );
10969        let requests = state.recorded_requests();
10970        assert!(
10971            requests
10972                .iter()
10973                .all(|request| request["method"] != "eth_sendRawTransaction"),
10974            "no broadcast may follow a pre-trade denial"
10975        );
10976
10977        drop_execution_schema(&admin_pool, &schema).await;
10978    }
10979
10980    #[tokio::test]
10981    async fn submit_order_denies_on_deadline_overflow() {
10982        let Some((admin_pool, schema, mut client, state, _)) = swap_client_with_database(
10983            "execution_submit_deadline_overflow_test",
10984            swap_rpc_state().await,
10985        )
10986        .await
10987        else {
10988            return;
10989        };
10990        client.transaction_limits.deadline_seconds = u64::MAX;
10991        let order = test_market_sell_order(test_pool().instrument_id);
10992        let mut receiver = start_with_events(&mut client);
10993
10994        client.submit_order(submit_order_cmd(&order)).unwrap();
10995        await_pending_tasks(&client).await;
10996
10997        let events = collect_order_events(&mut receiver);
10998        assert_eq!(events.len(), 1);
10999        let OrderEventAny::Denied(denied) = &events[0] else {
11000            panic!("expected OrderDenied, was {:?}", events[0]);
11001        };
11002        assert!(
11003            denied.reason.contains("deadline overflow"),
11004            "was: {}",
11005            denied.reason
11006        );
11007        let requests = state.recorded_requests();
11008        assert!(
11009            requests
11010                .iter()
11011                .all(|request| request["method"] != "eth_sendRawTransaction"),
11012            "no broadcast may follow a pre-trade denial"
11013        );
11014        assert!(client.in_flight.lock().is_none());
11015
11016        drop_execution_schema(&admin_pool, &schema).await;
11017    }
11018
11019    #[tokio::test]
11020    async fn submit_order_reconciles_node_rejection_to_finalized_receipt() {
11021        let state = swap_rpc_state()
11022            .await
11023            .with_response("eth_sendRawTransaction", SEND_RAW_TRANSACTION_REJECTED);
11024        let Some((admin_pool, schema, mut client, _state, _)) =
11025            swap_client_with_database("execution_submit_node_rejected_test", state).await
11026        else {
11027            return;
11028        };
11029        let order = test_market_sell_order(test_pool().instrument_id);
11030        let mut receiver = start_with_events(&mut client);
11031
11032        client.submit_order(submit_order_cmd(&order)).unwrap();
11033        await_pending_tasks(&client).await;
11034
11035        let events = collect_order_events(&mut receiver);
11036        assert_swap_submitted_and_filled(&events);
11037        let (purpose, status, client_order_id): (String, String, Option<String>) =
11038            sqlx::query_as(sqlx::AssertSqlSafe(format!(
11039                "SELECT purpose, status, client_order_id FROM {schema}.execution_intent"
11040            )))
11041            .fetch_one(&admin_pool)
11042            .await
11043            .unwrap();
11044        assert_eq!(purpose, "swap");
11045        assert_eq!(status, "finalized");
11046        assert_eq!(
11047            client_order_id.as_deref(),
11048            Some(order.client_order_id().as_str())
11049        );
11050        assert!(client.in_flight.lock().is_none());
11051
11052        drop_execution_schema(&admin_pool, &schema).await;
11053    }
11054
11055    #[tokio::test]
11056    async fn submit_order_acknowledges_uncertain_nonce_too_low() {
11057        let state = swap_rpc_state()
11058            .await
11059            .with_response("eth_sendRawTransaction", SEND_RAW_TRANSACTION_NONCE_TOO_LOW);
11060        let Some((admin_pool, schema, mut client, _state, _)) =
11061            swap_client_with_database("execution_submit_nonce_too_low_test", state).await
11062        else {
11063            return;
11064        };
11065        let order = test_market_sell_order(test_pool().instrument_id);
11066        let mut receiver = start_with_events(&mut client);
11067
11068        client.submit_order(submit_order_cmd(&order)).unwrap();
11069        await_pending_tasks(&client).await;
11070
11071        let events = collect_order_events(&mut receiver);
11072        assert_swap_submitted_and_filled(&events);
11073        let (purpose, status, client_order_id): (String, String, Option<String>) =
11074            sqlx::query_as(sqlx::AssertSqlSafe(format!(
11075                "SELECT purpose, status, client_order_id FROM {schema}.execution_intent"
11076            )))
11077            .fetch_one(&admin_pool)
11078            .await
11079            .unwrap();
11080        assert_eq!(purpose, "swap");
11081        assert_eq!(status, "finalized");
11082        assert_eq!(
11083            client_order_id.as_deref(),
11084            Some(order.client_order_id().as_str())
11085        );
11086        assert!(client.in_flight.lock().is_none());
11087
11088        drop_execution_schema(&admin_pool, &schema).await;
11089    }
11090
11091    #[tokio::test]
11092    async fn submit_order_rejected_on_reverted_receipt() {
11093        let expected_min_out = expected_min_amount_out(50);
11094        let (expected_hash, _) = expected_swap_tx(expected_min_out).await;
11095        let block = finalized_swap_block(expected_hash, expected_min_out);
11096        let receipt = receipt_with_transaction_hash(RECEIPT_REVERTED, expected_hash);
11097        let state = with_finalized_identity(
11098            swap_rpc_state()
11099                .await
11100                .with_response("eth_getTransactionReceipt", &receipt),
11101            &block,
11102            &receipt,
11103        );
11104        let Some((admin_pool, schema, mut client, _state, _)) =
11105            swap_client_with_database("execution_submit_reverted_test", state).await
11106        else {
11107            return;
11108        };
11109        let order = test_market_sell_order(test_pool().instrument_id);
11110        let mut receiver = start_with_events(&mut client);
11111        client.submit_order(submit_order_cmd(&order)).unwrap();
11112        await_pending_tasks(&client).await;
11113
11114        let events = collect_order_events(&mut receiver);
11115        assert_eq!(events.len(), 2);
11116        assert!(
11117            matches!(&events[0], OrderEventAny::Submitted(_)),
11118            "was: {:?}",
11119            events[0]
11120        );
11121        let OrderEventAny::Rejected(rejected) = &events[1] else {
11122            panic!("expected OrderRejected, was {:?}", events[1]);
11123        };
11124        assert!(
11125            rejected.reason.contains("reverted on-chain"),
11126            "was: {}",
11127            rejected.reason
11128        );
11129
11130        let record = client
11131            .cache
11132            .get_execution_transaction(42161, &expected_hash.to_string())
11133            .await
11134            .unwrap()
11135            .unwrap();
11136        assert_eq!(record.purpose, "swap");
11137        assert_eq!(record.status, "reverted");
11138        assert_eq!(
11139            record.client_order_id.as_deref(),
11140            Some(order.client_order_id().as_str())
11141        );
11142        assert!(client.in_flight.lock().is_none());
11143
11144        drop_execution_schema(&admin_pool, &schema).await;
11145    }
11146
11147    #[tokio::test]
11148    async fn submit_order_acknowledges_ambiguous_broadcast() {
11149        // A response the client cannot parse classifies as an ambiguous broadcast failure:
11150        // the transaction may be live, so the durable intent remains submitted and occupied
11151        let state = swap_rpc_state()
11152            .await
11153            .with_response("eth_sendRawTransaction", "not json");
11154        let Some((admin_pool, schema, mut client, _state, _)) =
11155            swap_client_with_database("execution_submit_ambiguous_test", state).await
11156        else {
11157            return;
11158        };
11159        let order = test_market_sell_order(test_pool().instrument_id);
11160        let mut receiver = start_with_events(&mut client);
11161
11162        client.submit_order(submit_order_cmd(&order)).unwrap();
11163        await_pending_tasks(&client).await;
11164
11165        let events = collect_order_events(&mut receiver);
11166        assert_swap_submitted_and_filled(&events);
11167
11168        let record = client
11169            .cache
11170            .get_execution_transaction(
11171                42161,
11172                &expected_swap_tx(expected_min_amount_out(50))
11173                    .await
11174                    .0
11175                    .to_string(),
11176            )
11177            .await
11178            .unwrap()
11179            .unwrap();
11180        assert_eq!(record.purpose, "swap");
11181        assert_eq!(record.status, "finalized");
11182        assert!(client.in_flight.lock().is_none());
11183
11184        drop_execution_schema(&admin_pool, &schema).await;
11185    }
11186
11187    #[tokio::test]
11188    async fn submit_order_acknowledges_broadcast_hash_mismatch() {
11189        // The static fixture's hash differs from the locally signed hash, so acceptance of the
11190        // signed transaction is unverified until the independent receipt reaches finality
11191        let state = swap_rpc_state_for_mismatch().await;
11192        let Some((admin_pool, schema, mut client, _state, _)) =
11193            swap_client_with_database("execution_submit_hash_mismatch_test", state).await
11194        else {
11195            return;
11196        };
11197        let order = test_market_sell_order(test_pool().instrument_id);
11198        let mut receiver = start_with_events(&mut client);
11199
11200        client.submit_order(submit_order_cmd(&order)).unwrap();
11201        await_pending_tasks(&client).await;
11202
11203        let events = collect_order_events(&mut receiver);
11204        assert_swap_submitted_and_filled(&events);
11205
11206        let record = client
11207            .cache
11208            .get_execution_transaction(
11209                42161,
11210                &expected_swap_tx(expected_min_amount_out(50))
11211                    .await
11212                    .0
11213                    .to_string(),
11214            )
11215            .await
11216            .unwrap()
11217            .unwrap();
11218        assert_eq!(record.purpose, "swap");
11219        assert_eq!(record.status, "finalized");
11220        assert!(client.in_flight.lock().is_none());
11221
11222        drop_execution_schema(&admin_pool, &schema).await;
11223    }
11224
11225    #[tokio::test]
11226    async fn submit_order_persists_authenticated_envelope_before_broadcast() {
11227        let state = swap_rpc_state().await;
11228        let Some((admin_pool, schema, mut client, state, _)) =
11229            swap_client_with_database("protected_submit_test", state).await
11230        else {
11231            return;
11232        };
11233        let order = test_market_sell_order(test_pool().instrument_id);
11234        let mut receiver = start_with_events(&mut client);
11235
11236        client.submit_order(submit_order_cmd(&order)).unwrap();
11237        await_pending_tasks(&client).await;
11238
11239        let events = collect_order_events(&mut receiver);
11240        assert_swap_submitted_and_filled(&events);
11241        let representations: Vec<(bool, bool)> = sqlx::query_as(sqlx::AssertSqlSafe(format!(
11242            "SELECT raw_transaction IS NULL, sealed_transaction IS NOT NULL \
11243             FROM {schema}.execution_transaction_hash WHERE payload_expected"
11244        )))
11245        .fetch_all(&admin_pool)
11246        .await
11247        .unwrap();
11248        let broadcasts = state
11249            .recorded_requests()
11250            .into_iter()
11251            .filter(|request| request["method"] == "eth_sendRawTransaction")
11252            .count();
11253        assert_eq!(representations, vec![(true, true)]);
11254        assert_eq!(broadcasts, 1);
11255
11256        drop_execution_schema(&admin_pool, &schema).await;
11257    }
11258
11259    #[tokio::test]
11260    async fn protected_persistence_failure_prevents_broadcast_and_acknowledgment() {
11261        let state = swap_rpc_state().await;
11262        let Some((admin_pool, schema, mut client, state, _)) =
11263            swap_client_with_database("protected_persist_failure_test", state).await
11264        else {
11265            return;
11266        };
11267
11268        for statement in [
11269            format!(
11270                "CREATE FUNCTION {schema}.reject_protected_payload() RETURNS trigger \
11271                 LANGUAGE plpgsql AS 'BEGIN RAISE EXCEPTION ''test payload rejection''; END'"
11272            ),
11273            format!(
11274                "CREATE TRIGGER reject_protected_payload BEFORE INSERT ON \
11275                 {schema}.execution_transaction_hash FOR EACH ROW \
11276                 EXECUTE FUNCTION {schema}.reject_protected_payload()"
11277            ),
11278        ] {
11279            sqlx::query(sqlx::AssertSqlSafe(statement))
11280                .execute(&admin_pool)
11281                .await
11282                .unwrap();
11283        }
11284        let order = test_market_sell_order(test_pool().instrument_id);
11285        let mut receiver = start_with_events(&mut client);
11286
11287        client.submit_order(submit_order_cmd(&order)).unwrap();
11288        await_pending_tasks(&client).await;
11289
11290        let events = collect_order_events(&mut receiver);
11291        assert!(
11292            events
11293                .iter()
11294                .all(|event| !matches!(event, OrderEventAny::Submitted(_)))
11295        );
11296        let broadcasts = state
11297            .recorded_requests()
11298            .into_iter()
11299            .filter(|request| request["method"] == "eth_sendRawTransaction")
11300            .count();
11301        let payload_rows: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
11302            "SELECT COUNT(*) FROM {schema}.execution_transaction_hash"
11303        )))
11304        .fetch_one(&admin_pool)
11305        .await
11306        .unwrap();
11307        assert_eq!(broadcasts, 0);
11308        assert_eq!(payload_rows, 0);
11309
11310        drop_execution_schema(&admin_pool, &schema).await;
11311    }
11312
11313    #[tokio::test]
11314    async fn submit_order_reservation_failure_denies_without_broadcast_and_releases_slot() {
11315        let Some((admin_pool, schema, mut client, state, cache)) =
11316            swap_client_with_database("execution_submit_persist_fail_test", swap_rpc_state().await)
11317                .await
11318        else {
11319            return;
11320        };
11321        sqlx::query(sqlx::AssertSqlSafe(format!(
11322            "DROP TABLE {schema}.execution_intent CASCADE"
11323        )))
11324        .execute(&admin_pool)
11325        .await
11326        .unwrap();
11327        let pool = test_pool();
11328        let first = test_market_sell_order(pool.instrument_id);
11329        let second = market_sell_order_with_id(pool.instrument_id, "O-SWAP-002");
11330        cache
11331            .borrow_mut()
11332            .add_order(second.clone(), None, None, false)
11333            .unwrap();
11334        let mut receiver = start_with_events(&mut client);
11335
11336        client.submit_order(submit_order_cmd(&first)).unwrap();
11337        await_pending_tasks(&client).await;
11338
11339        let events = collect_order_events(&mut receiver);
11340        assert_eq!(events.len(), 1);
11341        let OrderEventAny::Denied(denied) = &events[0] else {
11342            panic!("expected OrderDenied, was {:?}", events[0]);
11343        };
11344        assert!(
11345            denied
11346                .reason
11347                .contains("Execution intent reservation failed before commit"),
11348            "was: {}",
11349            denied.reason
11350        );
11351        client.submit_order(submit_order_cmd(&second)).unwrap();
11352        await_pending_tasks(&client).await;
11353        let retry_events = collect_order_events(&mut receiver);
11354        assert_eq!(retry_events.len(), 1);
11355        let OrderEventAny::Denied(retry_denied) = &retry_events[0] else {
11356            panic!("expected OrderDenied, was {:?}", retry_events[0]);
11357        };
11358        assert_eq!(
11359            retry_denied.reason,
11360            "Execution intent reservation failed before commit"
11361        );
11362        let broadcasts = state
11363            .recorded_requests()
11364            .into_iter()
11365            .filter(|request| request["method"] == "eth_sendRawTransaction")
11366            .count();
11367        assert_eq!(broadcasts, 0);
11368        assert!(client.in_flight.lock().is_none());
11369
11370        drop_execution_schema(&admin_pool, &schema).await;
11371    }
11372
11373    #[tokio::test]
11374    async fn submit_order_reservation_commit_failure_keeps_preparing_slot() {
11375        let Some((admin_pool, schema, mut client, state, cache)) = swap_client_with_database(
11376            "execution_submit_reservation_commit_fail_test",
11377            swap_rpc_state().await,
11378        )
11379        .await
11380        else {
11381            return;
11382        };
11383        install_reservation_commit_rejection(&admin_pool, &schema).await;
11384        let pool = test_pool();
11385        let first = test_market_sell_order(pool.instrument_id);
11386        let second = market_sell_order_with_id(pool.instrument_id, "O-SWAP-002");
11387        cache
11388            .borrow_mut()
11389            .add_order(second.clone(), None, None, false)
11390            .unwrap();
11391        let mut receiver = start_with_events(&mut client);
11392
11393        client.submit_order(submit_order_cmd(&first)).unwrap();
11394        await_pending_tasks(&client).await;
11395        client.submit_order(submit_order_cmd(&second)).unwrap();
11396        await_pending_tasks(&client).await;
11397
11398        let events = collect_order_events(&mut receiver);
11399        let submitted = events
11400            .iter()
11401            .filter(|event| matches!(event, OrderEventAny::Submitted(_)))
11402            .count();
11403        let denied_commit = events
11404            .iter()
11405            .filter(|event| {
11406                matches!(event, OrderEventAny::Denied(denied) if denied.reason == "Execution intent reservation commit outcome is unknown; reconciliation is required")
11407            })
11408            .count();
11409        let denied_in_flight = events
11410            .iter()
11411            .filter(|event| {
11412                matches!(event, OrderEventAny::Denied(denied) if denied.reason.contains("at most one transaction can be in flight"))
11413            })
11414            .count();
11415        let requests = state.recorded_requests();
11416        let intent_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
11417            "SELECT COUNT(*) FROM {schema}.execution_intent"
11418        )))
11419        .fetch_one(&admin_pool)
11420        .await
11421        .unwrap();
11422        let signed_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
11423            "SELECT COUNT(*) FROM {schema}.execution_transaction_hash"
11424        )))
11425        .fetch_one(&admin_pool)
11426        .await
11427        .unwrap();
11428
11429        assert_eq!(submitted, 0, "was: {events:?}");
11430        assert_eq!(denied_commit, 1, "was: {events:?}");
11431        assert_eq!(denied_in_flight, 1, "was: {events:?}");
11432        assert!(matches!(
11433            *client.in_flight.lock(),
11434            Some(InFlightSlot::Preparing(TransactionPurpose::Swap))
11435        ));
11436        assert_eq!(intent_count, 0);
11437        assert_eq!(signed_count, 0);
11438        assert!(
11439            requests
11440                .iter()
11441                .all(|request| request["method"] != "eth_getTransactionCount")
11442        );
11443        assert!(
11444            requests
11445                .iter()
11446                .all(|request| request["method"] != "eth_sendRawTransaction")
11447        );
11448
11449        drop_execution_schema(&admin_pool, &schema).await;
11450    }
11451
11452    #[tokio::test]
11453    async fn protected_payload_migration_failure_keeps_plaintext_and_blocks_ready() {
11454        let Some((admin_pool, schema, client, _)) = execution_client_with_unprotected_database(
11455            "protected_payload_migration_failure",
11456            execution_rpc_state(),
11457        )
11458        .await
11459        else {
11460            return;
11461        };
11462        let database = client.cache.database.as_ref().unwrap();
11463        let (intent, _) = persist_invalid_test_swap(database, None).await;
11464        let keys = payload_test_keys([0x31; 32], vec![], "migration-failure");
11465        let error = database
11466            .ensure_execution_payload_storage(&keys)
11467            .await
11468            .unwrap_err();
11469
11470        assert!(
11471            error
11472                .to_string()
11473                .contains("failed to authenticate execution payload")
11474        );
11475        let row = database
11476            .get_execution_transaction_hashes(intent.id)
11477            .await
11478            .unwrap()
11479            .pop()
11480            .unwrap();
11481        let operation: String = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
11482            "SELECT operation FROM {schema}.execution_payload_state"
11483        )))
11484        .fetch_one(&admin_pool)
11485        .await
11486        .unwrap();
11487        assert_eq!(operation, "migrate");
11488        assert!(row.raw_transaction.is_some());
11489        assert!(row.sealed_transaction.is_none());
11490
11491        drop_execution_schema(&admin_pool, &schema).await;
11492    }
11493
11494    #[tokio::test]
11495    async fn protected_payload_migration_restart_restore_rewrap_and_rollback() {
11496        let Some((admin_pool, pg_config)) =
11497            connect_test_postgres("protected payload lifecycle").await
11498        else {
11499            return;
11500        };
11501        let schema = format!("protected_payload_lifecycle_{}", std::process::id());
11502        setup_execution_schema(&admin_pool, &schema).await;
11503        let options: sqlx::postgres::PgConnectOptions = pg_config.into();
11504        let options = options.options([("search_path", schema.clone())]);
11505        let database = connect_test_database(options.clone()).await.unwrap();
11506        database
11507            .ensure_execution_transaction_schema()
11508            .await
11509            .unwrap();
11510        let intent = reserve_test_wrap_intent(&database).await;
11511        database
11512            .assign_execution_intent_nonce(intent.id, 7)
11513            .await
11514            .unwrap();
11515        let transaction = build_eip1559_transaction(
11516            42161,
11517            7,
11518            78_000,
11519            130_000_000,
11520            10_000_000,
11521            WETH_ADDRESS,
11522            U256::from(1_u64),
11523            Bytes::from(hex::decode("d0e30db0").unwrap()),
11524        );
11525        let (transaction_hash, raw_transaction) = sign_eip1559_transaction(
11526            transaction,
11527            &PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
11528        )
11529        .await
11530        .unwrap();
11531        database
11532            .add_execution_transaction_hash(
11533                intent.id,
11534                42161,
11535                &transaction_hash.to_string(),
11536                &raw_transaction,
11537            )
11538            .await
11539            .unwrap();
11540        let keys = payload_test_keys([0x41; 32], vec![], "restore-a");
11541
11542        database
11543            .ensure_execution_payload_storage(&keys)
11544            .await
11545            .unwrap();
11546        let operation: String = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
11547            "SELECT operation FROM {schema}.execution_payload_state"
11548        )))
11549        .fetch_one(&admin_pool)
11550        .await
11551        .unwrap();
11552        assert_eq!(operation, "ready");
11553        drop(database);
11554
11555        let restarted = connect_test_database(options).await.unwrap();
11556        restarted
11557            .ensure_execution_payload_storage(&keys)
11558            .await
11559            .unwrap();
11560        let row = restarted
11561            .get_execution_transaction_hashes(intent.id)
11562            .await
11563            .unwrap()
11564            .pop()
11565            .unwrap();
11566        assert!(row.raw_transaction.is_none());
11567        let sealed_transaction = row.sealed_transaction.clone().unwrap();
11568        let stored_intent = restarted.get_execution_intent(intent.id).await.unwrap();
11569        let context = payload_context(&stored_intent, &row, keys.deployment_id()).unwrap();
11570        let alternate_envelope = keys.seal(&raw_transaction, &context).unwrap();
11571        assert_ne!(alternate_envelope, sealed_transaction);
11572        let repeated = restarted
11573            .add_execution_transaction_envelope(
11574                intent.id,
11575                42161,
11576                &transaction_hash.to_string(),
11577                &alternate_envelope,
11578            )
11579            .await
11580            .unwrap();
11581        let check = restarted
11582            .check_execution_payload_storage(Some(&keys), None, 1)
11583            .await
11584            .unwrap();
11585        assert_eq!(
11586            repeated.sealed_transaction,
11587            Some(sealed_transaction.clone())
11588        );
11589        assert_eq!(check.plaintext_rows, 0);
11590        assert_eq!(check.original_rows, 1);
11591        assert_eq!(check.replacement_rows, 0);
11592        assert_eq!(check.authenticated_rows, 1);
11593        assert!(!check.read_roles.is_empty());
11594
11595        sqlx::query(sqlx::AssertSqlSafe(format!(
11596            "UPDATE {schema}.execution_payload_key_state SET seals = 4294967295"
11597        )))
11598        .execute(&admin_pool)
11599        .await
11600        .unwrap();
11601        let exhausted = restarted
11602            .reserve_execution_payload_seal(&keys)
11603            .await
11604            .unwrap_err();
11605        assert!(exhausted.to_string().contains("seal limit"));
11606        let seals: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
11607            "SELECT seals FROM {schema}.execution_payload_key_state"
11608        )))
11609        .fetch_one(&admin_pool)
11610        .await
11611        .unwrap();
11612        assert_eq!(seals, 4_294_967_295);
11613        sqlx::query(sqlx::AssertSqlSafe(format!(
11614            "UPDATE {schema}.execution_payload_key_state SET seals = 1"
11615        )))
11616        .execute(&admin_pool)
11617        .await
11618        .unwrap();
11619
11620        let missing_key = restarted
11621            .check_execution_payload_storage(None, None, 1)
11622            .await
11623            .unwrap_err();
11624        assert!(
11625            missing_key
11626                .to_string()
11627                .contains("no payload key is configured")
11628        );
11629        let restored_elsewhere = payload_test_keys([0x41; 32], vec![], "restore-b");
11630        let wrong_context = restarted
11631            .ensure_execution_payload_storage(&restored_elsewhere)
11632            .await
11633            .unwrap_err();
11634        assert!(wrong_context.to_string().contains("deployment ID"));
11635
11636        sqlx::query(sqlx::AssertSqlSafe(format!(
11637            "UPDATE {schema}.execution_transaction_hash \
11638             SET sealed_transaction = set_byte(sealed_transaction, octet_length(sealed_transaction) - 1, \
11639                 get_byte(sealed_transaction, octet_length(sealed_transaction) - 1) # 1)"
11640        )))
11641        .execute(&admin_pool)
11642        .await
11643        .unwrap();
11644        assert!(
11645            restarted
11646                .check_execution_payload_storage(Some(&keys), None, 1)
11647                .await
11648                .is_err()
11649        );
11650        sqlx::query(sqlx::AssertSqlSafe(format!(
11651            "UPDATE {schema}.execution_transaction_hash SET sealed_transaction = $1"
11652        )))
11653        .bind(&sealed_transaction)
11654        .execute(&admin_pool)
11655        .await
11656        .unwrap();
11657
11658        for statement in [
11659            format!(
11660                "ALTER TABLE {schema}.execution_transaction_hash \
11661                 DROP CONSTRAINT execution_transaction_payload_protected_check"
11662            ),
11663            format!("UPDATE {schema}.execution_transaction_hash SET sealed_transaction = NULL"),
11664        ] {
11665            sqlx::query(sqlx::AssertSqlSafe(statement))
11666                .execute(&admin_pool)
11667                .await
11668                .unwrap();
11669        }
11670        assert!(
11671            restarted
11672                .check_execution_payload_storage(Some(&keys), None, 1)
11673                .await
11674                .is_err()
11675        );
11676        sqlx::query(sqlx::AssertSqlSafe(format!(
11677            "UPDATE {schema}.execution_transaction_hash SET sealed_transaction = $1"
11678        )))
11679        .bind(&sealed_transaction)
11680        .execute(&admin_pool)
11681        .await
11682        .unwrap();
11683        sqlx::query(sqlx::AssertSqlSafe(format!(
11684            "ALTER TABLE {schema}.execution_transaction_hash \
11685             ADD CONSTRAINT execution_transaction_payload_protected_check CHECK ( \
11686                 (payload_expected AND raw_transaction IS NULL AND sealed_transaction IS NOT NULL) \
11687                 OR (NOT payload_expected AND raw_transaction IS NULL AND sealed_transaction IS NULL) \
11688             )"
11689        )))
11690        .execute(&admin_pool)
11691        .await
11692        .unwrap();
11693
11694        let rotated = payload_test_keys([0x52; 32], vec![[0x41; 32]], "restore-a");
11695        restarted
11696            .rewrap_execution_payload_storage(&rotated, 1)
11697            .await
11698            .unwrap();
11699        let rotated_check = restarted
11700            .check_execution_payload_storage(Some(&rotated), None, 1)
11701            .await
11702            .unwrap();
11703        assert_eq!(rotated_check.authenticated_rows, 1);
11704        assert_eq!(rotated_check.key_ids.len(), 1);
11705
11706        restarted
11707            .rollback_execution_payload_storage(&rotated, 1)
11708            .await
11709            .unwrap();
11710        let rolled_back = restarted
11711            .get_execution_transaction_hashes(intent.id)
11712            .await
11713            .unwrap()
11714            .pop()
11715            .unwrap();
11716        assert_eq!(
11717            rolled_back.raw_transaction.as_deref(),
11718            Some(raw_transaction.as_slice())
11719        );
11720        assert!(rolled_back.sealed_transaction.is_none());
11721        let legacy_check = restarted
11722            .check_execution_payload_storage(None, None, 1)
11723            .await
11724            .unwrap();
11725        assert!(!legacy_check.protected);
11726        assert_eq!(legacy_check.plaintext_rows, 1);
11727        assert_eq!(legacy_check.authenticated_rows, 1);
11728
11729        drop_execution_schema(&admin_pool, &schema).await;
11730    }
11731
11732    #[tokio::test]
11733    async fn protected_payload_storage_authenticates_multiple_execution_identities() {
11734        let Some((admin_pool, pg_config)) =
11735            connect_test_postgres("protected payload multiple identities").await
11736        else {
11737            return;
11738        };
11739        let schema = format!("protected_payload_identities_{}", std::process::id());
11740        setup_execution_schema(&admin_pool, &schema).await;
11741        let options: sqlx::postgres::PgConnectOptions = pg_config.into();
11742        let database = connect_test_database(options.options([("search_path", schema.clone())]))
11743            .await
11744            .unwrap();
11745        database
11746            .ensure_execution_transaction_schema()
11747            .await
11748            .unwrap();
11749
11750        let private_keys = [
11751            TEST_PRIVATE_KEY,
11752            "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d",
11753        ];
11754
11755        for (index, private_key) in private_keys.into_iter().enumerate() {
11756            let signer = PrivateKeySigner::from_str(private_key).unwrap();
11757            let nonce = 7 + u64::try_from(index).unwrap();
11758            let intent =
11759                reserve_test_wrap_intent_for_wallet(&database, &signer.address().to_string()).await;
11760            database
11761                .assign_execution_intent_nonce(intent.id, nonce)
11762                .await
11763                .unwrap();
11764            let transaction = build_eip1559_transaction(
11765                42161,
11766                nonce,
11767                78_000,
11768                130_000_000,
11769                10_000_000,
11770                WETH_ADDRESS,
11771                U256::from(1_u64),
11772                Bytes::from(hex::decode("d0e30db0").unwrap()),
11773            );
11774            let (transaction_hash, raw_transaction) =
11775                sign_eip1559_transaction(transaction, &signer)
11776                    .await
11777                    .unwrap();
11778            database
11779                .add_execution_transaction_hash(
11780                    intent.id,
11781                    42161,
11782                    &transaction_hash.to_string(),
11783                    &raw_transaction,
11784                )
11785                .await
11786                .unwrap();
11787        }
11788
11789        let keys = payload_test_keys([0x61; 32], vec![], "multiple-identities");
11790        database
11791            .ensure_execution_payload_storage(&keys)
11792            .await
11793            .unwrap();
11794        let lease = database
11795            .require_execution_payload_storage(
11796                &keys,
11797                PayloadPolicy {
11798                    chain_id: 42161,
11799                    signer: Address::from_str(WALLET).unwrap(),
11800                    gas_limit: 1_000_000,
11801                    max_fee_per_gas: 1_000_000_000,
11802                },
11803                1,
11804            )
11805            .await
11806            .unwrap();
11807        drop(lease);
11808        let check = database
11809            .check_execution_payload_storage(Some(&keys), None, 1)
11810            .await
11811            .unwrap();
11812
11813        assert!(check.protected);
11814        assert_eq!(check.plaintext_rows, 0);
11815        assert_eq!(check.original_rows, 2);
11816        assert_eq!(check.replacement_rows, 0);
11817        assert_eq!(check.authenticated_rows, 2);
11818        assert_eq!(check.key_ids.len(), 1);
11819
11820        drop_execution_schema(&admin_pool, &schema).await;
11821    }
11822
11823    #[rstest]
11824    #[case::finalized(TransactionStatus::Finalized)]
11825    #[case::reverted(TransactionStatus::Reverted)]
11826    #[tokio::test]
11827    async fn protected_payload_storage_ignores_current_policy_for_released_terminal_history(
11828        #[case] status: TransactionStatus,
11829    ) {
11830        let Some((admin_pool, pg_config)) =
11831            connect_test_postgres("protected payload terminal history").await
11832        else {
11833            return;
11834        };
11835        let schema = format!(
11836            "protected_payload_terminal_{}_{}",
11837            status.as_str(),
11838            std::process::id()
11839        );
11840        setup_execution_schema(&admin_pool, &schema).await;
11841        let options: sqlx::postgres::PgConnectOptions = pg_config.into();
11842        let database = connect_test_database(options.options([("search_path", schema.clone())]))
11843            .await
11844            .unwrap();
11845        database
11846            .ensure_execution_transaction_schema()
11847            .await
11848            .unwrap();
11849        let (intent, transaction_hash, _) = persist_test_wrap_broadcast(&database, None).await;
11850        database
11851            .record_execution_status(
11852                intent.id,
11853                &transaction_hash.to_string(),
11854                status,
11855                None,
11856                None,
11857                None,
11858                None,
11859                None,
11860            )
11861            .await
11862            .unwrap();
11863        database
11864            .mark_execution_event_emitted(intent.id, "terminal")
11865            .await
11866            .unwrap();
11867        let keys = payload_test_keys([0x62; 32], vec![], "terminal-history");
11868        database
11869            .ensure_execution_payload_storage(&keys)
11870            .await
11871            .unwrap();
11872
11873        let lease = database
11874            .require_execution_payload_storage(
11875                &keys,
11876                PayloadPolicy {
11877                    chain_id: 42161,
11878                    signer: Address::from_str(WALLET).unwrap(),
11879                    gas_limit: 1,
11880                    max_fee_per_gas: 1,
11881                },
11882                1,
11883            )
11884            .await
11885            .unwrap();
11886        drop(lease);
11887        let check = database
11888            .check_execution_payload_storage(Some(&keys), None, 1)
11889            .await
11890            .unwrap();
11891        let active_intent = reserve_test_wrap_intent(&database).await;
11892        database
11893            .assign_execution_intent_nonce(active_intent.id, 8)
11894            .await
11895            .unwrap();
11896        let active_intent = database
11897            .get_execution_intent(active_intent.id)
11898            .await
11899            .unwrap();
11900        let active_transaction = build_eip1559_transaction(
11901            42161,
11902            8,
11903            78_000,
11904            130_000_000,
11905            10_000_000,
11906            WETH_ADDRESS,
11907            U256::from(1_u64),
11908            Bytes::from(hex::decode("d0e30db0").unwrap()),
11909        );
11910        let (active_hash, active_raw_transaction) = sign_eip1559_transaction(
11911            active_transaction,
11912            &PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
11913        )
11914        .await
11915        .unwrap();
11916        persist_test_payload(
11917            &database,
11918            Some(&keys),
11919            &active_intent,
11920            active_hash,
11921            &active_raw_transaction,
11922        )
11923        .await;
11924        let active_error = match database
11925            .require_execution_payload_storage(
11926                &keys,
11927                PayloadPolicy {
11928                    chain_id: 42161,
11929                    signer: Address::from_str(WALLET).unwrap(),
11930                    gas_limit: 1,
11931                    max_fee_per_gas: 1,
11932                },
11933                1,
11934            )
11935            .await
11936        {
11937            Ok(_) => panic!("active execution payload unexpectedly passed current policy"),
11938            Err(e) => e,
11939        };
11940        let active_error = format!("{active_error:#}");
11941
11942        assert!(check.protected);
11943        assert_eq!(check.authenticated_rows, 1);
11944        assert!(
11945            active_error.contains(&format!(
11946                "execution intent {} transaction {active_hash} violates current execution policy",
11947                active_intent.id
11948            )),
11949            "was: {active_error}"
11950        );
11951        assert!(
11952            active_error.contains("gas limit 78000 exceeds configured ceiling 1"),
11953            "was: {active_error}"
11954        );
11955
11956        drop_execution_schema(&admin_pool, &schema).await;
11957    }
11958
11959    #[allow(unsafe_code)] // env-var mutation uses the test client's dedicated signer variable
11960    #[tokio::test]
11961    async fn rollback_blocks_execution_until_protection_and_full_check_succeed() {
11962        let state = ready_rpc_state();
11963        let Some((admin_pool, schema, mut client, state)) =
11964            execution_client_with_database("payload_rollback_reactivation", state).await
11965        else {
11966            return;
11967        };
11968        // SAFETY: this test binary owns the dedicated test signer variable
11969        unsafe { std::env::set_var("BLOCKCHAIN_TEST_PRIVATE_KEY", TEST_PRIVATE_KEY) };
11970        client.disconnect().await.unwrap();
11971        let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel();
11972        replace_exec_event_sender(sender);
11973        client.start().unwrap();
11974
11975        client.rollback_payload_storage(1).await.unwrap();
11976        let unprotected = client.check_payload_storage(1).await.unwrap();
11977        let error = client.connect().await.unwrap_err();
11978
11979        assert!(!unprotected.protected);
11980        assert_eq!(unprotected.plaintext_rows, 0);
11981        assert_eq!(unprotected.authenticated_rows, 0);
11982        assert!(
11983            error
11984                .to_string()
11985                .contains("Postgres execution requires protected payload storage"),
11986            "was: {error}"
11987        );
11988        assert!(client.signer.is_none());
11989        assert!(state.recorded_requests().is_empty());
11990
11991        client.protect_payload_storage().await.unwrap();
11992        let protected = client.check_payload_storage(1).await.unwrap();
11993        assert!(protected.protected);
11994        assert_eq!(protected.plaintext_rows, 0);
11995        assert_eq!(protected.authenticated_rows, 0);
11996
11997        client.connect().await.unwrap();
11998
11999        assert!(client.is_connected());
12000        assert!(client.transaction_executor().is_ok());
12001        assert!(!state.recorded_requests().is_empty());
12002
12003        drop(client);
12004        drop_execution_schema(&admin_pool, &schema).await;
12005    }
12006
12007    #[rstest]
12008    #[case::active_key(true)]
12009    #[case::deployment_identity(false)]
12010    #[tokio::test]
12011    async fn postgres_connect_rejects_missing_payload_identity_before_rpc(
12012        #[case] remove_active_key: bool,
12013    ) {
12014        let test_name = if remove_active_key {
12015            "payload_connect_missing_active_key"
12016        } else {
12017            "payload_connect_missing_deployment"
12018        };
12019        let Some((admin_pool, schema, mut client, state)) =
12020            execution_client_with_database(test_name, ready_rpc_state()).await
12021        else {
12022            return;
12023        };
12024        client.disconnect().await.unwrap();
12025        client.payload_keys = None;
12026        if remove_active_key {
12027            client.config.payload_key_env = None;
12028            client.config.payload_deployment_id = None;
12029        } else {
12030            client.config.payload_deployment_id = None;
12031        }
12032
12033        let error = client.connect().await.unwrap_err();
12034
12035        assert!(
12036            error.to_string().contains(if remove_active_key {
12037                "Postgres execution requires an active payload key and deployment identity"
12038            } else {
12039                "Payload deployment ID is required"
12040            }),
12041            "was: {error}"
12042        );
12043        assert!(client.signer.is_none());
12044        assert!(client.payload_keys.is_none());
12045        assert!(state.recorded_requests().is_empty());
12046
12047        drop(client);
12048        drop_execution_schema(&admin_pool, &schema).await;
12049    }
12050
12051    #[tokio::test]
12052    async fn postgres_connect_rejects_missing_envelope_key_before_rpc() {
12053        let Some((admin_pool, schema, mut client, state)) = execution_client_with_database(
12054            "payload_connect_missing_envelope_key",
12055            ready_rpc_state(),
12056        )
12057        .await
12058        else {
12059            return;
12060        };
12061        let database = client.cache.database.as_ref().unwrap().clone();
12062        persist_test_wrap_broadcast(&database, client.payload_keys.as_deref()).await;
12063        client.disconnect().await.unwrap();
12064        client.payload_keys = None;
12065        let keys = set_test_payload_key(&mut client, [0xb6; 32], &schema);
12066        sqlx::query(sqlx::AssertSqlSafe(format!(
12067            "UPDATE {schema}.execution_payload_state SET active_key_id = $1 \
12068             WHERE component = 'signed_transactions'"
12069        )))
12070        .bind(keys.active_key_id().as_slice())
12071        .execute(&admin_pool)
12072        .await
12073        .unwrap();
12074
12075        let error = client.connect().await.unwrap_err();
12076
12077        assert!(
12078            error
12079                .to_string()
12080                .contains("Stored execution payload requires unavailable key"),
12081            "was: {error}"
12082        );
12083        assert!(client.signer.is_none());
12084        assert!(client.payload_keys.is_none());
12085        assert!(state.recorded_requests().is_empty());
12086
12087        drop(client);
12088        drop_execution_schema(&admin_pool, &schema).await;
12089    }
12090
12091    #[tokio::test]
12092    async fn payload_action_lease_blocks_rewrap_transition_until_release() {
12093        let Some((admin_pool, schema, client, _)) = execution_client_with_unprotected_database(
12094            "payload_action_lease",
12095            execution_rpc_state(),
12096        )
12097        .await
12098        else {
12099            return;
12100        };
12101        let database = client.cache.database.as_ref().unwrap();
12102        let keys = payload_test_keys([0x71; 32], vec![], "action-lease");
12103        database
12104            .ensure_execution_payload_storage(&keys)
12105            .await
12106            .unwrap();
12107        let lease = database
12108            .acquire_execution_payload_lease(&keys)
12109            .await
12110            .unwrap();
12111        let rotated = payload_test_keys([0x72; 32], vec![[0x71; 32]], "action-lease");
12112        let mut operation = Box::pin(database.rewrap_execution_payload_storage(&rotated, 1));
12113
12114        assert!(
12115            tokio::time::timeout(Duration::from_millis(50), &mut operation)
12116                .await
12117                .is_err()
12118        );
12119        let state: String = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
12120            "SELECT operation FROM {schema}.execution_payload_state"
12121        )))
12122        .fetch_one(&admin_pool)
12123        .await
12124        .unwrap();
12125        assert_eq!(state, "ready");
12126
12127        drop(lease);
12128        tokio::time::timeout(Duration::from_secs(2), operation)
12129            .await
12130            .unwrap()
12131            .unwrap();
12132        let state: String = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
12133            "SELECT operation FROM {schema}.execution_payload_state"
12134        )))
12135        .fetch_one(&admin_pool)
12136        .await
12137        .unwrap();
12138        assert_eq!(state, "ready");
12139
12140        drop_execution_schema(&admin_pool, &schema).await;
12141    }
12142
12143    #[tokio::test]
12144    async fn payload_infrastructure_checks_are_scoped_to_the_execution_schema() {
12145        let Some((admin_pool, pg_config)) = connect_test_postgres("payload schema isolation").await
12146        else {
12147            return;
12148        };
12149        let schema_a = format!("payload_schema_a_{}", std::process::id());
12150        let schema_b = format!("payload_schema_b_{}", std::process::id());
12151        setup_execution_schema(&admin_pool, &schema_a).await;
12152        setup_execution_schema(&admin_pool, &schema_b).await;
12153        let options: sqlx::postgres::PgConnectOptions = pg_config.into();
12154        let database_a =
12155            connect_test_database(options.clone().options([("search_path", schema_a.clone())]))
12156                .await
12157                .unwrap();
12158        let database_b =
12159            connect_test_database(options.options([("search_path", schema_b.clone())]))
12160                .await
12161                .unwrap();
12162        database_a
12163            .ensure_execution_transaction_schema()
12164            .await
12165            .unwrap();
12166        database_b
12167            .ensure_execution_transaction_schema()
12168            .await
12169            .unwrap();
12170        let keys_a = payload_test_keys([0x91; 32], vec![], "schema-a");
12171        let keys_b = payload_test_keys([0x92; 32], vec![], "schema-b");
12172
12173        database_a
12174            .ensure_execution_payload_storage(&keys_a)
12175            .await
12176            .unwrap();
12177        database_b
12178            .ensure_execution_payload_storage(&keys_b)
12179            .await
12180            .unwrap();
12181
12182        for statement in [
12183            format!(
12184                "DROP TRIGGER execution_transaction_payload_fence ON \
12185                 {schema_a}.execution_transaction_hash"
12186            ),
12187            format!(
12188                "ALTER TABLE {schema_a}.execution_transaction_hash \
12189                 DROP CONSTRAINT execution_transaction_payload_protected_check"
12190            ),
12191        ] {
12192            sqlx::query(sqlx::AssertSqlSafe(statement))
12193                .execute(&admin_pool)
12194                .await
12195                .unwrap();
12196        }
12197
12198        let error = database_a
12199            .ensure_execution_payload_storage(&keys_a)
12200            .await
12201            .unwrap_err();
12202
12203        assert!(
12204            error
12205                .to_string()
12206                .contains("without its write fence or constraint"),
12207            "was: {error}"
12208        );
12209
12210        drop_execution_schema(&admin_pool, &schema_a).await;
12211        drop_execution_schema(&admin_pool, &schema_b).await;
12212    }
12213
12214    #[tokio::test]
12215    async fn execution_fresh_schema_preserves_nullable_wallet_address() {
12216        let Some((admin_pool, _)) = connect_test_postgres("fresh execution schema").await else {
12217            return;
12218        };
12219        let schema = format!("execution_fresh_test_{}", std::process::id());
12220        let mut transaction = admin_pool.begin().await.unwrap();
12221        sqlx::query(sqlx::AssertSqlSafe(format!("CREATE SCHEMA {schema}")))
12222            .execute(&mut *transaction)
12223            .await
12224            .unwrap();
12225        sqlx::query(sqlx::AssertSqlSafe(format!(
12226            "SET LOCAL search_path TO {schema}"
12227        )))
12228        .execute(&mut *transaction)
12229        .await
12230        .unwrap();
12231        sqlx::query("CREATE TABLE chain (chain_id INTEGER PRIMARY KEY)")
12232            .execute(&mut *transaction)
12233            .await
12234            .unwrap();
12235        sqlx::query("INSERT INTO chain (chain_id) VALUES (42161)")
12236            .execute(&mut *transaction)
12237            .await
12238            .unwrap();
12239        sqlx::query(sqlx::AssertSqlSafe(execution_transaction_create_sql()))
12240            .execute(&mut *transaction)
12241            .await
12242            .unwrap();
12243        sqlx::query(
12244            "INSERT INTO execution_transaction \
12245             (chain_id, nonce, transaction_hash, purpose, status) \
12246             VALUES (42161, 7, '0xfresh-legacy', 'wrap', 'rejected')",
12247        )
12248        .execute(&mut *transaction)
12249        .await
12250        .unwrap();
12251
12252        let is_nullable: String = sqlx::query_scalar(
12253            "SELECT is_nullable FROM information_schema.columns \
12254             WHERE table_schema = $1 AND table_name = 'execution_transaction' \
12255             AND column_name = 'wallet_address'",
12256        )
12257        .bind(&schema)
12258        .fetch_one(&mut *transaction)
12259        .await
12260        .unwrap();
12261        let wallet_address: Option<String> = sqlx::query_scalar(
12262            "SELECT wallet_address FROM execution_transaction \
12263             WHERE transaction_hash = '0xfresh-legacy'",
12264        )
12265        .fetch_one(&mut *transaction)
12266        .await
12267        .unwrap();
12268
12269        assert_eq!(is_nullable, "YES");
12270        assert_eq!(wallet_address, None);
12271        transaction.rollback().await.unwrap();
12272    }
12273
12274    #[tokio::test]
12275    async fn execution_schema_migration_preserves_existing_rows() {
12276        let Some((admin_pool, pg_config)) =
12277            connect_test_postgres("execution schema migration").await
12278        else {
12279            return;
12280        };
12281        let schema = format!("execution_migration_test_{}", std::process::id());
12282        setup_execution_schema(&admin_pool, &schema).await;
12283        sqlx::query(sqlx::AssertSqlSafe(format!(
12284            "INSERT INTO {schema}.execution_transaction \
12285             (chain_id, nonce, transaction_hash, purpose, status) \
12286             VALUES (42161, 7, '0xlegacy', 'wrap', 'rejected')"
12287        )))
12288        .execute(&admin_pool)
12289        .await
12290        .unwrap();
12291
12292        let db_options: sqlx::postgres::PgConnectOptions = pg_config.into();
12293        let db_options = db_options.options([("search_path", schema.clone())]);
12294        let database = connect_test_database(db_options).await.unwrap();
12295        database
12296            .ensure_execution_transaction_schema()
12297            .await
12298            .unwrap();
12299        let legacy = database
12300            .get_execution_transaction(42161, "0xlegacy")
12301            .await
12302            .unwrap()
12303            .unwrap();
12304        assert_eq!(legacy.purpose, "wrap");
12305        assert_eq!(legacy.status, "rejected");
12306        assert_eq!(legacy.client_order_id, None);
12307        assert_eq!(legacy.wallet_address, None);
12308        let is_nullable: String = sqlx::query_scalar(
12309            "SELECT is_nullable FROM information_schema.columns \
12310             WHERE table_schema = $1 AND table_name = 'execution_transaction' \
12311             AND column_name = 'wallet_address'",
12312        )
12313        .bind(&schema)
12314        .fetch_one(&admin_pool)
12315        .await
12316        .unwrap();
12317        let fence_error = database
12318            .add_execution_transaction(
12319                42161,
12320                WALLET,
12321                8,
12322                "0xswap",
12323                "swap",
12324                "pending",
12325                Some("O-SWAP-001"),
12326            )
12327            .await
12328            .err()
12329            .unwrap();
12330        assert!(
12331            fence_error
12332                .to_string()
12333                .contains("Legacy execution writer refused"),
12334            "was: {fence_error}"
12335        );
12336        assert_eq!(is_nullable, "YES");
12337
12338        drop_execution_schema(&admin_pool, &schema).await;
12339    }
12340
12341    #[tokio::test]
12342    async fn execution_schema_migration_drops_legacy_wallet_not_null() {
12343        let Some((admin_pool, pg_config)) =
12344            connect_test_postgres("legacy wallet address schema migration").await
12345        else {
12346            return;
12347        };
12348        let schema = format!("execution_wallet_migration_test_{}", std::process::id());
12349        setup_execution_schema(&admin_pool, &schema).await;
12350        sqlx::query(sqlx::AssertSqlSafe(format!(
12351            "ALTER TABLE {schema}.execution_transaction \
12352             ADD COLUMN wallet_address TEXT NOT NULL"
12353        )))
12354        .execute(&admin_pool)
12355        .await
12356        .unwrap();
12357        sqlx::query(sqlx::AssertSqlSafe(format!(
12358            "INSERT INTO {schema}.execution_transaction \
12359             (chain_id, wallet_address, nonce, transaction_hash, purpose, status) \
12360             VALUES (42161, '{WALLET}', 7, '0xlegacy-wallet', 'wrap', 'rejected')"
12361        )))
12362        .execute(&admin_pool)
12363        .await
12364        .unwrap();
12365
12366        let db_options: sqlx::postgres::PgConnectOptions = pg_config.into();
12367        let db_options = db_options.options([("search_path", schema.clone())]);
12368        let database = connect_test_database(db_options).await.unwrap();
12369        database
12370            .ensure_execution_transaction_schema()
12371            .await
12372            .unwrap();
12373        let legacy = database
12374            .get_execution_transaction(42161, "0xlegacy-wallet")
12375            .await
12376            .unwrap()
12377            .unwrap();
12378        let is_nullable: String = sqlx::query_scalar(
12379            "SELECT is_nullable FROM information_schema.columns \
12380             WHERE table_schema = $1 AND table_name = 'execution_transaction' \
12381             AND column_name = 'wallet_address'",
12382        )
12383        .bind(&schema)
12384        .fetch_one(&admin_pool)
12385        .await
12386        .unwrap();
12387
12388        assert_eq!(legacy.wallet_address.as_deref(), Some(WALLET));
12389        assert_eq!(is_nullable, "YES");
12390        drop_execution_schema(&admin_pool, &schema).await;
12391    }
12392
12393    #[tokio::test]
12394    async fn execution_schema_migration_refuses_unresolved_legacy_rows() {
12395        let Some((admin_pool, pg_config)) =
12396            connect_test_postgres("unsafe execution schema migration").await
12397        else {
12398            return;
12399        };
12400        let schema = format!("execution_unsafe_migration_test_{}", std::process::id());
12401        setup_execution_schema(&admin_pool, &schema).await;
12402        sqlx::query(sqlx::AssertSqlSafe(format!(
12403            "INSERT INTO {schema}.execution_transaction \
12404             (chain_id, nonce, transaction_hash, purpose, status) \
12405             VALUES (42161, 7, '0xunresolved', 'wrap', 'pending')"
12406        )))
12407        .execute(&admin_pool)
12408        .await
12409        .unwrap();
12410        let db_options: sqlx::postgres::PgConnectOptions = pg_config.into();
12411        let db_options = db_options.options([("search_path", schema.clone())]);
12412        let database = connect_test_database(db_options).await.unwrap();
12413
12414        let error = database
12415            .ensure_execution_transaction_schema()
12416            .await
12417            .unwrap_err();
12418        let legacy_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
12419            "SELECT COUNT(*) FROM {schema}.execution_transaction \
12420             WHERE transaction_hash = '0xunresolved' AND status = 'pending'"
12421        )))
12422        .fetch_one(&admin_pool)
12423        .await
12424        .unwrap();
12425        let v2_table: Option<String> = sqlx::query_scalar("SELECT to_regclass($1)::TEXT")
12426            .bind(format!("{schema}.execution_intent"))
12427            .fetch_one(&admin_pool)
12428            .await
12429            .unwrap();
12430
12431        assert!(
12432            error
12433                .to_string()
12434                .contains("Cannot safely migrate 1 unresolved execution schema version 1"),
12435            "was: {error}"
12436        );
12437        assert_eq!(legacy_count, 1);
12438        assert_eq!(v2_table, None);
12439
12440        drop_execution_schema(&admin_pool, &schema).await;
12441    }
12442
12443    #[rstest]
12444    fn receipt_max_polls_derives_from_timeout() {
12445        assert_eq!(receipt_max_polls(0), 1);
12446        assert_eq!(receipt_max_polls(1), 1);
12447        assert_eq!(receipt_max_polls(60), 60);
12448        assert_eq!(receipt_max_polls(u64::MAX), u32::MAX);
12449        assert_eq!(receipt_timeout(0), Duration::from_secs(1));
12450        assert_eq!(receipt_timeout(60), Duration::from_secs(60));
12451        assert_eq!(
12452            receipt_timeout(u64::MAX),
12453            Duration::from_secs(u64::from(u32::MAX))
12454        );
12455    }
12456
12457    #[rstest]
12458    fn submit_order_errors_when_order_not_cached() {
12459        let client = swap_client_with_cache(test_config("http://127.0.0.1:1".to_string())).0;
12460        let mut cmd = submit_order_cmd(&test_market_sell_order(test_pool().instrument_id));
12461        cmd.client_order_id = ClientOrderId::from("O-UNKNOWN");
12462
12463        let error = client.submit_order(cmd).unwrap_err();
12464
12465        assert!(!error.to_string().is_empty());
12466    }
12467
12468    #[tokio::test]
12469    async fn submit_order_denies_pool_fee_above_uint24() {
12470        let cache = Rc::new(RefCell::new(Cache::default()));
12471        let mut pool = test_pool();
12472        pool.fee = Some(16_777_216); // 2^24, above the uint24 calldata range
12473        cache.borrow_mut().add_pool(pool.clone()).unwrap();
12474        let order = test_market_sell_order(pool.instrument_id);
12475        cache
12476            .borrow_mut()
12477            .add_order(order.clone(), None, None, false)
12478            .unwrap();
12479        let core = ExecutionClientCore::new(
12480            TraderId::from("TRADER-001"),
12481            ClientId::from("BLOCKCHAIN-001"),
12482            *BLOCKCHAIN_VENUE,
12483            OmsType::Netting,
12484            AccountId::from("BLOCKCHAIN-001"),
12485            AccountType::Wallet,
12486            None,
12487            cache,
12488        );
12489        let mut client =
12490            BlockchainExecutionClient::new(core, test_config("http://127.0.0.1:1".to_string()))
12491                .unwrap();
12492        let mut receiver = start_with_events(&mut client);
12493
12494        client.submit_order(submit_order_cmd(&order)).unwrap();
12495
12496        let events = collect_order_events(&mut receiver);
12497        assert_eq!(events.len(), 1);
12498        let OrderEventAny::Denied(denied) = &events[0] else {
12499            panic!("expected OrderDenied, was {:?}", events[0]);
12500        };
12501        assert!(
12502            denied.reason.contains("exceeds uint24"),
12503            "was: {}",
12504            denied.reason
12505        );
12506    }
12507
12508    #[tokio::test]
12509    async fn submit_order_denies_uninitialized_profiler() {
12510        let cache = Rc::new(RefCell::new(Cache::default()));
12511        let pool = test_pool();
12512        cache.borrow_mut().add_pool(pool.clone()).unwrap();
12513        let order = test_market_sell_order(pool.instrument_id);
12514        cache
12515            .borrow_mut()
12516            .add_order(order.clone(), None, None, false)
12517            .unwrap();
12518        cache
12519            .borrow_mut()
12520            .add_pool_profiler(PoolProfiler::new(Arc::new(pool)))
12521            .unwrap();
12522        let core = ExecutionClientCore::new(
12523            TraderId::from("TRADER-001"),
12524            ClientId::from("BLOCKCHAIN-001"),
12525            *BLOCKCHAIN_VENUE,
12526            OmsType::Netting,
12527            AccountId::from("BLOCKCHAIN-001"),
12528            AccountType::Wallet,
12529            None,
12530            cache,
12531        );
12532        let mut client =
12533            BlockchainExecutionClient::new(core, test_config("http://127.0.0.1:1".to_string()))
12534                .unwrap();
12535        let mut receiver = start_with_events(&mut client);
12536
12537        client.submit_order(submit_order_cmd(&order)).unwrap();
12538
12539        let events = collect_order_events(&mut receiver);
12540        assert_eq!(events.len(), 1);
12541        let OrderEventAny::Denied(denied) = &events[0] else {
12542            panic!("expected OrderDenied, was {:?}", events[0]);
12543        };
12544        assert!(
12545            denied.reason.contains("is not initialized"),
12546            "was: {}",
12547            denied.reason
12548        );
12549    }
12550
12551    #[tokio::test]
12552    async fn submit_order_denies_when_quote_cannot_fill_order() {
12553        let cache = Rc::new(RefCell::new(Cache::default()));
12554        let pool = test_pool();
12555        cache.borrow_mut().add_pool(pool.clone()).unwrap();
12556        let order = test_market_sell_order(pool.instrument_id);
12557        cache
12558            .borrow_mut()
12559            .add_order(order.clone(), None, None, false)
12560            .unwrap();
12561        // A narrow one-unit-of-liquidity position cannot fill the order
12562        cache
12563            .borrow_mut()
12564            .add_pool_profiler(test_profiler_with_range(
12565                &pool,
12566                FIXTURE_BLOCK,
12567                FIXTURE_BLOCK_HASH,
12568                U160::from(1u128 << 96),
12569                -10,
12570                10,
12571                1,
12572            ))
12573            .unwrap();
12574        let core = ExecutionClientCore::new(
12575            TraderId::from("TRADER-001"),
12576            ClientId::from("BLOCKCHAIN-001"),
12577            *BLOCKCHAIN_VENUE,
12578            OmsType::Netting,
12579            AccountId::from("BLOCKCHAIN-001"),
12580            AccountType::Wallet,
12581            None,
12582            cache,
12583        );
12584        let mut client =
12585            BlockchainExecutionClient::new(core, test_config("http://127.0.0.1:1".to_string()))
12586                .unwrap();
12587        let mut receiver = start_with_events(&mut client);
12588
12589        client.submit_order(submit_order_cmd(&order)).unwrap();
12590
12591        let events = collect_order_events(&mut receiver);
12592        assert_eq!(events.len(), 1);
12593        let OrderEventAny::Denied(denied) = &events[0] else {
12594            panic!("expected OrderDenied, was {:?}", events[0]);
12595        };
12596        assert!(
12597            denied.reason.contains("cannot fill the order"),
12598            "was: {}",
12599            denied.reason
12600        );
12601    }
12602
12603    #[tokio::test]
12604    async fn submit_order_applies_slippage_param_override_at_ceiling() {
12605        let Some((admin_pool, schema, mut client, state, _)) = swap_client_with_database(
12606            "execution_submit_slippage_override_test",
12607            swap_rpc_state_with_min_amount_out(expected_min_amount_out(200)).await,
12608        )
12609        .await
12610        else {
12611            return;
12612        };
12613        let order = test_market_sell_order(test_pool().instrument_id);
12614        let mut cmd = submit_order_cmd(&order);
12615        // Exactly at the configured ceiling (200 bps): accepted, not denied
12616        cmd.params = Some(serde_json::from_str(r#"{"slippage_bps": 200}"#).unwrap());
12617        let mut receiver = start_with_events(&mut client);
12618        let expected_min_out = expected_min_amount_out(200);
12619
12620        client.submit_order(cmd).unwrap();
12621        await_pending_tasks(&client).await;
12622
12623        let events = collect_order_events(&mut receiver);
12624        assert_swap_submitted_and_filled(&events);
12625
12626        let (_, expected_raw) = expected_swap_tx(expected_min_out).await;
12627        let broadcasts: Vec<_> = state
12628            .recorded_requests()
12629            .into_iter()
12630            .filter(|request| request["method"] == "eth_sendRawTransaction")
12631            .collect();
12632        assert_eq!(broadcasts.len(), 1);
12633        assert_eq!(broadcasts[0]["params"][0].as_str().unwrap(), expected_raw);
12634
12635        drop_execution_schema(&admin_pool, &schema).await;
12636    }
12637
12638    #[tokio::test]
12639    async fn submit_order_accepts_quote_fresh_at_max_age_boundary() {
12640        let min_amount_out = expected_min_amount_out(50);
12641        let (tx_hash, _) = expected_swap_tx(min_amount_out).await;
12642        let head = finalized_swap_block(tx_hash, min_amount_out);
12643        let state = swap_rpc_state()
12644            .await
12645            .with_response("eth_getBlockByNumber", &head)
12646            .with_parameter_response("eth_getBlockByNumber", FIXTURE_BLOCK_PARAM, BLOCK_BY_NUMBER);
12647        let Some((admin_pool, schema, mut client, state, cache)) =
12648            swap_client_with_database_config(
12649                "execution_submit_fresh_boundary_test",
12650                state,
12651                |http_rpc_url| {
12652                    let mut config = test_config(http_rpc_url);
12653                    config.max_quote_age_blocks = Some(1);
12654                    config
12655                },
12656            )
12657            .await
12658        else {
12659            return;
12660        };
12661        let pool = test_pool();
12662        cache
12663            .borrow_mut()
12664            .add_pool_profiler(test_profiler_at_block(
12665                &pool,
12666                FIXTURE_BLOCK,
12667                FIXTURE_BLOCK_HASH,
12668            ))
12669            .unwrap();
12670        let order = test_market_sell_order(pool.instrument_id);
12671        let mut receiver = start_with_events(&mut client);
12672
12673        client.submit_order(submit_order_cmd(&order)).unwrap();
12674        await_pending_tasks(&client).await;
12675
12676        let events = collect_order_events(&mut receiver);
12677        assert_eq!(events.len(), 1, "was: {events:?}");
12678        assert!(matches!(&events[0], OrderEventAny::Submitted(_)));
12679        assert_eq!(
12680            state
12681                .recorded_requests()
12682                .iter()
12683                .filter(|request| request["method"] == "eth_sendRawTransaction")
12684                .count(),
12685            1
12686        );
12687
12688        drop_execution_schema(&admin_pool, &schema).await;
12689    }
12690
12691    #[tokio::test]
12692    async fn submit_order_sells_base_token_from_token1_position() {
12693        // A pool with the token order reversed relative to the convention: USDC is token0
12694        // and WETH is token1, so the base token (WETH, by token priority) sits in the
12695        // token1 position and the swap quotes zero_for_one = false
12696        let chain = Arc::new(chains::ARBITRUM.clone());
12697        let dex = UNISWAP_V3.dex.clone();
12698        let usdc = Token::new(
12699            chain.clone(),
12700            address!("af88d065e77c8cC2239327C5EDb3A432268e5831"),
12701            "USD Coin".to_string(),
12702            "USDC".to_string(),
12703            6,
12704        );
12705        let weth = Token::new(
12706            chain.clone(),
12707            address!("82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
12708            "Wrapped Ether".to_string(),
12709            "WETH".to_string(),
12710            18,
12711        );
12712        let pool = Pool::new(
12713            chain,
12714            dex,
12715            address!("C6962004f452bE9203591991D15f6b388e09E8D0"),
12716            PoolIdentifier::from_address(address!("C6962004f452bE9203591991D15f6b388e09E8D0")),
12717            55_000_000,
12718            usdc,
12719            weth,
12720            Some(500),
12721            Some(10),
12722            UnixNanos::default(),
12723        );
12724
12725        let cache = Rc::new(RefCell::new(Cache::default()));
12726        cache.borrow_mut().add_pool(pool.clone()).unwrap();
12727        let order = test_market_sell_order(pool.instrument_id);
12728        cache
12729            .borrow_mut()
12730            .add_order(order.clone(), None, None, false)
12731            .unwrap();
12732        // Asymmetric price so the zero_for_one direction changes the quote
12733        cache
12734            .borrow_mut()
12735            .add_pool_profiler(test_profiler_with_state(
12736                &pool,
12737                FIXTURE_BLOCK,
12738                U160::from(2u128 << 96),
12739                TEST_LIQUIDITY,
12740            ))
12741            .unwrap();
12742
12743        let (admin_pool, pg_config) = match connect_test_postgres("orientation").await {
12744            Some(setup) => setup,
12745            None => return,
12746        };
12747        let schema = format!("execution_submit_orientation_test_{}", std::process::id());
12748        setup_execution_schema(&admin_pool, &schema).await;
12749        let db_options: sqlx::postgres::PgConnectOptions = pg_config.into();
12750        let db_options = db_options.options([("search_path", schema.clone())]);
12751        let database = connect_test_database(db_options).await.unwrap();
12752
12753        let state = swap_rpc_state()
12754            .await
12755            .with_call_response(POOL_TOKEN0_SELECTOR, CALL_USDC)
12756            .with_call_response(POOL_TOKEN1_SELECTOR, CALL_WETH)
12757            .with_response("eth_getTransactionReceipt", RECEIPT_NULL);
12758        let addr = start_mock_rpc_server(state.clone()).await;
12759        let core = ExecutionClientCore::new(
12760            TraderId::from("TRADER-001"),
12761            ClientId::from("BLOCKCHAIN-001"),
12762            *BLOCKCHAIN_VENUE,
12763            OmsType::Netting,
12764            AccountId::from("BLOCKCHAIN-001"),
12765            AccountType::Wallet,
12766            None,
12767            cache,
12768        );
12769        let mut config = test_config(format!("http://{addr}"));
12770        let result = |response: &str| {
12771            serde_json::from_str::<serde_json::Value>(response).unwrap()["result"]
12772                .as_str()
12773                .unwrap()
12774                .to_string()
12775        };
12776        {
12777            let verification = config.verification.as_mut().unwrap();
12778            let pool_identity = &mut verification.deployment_manifest.pools[0];
12779            pool_identity.token0 = USDC.to_string();
12780            pool_identity.token1 = WETH.to_string();
12781            let pool_contract = verification
12782                .deployment_manifest
12783                .contracts
12784                .iter_mut()
12785                .find(|contract| contract.role == BlockchainContractRole::Pool)
12786                .unwrap();
12787
12788            for probe in &mut pool_contract.probes {
12789                if probe.call_data.starts_with(POOL_TOKEN0_SELECTOR) {
12790                    probe.expected_output = result(CALL_USDC);
12791                } else if probe.call_data.starts_with(POOL_TOKEN1_SELECTOR) {
12792                    probe.expected_output = result(CALL_WETH);
12793                }
12794            }
12795        }
12796        refresh_test_manifest_digest(&mut config);
12797        let mut client = BlockchainExecutionClient::new(core, config).unwrap();
12798        client.cache.database = Some(database);
12799        client
12800            .cache
12801            .ensure_execution_transaction_schema()
12802            .await
12803            .unwrap();
12804        protect_test_storage(&mut client, &schema).await;
12805        initialize_test_verification_ledger(&client).await;
12806        client.signer = Some(Arc::new(
12807            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
12808        ));
12809        client.core.set_connected();
12810        let mut receiver = start_with_events(&mut client);
12811
12812        let profiler = test_profiler_with_state(
12813            &pool,
12814            FIXTURE_BLOCK,
12815            U160::from(2u128 << 96),
12816            TEST_LIQUIDITY,
12817        );
12818        let quote = profiler
12819            .swap_exact_in(U256::from(1_000_000_000_000_000u64), false, None)
12820            .unwrap();
12821        let quoted = exact_output_amount(&quote, false).unwrap();
12822        let expected_min_out = derive_min_amount_out(quoted, 50).unwrap();
12823        let wrong_direction_quote = profiler
12824            .swap_exact_in(U256::from(1_000_000_000_000_000u64), true, None)
12825            .unwrap();
12826        let wrong_direction_out = exact_output_amount(&wrong_direction_quote, true).unwrap();
12827        assert_ne!(
12828            quoted, wrong_direction_out,
12829            "the asymmetric price must make the quote direction observable"
12830        );
12831        assert_ne!(
12832            expected_min_out,
12833            expected_min_amount_out(50),
12834            "the profiler quote must remain distinct from the independent quote fixture"
12835        );
12836
12837        client.submit_order(submit_order_cmd(&order)).unwrap();
12838        await_pending_tasks(&client).await;
12839
12840        let events = collect_order_events(&mut receiver);
12841        assert_eq!(events.len(), 1);
12842        assert!(
12843            matches!(&events[0], OrderEventAny::Submitted(_)),
12844            "was: {:?}",
12845            events[0]
12846        );
12847
12848        let (_, expected_raw) = expected_swap_tx(expected_min_amount_out(50)).await;
12849        let broadcasts: Vec<_> = state
12850            .recorded_requests()
12851            .into_iter()
12852            .filter(|request| request["method"] == "eth_sendRawTransaction")
12853            .collect();
12854        assert_eq!(broadcasts.len(), 1);
12855        assert_eq!(broadcasts[0]["params"][0].as_str().unwrap(), expected_raw);
12856
12857        drop_execution_schema(&admin_pool, &schema).await;
12858    }
12859
12860    #[tokio::test]
12861    async fn submit_order_denies_on_chain_mismatch() {
12862        let state = swap_rpc_state()
12863            .await
12864            .with_response("eth_chainId", CHAIN_ID_ETHEREUM);
12865        let Some((admin_pool, schema, mut client, state, _)) =
12866            swap_client_with_database("execution_submit_chain_mismatch_test", state).await
12867        else {
12868            return;
12869        };
12870        let order = test_market_sell_order(test_pool().instrument_id);
12871        let mut receiver = start_with_events(&mut client);
12872
12873        client.submit_order(submit_order_cmd(&order)).unwrap();
12874        await_pending_tasks(&client).await;
12875
12876        let events = collect_order_events(&mut receiver);
12877        assert_eq!(events.len(), 1);
12878        let OrderEventAny::Denied(denied) = &events[0] else {
12879            panic!("expected OrderDenied, was {:?}", events[0]);
12880        };
12881        assert!(
12882            denied
12883                .reason
12884                .contains("pre-sign chain ID verification disagreed"),
12885            "was: {}",
12886            denied.reason
12887        );
12888        let requests = state.recorded_requests();
12889        assert!(
12890            requests
12891                .iter()
12892                .all(|request| request["method"] != "eth_sendRawTransaction"),
12893            "no broadcast may follow a chain mismatch"
12894        );
12895        assert!(client.in_flight.lock().is_none());
12896
12897        drop_execution_schema(&admin_pool, &schema).await;
12898    }
12899
12900    #[tokio::test]
12901    async fn submit_order_finality_timeout_marks_dropped_and_keeps_ownership() {
12902        let receipt_release = Arc::new(tokio::sync::Semaphore::new(0));
12903        let state = swap_rpc_state()
12904            .await
12905            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
12906            .with_response_release("eth_getTransactionReceipt", Arc::clone(&receipt_release));
12907        let Some((admin_pool, schema, mut client, state, _)) =
12908            swap_client_with_database("execution_submit_inclusion_timeout_test", state).await
12909        else {
12910            return;
12911        };
12912        client.transaction_limits.receipt_timeout_secs = 1;
12913        let order = test_market_sell_order(test_pool().instrument_id);
12914        let mut receiver = start_with_events(&mut client);
12915
12916        client.submit_order(submit_order_cmd(&order)).unwrap();
12917        await_recorded_requests(&state, "eth_getTransactionReceipt", 3).await;
12918        tokio::time::timeout(Duration::from_secs(3), await_pending_tasks(&client))
12919            .await
12920            .unwrap();
12921        receipt_release.add_permits(3);
12922
12923        // Broadcast acceptance was observed, so the order is submitted; the unobserved
12924        // inclusion justifies no further event
12925        let events = collect_order_events(&mut receiver);
12926        assert_eq!(events.len(), 1);
12927        assert!(
12928            matches!(&events[0], OrderEventAny::Submitted(_)),
12929            "was: {:?}",
12930            events[0]
12931        );
12932
12933        let record = client
12934            .cache
12935            .get_execution_transaction(
12936                42161,
12937                &expected_swap_tx(expected_min_amount_out(50))
12938                    .await
12939                    .0
12940                    .to_string(),
12941            )
12942            .await
12943            .unwrap()
12944            .unwrap();
12945        assert_eq!(record.status, "dropped");
12946        assert!(client.in_flight.lock().is_some());
12947
12948        drop_execution_schema(&admin_pool, &schema).await;
12949    }
12950
12951    #[tokio::test]
12952    async fn included_receipt_finality_timeout_marks_dropped_and_keeps_ownership() {
12953        let state = execution_rpc_state()
12954            .with_response("eth_getTransactionCount", TRANSACTION_COUNT)
12955            .with_response("eth_estimateGas", ESTIMATE_GAS)
12956            .with_response("eth_call", CALL_BALANCE)
12957            .with_response("eth_getTransactionReceipt", RECEIPT_SUCCESS)
12958            .with_parameter_response("eth_getBlockByNumber", "finalized", BLOCK_BY_NUMBER)
12959            .with_send_raw_transaction_echo();
12960        let Some((admin_pool, schema, mut client, _)) =
12961            execution_client_with_database("execution_included_timeout_test", state).await
12962        else {
12963            return;
12964        };
12965        client.transaction_limits.receipt_timeout_secs = 1;
12966
12967        let error = client
12968            .wrap(U256::from(1_000_000_000_000_000_u64))
12969            .await
12970            .unwrap_err();
12971        let transitions: Vec<String> = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
12972            "SELECT to_status FROM {schema}.execution_transaction_transition ORDER BY id"
12973        )))
12974        .fetch_all(&admin_pool)
12975        .await
12976        .unwrap();
12977
12978        assert!(
12979            error.to_string().contains("Timed out awaiting finality"),
12980            "was: {error}"
12981        );
12982        assert_eq!(transitions, ["prepared", "signed", "broadcast", "dropped"]);
12983        assert!(client.in_flight.lock().is_some());
12984
12985        drop_execution_schema(&admin_pool, &schema).await;
12986    }
12987
12988    #[tokio::test]
12989    async fn submit_order_single_in_flight_rejects_concurrent_swap() {
12990        let broadcast_release = Arc::new(tokio::sync::Semaphore::new(0));
12991        let state = swap_rpc_state()
12992            .await
12993            .with_response_release("eth_sendRawTransaction", Arc::clone(&broadcast_release));
12994        let Some((admin_pool, schema, mut client, state, cache)) =
12995            swap_client_with_database("execution_submit_concurrent_test", state).await
12996        else {
12997            return;
12998        };
12999        let pool = test_pool();
13000        let first = test_market_sell_order(pool.instrument_id);
13001        let second = OrderTestBuilder::new(OrderType::Market)
13002            .trader_id(TraderId::from("TRADER-001"))
13003            .strategy_id(StrategyId::from("S-001"))
13004            .instrument_id(pool.instrument_id)
13005            .client_order_id(ClientOrderId::from("O-SWAP-002"))
13006            .side(OrderSide::Sell)
13007            .quantity(Quantity::from("0.001"))
13008            .build();
13009        cache
13010            .borrow_mut()
13011            .add_order(second.clone(), None, None, false)
13012            .unwrap();
13013        let mut receiver = start_with_events(&mut client);
13014
13015        client.submit_order(submit_order_cmd(&first)).unwrap();
13016        await_recorded_requests(&state, "eth_sendRawTransaction", 1).await;
13017        client.submit_order(submit_order_cmd(&second)).unwrap();
13018        let event = tokio::time::timeout(TEST_TIMEOUT, receiver.recv())
13019            .await
13020            .unwrap()
13021            .unwrap();
13022        let ExecutionEvent::Order(OrderEventAny::Denied(denied)) = event else {
13023            panic!("expected OrderDenied, was {event:?}");
13024        };
13025        assert_eq!(denied.client_order_id, second.client_order_id());
13026        assert!(
13027            denied
13028                .reason
13029                .contains("at most one transaction can be in flight"),
13030            "was: {}",
13031            denied.reason
13032        );
13033        broadcast_release.add_permits(1);
13034        await_pending_tasks(&client).await;
13035
13036        let events = collect_order_events(&mut receiver);
13037        assert_swap_submitted_and_filled(&events);
13038
13039        let broadcasts = state
13040            .recorded_requests()
13041            .into_iter()
13042            .filter(|request| request["method"] == "eth_sendRawTransaction")
13043            .count();
13044        assert_eq!(broadcasts, 1);
13045        let row_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
13046            "SELECT COUNT(*) FROM {schema}.execution_intent"
13047        )))
13048        .fetch_one(&admin_pool)
13049        .await
13050        .unwrap();
13051        assert_eq!(row_count, 1);
13052        assert!(client.in_flight.lock().is_none());
13053
13054        drop_execution_schema(&admin_pool, &schema).await;
13055    }
13056
13057    #[tokio::test]
13058    async fn preflight_ready_when_all_checks_pass() {
13059        let (client, state) = client_with_mock_rpc(ready_rpc_state()).await;
13060        let pool = test_pool();
13061
13062        let report = client.preflight(&pool.instrument_id).await.unwrap();
13063
13064        assert!(report.ready, "issues: {:?}", report.issues);
13065        assert!(report.issues.is_empty());
13066        assert_eq!(report.expected_chain_id, 42161);
13067        assert_eq!(report.actual_chain_id, 42161);
13068        assert!(report.chain_id_matches);
13069        assert_eq!(
13070            report.pool.address,
13071            address!("C6962004f452bE9203591991D15f6b388e09E8D0")
13072        );
13073        assert!(report.pool.has_deployed_code);
13074        assert_eq!(report.pool.fee, Some(500));
13075        assert_eq!(
13076            report.pool.base_token,
13077            address!("82aF49447D8a07e3bd95BD0d56f35241523fBab1")
13078        );
13079        assert_eq!(
13080            report.pool.quote_token,
13081            address!("af88d065e77c8cC2239327C5EDb3A432268e5831")
13082        );
13083        assert_eq!(report.routers.len(), 1);
13084        assert!(report.routers[0].has_deployed_code);
13085        assert_eq!(report.tokens.len(), 2);
13086        assert_eq!(
13087            report.tokens[0].wallet_balance,
13088            U256::from(500_000_000_000_000_000u64)
13089        );
13090        assert_eq!(
13091            report.tokens[0].router_allowances,
13092            vec![(
13093                address!("E592427A0AEce92De3Edee1F18E0157C05861564"),
13094                U256::from(1_000_000_000_000_000_000u64)
13095            )]
13096        );
13097        assert_eq!(
13098            report.native_balance_wei,
13099            U256::from(1_000_000_000_000_000_000u64)
13100        );
13101        assert_eq!(report.base_fee_per_gas_wei, 100_000_000);
13102        assert_eq!(report.max_priority_fee_per_gas_wei, 10_000_000);
13103        assert_eq!(report.derived_max_fee_per_gas_wei, 130_000_000);
13104        assert!(report.fee_within_ceiling);
13105
13106        let requests = state.recorded_requests();
13107        for method in ["eth_getCode", "eth_call", "eth_getBalance"] {
13108            let matching: Vec<_> = requests
13109                .iter()
13110                .filter(|request| request["method"] == method)
13111                .collect();
13112            assert!(!matching.is_empty(), "method {method}");
13113            assert!(
13114                matching
13115                    .iter()
13116                    .all(|request| request["params"][1] == "latest"),
13117                "method {method}: {matching:?}"
13118            );
13119        }
13120    }
13121
13122    #[tokio::test]
13123    async fn preflight_not_ready_without_pool_fee() {
13124        let addr = start_mock_rpc_server(ready_rpc_state()).await;
13125        let mut pool = test_pool();
13126        pool.fee = None;
13127        let client = test_client_from_config(test_config(format!("http://{addr}")), pool.clone());
13128
13129        let report = client.preflight(&pool.instrument_id).await.unwrap();
13130
13131        assert!(!report.ready);
13132        assert_eq!(report.pool.fee, None);
13133        assert_eq!(report.issues, vec!["Pool fee tier is missing"]);
13134    }
13135
13136    #[tokio::test]
13137    async fn preflight_not_ready_on_wrong_chain() {
13138        let state = ready_rpc_state().with_response("eth_chainId", CHAIN_ID_ETHEREUM);
13139        let (client, _) = client_with_mock_rpc(state).await;
13140        let pool = test_pool();
13141
13142        let report = client.preflight(&pool.instrument_id).await.unwrap();
13143
13144        assert!(!report.ready);
13145        assert!(!report.chain_id_matches);
13146        assert!(
13147            report
13148                .issues
13149                .iter()
13150                .any(|issue| issue.contains("Chain ID mismatch"))
13151        );
13152    }
13153
13154    #[tokio::test]
13155    async fn preflight_not_ready_without_deployed_code() {
13156        let state = ready_rpc_state().with_response("eth_getCode", GET_CODE_EMPTY);
13157        let (client, _) = client_with_mock_rpc(state).await;
13158        let pool = test_pool();
13159
13160        let report = client.preflight(&pool.instrument_id).await.unwrap();
13161
13162        assert!(!report.ready);
13163        assert!(!report.pool.has_deployed_code);
13164        assert!(!report.routers[0].has_deployed_code);
13165        assert!(report.tokens.iter().all(|t| !t.has_deployed_code));
13166        assert!(
13167            report
13168                .issues
13169                .iter()
13170                .any(|issue| issue.contains("No deployed bytecode at router address"))
13171        );
13172    }
13173
13174    #[tokio::test]
13175    async fn preflight_not_ready_with_zero_balances_and_allowance() {
13176        let state = ready_rpc_state()
13177            .with_response("eth_getBalance", GET_BALANCE_ZERO)
13178            .with_call_response(BALANCE_OF_SELECTOR, CALL_ZERO)
13179            .with_call_response(ALLOWANCE_SELECTOR, CALL_ZERO);
13180        let (client, _) = client_with_mock_rpc(state).await;
13181        let pool = test_pool();
13182
13183        let report = client.preflight(&pool.instrument_id).await.unwrap();
13184
13185        assert!(!report.ready);
13186        assert!(
13187            report
13188                .issues
13189                .iter()
13190                .any(|issue| issue.contains("Native currency balance is zero"))
13191        );
13192        assert!(
13193            report
13194                .issues
13195                .iter()
13196                .any(|issue| issue.contains("balance is zero"))
13197        );
13198        assert!(
13199            report
13200                .issues
13201                .iter()
13202                .any(|issue| issue.contains("No router allowance"))
13203        );
13204    }
13205
13206    #[tokio::test]
13207    async fn preflight_not_ready_when_fees_exceed_ceiling() {
13208        let addr = start_mock_rpc_server(ready_rpc_state()).await;
13209        let mut config = test_config(format!("http://{addr}"));
13210        config.max_fee_per_gas_wei = 1;
13211        let pool = test_pool();
13212        let client = test_client_from_config(config, pool.clone());
13213
13214        let report = client.preflight(&pool.instrument_id).await.unwrap();
13215
13216        assert!(!report.ready);
13217        assert!(!report.fee_within_ceiling);
13218        assert_eq!(report.derived_max_fee_per_gas_wei, 130_000_000);
13219        assert!(
13220            report
13221                .issues
13222                .iter()
13223                .any(|issue| issue.contains("exceeds ceiling"))
13224        );
13225    }
13226
13227    #[rstest]
13228    fn resolve_pool_rejects_unknown_pool() {
13229        let client = test_client("http://127.0.0.1:1".to_string());
13230        let unknown: InstrumentId = "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45.Arbitrum:UniswapV3"
13231            .parse()
13232            .unwrap();
13233
13234        let error = client.resolve_pool(&unknown).unwrap_err();
13235
13236        assert!(error.to_string().contains("Unknown pool"), "was: {error}");
13237    }
13238
13239    #[rstest]
13240    fn resolve_pool_rejects_mismatched_chain() {
13241        let client = test_client("http://127.0.0.1:1".to_string());
13242        let ethereum_pool: InstrumentId =
13243            "0xC6962004f452bE9203591991D15f6b388e09E8D0.Ethereum:UniswapV3"
13244                .parse()
13245                .unwrap();
13246
13247        let error = client.resolve_pool(&ethereum_pool).unwrap_err();
13248
13249        assert!(
13250            error
13251                .to_string()
13252                .contains("does not match the client chain"),
13253            "was: {error}"
13254        );
13255    }
13256
13257    #[rstest]
13258    fn resolve_pool_rejects_unsupported_dex() {
13259        let client = test_client("http://127.0.0.1:1".to_string());
13260        let v4_pool: InstrumentId = "0xC6962004f452bE9203591991D15f6b388e09E8D0.Arbitrum:UniswapV4"
13261            .parse()
13262            .unwrap();
13263
13264        let error = client.resolve_pool(&v4_pool).unwrap_err();
13265
13266        assert!(
13267            error.to_string().contains("only UniswapV3 is supported"),
13268            "was: {error}"
13269        );
13270    }
13271
13272    #[rstest]
13273    fn resolve_pool_rejects_pool_id_identifier() {
13274        let client = test_client("http://127.0.0.1:1".to_string());
13275        let pool_id: InstrumentId =
13276            "0x0000000000000000000000000000000000000000000000000000000000000000.Arbitrum:UniswapV3"
13277                .parse()
13278                .unwrap();
13279
13280        let error = client.resolve_pool(&pool_id).unwrap_err();
13281
13282        assert!(
13283            error
13284                .to_string()
13285                .contains("only address identifiers are supported"),
13286            "was: {error}"
13287        );
13288    }
13289
13290    #[rstest]
13291    fn resolve_pool_rejects_ambiguous_token_priority() {
13292        let chain = Arc::new(chains::ARBITRUM.clone());
13293        let dex = UNISWAP_V3.dex.clone();
13294        let token_a = Token::new(
13295            chain.clone(),
13296            address!("1111111111111111111111111111111111111111"),
13297            "Token A".to_string(),
13298            "TOKA".to_string(),
13299            18,
13300        );
13301        let token_b = Token::new(
13302            chain.clone(),
13303            address!("2222222222222222222222222222222222222222"),
13304            "Token B".to_string(),
13305            "TOKB".to_string(),
13306            18,
13307        );
13308        let pool = Pool::new(
13309            chain,
13310            dex,
13311            address!("3333333333333333333333333333333333333333"),
13312            PoolIdentifier::from_address(address!("3333333333333333333333333333333333333333")),
13313            55_000_000,
13314            token_a,
13315            token_b,
13316            Some(500),
13317            Some(10),
13318            UnixNanos::default(),
13319        );
13320        let client =
13321            test_client_from_config(test_config("http://127.0.0.1:1".to_string()), pool.clone());
13322
13323        let error = client.resolve_pool(&pool.instrument_id).unwrap_err();
13324
13325        assert!(error.to_string().contains("ambiguous"), "was: {error}");
13326    }
13327
13328    #[rstest]
13329    fn new_parses_pair_specific_quote_spend_limits_with_distinct_precisions() {
13330        let mut config = buy_test_config("http://127.0.0.1:1".to_string());
13331        config.quote_spend_limits = Some(vec![
13332            quote_spend_limit(WETH, USDC, 18, &U256::MAX.to_string()),
13333            quote_spend_limit(USDC, WETH, 6, "1000000000"),
13334        ]);
13335
13336        let client = test_client_from_config(config, test_pool());
13337        let sell_ceiling = client
13338            .transaction_limits
13339            .quote_spend_limits
13340            .get(&(WETH_ADDRESS, USDC_ADDRESS))
13341            .unwrap();
13342        let buy_ceiling = client
13343            .transaction_limits
13344            .quote_spend_limits
13345            .get(&(USDC_ADDRESS, WETH_ADDRESS))
13346            .unwrap();
13347
13348        assert_eq!(sell_ceiling.spend_token, WETH_ADDRESS);
13349        assert_eq!(sell_ceiling.spend_token_decimals, 18);
13350        assert_eq!(sell_ceiling.max_amount, U256::MAX);
13351        assert_eq!(buy_ceiling.spend_token, USDC_ADDRESS);
13352        assert_eq!(buy_ceiling.spend_token_decimals, 6);
13353        assert_eq!(buy_ceiling.max_amount, U256::from(1_000_000_000u64));
13354    }
13355
13356    #[rstest]
13357    fn new_rejects_quote_spend_limit_token_pair_mismatch() {
13358        let mut config = buy_test_config("http://127.0.0.1:1".to_string());
13359        config.quote_spend_limits.as_mut().unwrap()[0].spend_token = WETH.to_string();
13360
13361        let error = test_client_result(config, test_pool()).unwrap_err();
13362
13363        assert!(
13364            error
13365                .to_string()
13366                .contains("`spend_token` must match `token_in`"),
13367            "was: {error}"
13368        );
13369    }
13370
13371    #[rstest]
13372    fn new_rejects_quote_spend_limit_pair_outside_allowlist() {
13373        let mut config = buy_test_config("http://127.0.0.1:1".to_string());
13374        config.quote_spend_limits = Some(vec![quote_spend_limit(
13375            USDC,
13376            "0x1111111111111111111111111111111111111111",
13377            6,
13378            "1000000000",
13379        )]);
13380
13381        let error = test_client_result(config, test_pool()).unwrap_err();
13382
13383        assert!(
13384            error
13385                .to_string()
13386                .contains("is not in the `allowed_token_pairs` allowlist"),
13387            "was: {error}"
13388        );
13389    }
13390
13391    #[rstest]
13392    fn new_rejects_duplicate_quote_spend_pairs() {
13393        let mut config = buy_test_config("http://127.0.0.1:1".to_string());
13394        config.quote_spend_limits = Some(vec![
13395            quote_spend_limit(USDC, WETH, 6, "1000000000"),
13396            quote_spend_limit(USDC, WETH, 6, "2000000000"),
13397        ]);
13398
13399        let error = test_client_result(config, test_pool()).unwrap_err();
13400
13401        assert!(
13402            error
13403                .to_string()
13404                .contains("Duplicate quote spend limit for token pair"),
13405            "was: {error}"
13406        );
13407    }
13408
13409    #[rstest]
13410    #[case::empty("")]
13411    #[case::signed("-1")]
13412    #[case::fractional("1.5")]
13413    #[case::hexadecimal("0x10")]
13414    #[case::overflow(
13415        "115792089237316195423570985008687907853269984665640564039457584007913129639936"
13416    )]
13417    fn new_rejects_invalid_quote_spend_max_amount(#[case] max_amount: &str) {
13418        let mut config = buy_test_config("http://127.0.0.1:1".to_string());
13419        config.quote_spend_limits.as_mut().unwrap()[0].max_amount = max_amount.to_string();
13420
13421        let error = test_client_result(config, test_pool()).unwrap_err();
13422
13423        assert!(
13424            error.to_string().contains("Quote spend limit `max_amount`"),
13425            "was: {error}"
13426        );
13427    }
13428
13429    #[rstest]
13430    #[case::allowed_token_pairs("allowed_token_pairs")]
13431    #[case::slippage_bps("slippage_bps")]
13432    #[case::max_slippage_bps("max_slippage_bps")]
13433    #[case::max_order_amount("max_order_amount")]
13434    #[case::deadline_seconds("deadline_seconds")]
13435    #[case::max_quote_age_blocks("max_quote_age_blocks")]
13436    #[case::receipt_timeout_secs("receipt_timeout_secs")]
13437    fn new_rejects_each_missing_transaction_limit(#[case] missing: &str) {
13438        let mut config = test_config("http://127.0.0.1:1".to_string());
13439        match missing {
13440            "allowed_token_pairs" => config.allowed_token_pairs = None,
13441            "slippage_bps" => config.slippage_bps = None,
13442            "max_slippage_bps" => config.max_slippage_bps = None,
13443            "max_order_amount" => config.max_order_amount = None,
13444            "deadline_seconds" => config.deadline_seconds = None,
13445            "max_quote_age_blocks" => config.max_quote_age_blocks = None,
13446            "receipt_timeout_secs" => config.receipt_timeout_secs = None,
13447            _ => unreachable!(),
13448        }
13449
13450        let error = test_client_result(config, test_pool()).unwrap_err();
13451
13452        assert_eq!(
13453            error.to_string(),
13454            "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"
13455        );
13456    }
13457
13458    #[rstest]
13459    fn new_rejects_empty_router_allowlist() {
13460        let mut config = test_config("http://127.0.0.1:1".to_string());
13461        config.router_addresses = Vec::new();
13462
13463        let error = test_client_result(config, test_pool()).unwrap_err();
13464
13465        assert!(
13466            error.to_string().contains("at least one router address"),
13467            "was: {error}"
13468        );
13469    }
13470
13471    #[tokio::test]
13472    async fn wrap_refuses_without_durable_store() {
13473        let (mut client, _) = client_with_mock_rpc(ready_rpc_state()).await;
13474        client.core.set_connected();
13475
13476        let error = client.wrap(U256::from(1_000u64)).await.unwrap_err();
13477
13478        assert!(
13479            error.to_string().contains("No durable store configured"),
13480            "was: {error}"
13481        );
13482    }
13483
13484    #[tokio::test]
13485    async fn approve_rejects_router_outside_allowlist() {
13486        let (mut client, state) = client_with_mock_rpc(ready_rpc_state()).await;
13487
13488        let error = client
13489            .approve(
13490                WETH_ADDRESS,
13491                U256::from(1_000u64),
13492                address!("68b3465833fb72A70ecDF485E0e4C7bD8665Fc45"),
13493            )
13494            .await
13495            .err()
13496            .unwrap();
13497
13498        assert!(
13499            error
13500                .to_string()
13501                .contains("not in the configured `router_addresses` allowlist"),
13502            "was: {error}"
13503        );
13504        assert!(state.recorded_requests().is_empty());
13505    }
13506
13507    #[tokio::test]
13508    async fn approve_rejects_token_outside_input_allowlist() {
13509        let (mut client, state) = client_with_mock_rpc(ready_rpc_state()).await;
13510
13511        let error = client
13512            .approve(USDC_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
13513            .await
13514            .unwrap_err();
13515
13516        assert!(
13517            error
13518                .to_string()
13519                .contains("is not an input token in the configured `allowed_token_pairs`"),
13520            "was: {error}"
13521        );
13522        assert!(state.recorded_requests().is_empty());
13523    }
13524
13525    #[tokio::test]
13526    async fn in_flight_guard_rejects_second_transaction() {
13527        let (mut client, _) = client_with_mock_rpc(ready_rpc_state()).await;
13528        client.core.set_connected();
13529        *client.in_flight.lock() = Some(InFlightSlot::AwaitingFinality(InFlightTransaction {
13530            intent_id: 1,
13531            nonce: 7,
13532            tx_hash: B256::ZERO,
13533            purpose: TransactionPurpose::Wrap,
13534        }));
13535
13536        let error = client.wrap(U256::from(1_000u64)).await.unwrap_err();
13537
13538        assert!(
13539            error.to_string().contains("still awaiting finality"),
13540            "was: {error}"
13541        );
13542    }
13543
13544    #[tokio::test]
13545    async fn wrap_rejects_zero_amount() {
13546        let (mut client, _) = client_with_mock_rpc(ready_rpc_state()).await;
13547
13548        let error = client.wrap(U256::ZERO).await.unwrap_err();
13549
13550        assert!(
13551            error.to_string().contains("Wrap amount must be positive"),
13552            "was: {error}"
13553        );
13554    }
13555
13556    #[tokio::test]
13557    async fn wrap_rejects_code_free_target_before_broadcast() {
13558        let state = execution_rpc_state().with_response("eth_getCode", GET_CODE_EMPTY);
13559        let Some((admin_pool, schema, mut client, state)) =
13560            execution_client_with_database("execution_wrap_code_free_test", state).await
13561        else {
13562            return;
13563        };
13564
13565        let error = client
13566            .wrap(U256::from(1_000_000_000_000_000u64))
13567            .await
13568            .unwrap_err();
13569
13570        assert!(
13571            error
13572                .to_string()
13573                .contains("pre-sign deployment manifest verification disagreed"),
13574            "was: {error}"
13575        );
13576        assert!(client.in_flight.lock().is_none());
13577        let requests = state.recorded_requests();
13578        assert_eq!(
13579            requests
13580                .iter()
13581                .filter(|request| request["method"] == "eth_getCode")
13582                .count(),
13583            3
13584        );
13585        assert!(
13586            requests
13587                .iter()
13588                .all(|request| request["method"] != "eth_sendRawTransaction")
13589        );
13590
13591        drop_execution_schema(&admin_pool, &schema).await;
13592    }
13593
13594    #[tokio::test]
13595    async fn wrap_rejects_unrelated_target_before_broadcast() {
13596        let state = execution_rpc_state().with_response("eth_call", CALL_EMPTY);
13597        let Some((admin_pool, schema, mut client, state)) =
13598            execution_client_with_database("execution_wrap_unrelated_test", state).await
13599        else {
13600            return;
13601        };
13602
13603        let error = client
13604            .wrap(U256::from(1_000_000_000_000_000u64))
13605            .await
13606            .unwrap_err();
13607
13608        assert!(
13609            error
13610                .to_string()
13611                .contains("pre-sign wrapped token probe verification is unavailable"),
13612            "was: {error}"
13613        );
13614        assert!(client.in_flight.lock().is_none());
13615        let requests = state.recorded_requests();
13616        assert!(
13617            requests
13618                .iter()
13619                .all(|request| request["method"] != "eth_sendRawTransaction")
13620        );
13621
13622        drop_execution_schema(&admin_pool, &schema).await;
13623    }
13624
13625    #[tokio::test]
13626    async fn wrap_rejects_included_transaction_without_balance_delta() {
13627        let state = broadcast_rpc_state().with_response_sequence("eth_call", &[CALL_BALANCE; 9]);
13628        let Some((admin_pool, schema, mut client, state)) =
13629            execution_client_with_database("execution_wrap_no_delta_test", state).await
13630        else {
13631            return;
13632        };
13633
13634        let error = client
13635            .wrap(U256::from(1_000_000_000_000_000u64))
13636            .await
13637            .unwrap_err();
13638
13639        assert!(
13640            error.to_string().contains("did not increase"),
13641            "was: {error}"
13642        );
13643        let in_flight = awaiting_in_flight(&client);
13644        assert_eq!(in_flight.purpose, TransactionPurpose::Wrap);
13645        assert_eq!(
13646            execution_intent_markers(&admin_pool, &schema).await,
13647            vec![("wrap".into(), "broadcast".into(), false, true)]
13648        );
13649        let nonce_state = sqlx::query_as::<_, (i64, i64)>(sqlx::AssertSqlSafe(format!(
13650            "SELECT next_canonical_nonce, revision FROM {schema}.execution_verification_nonce"
13651        )))
13652        .fetch_one(&admin_pool)
13653        .await
13654        .unwrap();
13655        assert_eq!(nonce_state, (7, 0));
13656        let broadcasts = state
13657            .recorded_requests()
13658            .into_iter()
13659            .filter(|request| request["method"] == "eth_sendRawTransaction")
13660            .count();
13661        assert_eq!(broadcasts, 1);
13662
13663        let expected_hash = expected_wrap_tx_hash(U256::from(1_000_000_000_000_000u64)).await;
13664        let block = finalized_wrap_block(expected_hash);
13665        let receipt = receipt_with_transaction_hash(RECEIPT_SUCCESS, expected_hash);
13666        let restart_state = with_finalized_identity(
13667            execution_rpc_state()
13668                .with_response("eth_getTransactionReceipt", &receipt)
13669                .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block)
13670                .with_response_sequence("eth_call", &[CALL_BALANCE; 6]),
13671            &block,
13672            &receipt,
13673        );
13674        let addr = start_mock_rpc_server(restart_state).await;
13675        let error = later_reconnect(client, format!("http://{addr}")).await;
13676        assert!(
13677            error.to_string().contains("did not increase"),
13678            "was: {error}"
13679        );
13680        assert_eq!(
13681            execution_intent_markers(&admin_pool, &schema).await,
13682            vec![("wrap".into(), "broadcast".into(), false, true)]
13683        );
13684        let nonce_state = sqlx::query_as::<_, (i64, i64)>(sqlx::AssertSqlSafe(format!(
13685            "SELECT next_canonical_nonce, revision FROM {schema}.execution_verification_nonce"
13686        )))
13687        .fetch_one(&admin_pool)
13688        .await
13689        .unwrap();
13690        assert_eq!(nonce_state, (7, 0));
13691
13692        drop_execution_schema(&admin_pool, &schema).await;
13693    }
13694
13695    #[tokio::test]
13696    async fn wrap_reports_inclusion_when_postcondition_read_fails() {
13697        let state = broadcast_rpc_state().with_response_sequence(
13698            "eth_call",
13699            &[
13700                CALL_BALANCE,
13701                CALL_BALANCE,
13702                CALL_BALANCE,
13703                CALL_BALANCE,
13704                CALL_BALANCE,
13705                CALL_BALANCE,
13706                RPC_METHOD_NOT_FOUND,
13707                RPC_METHOD_NOT_FOUND,
13708                RPC_METHOD_NOT_FOUND,
13709            ],
13710        );
13711        let Some((admin_pool, schema, mut client, _)) =
13712            execution_client_with_database("execution_wrap_postcondition_rpc_test", state).await
13713        else {
13714            return;
13715        };
13716
13717        let error = client
13718            .wrap(U256::from(1_000_000_000_000_000u64))
13719            .await
13720            .unwrap_err();
13721
13722        let message = error.to_string();
13723        assert!(
13724            message.contains("failed to verify WETH balance after included transaction 0x"),
13725            "was: {message}"
13726        );
13727        assert!(message.contains("at block 30346561"), "was: {message}");
13728        let in_flight = awaiting_in_flight(&client);
13729        assert_eq!(in_flight.purpose, TransactionPurpose::Wrap);
13730        assert_eq!(
13731            execution_intent_markers(&admin_pool, &schema).await,
13732            vec![("wrap".into(), "broadcast".into(), false, true)]
13733        );
13734
13735        drop_execution_schema(&admin_pool, &schema).await;
13736    }
13737
13738    #[tokio::test]
13739    async fn approve_rejects_false_return_before_broadcast() {
13740        let state = execution_rpc_state().with_response("eth_call", CALL_ZERO);
13741        let Some((admin_pool, schema, mut client, state)) =
13742            execution_client_with_database("execution_approve_false_test", state).await
13743        else {
13744            return;
13745        };
13746
13747        let error = client
13748            .approve(WETH_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
13749            .await
13750            .unwrap_err();
13751
13752        assert!(error.to_string().contains("returned false"), "was: {error}");
13753        assert!(client.in_flight.lock().is_none());
13754        let requests = state.recorded_requests();
13755        let approval_calls = requests
13756            .iter()
13757            .filter(|request| {
13758                request["method"] == "eth_call"
13759                    && request["params"][0]["data"]
13760                        .as_str()
13761                        .is_some_and(|data| data.starts_with("0x095ea7b3"))
13762            })
13763            .collect::<Vec<_>>();
13764        assert_eq!(approval_calls.len(), 3);
13765        for request in approval_calls {
13766            assert_eq!(
13767                request["params"][0]["from"]
13768                    .as_str()
13769                    .unwrap()
13770                    .parse::<Address>()
13771                    .unwrap(),
13772                WALLET.parse::<Address>().unwrap()
13773            );
13774            assert_eq!(request["params"][1], FIXTURE_BLOCK_PARAM);
13775        }
13776        assert!(
13777            requests
13778                .iter()
13779                .all(|request| request["method"] != "eth_getTransactionCount")
13780        );
13781        assert!(
13782            requests
13783                .iter()
13784                .all(|request| request["method"] != "eth_sendRawTransaction")
13785        );
13786
13787        drop_execution_schema(&admin_pool, &schema).await;
13788    }
13789
13790    #[tokio::test]
13791    async fn approve_rejects_router_with_wrong_factory_before_signing() {
13792        let state = ready_rpc_state().with_call_response(FACTORY_SELECTOR, CALL_ZERO);
13793        let Some((admin_pool, schema, mut client, state)) =
13794            execution_client_with_database("execution_approve_factory_test", state).await
13795        else {
13796            return;
13797        };
13798
13799        let error = client
13800            .approve(WETH_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
13801            .await
13802            .unwrap_err();
13803
13804        assert!(
13805            error
13806                .to_string()
13807                .contains("pre-sign deployment manifest verification disagreed"),
13808            "was: {error}"
13809        );
13810        let requests = state.recorded_requests();
13811        assert!(
13812            requests
13813                .iter()
13814                .all(|request| request["method"] != "eth_getTransactionCount")
13815        );
13816        assert!(
13817            requests
13818                .iter()
13819                .all(|request| request["method"] != "eth_sendRawTransaction")
13820        );
13821
13822        drop_execution_schema(&admin_pool, &schema).await;
13823    }
13824
13825    #[tokio::test]
13826    async fn approve_rejects_router_with_wrong_weth_before_signing() {
13827        let state = ready_rpc_state().with_call_response(WETH9_SELECTOR, CALL_ZERO);
13828        let Some((admin_pool, schema, mut client, state)) =
13829            execution_client_with_database("execution_approve_weth_test", state).await
13830        else {
13831            return;
13832        };
13833
13834        let error = client
13835            .approve(WETH_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
13836            .await
13837            .unwrap_err();
13838
13839        assert!(
13840            error
13841                .to_string()
13842                .contains("pre-sign deployment manifest verification disagreed"),
13843            "was: {error}"
13844        );
13845        let requests = state.recorded_requests();
13846        assert!(
13847            requests
13848                .iter()
13849                .all(|request| request["method"] != "eth_getTransactionCount")
13850        );
13851        assert!(
13852            requests
13853                .iter()
13854                .all(|request| request["method"] != "eth_sendRawTransaction")
13855        );
13856
13857        drop_execution_schema(&admin_pool, &schema).await;
13858    }
13859
13860    #[tokio::test]
13861    async fn approve_rejects_nonzero_to_nonzero_transition_before_signing() {
13862        let state = ready_rpc_state();
13863        let Some((admin_pool, schema, mut client, state)) =
13864            execution_client_with_database("execution_approve_nonzero_test", state).await
13865        else {
13866            return;
13867        };
13868
13869        let error = client
13870            .approve(WETH_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
13871            .await
13872            .unwrap_err();
13873
13874        assert!(
13875            error.to_string().contains("approve zero before setting"),
13876            "was: {error}"
13877        );
13878        let requests = state.recorded_requests();
13879        assert!(
13880            requests
13881                .iter()
13882                .all(|request| request["method"] != "eth_getTransactionCount")
13883        );
13884        assert!(
13885            requests
13886                .iter()
13887                .all(|request| request["method"] != "eth_sendRawTransaction")
13888        );
13889
13890        drop_execution_schema(&admin_pool, &schema).await;
13891    }
13892
13893    #[tokio::test]
13894    async fn approve_zero_revokes_under_unlimited_policy() {
13895        let state = broadcast_rpc_state()
13896            .with_response("eth_call", CALL_BOOL_TRUE)
13897            .with_call_response_sequence(
13898                ALLOWANCE_SELECTOR,
13899                &[
13900                    CALL_ALLOWANCE,
13901                    CALL_ALLOWANCE,
13902                    CALL_ALLOWANCE,
13903                    CALL_ZERO,
13904                    CALL_ZERO,
13905                    CALL_ZERO,
13906                ],
13907            );
13908        let Some((admin_pool, schema, mut client, state)) =
13909            execution_client_with_database("execution_approve_revoke_test", state).await
13910        else {
13911            return;
13912        };
13913        client.config.unlimited_approval = true;
13914
13915        let tx_hash = client
13916            .approve(WETH_ADDRESS, U256::ZERO, ROUTER_ADDRESS)
13917            .await
13918            .unwrap();
13919
13920        assert_eq!(tx_hash, expected_approve_tx_hash(U256::ZERO).await);
13921        let approve_data = state
13922            .recorded_requests()
13923            .into_iter()
13924            .find_map(|request| {
13925                (request["method"] == "eth_estimateGas")
13926                    .then(|| request["params"][0]["data"].as_str().map(str::to_owned))
13927                    .flatten()
13928            })
13929            .unwrap();
13930        assert!(approve_data.starts_with("0x095ea7b3"));
13931        assert!(approve_data.ends_with(&"0".repeat(64)));
13932        assert_eq!(
13933            state
13934                .recorded_requests()
13935                .iter()
13936                .filter(|request| {
13937                    request["method"] == "eth_call"
13938                        && request["params"][0]["data"]
13939                            .as_str()
13940                            .is_some_and(|data| data.starts_with(FACTORY_SELECTOR))
13941                })
13942                .count(),
13943            12
13944        );
13945        assert_eq!(
13946            execution_intent_markers(&admin_pool, &schema).await,
13947            vec![("approve".into(), "finalized".into(), true, false)]
13948        );
13949
13950        drop_execution_schema(&admin_pool, &schema).await;
13951    }
13952
13953    #[tokio::test]
13954    async fn approve_accepts_empty_return_with_sufficient_allowance() {
13955        let state = broadcast_rpc_state()
13956            .with_response("eth_call", CALL_EMPTY)
13957            .with_call_response_sequence(
13958                ALLOWANCE_SELECTOR,
13959                &[
13960                    CALL_ZERO,
13961                    CALL_ZERO,
13962                    CALL_ZERO,
13963                    CALL_ALLOWANCE_1000,
13964                    CALL_ALLOWANCE_1000,
13965                    CALL_ALLOWANCE_1000,
13966                ],
13967            );
13968        let Some((admin_pool, schema, mut client, _)) =
13969            execution_client_with_database("execution_approve_empty_test", state).await
13970        else {
13971            return;
13972        };
13973
13974        let tx_hash = client
13975            .approve(WETH_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
13976            .await
13977            .unwrap();
13978
13979        let record = client
13980            .cache
13981            .get_execution_transaction(42161, &tx_hash.to_string())
13982            .await
13983            .unwrap()
13984            .unwrap();
13985        assert_eq!(record.purpose, "approve");
13986        assert_eq!(record.status, "finalized");
13987        assert!(client.in_flight.lock().is_none());
13988        assert_eq!(
13989            execution_intent_markers(&admin_pool, &schema).await,
13990            vec![("approve".into(), "finalized".into(), true, false)]
13991        );
13992
13993        drop_execution_schema(&admin_pool, &schema).await;
13994    }
13995
13996    #[tokio::test]
13997    async fn approve_rejects_empty_return_with_insufficient_allowance() {
13998        let state = broadcast_rpc_state()
13999            .with_response("eth_call", CALL_EMPTY)
14000            .with_call_response_sequence(ALLOWANCE_SELECTOR, &[CALL_ZERO; 6]);
14001        let Some((admin_pool, schema, mut client, _)) =
14002            execution_client_with_database("execution_approve_insufficient_test", state).await
14003        else {
14004            return;
14005        };
14006
14007        let error = client
14008            .approve(WETH_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
14009            .await
14010            .unwrap_err();
14011
14012        assert!(
14013            error
14014                .to_string()
14015                .contains("does not equal the requested amount"),
14016            "was: {error}"
14017        );
14018        let in_flight = awaiting_in_flight(&client);
14019        assert_eq!(in_flight.purpose, TransactionPurpose::Approve);
14020        assert_eq!(
14021            execution_intent_markers(&admin_pool, &schema).await,
14022            vec![("approve".into(), "broadcast".into(), false, true)]
14023        );
14024        let nonce_state = sqlx::query_as::<_, (i64, i64)>(sqlx::AssertSqlSafe(format!(
14025            "SELECT next_canonical_nonce, revision FROM {schema}.execution_verification_nonce"
14026        )))
14027        .fetch_one(&admin_pool)
14028        .await
14029        .unwrap();
14030        assert_eq!(nonce_state, (7, 0));
14031
14032        let expected_hash = expected_approve_tx_hash(U256::from(1_000u64)).await;
14033        let block = finalized_approve_block(expected_hash, U256::from(1_000u64));
14034        let receipt = receipt_with_transaction_hash(RECEIPT_SUCCESS, expected_hash);
14035        let restart_state = with_finalized_identity(
14036            execution_rpc_state()
14037                .with_response("eth_getTransactionReceipt", &receipt)
14038                .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block)
14039                .with_call_response(ALLOWANCE_SELECTOR, CALL_ZERO),
14040            &block,
14041            &receipt,
14042        );
14043        let addr = start_mock_rpc_server(restart_state).await;
14044        let error = later_reconnect(client, format!("http://{addr}")).await;
14045        assert!(
14046            error
14047                .to_string()
14048                .contains("does not equal the requested amount"),
14049            "was: {error}"
14050        );
14051        assert_eq!(
14052            execution_intent_markers(&admin_pool, &schema).await,
14053            vec![("approve".into(), "broadcast".into(), false, true)]
14054        );
14055        let nonce_state = sqlx::query_as::<_, (i64, i64)>(sqlx::AssertSqlSafe(format!(
14056            "SELECT next_canonical_nonce, revision FROM {schema}.execution_verification_nonce"
14057        )))
14058        .fetch_one(&admin_pool)
14059        .await
14060        .unwrap();
14061        assert_eq!(nonce_state, (7, 0));
14062
14063        drop_execution_schema(&admin_pool, &schema).await;
14064    }
14065
14066    #[tokio::test]
14067    async fn approve_reports_inclusion_when_postcondition_read_fails() {
14068        let state = broadcast_rpc_state()
14069            .with_response("eth_call", CALL_BOOL_TRUE)
14070            .with_call_response_sequence(
14071                ALLOWANCE_SELECTOR,
14072                &[
14073                    CALL_ZERO,
14074                    CALL_ZERO,
14075                    CALL_ZERO,
14076                    RPC_METHOD_NOT_FOUND,
14077                    RPC_METHOD_NOT_FOUND,
14078                    RPC_METHOD_NOT_FOUND,
14079                ],
14080            );
14081        let Some((admin_pool, schema, mut client, _)) =
14082            execution_client_with_database("execution_approve_postcondition_rpc_test", state).await
14083        else {
14084            return;
14085        };
14086
14087        let error = client
14088            .approve(WETH_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
14089            .await
14090            .unwrap_err();
14091
14092        let message = error.to_string();
14093        assert!(
14094            message.contains("failed to verify router allowance after included transaction 0x"),
14095            "was: {message}"
14096        );
14097        assert!(message.contains("at block 30346561"), "was: {message}");
14098        let in_flight = awaiting_in_flight(&client);
14099        assert_eq!(in_flight.purpose, TransactionPurpose::Approve);
14100        assert_eq!(
14101            execution_intent_markers(&admin_pool, &schema).await,
14102            vec![("approve".into(), "broadcast".into(), false, true)]
14103        );
14104
14105        drop_execution_schema(&admin_pool, &schema).await;
14106    }
14107
14108    #[tokio::test]
14109    async fn wallet_balance_refresh_replaces_complete_snapshot_with_exact_precision() {
14110        let state = execution_rpc_state()
14111            .with_response_sequence("eth_getBalance", &[GET_BALANCE, GET_BALANCE_ZERO])
14112            .with_response_sequence(
14113                "eth_call",
14114                &[
14115                    CALL_BALANCE_WETH,
14116                    CALL_BALANCE_USDC,
14117                    CALL_BALANCE_WETH_UPDATED,
14118                    CALL_BALANCE_USDC_UPDATED,
14119                ],
14120            );
14121        let (mut client, _, _) =
14122            client_with_token_mock_rpc(state, "BLOCKCHAIN_TEST_BALANCE_REPLACE").await;
14123        let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel();
14124        replace_exec_event_sender(sender);
14125        client.start().unwrap();
14126
14127        client.refresh_wallet_balances().await.unwrap();
14128        let balances = client.wallet_balance.lock().as_account_balances().unwrap();
14129
14130        assert_eq!(balances.len(), 3);
14131        assert_eq!(balances[0].currency.code, "ETH");
14132        assert_eq!(balances[0].currency.name, "Ethereum");
14133        assert_eq!(balances[0].currency.precision, 18);
14134        assert_eq!(balances[0].total.raw(), 1_000_000_000_000_000_000);
14135        assert_eq!(balances[0].free, balances[0].total);
14136        assert_eq!(balances[0].locked, Money::zero(balances[0].currency));
14137        assert_eq!(balances[1].currency.code, "WETH");
14138        assert_eq!(balances[1].currency.name, "Wrapped Ether");
14139        assert_eq!(balances[1].currency.precision, 18);
14140        assert_eq!(balances[1].total.raw(), 1_234_567_890_123_456_789);
14141        assert_eq!(balances[1].free, balances[1].total);
14142        assert_eq!(balances[1].locked, Money::zero(balances[1].currency));
14143        assert_eq!(balances[2].currency.code, "USDC");
14144        assert_eq!(balances[2].currency.name, "USD Coin");
14145        assert_eq!(balances[2].currency.precision, 6);
14146        assert_eq!(balances[2].total.raw(), 9_876_543_210_000_000_000);
14147        assert_eq!(balances[2].free, balances[2].total);
14148        assert_eq!(balances[2].locked, Money::zero(balances[2].currency));
14149
14150        client.refresh_wallet_balances().await.unwrap();
14151        let balances = client.wallet_balance.lock().as_account_balances().unwrap();
14152
14153        assert_eq!(balances.len(), 3);
14154        assert_eq!(client.wallet_balance.lock().token_balances.len(), 2);
14155        assert_eq!(balances[0].total.raw(), 0);
14156        assert_eq!(balances[1].total.raw(), 2_000_000_000_000_000_000);
14157        assert_eq!(balances[2].total.raw(), 12_345_670_000_000_000);
14158    }
14159
14160    #[allow(unsafe_code)] // env-var mutation in tests; unique var names avoid cross-test races
14161    #[tokio::test]
14162    async fn failed_connect_refresh_retains_snapshot_and_publishes_nothing() {
14163        let state = execution_rpc_state()
14164            .with_response_sequence("eth_getBalance", &[GET_BALANCE, GET_BALANCE_ZERO])
14165            .with_response_sequence(
14166                "eth_call",
14167                &[
14168                    CALL_BALANCE_WETH,
14169                    CALL_BALANCE_USDC,
14170                    CALL_BALANCE_WETH_UPDATED,
14171                    RPC_METHOD_NOT_FOUND,
14172                ],
14173            );
14174        let (mut client, _, _) =
14175            client_with_token_mock_rpc(state, "BLOCKCHAIN_TEST_BALANCE_ATOMIC").await;
14176        let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
14177        replace_exec_event_sender(sender);
14178        client.start().unwrap();
14179        client.refresh_wallet_balances().await.unwrap();
14180        let retained = client.wallet_balance.lock().as_account_balances().unwrap();
14181        receiver.try_recv().unwrap();
14182        // SAFETY: this variable name is unique to this test across the test binary
14183        unsafe { std::env::set_var("BLOCKCHAIN_TEST_BALANCE_ATOMIC", TEST_PRIVATE_KEY) };
14184
14185        let error = client.connect().await.unwrap_err();
14186        let failed_address = Address::from_str(USDC).unwrap();
14187
14188        assert!(
14189            error.to_string().contains(&format!(
14190                "failed to fetch token balance for {failed_address}"
14191            )),
14192            "was: {error}"
14193        );
14194        assert!(!client.is_connected());
14195        assert!(client.signer.is_none());
14196        assert_eq!(
14197            client.wallet_balance.lock().as_account_balances().unwrap(),
14198            retained
14199        );
14200        assert!(matches!(
14201            receiver.try_recv(),
14202            Err(tokio::sync::mpsc::error::TryRecvError::Empty)
14203        ));
14204    }
14205
14206    #[allow(unsafe_code)] // env-var mutation in tests; unique var names avoid cross-test races
14207    #[tokio::test]
14208    async fn failed_connect_publication_retains_snapshot() {
14209        let state = execution_rpc_state()
14210            .with_response_sequence("eth_getBalance", &[GET_BALANCE, GET_BALANCE_ZERO])
14211            .with_response_sequence(
14212                "eth_call",
14213                &[
14214                    CALL_BALANCE_WETH,
14215                    CALL_BALANCE_USDC,
14216                    CALL_BALANCE_WETH_UPDATED,
14217                    CALL_BALANCE_USDC_UPDATED,
14218                ],
14219            );
14220        let (mut client, _, _) =
14221            client_with_token_mock_rpc(state, "BLOCKCHAIN_TEST_BALANCE_PUBLICATION").await;
14222        let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
14223        replace_exec_event_sender(sender);
14224        client.start().unwrap();
14225        client.refresh_wallet_balances().await.unwrap();
14226        let retained = client.wallet_balance.lock().as_account_balances().unwrap();
14227        receiver.try_recv().unwrap();
14228        drop(receiver);
14229        // SAFETY: this variable name is unique to this test across the test binary
14230        unsafe { std::env::set_var("BLOCKCHAIN_TEST_BALANCE_PUBLICATION", TEST_PRIVATE_KEY) };
14231
14232        let error = client.connect().await.unwrap_err();
14233
14234        assert!(
14235            error.to_string().contains("Failed to send account state"),
14236            "was: {error}"
14237        );
14238        assert!(!client.is_connected());
14239        assert!(client.signer.is_none());
14240        assert_eq!(
14241            client.wallet_balance.lock().as_account_balances().unwrap(),
14242            retained
14243        );
14244    }
14245
14246    #[allow(unsafe_code)] // env-var mutation in tests; unique var names avoid cross-test races
14247    #[tokio::test]
14248    async fn connect_and_repeated_query_publish_wallet_account_state() {
14249        let state = execution_rpc_state()
14250            .with_response_sequence("eth_call", &[CALL_BALANCE_WETH, CALL_BALANCE_USDC]);
14251        let (mut client, state, cache) =
14252            client_with_token_mock_rpc(state, "BLOCKCHAIN_TEST_ACCOUNT_STATE").await;
14253        let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
14254        replace_exec_event_sender(sender);
14255        client.start().unwrap();
14256        // SAFETY: this variable name is unique to this test across the test binary
14257        unsafe { std::env::set_var("BLOCKCHAIN_TEST_ACCOUNT_STATE", TEST_PRIVATE_KEY) };
14258
14259        client.connect().await.unwrap();
14260
14261        let ExecutionEvent::Account(connected) = receiver.try_recv().unwrap() else {
14262            panic!("expected account state event")
14263        };
14264        assert_eq!(connected.account_id, AccountId::from("BLOCKCHAIN-001"));
14265        assert_eq!(connected.account_type, AccountType::Wallet);
14266        assert_eq!(connected.base_currency, None);
14267        assert_eq!(connected.balances.len(), 3);
14268        assert!(connected.margins.is_empty());
14269        assert!(connected.is_reported);
14270        cache.borrow_mut().update_account_state(&connected).unwrap();
14271
14272        let account = client.get_account().unwrap();
14273        assert!(matches!(account, AccountAny::Wallet(_)));
14274        assert_eq!(account.id(), AccountId::from("BLOCKCHAIN-001"));
14275        assert_eq!(account.last_event(), Some(connected.clone()));
14276
14277        let requests_before = state.recorded_requests().len();
14278        let query = || {
14279            QueryAccount::new(
14280                TraderId::from("TRADER-001"),
14281                Some(ClientId::from("BLOCKCHAIN-001")),
14282                AccountId::from("BLOCKCHAIN-001"),
14283                UUID4::new(),
14284                UnixNanos::default(),
14285                None,
14286                None,
14287            )
14288        };
14289        client.query_account(query()).unwrap();
14290        client.query_account(query()).unwrap();
14291
14292        let ExecutionEvent::Account(first_query) = receiver.try_recv().unwrap() else {
14293            panic!("expected first query account state event")
14294        };
14295        let ExecutionEvent::Account(second_query) = receiver.try_recv().unwrap() else {
14296            panic!("expected second query account state event")
14297        };
14298        assert!(connected.has_same_balances_and_margins(&first_query));
14299        assert!(first_query.has_same_balances_and_margins(&second_query));
14300        assert_eq!(first_query.account_id, connected.account_id);
14301        assert_eq!(first_query.account_type, connected.account_type);
14302        assert_eq!(first_query.base_currency, connected.base_currency);
14303        assert_eq!(first_query.is_reported, connected.is_reported);
14304        assert_ne!(first_query.event_id, second_query.event_id);
14305        assert_eq!(state.recorded_requests().len(), requests_before);
14306
14307        client.stop().unwrap();
14308        assert!(client.core.is_stopped());
14309        assert!(!client.is_connected());
14310        assert!(client.signer.is_none());
14311    }
14312
14313    #[tokio::test]
14314    async fn connect_rejects_on_chain_mismatch() {
14315        let state = ready_rpc_state().with_response("eth_chainId", CHAIN_ID_ETHEREUM);
14316        let (mut client, _) = client_with_mock_rpc(state).await;
14317
14318        let error = client.connect().await.unwrap_err();
14319
14320        assert!(
14321            error
14322                .to_string()
14323                .contains("Blockchain chain ID verification disagreed"),
14324            "was: {error}"
14325        );
14326    }
14327
14328    #[allow(unsafe_code)] // env-var mutation in tests; unique var names avoid cross-test races
14329    #[tokio::test]
14330    async fn connect_rejects_on_signer_wallet_mismatch() {
14331        let addr = start_mock_rpc_server(ready_rpc_state()).await;
14332        let config = test_config_with_signer_env(
14333            format!("http://{addr}"),
14334            "BLOCKCHAIN_TEST_PRIVATE_KEY_MISMATCH",
14335        );
14336        let mut client = test_client_from_config(config, test_pool());
14337        // Anvil development key #1 derives a different address than the configured wallet
14338        // SAFETY: this variable name is unique to this test across the test binary
14339        unsafe {
14340            std::env::set_var(
14341                "BLOCKCHAIN_TEST_PRIVATE_KEY_MISMATCH",
14342                "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d",
14343            )
14344        };
14345
14346        let error = client.connect().await.unwrap_err();
14347
14348        assert!(
14349            error
14350                .to_string()
14351                .contains("does not match configured wallet address"),
14352            "was: {error}"
14353        );
14354    }
14355
14356    #[allow(unsafe_code)] // env-var mutation in tests; unique var names avoid cross-test races
14357    #[tokio::test]
14358    async fn connect_rejects_missing_trace_capability_before_signer_load() {
14359        let state = ready_rpc_state().with_parameter_response(
14360            "debug_traceTransaction",
14361            &B256::ZERO.to_string(),
14362            RPC_METHOD_NOT_FOUND,
14363        );
14364        let addr = start_mock_rpc_server(state.clone()).await;
14365        let config = test_config_with_signer_env(
14366            format!("http://{addr}"),
14367            "BLOCKCHAIN_TEST_TRACE_CAPABILITY",
14368        );
14369        let mut client = test_client_from_config(config, test_pool());
14370        // SAFETY: this variable name is unique to this test across the test binary
14371        unsafe { std::env::set_var("BLOCKCHAIN_TEST_TRACE_CAPABILITY", TEST_PRIVATE_KEY) };
14372
14373        let error = client.connect().await.unwrap_err();
14374
14375        assert!(
14376            error
14377                .to_string()
14378                .contains("Blockchain call trace capability verification is locally invalid"),
14379            "was: {error}"
14380        );
14381        assert!(client.signer.is_none());
14382        assert!(
14383            state
14384                .recorded_requests()
14385                .iter()
14386                .all(|request| request["method"] != "eth_getBalance")
14387        );
14388    }
14389
14390    #[allow(unsafe_code)] // env-var mutation in tests; unique var names avoid cross-test races
14391    #[tokio::test]
14392    async fn connect_initializes_signer_from_env() {
14393        let addr = start_mock_rpc_server(ready_rpc_state()).await;
14394        let config =
14395            test_config_with_signer_env(format!("http://{addr}"), "BLOCKCHAIN_TEST_PRIVATE_KEY_OK");
14396        let mut client = test_client_from_config(config, test_pool());
14397        let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel();
14398        replace_exec_event_sender(sender);
14399        client.start().unwrap();
14400        // SAFETY: this variable name is unique to this test across the test binary
14401        unsafe { std::env::set_var("BLOCKCHAIN_TEST_PRIVATE_KEY_OK", TEST_PRIVATE_KEY) };
14402
14403        client.connect().await.unwrap();
14404
14405        let signer = client.signer.as_ref().unwrap();
14406        assert_eq!(
14407            signer.address(),
14408            address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266")
14409        );
14410    }
14411
14412    #[allow(unsafe_code)] // env-var mutation in tests; unique var names avoid cross-test races
14413    #[tokio::test]
14414    async fn reconnect_resumes_at_the_durable_finalized_tip() {
14415        let Some((admin_pool, pg_config)) =
14416            connect_test_postgres("verification ledger resume").await
14417        else {
14418            return;
14419        };
14420        let schema = format!("verification_ledger_resume_{}", std::process::id());
14421        setup_execution_schema(&admin_pool, &schema).await;
14422        let options: sqlx::postgres::PgConnectOptions = pg_config.into();
14423        let options = options.options([("search_path", schema.clone())]);
14424        let database = connect_test_database(options).await.unwrap();
14425        database
14426            .ensure_execution_transaction_schema()
14427            .await
14428            .unwrap();
14429
14430        let state = ready_rpc_state();
14431        let addr = start_mock_rpc_server(state.clone()).await;
14432        let config = test_config_with_signer_env(
14433            format!("http://{addr}"),
14434            "BLOCKCHAIN_TEST_VERIFICATION_RESUME",
14435        );
14436        let mut client = test_client_from_config(config, test_pool());
14437        client.cache.database = Some(database);
14438        protect_test_storage(&mut client, &schema).await;
14439        let finalized_headers = [
14440            ExecutionVerifiedHeader {
14441                number: FIXTURE_BLOCK,
14442                hash: FIXTURE_BLOCK_HASH.to_string(),
14443                parent_hash: "0x0000000000000000000000000000000000000000000000000000000000000001"
14444                    .to_string(),
14445                timestamp: FIXTURE_BLOCK_TIMESTAMP,
14446                base_fee_per_gas: Some(100_000_000),
14447            },
14448            ExecutionVerifiedHeader {
14449                number: FIXTURE_BLOCK + 1,
14450                hash: B256::from([0x22; 32]).to_string(),
14451                parent_hash: FIXTURE_BLOCK_HASH.to_string(),
14452                timestamp: FIXTURE_BLOCK_TIMESTAMP + 1,
14453                base_fee_per_gas: Some(100_000_000),
14454            },
14455            ExecutionVerifiedHeader {
14456                number: FIXTURE_BLOCK + 2,
14457                hash: B256::from([0x33; 32]).to_string(),
14458                parent_hash: B256::from([0x22; 32]).to_string(),
14459                timestamp: FIXTURE_BLOCK_TIMESTAMP + 2,
14460                base_fee_per_gas: Some(100_000_000),
14461            },
14462        ];
14463        initialize_test_verification_ledger_with_headers(&client, &finalized_headers).await;
14464        let verification = client.config.verification.as_ref().unwrap();
14465        let position = client
14466            .cache
14467            .database
14468            .as_ref()
14469            .unwrap()
14470            .load_execution_verification_position(
14471                42_161,
14472                WALLET,
14473                &verification.manifest_version,
14474                &verification.manifest_digest,
14475            )
14476            .await
14477            .unwrap()
14478            .unwrap();
14479        let resume = client
14480            .cache
14481            .database
14482            .as_ref()
14483            .unwrap()
14484            .load_execution_verification_resume(
14485                42_161,
14486                WALLET,
14487                &verification.manifest_version,
14488                &verification.manifest_digest,
14489            )
14490            .await
14491            .unwrap()
14492            .unwrap();
14493        assert_eq!(position.next_canonical_nonce, 7);
14494        assert_eq!(position.revision, 0);
14495        assert_eq!(position.finalized_tip, finalized_headers[2]);
14496        assert_eq!(resume.next_canonical_nonce, 7);
14497        assert_eq!(resume.revision, 0);
14498        assert_eq!(resume.finalized_headers, finalized_headers);
14499
14500        let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel();
14501        replace_exec_event_sender(sender);
14502        client.start().unwrap();
14503        // SAFETY: this variable name is unique to this test across the test binary
14504        unsafe { std::env::set_var("BLOCKCHAIN_TEST_VERIFICATION_RESUME", TEST_PRIVATE_KEY) };
14505
14506        client.connect().await.unwrap();
14507
14508        let skipped_height = format!("0x{:x}", FIXTURE_BLOCK + 1);
14509        assert!(client.is_connected());
14510        assert!(state.recorded_requests().iter().all(|request| {
14511            request["method"] != "eth_getBlockByNumber"
14512                || request["params"][0].as_str() != Some(&skipped_height)
14513        }));
14514        let header_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
14515            "SELECT COUNT(*) FROM {schema}.execution_verified_finalized_header"
14516        )))
14517        .fetch_one(&admin_pool)
14518        .await
14519        .unwrap();
14520        assert_eq!(header_count, 3);
14521
14522        drop(client);
14523        drop_execution_schema(&admin_pool, &schema).await;
14524    }
14525
14526    #[tokio::test]
14527    async fn disconnect_revokes_signer_and_blocks_execution() {
14528        let (mut client, state) = client_with_mock_rpc(ready_rpc_state()).await;
14529        client.core.set_connected();
14530        client.signer = Some(Arc::new(
14531            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
14532        ));
14533
14534        client.disconnect().await.unwrap();
14535        let error = client.wrap(U256::from(1_000u64)).await.unwrap_err();
14536
14537        assert!(!client.is_connected());
14538        assert!(client.signer.is_none());
14539        assert!(
14540            error.to_string().contains("is not connected"),
14541            "was: {error}"
14542        );
14543        assert!(state.recorded_requests().is_empty());
14544    }
14545
14546    #[allow(unsafe_code)] // env-var mutation in tests; unique var names avoid cross-test races
14547    #[tokio::test]
14548    async fn connect_releases_stale_preparing_claim_after_tasks_finish() {
14549        let addr = start_mock_rpc_server(ready_rpc_state()).await;
14550        let config = test_config_with_signer_env(
14551            format!("http://{addr}"),
14552            "BLOCKCHAIN_TEST_RECONNECT_CLAIM",
14553        );
14554        let mut client = test_client_from_config(config, test_pool());
14555        let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel();
14556        replace_exec_event_sender(sender);
14557        client.start().unwrap();
14558        // SAFETY: this variable name is unique to this test across the test binary
14559        unsafe { std::env::set_var("BLOCKCHAIN_TEST_RECONNECT_CLAIM", TEST_PRIVATE_KEY) };
14560        *client.in_flight.lock() = Some(InFlightSlot::Preparing(TransactionPurpose::Swap));
14561        client
14562            .pending_tasks
14563            .spawn(async {})
14564            .expect("stale task spawn");
14565
14566        client.connect().await.unwrap();
14567
14568        assert!(client.in_flight.lock().is_none());
14569    }
14570
14571    #[allow(unsafe_code)] // env-var mutation in tests; unique var names avoid cross-test races
14572    #[tokio::test]
14573    async fn reconnect_after_aborted_submission_releases_preparing_claim() {
14574        // The swap task blocks in its first RPC; disconnect aborts it mid-preparation, and the
14575        // following connect awaits termination and releases the stale claim
14576        let state = ready_rpc_state().with_sleep("eth_getBlockByNumber", Duration::from_secs(30));
14577        let Some((admin_pool, schema, mut client, state, _)) =
14578            swap_client_with_database("execution_reconnect_claim_test", state).await
14579        else {
14580            return;
14581        };
14582        let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel();
14583        replace_exec_event_sender(sender);
14584        client.start().unwrap();
14585        // SAFETY: this variable name is unique to this test across the test binary
14586        unsafe { std::env::set_var("BLOCKCHAIN_TEST_PRIVATE_KEY", TEST_PRIVATE_KEY) };
14587
14588        let order = test_market_sell_order(test_pool().instrument_id);
14589        client.submit_order(submit_order_cmd(&order)).unwrap();
14590        await_recorded_requests(&state, "eth_getBlockByNumber", 1).await;
14591        assert!(client.in_flight.lock().is_some());
14592
14593        client.disconnect().await.unwrap();
14594        let ready_addr = start_mock_rpc_server(ready_rpc_state()).await;
14595        let ready = test_client(format!("http://{ready_addr}"));
14596        client.http_rpc_client = ready.http_rpc_client;
14597        client.verification = ready.verification;
14598        client.connect().await.unwrap();
14599
14600        assert!(client.in_flight.lock().is_none());
14601
14602        drop_execution_schema(&admin_pool, &schema).await;
14603    }
14604
14605    #[tokio::test]
14606    async fn poll_for_receipt_returns_none_after_exhaustion() {
14607        let state = ready_rpc_state().with_response("eth_getTransactionReceipt", RECEIPT_NULL);
14608        let (client, state) = client_with_mock_rpc(state).await;
14609
14610        let receipt = poll_for_receipt(&client.http_rpc_client, &B256::ZERO, 3, Duration::ZERO)
14611            .await
14612            .unwrap();
14613
14614        assert!(receipt.is_none());
14615        let requests = state.recorded_requests();
14616        assert_eq!(requests.len(), 3);
14617    }
14618
14619    #[tokio::test]
14620    async fn poll_for_receipt_returns_last_error_when_every_poll_fails() {
14621        let state =
14622            ready_rpc_state().with_response("eth_getTransactionReceipt", RPC_METHOD_NOT_FOUND);
14623        let (client, state) = client_with_mock_rpc(state).await;
14624
14625        let error = poll_for_receipt(&client.http_rpc_client, &B256::ZERO, 3, Duration::ZERO)
14626            .await
14627            .unwrap_err();
14628
14629        assert_eq!(
14630            error.to_string(),
14631            "eth_getTransactionReceipt RPC error -32601"
14632        );
14633        let requests = state.recorded_requests();
14634        assert_eq!(requests.len(), 3);
14635    }
14636
14637    #[tokio::test]
14638    async fn poll_for_receipt_returns_none_after_pending_then_errors() {
14639        let state = ready_rpc_state().with_response_sequence(
14640            "eth_getTransactionReceipt",
14641            &[RECEIPT_NULL, RPC_METHOD_NOT_FOUND, RPC_METHOD_NOT_FOUND],
14642        );
14643        let (client, state) = client_with_mock_rpc(state).await;
14644
14645        let receipt = poll_for_receipt(&client.http_rpc_client, &B256::ZERO, 3, Duration::ZERO)
14646            .await
14647            .unwrap();
14648
14649        assert!(receipt.is_none());
14650        let requests = state.recorded_requests();
14651        assert_eq!(requests.len(), 3);
14652    }
14653
14654    #[tokio::test]
14655    async fn cancellation_during_persistence_keeps_in_flight_slot() {
14656        let Some((admin_pool, pg_config)) = connect_test_postgres("persistence cancellation").await
14657        else {
14658            return;
14659        };
14660
14661        let schema = format!("execution_persist_cancel_test_{}", std::process::id());
14662        setup_execution_schema(&admin_pool, &schema).await;
14663
14664        let db_options: sqlx::postgres::PgConnectOptions = pg_config.into();
14665        let db_options = db_options.options([("search_path", schema.clone())]);
14666        let database = connect_test_database(db_options).await.unwrap();
14667
14668        let state = signing_rpc_state();
14669        let addr = start_mock_rpc_server(state.clone()).await;
14670
14671        let mut client = test_client(format!("http://{addr}"));
14672        client.cache.database = Some(database);
14673        // Mirror the connect-time migration: tests create the pre-submission table shape
14674        client
14675            .cache
14676            .ensure_execution_transaction_schema()
14677            .await
14678            .unwrap();
14679        protect_test_storage(&mut client, &schema).await;
14680        initialize_test_verification_ledger(&client).await;
14681        client.signer = Some(Arc::new(
14682            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
14683        ));
14684        client.core.set_connected();
14685
14686        let advisory_lock = i64::from(std::process::id());
14687
14688        for statement in [
14689            format!(
14690                "CREATE FUNCTION {schema}.block_execution_hash_insert() RETURNS trigger \
14691                 LANGUAGE plpgsql AS 'BEGIN PERFORM pg_advisory_xact_lock({advisory_lock}); \
14692                 RETURN NEW; END'"
14693            ),
14694            format!(
14695                "CREATE TRIGGER block_execution_hash_insert BEFORE INSERT ON \
14696                 {schema}.execution_transaction_hash FOR EACH ROW EXECUTE FUNCTION \
14697                 {schema}.block_execution_hash_insert()"
14698            ),
14699        ] {
14700            sqlx::query(sqlx::AssertSqlSafe(statement))
14701                .execute(&admin_pool)
14702                .await
14703                .unwrap();
14704        }
14705        let mut lock_transaction = admin_pool.begin().await.unwrap();
14706        sqlx::query("SELECT pg_advisory_xact_lock($1)")
14707            .bind(advisory_lock)
14708            .execute(&mut *lock_transaction)
14709            .await
14710            .unwrap();
14711
14712        let value = U256::from(1_000_000_000_000_000u64);
14713        let in_flight = Arc::clone(&client.in_flight);
14714        let mut wrap = Box::pin(client.wrap(value));
14715        tokio::time::timeout(Duration::from_secs(2), async {
14716            loop {
14717                tokio::select! {
14718                    result = &mut wrap => {
14719                        panic!("persistence completed while the table was locked: {result:?}")
14720                    }
14721                    () = tokio::time::sleep(Duration::from_millis(1)) => {}
14722                }
14723
14724                if matches!(*in_flight.lock(), Some(InFlightSlot::AwaitingFinality(_))) {
14725                    break;
14726                }
14727            }
14728        })
14729        .await
14730        .unwrap();
14731        drop(wrap);
14732        lock_transaction.rollback().await.unwrap();
14733
14734        let slot = *client.in_flight.lock();
14735        let second_error = client
14736            .wrap(U256::from(2_000_000_000_000_000u64))
14737            .await
14738            .unwrap_err();
14739        let broadcasts = state
14740            .recorded_requests()
14741            .into_iter()
14742            .filter(|request| request["method"] == "eth_sendRawTransaction")
14743            .count();
14744        let status: String = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
14745            "SELECT status FROM {schema}.execution_intent"
14746        )))
14747        .fetch_one(&admin_pool)
14748        .await
14749        .unwrap();
14750
14751        assert!(matches!(slot, Some(InFlightSlot::AwaitingFinality(_))));
14752        assert_eq!(status, "prepared");
14753        assert!(
14754            second_error.to_string().contains("awaiting finality"),
14755            "was: {second_error}"
14756        );
14757        assert_eq!(broadcasts, 0);
14758
14759        drop_execution_schema(&admin_pool, &schema).await;
14760    }
14761
14762    #[tokio::test]
14763    async fn reservation_failure_releases_preparing_slot() {
14764        let state = signing_rpc_state();
14765        let Some((admin_pool, schema, mut client, state)) =
14766            execution_client_with_database("execution_reservation_fail_test", state).await
14767        else {
14768            return;
14769        };
14770        sqlx::query(sqlx::AssertSqlSafe(format!(
14771            "DROP TABLE {schema}.execution_intent CASCADE"
14772        )))
14773        .execute(&admin_pool)
14774        .await
14775        .unwrap();
14776
14777        let error = client
14778            .wrap(U256::from(1_000_000_000_000_000u64))
14779            .await
14780            .unwrap_err();
14781        let retry_error = client
14782            .wrap(U256::from(2_000_000_000_000_000u64))
14783            .await
14784            .unwrap_err();
14785        let broadcasts = state
14786            .recorded_requests()
14787            .into_iter()
14788            .filter(|request| request["method"] == "eth_sendRawTransaction")
14789            .count();
14790
14791        assert_eq!(
14792            error.to_string(),
14793            "Execution intent reservation failed before commit"
14794        );
14795        assert!(reservation_failure_proven_not_committed(&error));
14796        assert_eq!(
14797            retry_error.to_string(),
14798            "Execution intent reservation failed before commit"
14799        );
14800        assert_eq!(broadcasts, 0);
14801        assert!(client.in_flight.lock().is_none());
14802
14803        drop_execution_schema(&admin_pool, &schema).await;
14804    }
14805
14806    #[tokio::test]
14807    async fn reservation_commit_failure_keeps_preparing_slot() {
14808        let state = signing_rpc_state();
14809        let Some((admin_pool, schema, mut client, state)) =
14810            execution_client_with_database("execution_reservation_commit_fail_test", state).await
14811        else {
14812            return;
14813        };
14814        install_reservation_commit_rejection(&admin_pool, &schema).await;
14815
14816        let error = client
14817            .wrap(U256::from(1_000_000_000_000_000u64))
14818            .await
14819            .unwrap_err();
14820        let slot = *client.in_flight.lock();
14821        let second_error = client
14822            .wrap(U256::from(2_000_000_000_000_000u64))
14823            .await
14824            .unwrap_err();
14825        let requests = state.recorded_requests();
14826        let intent_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
14827            "SELECT COUNT(*) FROM {schema}.execution_intent"
14828        )))
14829        .fetch_one(&admin_pool)
14830        .await
14831        .unwrap();
14832        let signed_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
14833            "SELECT COUNT(*) FROM {schema}.execution_transaction_hash"
14834        )))
14835        .fetch_one(&admin_pool)
14836        .await
14837        .unwrap();
14838
14839        assert_eq!(
14840            error.to_string(),
14841            "Execution intent reservation commit outcome is unknown; reconciliation is required"
14842        );
14843        assert!(!reservation_failure_proven_not_committed(&error));
14844        assert!(matches!(
14845            slot,
14846            Some(InFlightSlot::Preparing(TransactionPurpose::Wrap))
14847        ));
14848        assert!(
14849            second_error.to_string().contains("being prepared"),
14850            "was: {second_error}"
14851        );
14852        assert_eq!(intent_count, 0);
14853        assert_eq!(signed_count, 0);
14854        assert!(
14855            requests
14856                .iter()
14857                .all(|request| request["method"] != "eth_getTransactionCount")
14858        );
14859        assert!(
14860            requests
14861                .iter()
14862                .all(|request| request["method"] != "eth_sendRawTransaction")
14863        );
14864
14865        drop_execution_schema(&admin_pool, &schema).await;
14866    }
14867
14868    #[tokio::test]
14869    async fn persistence_failure_keeps_unbroadcast_slot() {
14870        let state = signing_rpc_state();
14871        let Some((admin_pool, schema, mut client, state)) =
14872            execution_client_with_database("execution_persist_fail_test", state).await
14873        else {
14874            return;
14875        };
14876        sqlx::query(sqlx::AssertSqlSafe(format!(
14877            "ALTER TABLE {schema}.execution_transaction_hash \
14878             ADD CONSTRAINT execution_hash_reject CHECK (FALSE)"
14879        )))
14880        .execute(&admin_pool)
14881        .await
14882        .unwrap();
14883
14884        let value = U256::from(1_000_000_000_000_000u64);
14885        let error = client.wrap(value).await.unwrap_err();
14886        let in_flight = awaiting_in_flight(&client);
14887        let second_error = client
14888            .wrap(U256::from(2_000_000_000_000_000u64))
14889            .await
14890            .unwrap_err();
14891        let broadcasts = state
14892            .recorded_requests()
14893            .into_iter()
14894            .filter(|request| request["method"] == "eth_sendRawTransaction")
14895            .count();
14896        let expected_hash = expected_wrap_tx_hash(value).await;
14897
14898        let error_message = error.to_string();
14899        assert!(error_message.starts_with(&format!(
14900            "Failed to persist transaction {expected_hash}: Failed to persist signed transaction"
14901        )), "was: {error_message}");
14902        assert!(
14903            error_message.ends_with("the in-flight slot stays occupied"),
14904            "was: {error_message}"
14905        );
14906        assert_eq!(in_flight.nonce, 7);
14907        assert_eq!(in_flight.tx_hash, expected_hash);
14908        assert_eq!(in_flight.purpose, TransactionPurpose::Wrap);
14909        assert!(
14910            second_error.to_string().contains("still awaiting finality"),
14911            "was: {second_error}"
14912        );
14913        assert_eq!(broadcasts, 0);
14914
14915        drop_execution_schema(&admin_pool, &schema).await;
14916    }
14917
14918    #[tokio::test]
14919    async fn cancellation_after_dispatch_keeps_record_and_in_flight_slot() {
14920        let state = signing_rpc_state().with_sleep(
14921            "eth_sendRawTransaction",
14922            Duration::from_secs(EXECUTION_RPC_TIMEOUT_SECS + 2),
14923        );
14924        let Some((admin_pool, schema, mut client, state)) =
14925            execution_client_with_database("execution_cancel_test", state).await
14926        else {
14927            return;
14928        };
14929
14930        let mut wrap = Box::pin(client.wrap(U256::from(1_000_000_000_000_000u64)));
14931        tokio::select! {
14932            result = &mut wrap => panic!("broadcast completed before cancellation: {result:?}"),
14933            () = await_recorded_requests(&state, "eth_sendRawTransaction", 1) => {}
14934        }
14935        drop(wrap);
14936
14937        let in_flight = awaiting_in_flight(&client);
14938        let error = client
14939            .wrap(U256::from(2_000_000_000_000_000u64))
14940            .await
14941            .unwrap_err();
14942        let record = client
14943            .cache
14944            .get_execution_transaction(42161, &in_flight.tx_hash.to_string())
14945            .await
14946            .unwrap()
14947            .unwrap();
14948        let broadcasts = state
14949            .recorded_requests()
14950            .into_iter()
14951            .filter(|request| request["method"] == "eth_sendRawTransaction")
14952            .count();
14953
14954        assert!(
14955            error.to_string().contains("still awaiting finality"),
14956            "was: {error}"
14957        );
14958        assert_eq!(record.nonce, 7);
14959        assert_eq!(record.purpose, "wrap");
14960        assert_eq!(record.status, "broadcast");
14961        assert_eq!(broadcasts, 1);
14962
14963        drop_execution_schema(&admin_pool, &schema).await;
14964    }
14965
14966    #[tokio::test]
14967    async fn cancellation_during_receipt_polling_keeps_record_and_in_flight_slot() {
14968        let state = signing_rpc_state()
14969            .with_send_raw_transaction_echo()
14970            .with_response("eth_getTransactionReceipt", RECEIPT_SUCCESS)
14971            .with_sleep(
14972                "eth_getTransactionReceipt",
14973                Duration::from_secs(EXECUTION_RPC_TIMEOUT_SECS + 2),
14974            );
14975        let Some((admin_pool, schema, mut client, state)) =
14976            execution_client_with_database("execution_receipt_cancel_test", state).await
14977        else {
14978            return;
14979        };
14980
14981        let mut wrap = Box::pin(client.wrap(U256::from(1_000_000_000_000_000u64)));
14982        tokio::select! {
14983            result = &mut wrap => panic!("receipt polling completed before cancellation: {result:?}"),
14984            () = await_recorded_requests(&state, "eth_getTransactionReceipt", 3) => {}
14985        }
14986        drop(wrap);
14987
14988        let in_flight = awaiting_in_flight(&client);
14989        let second_error = client
14990            .wrap(U256::from(2_000_000_000_000_000u64))
14991            .await
14992            .unwrap_err();
14993        let record = client
14994            .cache
14995            .get_execution_transaction(42161, &in_flight.tx_hash.to_string())
14996            .await
14997            .unwrap()
14998            .unwrap();
14999        let requests = state.recorded_requests();
15000
15001        assert_eq!(in_flight.nonce, 7);
15002        assert_eq!(in_flight.purpose, TransactionPurpose::Wrap);
15003        assert!(
15004            second_error.to_string().contains("still awaiting finality"),
15005            "was: {second_error}"
15006        );
15007        assert_eq!(record.nonce, 7);
15008        assert_eq!(record.transaction_hash, in_flight.tx_hash.to_string());
15009        assert_eq!(record.purpose, "wrap");
15010        assert_eq!(record.status, "broadcast");
15011        assert_eq!(
15012            requests
15013                .iter()
15014                .filter(|request| request["method"] == "eth_sendRawTransaction")
15015                .count(),
15016            1
15017        );
15018        assert_eq!(
15019            requests
15020                .iter()
15021                .filter(|request| request["method"] == "eth_getTransactionReceipt")
15022                .count(),
15023            3
15024        );
15025
15026        drop_execution_schema(&admin_pool, &schema).await;
15027    }
15028
15029    #[tokio::test]
15030    async fn rejected_broadcast_stays_dropped_and_occupied() {
15031        let state = signing_rpc_state()
15032            .with_response("eth_sendRawTransaction", SEND_RAW_TRANSACTION_REJECTED)
15033            .with_response("eth_getTransactionReceipt", RECEIPT_NULL);
15034        let Some((admin_pool, schema, mut client, state)) =
15035            execution_client_with_database("execution_rejected_test", state).await
15036        else {
15037            return;
15038        };
15039
15040        let error = client
15041            .wrap(U256::from(1_000_000_000_000_000u64))
15042            .await
15043            .unwrap_err();
15044        let (purpose, status): (String, String) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
15045            "SELECT purpose, status FROM {schema}.execution_intent"
15046        )))
15047        .fetch_one(&admin_pool)
15048        .await
15049        .unwrap();
15050        let broadcasts = state
15051            .recorded_requests()
15052            .into_iter()
15053            .filter(|request| request["method"] == "eth_sendRawTransaction")
15054            .count();
15055
15056        assert!(
15057            error.to_string().contains("Timed out awaiting finality"),
15058            "was: {error}"
15059        );
15060        assert!(client.in_flight.lock().is_some());
15061        assert_eq!(purpose, "wrap");
15062        assert_eq!(status, "dropped");
15063        assert_eq!(broadcasts, 1);
15064
15065        drop_execution_schema(&admin_pool, &schema).await;
15066    }
15067
15068    #[tokio::test]
15069    async fn legacy_table_loss_does_not_release_rejected_broadcast() {
15070        let state = signing_rpc_state()
15071            .with_response("eth_sendRawTransaction", SEND_RAW_TRANSACTION_REJECTED)
15072            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
15073            .with_sleep("eth_sendRawTransaction", Duration::from_secs(1));
15074        let Some((admin_pool, schema, mut client, state)) =
15075            execution_client_with_database("execution_rejected_update_test", state).await
15076        else {
15077            return;
15078        };
15079
15080        let mut wrap = Box::pin(client.wrap(U256::from(1_000_000_000_000_000u64)));
15081        tokio::select! {
15082            result = &mut wrap => panic!("broadcast completed before database failure: {result:?}"),
15083            () = await_recorded_requests(&state, "eth_sendRawTransaction", 1) => {}
15084        }
15085        sqlx::query(sqlx::AssertSqlSafe(format!(
15086            "DROP TABLE {schema}.execution_transaction"
15087        )))
15088        .execute(&admin_pool)
15089        .await
15090        .unwrap();
15091
15092        let error = wrap.await.unwrap_err();
15093
15094        assert!(
15095            error.to_string().contains("Timed out awaiting finality"),
15096            "was: {error}"
15097        );
15098        let in_flight = awaiting_in_flight(&client);
15099        assert_eq!(in_flight.nonce, 7);
15100        assert_eq!(in_flight.purpose, TransactionPurpose::Wrap);
15101
15102        drop_execution_schema(&admin_pool, &schema).await;
15103    }
15104
15105    #[tokio::test]
15106    async fn finalized_receipt_keeps_slot_when_status_update_fails() {
15107        let expected_hash = expected_wrap_tx_hash(U256::from(1_000_000_000_000_000u64)).await;
15108        let block = finalized_wrap_block(expected_hash);
15109        let receipt = receipt_with_transaction_hash(RECEIPT_SUCCESS, expected_hash);
15110        let state = with_finalized_identity(
15111            signing_rpc_state()
15112                .with_send_raw_transaction_echo()
15113                .with_response("eth_getTransactionReceipt", &receipt)
15114                .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block)
15115                .with_call_response_sequence(
15116                    BALANCE_OF_SELECTOR,
15117                    &[
15118                        CALL_BALANCE,
15119                        CALL_BALANCE,
15120                        CALL_BALANCE,
15121                        CALL_BALANCE,
15122                        CALL_BALANCE,
15123                        CALL_BALANCE,
15124                        CALL_BALANCE_AFTER_WRAP,
15125                        CALL_BALANCE_AFTER_WRAP,
15126                        CALL_BALANCE_AFTER_WRAP,
15127                    ],
15128                ),
15129            &block,
15130            &receipt,
15131        );
15132        let Some((admin_pool, schema, mut client, state)) =
15133            execution_client_with_database("execution_finalized_update_test", state).await
15134        else {
15135            return;
15136        };
15137
15138        for statement in [
15139            format!(
15140                "CREATE FUNCTION {schema}.reject_finalized_status() RETURNS trigger \
15141                 LANGUAGE plpgsql AS 'BEGIN IF NEW.status = ''finalized'' THEN \
15142                 RAISE EXCEPTION ''test finalized status rejection''; END IF; \
15143                 RETURN NEW; END'"
15144            ),
15145            format!(
15146                "CREATE TRIGGER reject_finalized_status BEFORE UPDATE ON \
15147                 {schema}.execution_transaction_hash FOR EACH ROW \
15148                 EXECUTE FUNCTION {schema}.reject_finalized_status()"
15149            ),
15150        ] {
15151            sqlx::query(sqlx::AssertSqlSafe(statement))
15152                .execute(&admin_pool)
15153                .await
15154                .unwrap();
15155        }
15156
15157        let error = client
15158            .wrap(U256::from(1_000_000_000_000_000u64))
15159            .await
15160            .unwrap_err();
15161        let in_flight = awaiting_in_flight(&client);
15162        let requests = state.recorded_requests();
15163
15164        assert!(
15165            error
15166                .to_string()
15167                .contains("test finalized status rejection"),
15168            "was: {error}"
15169        );
15170        assert_eq!(in_flight.nonce, 7);
15171        assert_eq!(in_flight.purpose, TransactionPurpose::Wrap);
15172        assert_eq!(
15173            requests
15174                .iter()
15175                .filter(|request| request["method"] == "eth_sendRawTransaction")
15176                .count(),
15177            1
15178        );
15179        assert_eq!(
15180            requests
15181                .iter()
15182                .filter(|request| request["method"] == "eth_getTransactionReceipt")
15183                .count(),
15184            3
15185        );
15186
15187        drop_execution_schema(&admin_pool, &schema).await;
15188    }
15189
15190    #[tokio::test]
15191    async fn broadcast_timeout_marks_dropped_and_keeps_ownership() {
15192        let state = signing_rpc_state()
15193            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
15194            .with_sleep(
15195                "eth_sendRawTransaction",
15196                Duration::from_secs(EXECUTION_RPC_TIMEOUT_SECS + 2),
15197            );
15198        let Some((admin_pool, schema, mut client, _)) =
15199            execution_client_with_database("execution_timeout_test", state).await
15200        else {
15201            return;
15202        };
15203
15204        let error = client
15205            .wrap(U256::from(1_000_000_000_000_000u64))
15206            .await
15207            .unwrap_err();
15208
15209        assert!(
15210            error.to_string().contains("Timed out awaiting finality"),
15211            "was: {error}"
15212        );
15213        let in_flight = awaiting_in_flight(&client);
15214        assert_eq!(in_flight.purpose, TransactionPurpose::Wrap);
15215        assert_eq!(in_flight.nonce, 7);
15216
15217        // The persisted record stays active for reconciliation instead of rebroadcasting.
15218        let record = client
15219            .cache
15220            .get_execution_transaction(42161, &in_flight.tx_hash.to_string())
15221            .await
15222            .unwrap()
15223            .unwrap();
15224        assert_eq!(record.status, "dropped");
15225
15226        drop_execution_schema(&admin_pool, &schema).await;
15227    }
15228
15229    #[tokio::test]
15230    async fn restart_reconciles_dropped_transaction_without_rebroadcast() {
15231        let initial_state = broadcast_rpc_state()
15232            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
15233            .with_call_response(BALANCE_OF_SELECTOR, CALL_BALANCE);
15234        let Some((admin_pool, schema, mut first_client, _)) =
15235            execution_client_with_database("execution_restart_test", initial_state).await
15236        else {
15237            return;
15238        };
15239        let expected_hash = expected_wrap_tx_hash(U256::from(1_000_000_000_000_000_u64)).await;
15240        let error = first_client
15241            .wrap(U256::from(1_000_000_000_000_000_u64))
15242            .await
15243            .unwrap_err();
15244        assert!(
15245            error.to_string().contains("Timed out awaiting finality"),
15246            "was: {error}"
15247        );
15248        let database = first_client.cache.database.as_ref().unwrap().clone();
15249        let payload_keys = first_client.payload_keys.clone();
15250        drop(first_client);
15251
15252        let block = finalized_wrap_block(expected_hash);
15253        let receipt = receipt_with_transaction_hash(RECEIPT_SUCCESS, expected_hash);
15254        let restart_state = with_finalized_identity(
15255            execution_rpc_state()
15256                .with_response("eth_getTransactionReceipt", &receipt)
15257                .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block)
15258                .with_response_sequence(
15259                    "eth_call",
15260                    &[
15261                        CALL_BALANCE,
15262                        CALL_BALANCE,
15263                        CALL_BALANCE,
15264                        CALL_BALANCE_AFTER_WRAP,
15265                        CALL_BALANCE_AFTER_WRAP,
15266                        CALL_BALANCE_AFTER_WRAP,
15267                    ],
15268                ),
15269            &block,
15270            &receipt,
15271        );
15272        let addr = start_mock_rpc_server(restart_state.clone()).await;
15273        let mut restarted = test_client(format!("http://{addr}"));
15274        restarted.cache.database = Some(database);
15275        restarted.payload_keys = payload_keys;
15276        restarted.signer = Some(Arc::new(
15277            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
15278        ));
15279
15280        restarted.reconcile_unresolved_execution().await.unwrap();
15281        restarted.reconcile_unresolved_execution().await.unwrap();
15282
15283        let record = restarted
15284            .cache
15285            .get_execution_transaction(42161, &expected_hash.to_string())
15286            .await
15287            .unwrap()
15288            .unwrap();
15289        let requests = restart_state.recorded_requests();
15290        assert_eq!(record.status, "finalized");
15291        assert!(restarted.in_flight.lock().is_none());
15292        assert_eq!(
15293            requests
15294                .iter()
15295                .filter(|request| request["method"] == "eth_getTransactionReceipt")
15296                .count(),
15297            6
15298        );
15299        assert_eq!(
15300            requests
15301                .iter()
15302                .filter(|request| {
15303                    request["method"] == "eth_call"
15304                        && request["params"][0]["data"]
15305                            .as_str()
15306                            .is_some_and(|data| data.starts_with(BALANCE_OF_SELECTOR))
15307                })
15308                .count(),
15309            6
15310        );
15311        assert_eq!(
15312            requests
15313                .iter()
15314                .filter(|request| request["method"] == "eth_sendRawTransaction")
15315                .count(),
15316            0
15317        );
15318
15319        drop_execution_schema(&admin_pool, &schema).await;
15320    }
15321
15322    fn migration_test_intent(id: i64) -> ExecutionIntentRow {
15323        ExecutionIntentRow {
15324            id,
15325            schema_version: crate::execution::transaction::EXECUTION_SCHEMA_VERSION,
15326            chain_id: 42_161,
15327            wallet_address: WALLET.to_string(),
15328            nonce: None,
15329            purpose: "wrap".to_string(),
15330            status: "prepared".to_string(),
15331            client_order_id: None,
15332            trader_id: None,
15333            strategy_id: None,
15334            account_id: None,
15335            instrument_id: None,
15336            pool_address: None,
15337            transaction_to: WETH.to_string(),
15338            transaction_input: "0xd0e30db0".to_string(),
15339            transaction_value: "1".to_string(),
15340            amount_in: None,
15341            created_block: FIXTURE_BLOCK,
15342            acknowledgement_emitted: false,
15343            fill_emitted: false,
15344            terminal_emitted: false,
15345            active: true,
15346        }
15347    }
15348
15349    fn migration_nonce_verification() -> Verified<u64> {
15350        Verified {
15351            value: 7,
15352            read: crate::rpc::verification::VerificationRead::TransactionCount,
15353            provider_ids: [
15354                "authoritative".to_string(),
15355                "verifier-a".to_string(),
15356                "verifier-b".to_string(),
15357            ],
15358            normalized_value_digest: keccak256(7_u64.to_be_bytes()),
15359        }
15360    }
15361
15362    fn migration_finalized_header() -> VerifiedBlockHeader {
15363        VerifiedBlockHeader {
15364            number: FIXTURE_BLOCK,
15365            hash: B256::from_str(FIXTURE_BLOCK_HASH).unwrap(),
15366            parent_hash: B256::from_str(
15367                "0x0000000000000000000000000000000000000000000000000000000000000001",
15368            )
15369            .unwrap(),
15370            timestamp: FIXTURE_BLOCK_TIMESTAMP,
15371            base_fee_per_gas: Some(100_000_000),
15372        }
15373    }
15374
15375    #[tokio::test]
15376    async fn verification_migration_recovers_prepared_unassigned_intent() {
15377        let client = test_client("http://127.0.0.1:1".to_string());
15378        let snapshot = ExecutionVerificationMigrationSnapshot {
15379            intents: vec![migration_test_intent(1)],
15380            hashes: Vec::new(),
15381        };
15382        let finalized = migration_finalized_header();
15383        let migration = client
15384            .build_execution_verification_migration(
15385                snapshot,
15386                finalized,
15387                &[finalized],
15388                &migration_nonce_verification(),
15389            )
15390            .await
15391            .unwrap();
15392
15393        assert_eq!(migration.records.len(), 1);
15394        let record = &migration.records[0];
15395        assert_eq!(record.intent_id, 1);
15396        assert_eq!(record.nonce, None);
15397        assert_eq!(record.transaction_hash, None);
15398        assert!(record.recover_prepared);
15399        assert_eq!(record.decisions.len(), 1);
15400    }
15401
15402    #[tokio::test]
15403    async fn verification_migration_rejects_inconsistent_released_history() {
15404        let client = test_client("http://127.0.0.1:1".to_string());
15405        let finalized = migration_finalized_header();
15406        let nonce_verification = migration_nonce_verification();
15407        let mut missing_marker = migration_test_intent(1);
15408        missing_marker.active = false;
15409        missing_marker.status = "finalized".to_string();
15410        missing_marker.nonce = Some(6);
15411        let error = client
15412            .build_execution_verification_migration(
15413                ExecutionVerificationMigrationSnapshot {
15414                    intents: vec![missing_marker],
15415                    hashes: Vec::new(),
15416                },
15417                finalized,
15418                &[finalized],
15419                &nonce_verification,
15420            )
15421            .await
15422            .err()
15423            .unwrap();
15424        assert!(
15425            error.to_string().contains("has no durable event marker"),
15426            "was: {error}"
15427        );
15428
15429        let mut first = migration_test_intent(1);
15430        first.active = false;
15431        first.status = "recoverable".to_string();
15432        first.nonce = Some(6);
15433        let mut second = first.clone();
15434        second.id = 2;
15435        let error = client
15436            .build_execution_verification_migration(
15437                ExecutionVerificationMigrationSnapshot {
15438                    intents: vec![first, second],
15439                    hashes: Vec::new(),
15440                },
15441                finalized,
15442                &[finalized],
15443                &nonce_verification,
15444            )
15445            .await
15446            .err()
15447            .unwrap();
15448        assert!(
15449            error
15450                .to_string()
15451                .contains("duplicate signer nonce ownership"),
15452            "was: {error}"
15453        );
15454    }
15455
15456    #[tokio::test]
15457    async fn verification_migration_reconstructs_consumed_active_intent() {
15458        let expected_hash = expected_wrap_tx_hash(U256::from(1u64)).await;
15459        let mut block_value: serde_json::Value =
15460            serde_json::from_str(&finalized_wrap_block(expected_hash)).unwrap();
15461        block_value["result"]["transactions"][0]["value"] = serde_json::json!("0x1");
15462        let block = block_value.to_string();
15463        let receipt = receipt_with_transaction_hash(RECEIPT_SUCCESS, expected_hash);
15464        let state = with_finalized_identity(
15465            execution_rpc_state()
15466                .with_response("eth_getTransactionCount", TRANSACTION_COUNT_NEXT)
15467                .with_response("eth_getTransactionReceipt", &receipt)
15468                .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block),
15469            &block,
15470            &receipt,
15471        );
15472        let Some((admin_pool, pg_config)) =
15473            connect_test_postgres("verification_migration_reconstructs_consumed_active_intent")
15474                .await
15475        else {
15476            return;
15477        };
15478        let schema = format!(
15479            "verification_migration_reconstructs_consumed_active_intent_{}",
15480            std::process::id()
15481        );
15482        setup_execution_schema(&admin_pool, &schema).await;
15483        let options: sqlx::postgres::PgConnectOptions = pg_config.into();
15484        let database = connect_test_database(options.options([("search_path", schema.clone())]))
15485            .await
15486            .unwrap();
15487        let addr = start_mock_rpc_server(state).await;
15488        let (mut client, _) = swap_client_with_cache(test_config(format!("http://{addr}")));
15489        client.cache.database = Some(database.clone());
15490        client
15491            .cache
15492            .ensure_execution_transaction_schema()
15493            .await
15494            .unwrap();
15495        let (intent, persisted_hash, _) = persist_test_wrap_broadcast(&database, None).await;
15496        protect_test_storage(&mut client, &schema).await;
15497        assert_eq!(persisted_hash, expected_hash);
15498
15499        let snapshot = database
15500            .load_execution_verification_migration_snapshot(42_161, WALLET)
15501            .await
15502            .unwrap();
15503        let nonce_verification = required_verification(
15504            client
15505                .verification
15506                .verify_transaction_count(&Address::from_str(WALLET).unwrap(), FIXTURE_BLOCK + 1)
15507                .await,
15508            "test migration nonce",
15509        )
15510        .unwrap();
15511        let headers = [
15512            VerifiedBlockHeader {
15513                number: FIXTURE_BLOCK,
15514                hash: B256::from_str(FIXTURE_BLOCK_HASH).unwrap(),
15515                parent_hash: B256::from_str(
15516                    "0x0000000000000000000000000000000000000000000000000000000000000001",
15517                )
15518                .unwrap(),
15519                timestamp: FIXTURE_BLOCK_TIMESTAMP,
15520                base_fee_per_gas: Some(100_000_000),
15521            },
15522            VerifiedBlockHeader {
15523                number: FIXTURE_BLOCK + 1,
15524                hash: B256::from([0x22; 32]),
15525                parent_hash: B256::from_str(FIXTURE_BLOCK_HASH).unwrap(),
15526                timestamp: FIXTURE_BLOCK_TIMESTAMP + 1,
15527                base_fee_per_gas: Some(100_000_000),
15528            },
15529        ];
15530        let migration = client
15531            .build_execution_verification_migration(
15532                snapshot,
15533                headers[1],
15534                &headers,
15535                &nonce_verification,
15536            )
15537            .await
15538            .unwrap();
15539        let finalized_headers = headers
15540            .iter()
15541            .map(|header| ExecutionVerifiedHeader {
15542                number: header.number,
15543                hash: header.hash.to_string(),
15544                parent_hash: header.parent_hash.to_string(),
15545                timestamp: header.timestamp,
15546                base_fee_per_gas: header.base_fee_per_gas,
15547            })
15548            .collect::<Vec<_>>();
15549        let decisions = [verification_decision(
15550            &nonce_verification,
15551            Some(FIXTURE_BLOCK + 1),
15552            Some(FIXTURE_BLOCK + 1),
15553        )];
15554
15555        initialize_test_verification_migration(
15556            &client,
15557            &finalized_headers,
15558            8,
15559            &decisions,
15560            &migration,
15561        )
15562        .await;
15563
15564        let migrated = database.get_execution_intent(intent.id).await.unwrap();
15565        let resume = database
15566            .load_execution_verification_resume(
15567                42_161,
15568                WALLET,
15569                &client
15570                    .config
15571                    .verification
15572                    .as_ref()
15573                    .unwrap()
15574                    .manifest_version,
15575                &client.config.verification.as_ref().unwrap().manifest_digest,
15576            )
15577            .await
15578            .unwrap()
15579            .unwrap();
15580        let evidence: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
15581            "SELECT COUNT(*) FROM {schema}.execution_verification_decision \
15582             WHERE intent_id = $1 AND decision_class = 'migration'"
15583        )))
15584        .bind(intent.id)
15585        .fetch_one(&admin_pool)
15586        .await
15587        .unwrap();
15588
15589        assert_eq!(migrated.status, "finalized");
15590        assert!(migrated.active);
15591        assert_eq!(resume.next_canonical_nonce, 8);
15592        assert!(evidence >= 5);
15593
15594        drop_execution_schema(&admin_pool, &schema).await;
15595    }
15596
15597    #[tokio::test]
15598    async fn verification_resume_accepts_one_consumed_owned_nonce() {
15599        let Some((admin_pool, schema, client, _)) = execution_client_with_database(
15600            "verification_resume_owned_nonce_test",
15601            ready_rpc_state(),
15602        )
15603        .await
15604        else {
15605            return;
15606        };
15607        let database = client.cache.database.as_ref().unwrap();
15608        persist_test_wrap_broadcast(database, client.payload_keys.as_deref()).await;
15609        let resume = database
15610            .load_execution_verification_resume(
15611                42_161,
15612                WALLET,
15613                &client
15614                    .config
15615                    .verification
15616                    .as_ref()
15617                    .unwrap()
15618                    .manifest_version,
15619                &client.config.verification.as_ref().unwrap().manifest_digest,
15620            )
15621            .await
15622            .unwrap()
15623            .unwrap();
15624
15625        ensure_test_verification_ledger(&client, &resume.finalized_headers, 7, 8)
15626            .await
15627            .unwrap();
15628
15629        let nonce_state = sqlx::query_as::<_, (i64, i64)>(sqlx::AssertSqlSafe(format!(
15630            "SELECT next_canonical_nonce, revision FROM {schema}.execution_verification_nonce"
15631        )))
15632        .fetch_one(&admin_pool)
15633        .await
15634        .unwrap();
15635        assert_eq!(nonce_state, (7, 0));
15636
15637        drop_execution_schema(&admin_pool, &schema).await;
15638    }
15639
15640    #[tokio::test]
15641    async fn verification_resume_rejects_unowned_nonce_advance() {
15642        for (test_name, mutation, observed_canonical_nonce) in [
15643            ("verification_resume_no_active_test", "no_active", 8),
15644            ("verification_resume_excess_test", "unchanged", 9),
15645            ("verification_resume_signed_test", "signed", 8),
15646            ("verification_resume_prepared_test", "prepared", 8),
15647            (
15648                "verification_resume_missing_payload_test",
15649                "missing_payload",
15650                8,
15651            ),
15652        ] {
15653            let Some((admin_pool, schema, client, _)) =
15654                execution_client_with_database(test_name, ready_rpc_state()).await
15655            else {
15656                return;
15657            };
15658            let database = client.cache.database.as_ref().unwrap();
15659            let (intent, _, _) =
15660                persist_test_wrap_broadcast(database, client.payload_keys.as_deref()).await;
15661
15662            match mutation {
15663                "no_active" => {
15664                    sqlx::query(sqlx::AssertSqlSafe(format!(
15665                        "UPDATE {schema}.execution_intent SET active = FALSE WHERE id = $1"
15666                    )))
15667                    .bind(intent.id)
15668                    .execute(&admin_pool)
15669                    .await
15670                    .unwrap();
15671                }
15672                "signed" => {
15673                    sqlx::query(sqlx::AssertSqlSafe(format!(
15674                        "UPDATE {schema}.execution_intent SET status = 'signed' WHERE id = $1"
15675                    )))
15676                    .bind(intent.id)
15677                    .execute(&admin_pool)
15678                    .await
15679                    .unwrap();
15680                    sqlx::query(sqlx::AssertSqlSafe(format!(
15681                        "UPDATE {schema}.execution_transaction_hash SET status = 'signed' \
15682                         WHERE intent_id = $1"
15683                    )))
15684                    .bind(intent.id)
15685                    .execute(&admin_pool)
15686                    .await
15687                    .unwrap();
15688                }
15689                "prepared" => {
15690                    sqlx::query(sqlx::AssertSqlSafe(format!(
15691                        "UPDATE {schema}.execution_intent \
15692                         SET nonce = NULL, status = 'prepared' WHERE id = $1"
15693                    )))
15694                    .bind(intent.id)
15695                    .execute(&admin_pool)
15696                    .await
15697                    .unwrap();
15698                    sqlx::query(sqlx::AssertSqlSafe(format!(
15699                        "UPDATE {schema}.execution_transaction_hash SET current = FALSE \
15700                         WHERE intent_id = $1"
15701                    )))
15702                    .bind(intent.id)
15703                    .execute(&admin_pool)
15704                    .await
15705                    .unwrap();
15706                }
15707                "missing_payload" => {
15708                    sqlx::query(sqlx::AssertSqlSafe(format!(
15709                        "UPDATE {schema}.execution_transaction_hash \
15710                         SET payload_expected = FALSE, sealed_transaction = NULL \
15711                         WHERE intent_id = $1"
15712                    )))
15713                    .bind(intent.id)
15714                    .execute(&admin_pool)
15715                    .await
15716                    .unwrap();
15717                }
15718                "unchanged" => {}
15719                _ => unreachable!(),
15720            }
15721            let resume = database
15722                .load_execution_verification_resume(
15723                    42_161,
15724                    WALLET,
15725                    &client
15726                        .config
15727                        .verification
15728                        .as_ref()
15729                        .unwrap()
15730                        .manifest_version,
15731                    &client.config.verification.as_ref().unwrap().manifest_digest,
15732                )
15733                .await
15734                .unwrap()
15735                .unwrap();
15736
15737            let error = ensure_test_verification_ledger(
15738                &client,
15739                &resume.finalized_headers,
15740                7,
15741                observed_canonical_nonce,
15742            )
15743            .await
15744            .unwrap_err();
15745
15746            assert!(
15747                error
15748                    .to_string()
15749                    .contains(if observed_canonical_nonce == 9 {
15750                        "outside the owned recovery range"
15751                    } else {
15752                        "without"
15753                    }),
15754                "{mutation}: {error}"
15755            );
15756            let nonce_state = sqlx::query_as::<_, (i64, i64)>(sqlx::AssertSqlSafe(format!(
15757                "SELECT next_canonical_nonce, revision FROM {schema}.execution_verification_nonce"
15758            )))
15759            .fetch_one(&admin_pool)
15760            .await
15761            .unwrap();
15762            assert_eq!(nonce_state, (7, 0), "{mutation}");
15763
15764            drop_execution_schema(&admin_pool, &schema).await;
15765        }
15766    }
15767
15768    #[tokio::test]
15769    async fn restart_marks_prepared_intent_recoverable_without_broadcast() {
15770        let Some((admin_pool, schema, client, state)) =
15771            execution_client_with_database("execution_prepared_restart_test", ready_rpc_state())
15772                .await
15773        else {
15774            return;
15775        };
15776        let database = client.cache.database.as_ref().unwrap();
15777        let intent = reserve_test_wrap_intent(database).await;
15778        *client.in_flight.lock() = Some(InFlightSlot::Preparing(TransactionPurpose::Wrap));
15779
15780        client.reconcile_unresolved_execution().await.unwrap();
15781
15782        let (status, active): (String, bool) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
15783            "SELECT status, active FROM {schema}.execution_intent WHERE id = {}",
15784            intent.id
15785        )))
15786        .fetch_one(&admin_pool)
15787        .await
15788        .unwrap();
15789        let transitions: Vec<String> = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
15790            "SELECT to_status FROM {schema}.execution_transaction_transition ORDER BY id"
15791        )))
15792        .fetch_all(&admin_pool)
15793        .await
15794        .unwrap();
15795
15796        assert_eq!(status, "recoverable");
15797        assert!(!active);
15798        assert_eq!(transitions, ["prepared", "recoverable"]);
15799        assert!(client.in_flight.lock().is_none());
15800        assert!(
15801            state
15802                .recorded_requests()
15803                .iter()
15804                .all(|request| request["method"] != "eth_sendRawTransaction")
15805        );
15806
15807        drop_execution_schema(&admin_pool, &schema).await;
15808    }
15809
15810    #[tokio::test]
15811    async fn protected_restart_authenticates_envelope_before_recovery() {
15812        let initial_state = broadcast_rpc_state()
15813            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
15814            .with_call_response(BALANCE_OF_SELECTOR, CALL_BALANCE);
15815        let Some((admin_pool, schema, mut first_client, _)) =
15816            execution_client_with_database("protected_restart_test", initial_state).await
15817        else {
15818            return;
15819        };
15820        let database = first_client.cache.database.as_ref().unwrap().clone();
15821        let value = U256::from(1_000_000_000_000_000_u64);
15822        let expected_hash = expected_wrap_tx_hash(value).await;
15823
15824        let error = first_client.wrap(value).await.unwrap_err();
15825
15826        assert!(error.to_string().contains("Timed out awaiting finality"));
15827        let intent = database
15828            .get_active_execution_intent(42161, WALLET)
15829            .await
15830            .unwrap()
15831            .unwrap();
15832        let payload = database
15833            .get_execution_transaction_hashes(intent.id)
15834            .await
15835            .unwrap()
15836            .pop()
15837            .unwrap();
15838        assert!(payload.raw_transaction.is_none());
15839        assert!(payload.sealed_transaction.is_some());
15840        let keys = first_client.payload_keys.take().unwrap();
15841        drop(first_client);
15842
15843        let block = finalized_wrap_block(expected_hash);
15844        let receipt = receipt_with_transaction_hash(RECEIPT_SUCCESS, expected_hash);
15845        let restart_state = with_finalized_identity(
15846            execution_rpc_state()
15847                .with_response("eth_getTransactionReceipt", &receipt)
15848                .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block)
15849                .with_response_sequence(
15850                    "eth_call",
15851                    &[
15852                        CALL_BALANCE,
15853                        CALL_BALANCE,
15854                        CALL_BALANCE,
15855                        CALL_BALANCE_AFTER_WRAP,
15856                        CALL_BALANCE_AFTER_WRAP,
15857                        CALL_BALANCE_AFTER_WRAP,
15858                    ],
15859                ),
15860            &block,
15861            &receipt,
15862        );
15863        let addr = start_mock_rpc_server(restart_state.clone()).await;
15864        let mut restarted = test_client(format!("http://{addr}"));
15865        restarted.cache.database = Some(database);
15866        restarted.payload_keys = Some(keys);
15867        restarted.signer = Some(Arc::new(
15868            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
15869        ));
15870
15871        restarted.reconcile_unresolved_execution().await.unwrap();
15872
15873        let record = restarted
15874            .cache
15875            .get_execution_transaction(42161, &expected_hash.to_string())
15876            .await
15877            .unwrap()
15878            .unwrap();
15879        let requests = restart_state.recorded_requests();
15880        assert_eq!(record.status, "finalized");
15881        assert!(restarted.in_flight.lock().is_none());
15882        assert_eq!(
15883            requests
15884                .iter()
15885                .filter(|request| request["method"] == "eth_sendRawTransaction")
15886                .count(),
15887            0
15888        );
15889
15890        drop_execution_schema(&admin_pool, &schema).await;
15891    }
15892
15893    #[tokio::test]
15894    async fn active_intent_reconciliation_waits_for_reservation_fence() {
15895        let Some((admin_pool, schema, client, _)) = execution_client_with_database(
15896            "execution_active_intent_reservation_fence_test",
15897            ready_rpc_state(),
15898        )
15899        .await
15900        else {
15901            return;
15902        };
15903        let database = client.cache.database.as_ref().unwrap().clone();
15904        let lock = PgAdvisoryLock::new(format!(
15905            "nautilus:blockchain:execution:42161:{}",
15906            WALLET.to_ascii_lowercase()
15907        ));
15908        let PgAdvisoryLockKey::BigInt(lock_key) = lock.key() else {
15909            unreachable!("string advisory locks use the 64-bit key space");
15910        };
15911        let mut reservation = admin_pool.begin().await.unwrap();
15912        sqlx::query("SELECT pg_advisory_xact_lock($1)")
15913            .bind(*lock_key)
15914            .execute(&mut *reservation)
15915            .await
15916            .unwrap();
15917        sqlx::query(sqlx::AssertSqlSafe(format!(
15918            "INSERT INTO {schema}.execution_intent (\
15919                schema_version, chain_id, wallet_address, purpose, status, transaction_to, \
15920                transaction_input, transaction_value, created_block\
15921             ) VALUES (2, $1, $2, 'wrap', 'prepared', $3, '0xd0e30db0', '1', $4)"
15922        )))
15923        .bind(42161_i32)
15924        .bind(WALLET)
15925        .bind(WETH_ADDRESS.to_string())
15926        .bind(i64::try_from(FIXTURE_BLOCK).unwrap())
15927        .execute(&mut *reservation)
15928        .await
15929        .unwrap();
15930
15931        let mut reconciliation = Box::pin(database.get_active_execution_intent(42161, WALLET));
15932        assert!(
15933            tokio::time::timeout(Duration::from_millis(100), &mut reconciliation)
15934                .await
15935                .is_err(),
15936            "reconciliation did not wait for the reservation fence"
15937        );
15938
15939        reservation.commit().await.unwrap();
15940        let intent = tokio::time::timeout(Duration::from_secs(2), reconciliation)
15941            .await
15942            .unwrap()
15943            .unwrap()
15944            .unwrap();
15945
15946        assert_eq!(intent.chain_id, 42161);
15947        assert_eq!(intent.wallet_address, WALLET);
15948        assert_eq!(intent.purpose, "wrap");
15949        assert_eq!(intent.status, "prepared");
15950        assert!(intent.active);
15951
15952        drop_execution_schema(&admin_pool, &schema).await;
15953    }
15954
15955    #[tokio::test]
15956    async fn restart_keeps_unbroadcast_signed_intent_reserved() {
15957        let Some((admin_pool, schema, client, state)) =
15958            execution_client_with_database("execution_signed_restart_test", ready_rpc_state())
15959                .await
15960        else {
15961            return;
15962        };
15963        let database = client.cache.database.as_ref().unwrap();
15964        let intent = reserve_test_wrap_intent(database).await;
15965        let transaction = build_eip1559_transaction(
15966            42161,
15967            7,
15968            78_000,
15969            130_000_000,
15970            10_000_000,
15971            WETH_ADDRESS,
15972            U256::from(1u64),
15973            Bytes::from(nautilus_core::hex::decode("d0e30db0").unwrap()),
15974        );
15975        let (tx_hash, raw_transaction) = sign_eip1559_transaction(
15976            transaction,
15977            &PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
15978        )
15979        .await
15980        .unwrap();
15981        database
15982            .assign_execution_intent_nonce(intent.id, 7)
15983            .await
15984            .unwrap();
15985        let intent = database.get_execution_intent(intent.id).await.unwrap();
15986        persist_test_payload(
15987            database,
15988            client.payload_keys.as_deref(),
15989            &intent,
15990            tx_hash,
15991            &raw_transaction,
15992        )
15993        .await;
15994
15995        let error = client.reconcile_unresolved_execution().await.unwrap_err();
15996
15997        assert!(
15998            error
15999                .to_string()
16000                .contains("was not authorized for broadcast"),
16001            "was: {error}"
16002        );
16003        let recovery = recovering_in_flight(&client);
16004        assert_eq!(recovery.intent_id, intent.id);
16005        assert_eq!(recovery.nonce, 7);
16006        assert_eq!(recovery.purpose, TransactionPurpose::Wrap);
16007        assert_eq!(
16008            execution_intent_markers(&admin_pool, &schema).await,
16009            vec![("wrap".into(), "signed".into(), false, true)]
16010        );
16011        assert!(
16012            state
16013                .recorded_requests()
16014                .iter()
16015                .all(|request| request["method"] != "eth_sendRawTransaction")
16016        );
16017
16018        drop_execution_schema(&admin_pool, &schema).await;
16019    }
16020
16021    #[tokio::test]
16022    async fn restart_quarantines_legacy_recoverable_signed_intent() {
16023        let Some((admin_pool, schema, client, state)) = execution_client_with_database(
16024            "execution_legacy_recoverable_restart_test",
16025            ready_rpc_state(),
16026        )
16027        .await
16028        else {
16029            return;
16030        };
16031        let database = client.cache.database.as_ref().unwrap();
16032        let intent = reserve_test_wrap_intent(database).await;
16033        let tx_hash = B256::from([0x55; 32]);
16034        database
16035            .assign_execution_intent_nonce(intent.id, 7)
16036            .await
16037            .unwrap();
16038        let intent = database.get_execution_intent(intent.id).await.unwrap();
16039        persist_test_payload(
16040            database,
16041            client.payload_keys.as_deref(),
16042            &intent,
16043            tx_hash,
16044            &[0x01, 0x02, 0x03],
16045        )
16046        .await;
16047        sqlx::query(sqlx::AssertSqlSafe(format!(
16048            "UPDATE {schema}.execution_intent SET status = 'recoverable', active = FALSE WHERE id = {}",
16049            intent.id
16050        )))
16051        .execute(&admin_pool)
16052        .await
16053        .unwrap();
16054
16055        let error = client.reconcile_unresolved_execution().await.unwrap_err();
16056
16057        assert!(
16058            error
16059                .to_string()
16060                .contains("retains signed transaction bytes"),
16061            "was: {error}"
16062        );
16063        assert!(client.in_flight.lock().is_none());
16064        assert!(
16065            state
16066                .recorded_requests()
16067                .iter()
16068                .all(|request| request["method"] != "eth_sendRawTransaction")
16069        );
16070
16071        drop_execution_schema(&admin_pool, &schema).await;
16072    }
16073
16074    #[rstest]
16075    #[case::current("execution_invalid_signed_restart_test", false, "broadcast")]
16076    #[case::historical("execution_invalid_historical_restart_test", true, "replaced")]
16077    #[tokio::test]
16078    async fn restart_rejects_invalid_signed_bytes_before_recovery_effects(
16079        #[case] test_name: &str,
16080        #[case] historical: bool,
16081        #[case] expected_status: &str,
16082    ) {
16083        let Some((admin_pool, schema, first_client, state, _)) =
16084            swap_client_with_database(test_name, ready_rpc_state()).await
16085        else {
16086            return;
16087        };
16088        let database = first_client.cache.database.as_ref().unwrap().clone();
16089        let (intent, _) =
16090            persist_invalid_test_swap(&database, first_client.payload_keys.as_deref()).await;
16091
16092        if historical {
16093            sqlx::query(sqlx::AssertSqlSafe(format!(
16094                "UPDATE {schema}.execution_transaction_hash \
16095                 SET current = FALSE, status = 'replaced' WHERE intent_id = $1"
16096            )))
16097            .bind(intent.id)
16098            .execute(&admin_pool)
16099            .await
16100            .unwrap();
16101            sqlx::query(sqlx::AssertSqlSafe(format!(
16102                "INSERT INTO {schema}.execution_transaction_hash (\
16103                     intent_id, chain_id, transaction_hash, payload_expected, status, current\
16104                 ) VALUES ($1, 42161, $2, FALSE, 'replaced', TRUE)"
16105            )))
16106            .bind(intent.id)
16107            .bind(B256::from([0x44; 32]).to_string())
16108            .execute(&admin_pool)
16109            .await
16110            .unwrap();
16111            sqlx::query(sqlx::AssertSqlSafe(format!(
16112                "UPDATE {schema}.execution_intent SET status = 'replaced' WHERE id = $1"
16113            )))
16114            .bind(intent.id)
16115            .execute(&admin_pool)
16116            .await
16117            .unwrap();
16118        }
16119        let transitions_before: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
16120            "SELECT COUNT(*) FROM {schema}.execution_transaction_transition"
16121        )))
16122        .fetch_one(&admin_pool)
16123        .await
16124        .unwrap();
16125        let config = first_client.config.clone();
16126        let payload_keys = first_client.payload_keys.clone();
16127        drop(first_client);
16128
16129        let (mut restarted, _) = swap_client_with_cache(config);
16130        restarted.cache.database = Some(database);
16131        restarted.payload_keys = payload_keys;
16132        restarted.signer = Some(Arc::new(
16133            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16134        ));
16135        let mut receiver = start_with_events(&mut restarted);
16136
16137        let error = restarted
16138            .reconcile_unresolved_execution()
16139            .await
16140            .unwrap_err();
16141
16142        let recovery = recovering_in_flight(&restarted);
16143        let (status, acknowledgement_emitted, active): (String, bool, bool) =
16144            sqlx::query_as(sqlx::AssertSqlSafe(format!(
16145                "SELECT status, acknowledgement_emitted, active FROM {schema}.execution_intent"
16146            )))
16147            .fetch_one(&admin_pool)
16148            .await
16149            .unwrap();
16150        let transitions_after: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
16151            "SELECT COUNT(*) FROM {schema}.execution_transaction_transition"
16152        )))
16153        .fetch_one(&admin_pool)
16154        .await
16155        .unwrap();
16156
16157        assert!(
16158            error
16159                .to_string()
16160                .contains("is not a complete EIP-2718 envelope"),
16161            "was: {error}"
16162        );
16163        assert_eq!(recovery.intent_id, intent.id);
16164        assert_eq!(recovery.nonce, 7);
16165        assert_eq!(recovery.purpose, TransactionPurpose::Swap);
16166        assert_eq!(status, expected_status);
16167        assert!(!acknowledgement_emitted);
16168        assert!(active);
16169        assert_eq!(transitions_after, transitions_before);
16170        assert!(collect_order_events(&mut receiver).is_empty());
16171        assert!(
16172            state
16173                .recorded_requests()
16174                .iter()
16175                .all(|request| request["method"] != "eth_sendRawTransaction")
16176        );
16177
16178        drop_execution_schema(&admin_pool, &schema).await;
16179    }
16180
16181    #[tokio::test]
16182    async fn restart_rebroadcasts_only_durably_authorized_bytes() {
16183        let Some((admin_pool, schema, first_client, _)) =
16184            execution_client_with_database("execution_broadcast_restart_test", ready_rpc_state())
16185                .await
16186        else {
16187            return;
16188        };
16189        let database = first_client.cache.database.as_ref().unwrap().clone();
16190        let (_, _, raw_tx) =
16191            persist_test_wrap_broadcast(&database, first_client.payload_keys.as_deref()).await;
16192        let payload_keys = first_client.payload_keys.clone();
16193        drop(first_client);
16194
16195        let restart_state = execution_rpc_state()
16196            .with_response("eth_getTransactionCount", TRANSACTION_COUNT)
16197            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
16198            .with_response("eth_call", CALL_EMPTY)
16199            .with_send_raw_transaction_echo();
16200        let addr = start_mock_rpc_server(restart_state.clone()).await;
16201        let mut restarted = test_client(format!("http://{addr}"));
16202        restarted.cache.database = Some(database);
16203        restarted.payload_keys = payload_keys;
16204        restarted.signer = Some(Arc::new(
16205            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16206        ));
16207
16208        restarted.reconcile_unresolved_execution().await.unwrap();
16209
16210        let broadcasts = restart_state
16211            .recorded_requests()
16212            .into_iter()
16213            .filter(|request| request["method"] == "eth_sendRawTransaction")
16214            .collect::<Vec<_>>();
16215        assert_eq!(broadcasts.len(), 1);
16216        assert_eq!(broadcasts[0]["params"][0], hex::encode_prefixed(&raw_tx));
16217        assert_eq!(
16218            execution_intent_markers(&admin_pool, &schema).await,
16219            vec![("wrap".into(), "dropped".into(), false, true)]
16220        );
16221
16222        drop_execution_schema(&admin_pool, &schema).await;
16223    }
16224
16225    #[tokio::test]
16226    async fn restart_suppresses_rebroadcast_when_canonical_nonce_advanced() {
16227        let Some((admin_pool, schema, first_client, _)) = execution_client_with_database(
16228            "execution_rebroadcast_nonce_advanced_test",
16229            ready_rpc_state(),
16230        )
16231        .await
16232        else {
16233            return;
16234        };
16235        let database = first_client.cache.database.as_ref().unwrap().clone();
16236        let (intent, _, _) =
16237            persist_test_wrap_broadcast(&database, first_client.payload_keys.as_deref()).await;
16238        let payload_keys = first_client.payload_keys.clone();
16239        drop(first_client);
16240
16241        let mut empty_block: serde_json::Value =
16242            serde_json::from_str(&replacement_head_block(B256::from([0x44; 32]))).unwrap();
16243        empty_block["result"]["transactions"] = serde_json::json!([]);
16244        let empty_block = empty_block.to_string();
16245        let restart_state = execution_rpc_state()
16246            .with_response("eth_getTransactionCount", TRANSACTION_COUNT_NEXT)
16247            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
16248            .with_response("eth_call", CALL_EMPTY)
16249            .with_parameter_response("eth_getBlockByNumber", "0x1cf0d40", &empty_block)
16250            .with_send_raw_transaction_echo();
16251        let addr = start_mock_rpc_server(restart_state.clone()).await;
16252        let mut restarted = test_client(format!("http://{addr}"));
16253        restarted.cache.database = Some(database);
16254        restarted.payload_keys = payload_keys;
16255        restarted.signer = Some(Arc::new(
16256            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16257        ));
16258
16259        let error = restarted
16260            .reconcile_unresolved_execution()
16261            .await
16262            .unwrap_err();
16263
16264        let requests = restart_state.recorded_requests();
16265        assert!(
16266            requests
16267                .iter()
16268                .all(|request| request["method"] != "eth_sendRawTransaction")
16269        );
16270        assert!(requests.iter().all(|request| {
16271            request["method"] != "eth_call" || request["params"][0]["data"] != "0xd0e30db0"
16272        }));
16273        let decision_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
16274            "SELECT COUNT(*) FROM {schema}.execution_verification_decision \
16275             WHERE intent_id = $1 AND decision_class = 'rebroadcast'"
16276        )))
16277        .bind(intent.id)
16278        .fetch_one(&admin_pool)
16279        .await
16280        .unwrap();
16281        assert!(
16282            error
16283                .to_string()
16284                .contains("without an authenticated signer transaction"),
16285            "was: {error}"
16286        );
16287        assert_eq!(decision_count, 6);
16288        let replacement_decision_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
16289            "SELECT COUNT(*) FROM {schema}.execution_verification_decision \
16290                 WHERE intent_id = $1 AND decision_class = 'replacement_scan'"
16291        )))
16292        .bind(intent.id)
16293        .fetch_one(&admin_pool)
16294        .await
16295        .unwrap();
16296        assert_eq!(replacement_decision_count, 2);
16297        let nonce_state = sqlx::query_as::<_, (i64, i64)>(sqlx::AssertSqlSafe(format!(
16298            "SELECT next_canonical_nonce, revision FROM {schema}.execution_verification_nonce"
16299        )))
16300        .fetch_one(&admin_pool)
16301        .await
16302        .unwrap();
16303        assert_eq!(nonce_state, (7, 0));
16304
16305        drop_execution_schema(&admin_pool, &schema).await;
16306    }
16307
16308    #[tokio::test]
16309    async fn restart_suppresses_rebroadcast_when_receipt_exists() {
16310        let Some((admin_pool, schema, first_client, _)) = execution_client_with_database(
16311            "execution_rebroadcast_receipt_present_test",
16312            ready_rpc_state(),
16313        )
16314        .await
16315        else {
16316            return;
16317        };
16318        let database = first_client.cache.database.as_ref().unwrap().clone();
16319        let (intent, _, _) =
16320            persist_test_wrap_broadcast(&database, first_client.payload_keys.as_deref()).await;
16321        let payload_keys = first_client.payload_keys.clone();
16322        drop(first_client);
16323
16324        let restart_state = execution_rpc_state()
16325            .with_response("eth_getTransactionCount", TRANSACTION_COUNT)
16326            .with_response("eth_getTransactionReceipt", RECEIPT_SUCCESS)
16327            .with_response("eth_call", CALL_EMPTY)
16328            .with_send_raw_transaction_echo();
16329        let addr = start_mock_rpc_server(restart_state.clone()).await;
16330        let mut restarted = test_client(format!("http://{addr}"));
16331        restarted.cache.database = Some(database);
16332        restarted.payload_keys = payload_keys;
16333        restarted.signer = Some(Arc::new(
16334            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16335        ));
16336
16337        let error = restarted
16338            .reconcile_unresolved_execution()
16339            .await
16340            .unwrap_err();
16341
16342        assert!(
16343            error
16344                .to_string()
16345                .contains("finalized transaction verification is locally invalid"),
16346            "was: {error}"
16347        );
16348        let requests = restart_state.recorded_requests();
16349        assert!(
16350            requests
16351                .iter()
16352                .all(|request| request["method"] != "eth_sendRawTransaction")
16353        );
16354        assert!(requests.iter().all(|request| {
16355            request["method"] != "eth_call" || request["params"][0]["data"] != "0xd0e30db0"
16356        }));
16357        let decision_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
16358            "SELECT COUNT(*) FROM {schema}.execution_verification_decision \
16359             WHERE intent_id = $1 AND decision_class = 'rebroadcast'"
16360        )))
16361        .bind(intent.id)
16362        .fetch_one(&admin_pool)
16363        .await
16364        .unwrap();
16365        assert_eq!(decision_count, 6);
16366
16367        drop_execution_schema(&admin_pool, &schema).await;
16368    }
16369
16370    #[rstest]
16371    #[case("execution_rebroadcast_false_test", CALL_ZERO)]
16372    #[case("execution_rebroadcast_revert_test", CALL_REVERTED)]
16373    #[tokio::test]
16374    async fn restart_suppresses_rebroadcast_when_simulation_denies(
16375        #[case] test_name: &str,
16376        #[case] simulation_response: &str,
16377    ) {
16378        let Some((admin_pool, schema, first_client, _)) =
16379            execution_client_with_database(test_name, ready_rpc_state()).await
16380        else {
16381            return;
16382        };
16383        let database = first_client.cache.database.as_ref().unwrap().clone();
16384        let (intent, _, _) =
16385            persist_test_wrap_broadcast(&database, first_client.payload_keys.as_deref()).await;
16386        let payload_keys = first_client.payload_keys.clone();
16387        drop(first_client);
16388
16389        let restart_state = execution_rpc_state()
16390            .with_response("eth_getTransactionCount", TRANSACTION_COUNT)
16391            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
16392            .with_response("eth_call", simulation_response)
16393            .with_send_raw_transaction_echo();
16394        let addr = start_mock_rpc_server(restart_state.clone()).await;
16395        let mut restarted = test_client(format!("http://{addr}"));
16396        restarted.cache.database = Some(database);
16397        restarted.payload_keys = payload_keys;
16398        restarted.signer = Some(Arc::new(
16399            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16400        ));
16401
16402        restarted.reconcile_unresolved_execution().await.unwrap();
16403
16404        let requests = restart_state.recorded_requests();
16405        assert!(
16406            requests
16407                .iter()
16408                .all(|request| request["method"] != "eth_sendRawTransaction")
16409        );
16410        let simulation_calls = requests
16411            .iter()
16412            .filter(|request| {
16413                request["method"] == "eth_call"
16414                    && request["params"][0]["data"] == "0xd0e30db0"
16415                    && request["params"][1] == FIXTURE_BLOCK_PARAM
16416            })
16417            .count();
16418        assert_eq!(simulation_calls, 3);
16419        let decision_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
16420            "SELECT COUNT(*) FROM {schema}.execution_verification_decision \
16421             WHERE intent_id = $1 AND decision_class = 'rebroadcast'"
16422        )))
16423        .bind(intent.id)
16424        .fetch_one(&admin_pool)
16425        .await
16426        .unwrap();
16427        assert_eq!(decision_count, 7);
16428
16429        drop_execution_schema(&admin_pool, &schema).await;
16430    }
16431
16432    #[tokio::test]
16433    async fn restart_wrap_identity_mismatch_keeps_signer_ownership() {
16434        let initial_state = broadcast_rpc_state()
16435            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
16436            .with_call_response(BALANCE_OF_SELECTOR, CALL_BALANCE);
16437        let Some((admin_pool, schema, mut first_client, _)) =
16438            execution_client_with_database("execution_restart_mismatch_test", initial_state).await
16439        else {
16440            return;
16441        };
16442        let expected_hash = expected_wrap_tx_hash(U256::from(1_000_000_000_000_000_u64)).await;
16443        let error = first_client
16444            .wrap(U256::from(1_000_000_000_000_000_u64))
16445            .await
16446            .unwrap_err();
16447        assert!(
16448            error.to_string().contains("Timed out awaiting finality"),
16449            "was: {error}"
16450        );
16451        let database = first_client.cache.database.as_ref().unwrap().clone();
16452        let payload_keys = first_client.payload_keys.clone();
16453        drop(first_client);
16454
16455        // The finalized transaction carries no value, so it cannot be the persisted wrap
16456        let mismatched_block = serde_json::json!({
16457            "jsonrpc": "2.0",
16458            "id": 1,
16459            "result": {
16460                "number": "0x1cf0d41",
16461                "hash": "0x2222222222222222222222222222222222222222222222222222222222222222",
16462                "parentHash": FIXTURE_BLOCK_HASH,
16463                "timestamp": "0x69044a21",
16464                "baseFeePerGas": "0x5f5e100",
16465                "transactions": [{
16466                    "hash": expected_hash.to_string(),
16467                    "from": WALLET,
16468                    "nonce": "0x7",
16469                    "chainId": "0xa4b1",
16470                    "type": "0x2",
16471                    "to": WETH,
16472                    "input": "0xd0e30db0",
16473                    "value": "0x0",
16474                    "gas": "0x130b0",
16475                    "maxFeePerGas": "0x7bfa480",
16476                    "maxPriorityFeePerGas": "0x989680"
16477                }]
16478            }
16479        })
16480        .to_string();
16481        let receipt = receipt_with_transaction_hash(RECEIPT_SUCCESS, expected_hash);
16482        let restart_state = with_finalized_identity(
16483            execution_rpc_state()
16484                .with_response("eth_getTransactionReceipt", &receipt)
16485                .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &mismatched_block),
16486            &mismatched_block,
16487            &receipt,
16488        );
16489        let addr = start_mock_rpc_server(restart_state.clone()).await;
16490        let mut restarted = test_client(format!("http://{addr}"));
16491        restarted.cache.database = Some(database);
16492        restarted.payload_keys = payload_keys;
16493        restarted.signer = Some(Arc::new(
16494            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16495        ));
16496
16497        let error = restarted
16498            .reconcile_unresolved_execution()
16499            .await
16500            .unwrap_err();
16501
16502        assert!(
16503            error
16504                .to_string()
16505                .contains("finalized transaction identity mismatch"),
16506            "was: {error}"
16507        );
16508        let in_flight = awaiting_in_flight(&restarted);
16509        assert_eq!(in_flight.nonce, 7);
16510        assert_eq!(in_flight.purpose, TransactionPurpose::Wrap);
16511        assert_eq!(in_flight.tx_hash, expected_hash);
16512        let requests = restart_state.recorded_requests();
16513        assert!(
16514            requests.iter().all(|request| {
16515                request["method"] != "eth_call"
16516                    || !request["params"][0]["data"]
16517                        .as_str()
16518                        .is_some_and(|data| data.starts_with(BALANCE_OF_SELECTOR))
16519            }),
16520            "the postcondition must not run when call identity is unproven"
16521        );
16522        assert!(
16523            requests
16524                .iter()
16525                .all(|request| request["method"] != "eth_sendRawTransaction")
16526        );
16527        let (status, active): (String, bool) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
16528            "SELECT status, active FROM {schema}.execution_intent"
16529        )))
16530        .fetch_one(&admin_pool)
16531        .await
16532        .unwrap();
16533        assert_eq!(status, "dropped");
16534        assert!(active);
16535
16536        let database = restarted.cache.database.as_ref().unwrap().clone();
16537        let payload_keys = restarted.payload_keys.clone();
16538        drop(restarted);
16539        let mut second = test_client(format!("http://{addr}"));
16540        second.cache.database = Some(database);
16541        second.payload_keys = payload_keys;
16542        second.signer = Some(Arc::new(
16543            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16544        ));
16545        let error = second.reconcile_unresolved_execution().await.unwrap_err();
16546        assert!(
16547            error
16548                .to_string()
16549                .contains("finalized transaction identity mismatch"),
16550            "was: {error}"
16551        );
16552        let active: bool = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
16553            "SELECT active FROM {schema}.execution_intent"
16554        )))
16555        .fetch_one(&admin_pool)
16556        .await
16557        .unwrap();
16558        assert!(active);
16559
16560        drop_execution_schema(&admin_pool, &schema).await;
16561    }
16562
16563    #[tokio::test]
16564    async fn restart_wrap_postcondition_failure_keeps_signer_ownership() {
16565        let initial_state = broadcast_rpc_state()
16566            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
16567            .with_call_response(BALANCE_OF_SELECTOR, CALL_BALANCE);
16568        let Some((admin_pool, schema, mut first_client, _)) =
16569            execution_client_with_database("execution_restart_postcondition_test", initial_state)
16570                .await
16571        else {
16572            return;
16573        };
16574        let expected_hash = expected_wrap_tx_hash(U256::from(1_000_000_000_000_000_u64)).await;
16575        let error = first_client
16576            .wrap(U256::from(1_000_000_000_000_000_u64))
16577            .await
16578            .unwrap_err();
16579        assert!(
16580            error.to_string().contains("Timed out awaiting finality"),
16581            "was: {error}"
16582        );
16583        let database = first_client.cache.database.as_ref().unwrap().clone();
16584        let payload_keys = first_client.payload_keys.clone();
16585        drop(first_client);
16586
16587        // Call identity matches, but the wrapped balance does not increase
16588        let failing_wrap_state = || {
16589            let block = finalized_wrap_block(expected_hash);
16590            let receipt = receipt_with_transaction_hash(RECEIPT_SUCCESS, expected_hash);
16591            with_finalized_identity(
16592                execution_rpc_state()
16593                    .with_response("eth_getTransactionReceipt", &receipt)
16594                    .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block)
16595                    .with_response_sequence("eth_call", &[CALL_BALANCE; 6]),
16596                &block,
16597                &receipt,
16598            )
16599        };
16600        let restart_state = failing_wrap_state();
16601        let addr = start_mock_rpc_server(restart_state.clone()).await;
16602        let mut restarted = test_client(format!("http://{addr}"));
16603        restarted.cache.database = Some(database);
16604        restarted.payload_keys = payload_keys;
16605        restarted.signer = Some(Arc::new(
16606            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16607        ));
16608
16609        let error = restarted
16610            .reconcile_unresolved_execution()
16611            .await
16612            .unwrap_err();
16613
16614        assert!(
16615            error.to_string().contains("did not increase by"),
16616            "was: {error}"
16617        );
16618        let in_flight = awaiting_in_flight(&restarted);
16619        assert_eq!(in_flight.nonce, 7);
16620        assert_eq!(in_flight.purpose, TransactionPurpose::Wrap);
16621        assert!(
16622            restart_state
16623                .recorded_requests()
16624                .iter()
16625                .all(|request| request["method"] != "eth_sendRawTransaction")
16626        );
16627        assert_eq!(
16628            execution_intent_markers(&admin_pool, &schema).await,
16629            vec![("wrap".into(), "dropped".into(), false, true)]
16630        );
16631
16632        let addr = start_mock_rpc_server(failing_wrap_state()).await;
16633        let error = later_reconnect(restarted, format!("http://{addr}")).await;
16634        assert!(
16635            error.to_string().contains("did not increase by"),
16636            "was: {error}"
16637        );
16638        assert_eq!(
16639            execution_intent_markers(&admin_pool, &schema).await,
16640            vec![("wrap".into(), "dropped".into(), false, true)]
16641        );
16642
16643        drop_execution_schema(&admin_pool, &schema).await;
16644    }
16645
16646    #[tokio::test]
16647    async fn restart_wrap_revert_marks_terminal_and_releases() {
16648        let initial_state = broadcast_rpc_state()
16649            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
16650            .with_call_response(BALANCE_OF_SELECTOR, CALL_BALANCE);
16651        let Some((admin_pool, schema, mut first_client, _)) =
16652            execution_client_with_database("execution_restart_wrap_revert_test", initial_state)
16653                .await
16654        else {
16655            return;
16656        };
16657        let expected_hash = expected_wrap_tx_hash(U256::from(1_000_000_000_000_000_u64)).await;
16658        let error = first_client
16659            .wrap(U256::from(1_000_000_000_000_000_u64))
16660            .await
16661            .unwrap_err();
16662        assert!(
16663            error.to_string().contains("Timed out awaiting finality"),
16664            "was: {error}"
16665        );
16666        let database = first_client.cache.database.as_ref().unwrap().clone();
16667        let payload_keys = first_client.payload_keys.clone();
16668        drop(first_client);
16669
16670        let block = finalized_wrap_block(expected_hash);
16671        let receipt = receipt_with_transaction_hash(RECEIPT_REVERTED, expected_hash);
16672        let restart_state = with_finalized_identity(
16673            execution_rpc_state()
16674                .with_response("eth_getTransactionReceipt", &receipt)
16675                .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block),
16676            &block,
16677            &receipt,
16678        );
16679        let addr = start_mock_rpc_server(restart_state.clone()).await;
16680        let mut restarted = test_client(format!("http://{addr}"));
16681        restarted.cache.database = Some(database);
16682        restarted.payload_keys = payload_keys;
16683        restarted.signer = Some(Arc::new(
16684            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16685        ));
16686
16687        restarted.reconcile_unresolved_execution().await.unwrap();
16688
16689        assert!(restarted.in_flight.lock().is_none());
16690        assert!(
16691            restart_state
16692                .recorded_requests()
16693                .iter()
16694                .all(|request| request["method"] != "eth_sendRawTransaction")
16695        );
16696        assert_eq!(
16697            execution_intent_markers(&admin_pool, &schema).await,
16698            vec![("wrap".into(), "reverted".into(), true, false)]
16699        );
16700
16701        drop_execution_schema(&admin_pool, &schema).await;
16702    }
16703
16704    #[tokio::test]
16705    async fn restart_quarantines_unretained_same_nonce_wrap_replacement() {
16706        let initial_state = broadcast_rpc_state()
16707            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
16708            .with_call_response(BALANCE_OF_SELECTOR, CALL_BALANCE);
16709        let Some((admin_pool, schema, mut first_client, _)) =
16710            execution_client_with_database("execution_restart_replacement_test", initial_state)
16711                .await
16712        else {
16713            return;
16714        };
16715        let original_hash = expected_wrap_tx_hash(U256::from(1_000_000_000_000_000_u64)).await;
16716        let error = first_client
16717            .wrap(U256::from(1_000_000_000_000_000_u64))
16718            .await
16719            .unwrap_err();
16720        assert!(
16721            error.to_string().contains("Timed out awaiting finality"),
16722            "was: {error}"
16723        );
16724        let database = first_client.cache.database.as_ref().unwrap().clone();
16725        let payload_keys = first_client.payload_keys.clone();
16726        drop(first_client);
16727
16728        // Matching call fields do not authenticate an unknown same-nonce payload.
16729        let replacement_hash = B256::from([0x44; 32]);
16730        let restart_state = execution_rpc_state()
16731            .with_response("eth_getTransactionCount", TRANSACTION_COUNT_NEXT)
16732            .with_response_sequence("eth_call", &[CALL_BALANCE, CALL_BALANCE_AFTER_WRAP])
16733            .with_response_sequence(
16734                "eth_getTransactionReceipt",
16735                &[RECEIPT_NULL, RECEIPT_NULL, RECEIPT_NULL],
16736            )
16737            .with_parameter_response(
16738                "eth_getBlockByNumber",
16739                "0x1cf0d40",
16740                &replacement_head_block(replacement_hash),
16741            )
16742            .with_parameter_response(
16743                "eth_getBlockByNumber",
16744                "0x1cf0d41",
16745                &finalized_wrap_block(replacement_hash),
16746            );
16747        let addr = start_mock_rpc_server(restart_state.clone()).await;
16748        let mut restarted = test_client(format!("http://{addr}"));
16749        restarted.cache.database = Some(database);
16750        restarted.payload_keys = payload_keys;
16751        restarted.signer = Some(Arc::new(
16752            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16753        ));
16754        restarted.transaction_limits.receipt_timeout_secs = 2;
16755
16756        let error = restarted
16757            .reconcile_unresolved_execution()
16758            .await
16759            .unwrap_err();
16760
16761        let hashes: Vec<(String, String, bool)> = sqlx::query_as(sqlx::AssertSqlSafe(format!(
16762            "SELECT transaction_hash, status, current FROM \
16763                 {schema}.execution_transaction_hash ORDER BY id"
16764        )))
16765        .fetch_all(&admin_pool)
16766        .await
16767        .unwrap();
16768        let requests = restart_state.recorded_requests();
16769
16770        assert!(
16771            error
16772                .to_string()
16773                .contains("has no authenticated retained payload"),
16774            "was: {error}"
16775        );
16776        assert_eq!(
16777            hashes,
16778            [(original_hash.to_string(), "dropped".to_string(), true)]
16779        );
16780        assert!(restarted.in_flight.lock().is_some());
16781        assert_eq!(
16782            execution_intent_markers(&admin_pool, &schema).await,
16783            vec![("wrap".into(), "dropped".into(), false, true)]
16784        );
16785        assert_eq!(
16786            requests
16787                .iter()
16788                .filter(|request| request["method"] == "eth_getTransactionReceipt")
16789                .count(),
16790            3
16791        );
16792        assert!(requests.iter().all(|request| {
16793            request["method"] != "eth_call" || request["params"][0]["data"] != "0xd0e30db0"
16794        }));
16795        assert!(
16796            requests
16797                .iter()
16798                .all(|request| request["method"] != "eth_sendRawTransaction")
16799        );
16800
16801        drop_execution_schema(&admin_pool, &schema).await;
16802    }
16803
16804    #[tokio::test]
16805    async fn replacement_scan_accepts_only_an_authenticated_retained_payload() {
16806        let expected_hash = expected_wrap_tx_hash(U256::from(1_u64)).await;
16807        let mut replacement_block: serde_json::Value =
16808            serde_json::from_str(&replacement_head_block(expected_hash)).unwrap();
16809        replacement_block["result"]["transactions"][1]["value"] = serde_json::json!("0x1");
16810        let replacement_block = replacement_block.to_string();
16811        let state = execution_rpc_state().with_parameter_response(
16812            "eth_getBlockByNumber",
16813            "0x1cf0d40",
16814            &replacement_block,
16815        );
16816        let Some((admin_pool, schema, client, _)) =
16817            execution_client_with_database("replacement_scan_authenticated", state).await
16818        else {
16819            return;
16820        };
16821        let database = client.cache.database.as_ref().unwrap();
16822        let (intent, persisted_hash, persisted_raw) =
16823            persist_test_wrap_broadcast(database, client.payload_keys.as_deref()).await;
16824        assert_eq!(persisted_hash, expected_hash);
16825        let authenticated_payloads = HashMap::from([(expected_hash, persisted_raw.clone())]);
16826        let head = VerifiedBlockHeader {
16827            number: FIXTURE_BLOCK,
16828            hash: B256::from_str(FIXTURE_BLOCK_HASH).unwrap(),
16829            parent_hash: B256::from_str(
16830                "0x0000000000000000000000000000000000000000000000000000000000000001",
16831            )
16832            .unwrap(),
16833            timestamp: FIXTURE_BLOCK_TIMESTAMP,
16834            base_fee_per_gas: Some(100_000_000),
16835        };
16836
16837        let matched = client
16838            .transaction_executor()
16839            .unwrap()
16840            .scan_canonical_replacement(&intent, 7, head, &authenticated_payloads)
16841            .await
16842            .unwrap()
16843            .unwrap();
16844
16845        assert_eq!(matched, (expected_hash, persisted_raw));
16846        let cursor: (i64, String) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
16847            "SELECT finalized_cursor_number, finalized_cursor_hash \
16848             FROM {schema}.execution_replacement_scan WHERE intent_id = $1"
16849        )))
16850        .bind(intent.id)
16851        .fetch_one(&admin_pool)
16852        .await
16853        .unwrap();
16854        assert_eq!(
16855            cursor,
16856            (FIXTURE_BLOCK as i64, FIXTURE_BLOCK_HASH.to_string())
16857        );
16858        let evidence_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
16859            "SELECT COUNT(*) FROM {schema}.execution_verification_decision \
16860             WHERE intent_id = $1 AND decision_class = 'replacement_scan'"
16861        )))
16862        .bind(intent.id)
16863        .fetch_one(&admin_pool)
16864        .await
16865        .unwrap();
16866        assert_eq!(evidence_count, 2);
16867
16868        drop_execution_schema(&admin_pool, &schema).await;
16869    }
16870
16871    #[tokio::test]
16872    async fn restart_reconciles_finalized_approve_after_validation() {
16873        let initial_state = broadcast_rpc_state()
16874            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
16875            .with_response("eth_call", CALL_BOOL_TRUE)
16876            .with_call_response_sequence(ALLOWANCE_SELECTOR, &[CALL_ZERO; 3])
16877            .with_call_response(ALLOWANCE_SELECTOR, CALL_ZERO);
16878        let Some((admin_pool, schema, mut first_client, _)) =
16879            execution_client_with_database("execution_restart_approve_test", initial_state).await
16880        else {
16881            return;
16882        };
16883        let expected_hash = expected_approve_tx_hash(U256::from(1_000u64)).await;
16884        let error = first_client
16885            .approve(WETH_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
16886            .await
16887            .unwrap_err();
16888        assert!(
16889            error.to_string().contains("Timed out awaiting finality"),
16890            "was: {error}"
16891        );
16892        let database = first_client.cache.database.as_ref().unwrap().clone();
16893        let payload_keys = first_client.payload_keys.clone();
16894        drop(first_client);
16895
16896        let block = finalized_approve_block(expected_hash, U256::from(1_000u64));
16897        let receipt = receipt_with_transaction_hash(RECEIPT_SUCCESS, expected_hash);
16898        let restart_state = with_finalized_identity(
16899            execution_rpc_state()
16900                .with_response("eth_getTransactionReceipt", &receipt)
16901                .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block)
16902                .with_call_response(ALLOWANCE_SELECTOR, CALL_ALLOWANCE_1000),
16903            &block,
16904            &receipt,
16905        );
16906        let addr = start_mock_rpc_server(restart_state.clone()).await;
16907        let mut restarted = test_client(format!("http://{addr}"));
16908        restarted.cache.database = Some(database);
16909        restarted.payload_keys = payload_keys;
16910        restarted.signer = Some(Arc::new(
16911            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16912        ));
16913
16914        restarted.reconcile_unresolved_execution().await.unwrap();
16915
16916        let record = restarted
16917            .cache
16918            .get_execution_transaction(42161, &expected_hash.to_string())
16919            .await
16920            .unwrap()
16921            .unwrap();
16922        let requests = restart_state.recorded_requests();
16923        assert_eq!(record.status, "finalized");
16924        assert_eq!(record.purpose, "approve");
16925        assert!(restarted.in_flight.lock().is_none());
16926        assert_eq!(
16927            execution_intent_markers(&admin_pool, &schema).await,
16928            vec![("approve".into(), "finalized".into(), true, false)]
16929        );
16930        assert_eq!(
16931            requests
16932                .iter()
16933                .filter(|request| {
16934                    request["method"] == "eth_call"
16935                        && request["params"][0]["data"]
16936                            .as_str()
16937                            .is_some_and(|data| data.starts_with(ALLOWANCE_SELECTOR))
16938                })
16939                .count(),
16940            3
16941        );
16942        assert_eq!(
16943            requests
16944                .iter()
16945                .filter(|request| request["method"] == "eth_sendRawTransaction")
16946                .count(),
16947            0
16948        );
16949
16950        drop_execution_schema(&admin_pool, &schema).await;
16951    }
16952
16953    #[tokio::test]
16954    async fn restart_approve_postcondition_failure_keeps_signer_ownership() {
16955        let initial_state = broadcast_rpc_state()
16956            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
16957            .with_response("eth_call", CALL_BOOL_TRUE)
16958            .with_call_response_sequence(ALLOWANCE_SELECTOR, &[CALL_ZERO; 3])
16959            .with_call_response(ALLOWANCE_SELECTOR, CALL_ZERO);
16960        let Some((admin_pool, schema, mut first_client, _)) =
16961            execution_client_with_database("execution_restart_approve_post_test", initial_state)
16962                .await
16963        else {
16964            return;
16965        };
16966        let expected_hash = expected_approve_tx_hash(U256::from(1_000u64)).await;
16967        let error = first_client
16968            .approve(WETH_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
16969            .await
16970            .unwrap_err();
16971        assert!(
16972            error.to_string().contains("Timed out awaiting finality"),
16973            "was: {error}"
16974        );
16975        let database = first_client.cache.database.as_ref().unwrap().clone();
16976        let payload_keys = first_client.payload_keys.clone();
16977        drop(first_client);
16978
16979        // Call identity matches, but the allowance does not equal the approved amount
16980        let block = finalized_approve_block(expected_hash, U256::from(1_000u64));
16981        let receipt = receipt_with_transaction_hash(RECEIPT_SUCCESS, expected_hash);
16982        let restart_state = with_finalized_identity(
16983            execution_rpc_state()
16984                .with_response("eth_getTransactionReceipt", &receipt)
16985                .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block)
16986                .with_call_response(ALLOWANCE_SELECTOR, CALL_ZERO),
16987            &block,
16988            &receipt,
16989        );
16990        let addr = start_mock_rpc_server(restart_state.clone()).await;
16991        let mut restarted = test_client(format!("http://{addr}"));
16992        restarted.cache.database = Some(database);
16993        restarted.payload_keys = payload_keys;
16994        restarted.signer = Some(Arc::new(
16995            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16996        ));
16997
16998        let error = restarted
16999            .reconcile_unresolved_execution()
17000            .await
17001            .unwrap_err();
17002
17003        assert!(
17004            error
17005                .to_string()
17006                .contains("does not equal the requested amount"),
17007            "was: {error}"
17008        );
17009        let in_flight = awaiting_in_flight(&restarted);
17010        assert_eq!(in_flight.nonce, 7);
17011        assert_eq!(in_flight.purpose, TransactionPurpose::Approve);
17012        assert!(
17013            restart_state
17014                .recorded_requests()
17015                .iter()
17016                .all(|request| request["method"] != "eth_sendRawTransaction")
17017        );
17018        assert_eq!(
17019            execution_intent_markers(&admin_pool, &schema).await,
17020            vec![("approve".into(), "dropped".into(), false, true)]
17021        );
17022
17023        let error = later_reconnect(restarted, format!("http://{addr}")).await;
17024        assert!(
17025            error
17026                .to_string()
17027                .contains("does not equal the requested amount"),
17028            "was: {error}"
17029        );
17030        assert_eq!(
17031            execution_intent_markers(&admin_pool, &schema).await,
17032            vec![("approve".into(), "dropped".into(), false, true)]
17033        );
17034
17035        drop_execution_schema(&admin_pool, &schema).await;
17036    }
17037
17038    #[tokio::test]
17039    async fn disappearing_unfinalized_receipt_drops_without_committing_inclusion() {
17040        let state = execution_rpc_state()
17041            .with_response("eth_getTransactionCount", TRANSACTION_COUNT)
17042            .with_response("eth_estimateGas", ESTIMATE_GAS)
17043            .with_response("eth_call", CALL_BALANCE)
17044            .with_response_sequence(
17045                "eth_getTransactionReceipt",
17046                &[
17047                    RECEIPT_SUCCESS,
17048                    RECEIPT_SUCCESS,
17049                    RECEIPT_SUCCESS,
17050                    RECEIPT_NULL,
17051                    RECEIPT_NULL,
17052                    RECEIPT_NULL,
17053                ],
17054            )
17055            .with_parameter_response("eth_getBlockByNumber", "finalized", BLOCK_BY_NUMBER)
17056            .with_send_raw_transaction_echo();
17057        let Some((admin_pool, schema, mut client, _)) =
17058            execution_client_with_database("execution_receipt_disappeared_test", state).await
17059        else {
17060            return;
17061        };
17062        client.transaction_limits.receipt_timeout_secs = 2;
17063
17064        let error = client
17065            .wrap(U256::from(1_000_000_000_000_000_u64))
17066            .await
17067            .unwrap_err();
17068        let transitions: Vec<String> = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
17069            "SELECT to_status FROM {schema}.execution_transaction_transition ORDER BY id"
17070        )))
17071        .fetch_all(&admin_pool)
17072        .await
17073        .unwrap();
17074
17075        assert!(
17076            error.to_string().contains("Timed out awaiting finality"),
17077            "was: {error}"
17078        );
17079        assert_eq!(transitions, ["prepared", "signed", "broadcast", "dropped"]);
17080        assert!(client.in_flight.lock().is_some());
17081
17082        drop_execution_schema(&admin_pool, &schema).await;
17083    }
17084
17085    #[tokio::test]
17086    async fn changed_unfinalized_block_drops_without_committing_inclusion() {
17087        let changed_block = serde_json::json!({
17088            "jsonrpc": "2.0",
17089            "id": 1,
17090            "result": {
17091                "number": "0x1cf0d41",
17092                "hash": "0x4444444444444444444444444444444444444444444444444444444444444444",
17093                "parentHash": FIXTURE_BLOCK_HASH,
17094                "timestamp": "0x69044a21",
17095                "baseFeePerGas": "0x5f5e100",
17096                "transactions": []
17097            }
17098        })
17099        .to_string();
17100        let state = execution_rpc_state()
17101            .with_response("eth_getTransactionCount", TRANSACTION_COUNT)
17102            .with_response("eth_estimateGas", ESTIMATE_GAS)
17103            .with_response("eth_call", CALL_BALANCE)
17104            .with_response_sequence(
17105                "eth_getTransactionReceipt",
17106                &[
17107                    RECEIPT_SUCCESS,
17108                    RECEIPT_SUCCESS,
17109                    RECEIPT_SUCCESS,
17110                    RECEIPT_SUCCESS,
17111                    RECEIPT_SUCCESS,
17112                    RECEIPT_SUCCESS,
17113                ],
17114            )
17115            .with_parameter_response_sequence(
17116                "eth_getBlockByNumber",
17117                "0x1cf0d41",
17118                &[
17119                    BLOCK_CANONICAL,
17120                    BLOCK_CANONICAL,
17121                    BLOCK_CANONICAL,
17122                    &changed_block,
17123                    &changed_block,
17124                    &changed_block,
17125                ],
17126            )
17127            .with_parameter_response("eth_getBlockByNumber", "finalized", BLOCK_BY_NUMBER)
17128            .with_send_raw_transaction_echo();
17129        let Some((admin_pool, schema, mut client, _)) =
17130            execution_client_with_database("execution_reorg_test", state).await
17131        else {
17132            return;
17133        };
17134        client.transaction_limits.receipt_timeout_secs = 2;
17135
17136        let error = client
17137            .wrap(U256::from(1_000_000_000_000_000_u64))
17138            .await
17139            .unwrap_err();
17140        let transitions: Vec<String> = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
17141            "SELECT to_status FROM {schema}.execution_transaction_transition ORDER BY id"
17142        )))
17143        .fetch_all(&admin_pool)
17144        .await
17145        .unwrap();
17146
17147        assert!(
17148            error.to_string().contains("Timed out awaiting finality"),
17149            "was: {error}"
17150        );
17151        assert_eq!(transitions, ["prepared", "signed", "broadcast", "dropped"]);
17152        assert!(client.in_flight.lock().is_some());
17153
17154        drop_execution_schema(&admin_pool, &schema).await;
17155    }
17156
17157    #[tokio::test]
17158    async fn pre_sign_pending_nonce_drift_blocks_wrap_before_signature() {
17159        let state = execution_rpc_state()
17160            .with_response_sequence(
17161                "eth_getTransactionCount",
17162                &[
17163                    TRANSACTION_COUNT,
17164                    TRANSACTION_COUNT,
17165                    TRANSACTION_COUNT,
17166                    TRANSACTION_COUNT_NEXT,
17167                    TRANSACTION_COUNT_NEXT,
17168                    TRANSACTION_COUNT_NEXT,
17169                ],
17170            )
17171            .with_response("eth_estimateGas", ESTIMATE_GAS)
17172            .with_call_response(BALANCE_OF_SELECTOR, CALL_BALANCE)
17173            .with_send_raw_transaction_echo();
17174        let Some((admin_pool, schema, mut client, state)) =
17175            execution_client_with_database("pre_sign_pending_nonce_drift", state).await
17176        else {
17177            return;
17178        };
17179
17180        let error = client
17181            .wrap(U256::from(1_000_000_000_000_000_u64))
17182            .await
17183            .unwrap_err();
17184        let signed_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
17185            "SELECT COUNT(*) FROM {schema}.execution_transaction_hash"
17186        )))
17187        .fetch_one(&admin_pool)
17188        .await
17189        .unwrap();
17190
17191        assert!(
17192            error
17193                .to_string()
17194                .contains("Pending nonce does not match the verified canonical nonce"),
17195            "was: {error}"
17196        );
17197        assert_eq!(signed_count, 0);
17198        assert!(
17199            state
17200                .recorded_requests()
17201                .iter()
17202                .all(|request| request["method"] != "eth_sendRawTransaction")
17203        );
17204        assert!(client.in_flight.lock().is_none());
17205
17206        drop_execution_schema(&admin_pool, &schema).await;
17207    }
17208
17209    #[tokio::test]
17210    async fn unknown_same_nonce_swap_replacement_emits_no_rejection() {
17211        let replacement_hash = B256::from([0x44; 32]);
17212        let replacement_block = replacement_head_block(replacement_hash);
17213        let state = execution_rpc_state()
17214            .with_parameter_response("eth_getBlockByNumber", "0x1cf0d40", &replacement_block)
17215            .with_send_raw_transaction_echo();
17216        let Some((admin_pool, schema, mut client, _, _)) =
17217            swap_client_with_database("unknown_same_nonce_swap_replacement", state).await
17218        else {
17219            return;
17220        };
17221        let mut receiver = start_with_events(&mut client);
17222        let database = client.cache.database.as_ref().unwrap();
17223        let (intent, original_hash, original_payload) =
17224            persist_test_swap_broadcast(database, client.payload_keys.as_deref()).await;
17225        let authenticated_payloads = HashMap::from([(original_hash, original_payload)]);
17226        let head = VerifiedBlockHeader {
17227            number: FIXTURE_BLOCK,
17228            hash: B256::from_str(FIXTURE_BLOCK_HASH).unwrap(),
17229            parent_hash: B256::from_str(
17230                "0x0000000000000000000000000000000000000000000000000000000000000001",
17231            )
17232            .unwrap(),
17233            timestamp: FIXTURE_BLOCK_TIMESTAMP,
17234            base_fee_per_gas: Some(100_000_000),
17235        };
17236
17237        let error = client
17238            .transaction_executor()
17239            .unwrap()
17240            .scan_canonical_replacement(&intent, 7, head, &authenticated_payloads)
17241            .await
17242            .unwrap_err();
17243        let events = collect_order_events(&mut receiver);
17244        let (status, active): (String, bool) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
17245            "SELECT status, active FROM {schema}.execution_intent"
17246        )))
17247        .fetch_one(&admin_pool)
17248        .await
17249        .unwrap();
17250
17251        assert!(
17252            error
17253                .to_string()
17254                .contains("has no authenticated retained payload"),
17255            "was: {error}"
17256        );
17257        assert!(events.is_empty(), "was: {events:?}");
17258        assert_eq!(status, "broadcast");
17259        assert!(active);
17260
17261        drop_execution_schema(&admin_pool, &schema).await;
17262    }
17263
17264    #[tokio::test]
17265    async fn replacement_scan_rejects_rpc_fields_that_conflict_with_the_payload() {
17266        let expected_hash = expected_wrap_tx_hash(U256::from(1_u64)).await;
17267        let mut replacement_block: serde_json::Value =
17268            serde_json::from_str(&replacement_head_block(expected_hash)).unwrap();
17269        replacement_block["result"]["transactions"][1]["value"] = serde_json::json!("0x2");
17270        let replacement_block = replacement_block.to_string();
17271        let state = execution_rpc_state().with_parameter_response(
17272            "eth_getBlockByNumber",
17273            "0x1cf0d40",
17274            &replacement_block,
17275        );
17276        let Some((admin_pool, schema, client, _)) =
17277            execution_client_with_database("replacement_scan_payload_mismatch", state).await
17278        else {
17279            return;
17280        };
17281        let database = client.cache.database.as_ref().unwrap();
17282        let (intent, persisted_hash, persisted_payload) =
17283            persist_test_wrap_broadcast(database, client.payload_keys.as_deref()).await;
17284        assert_eq!(persisted_hash, expected_hash);
17285        let authenticated_payloads = HashMap::from([(persisted_hash, persisted_payload)]);
17286        let head = VerifiedBlockHeader {
17287            number: FIXTURE_BLOCK,
17288            hash: B256::from_str(FIXTURE_BLOCK_HASH).unwrap(),
17289            parent_hash: B256::from_str(
17290                "0x0000000000000000000000000000000000000000000000000000000000000001",
17291            )
17292            .unwrap(),
17293            timestamp: FIXTURE_BLOCK_TIMESTAMP,
17294            base_fee_per_gas: Some(100_000_000),
17295        };
17296
17297        let error = client
17298            .transaction_executor()
17299            .unwrap()
17300            .scan_canonical_replacement(&intent, 7, head, &authenticated_payloads)
17301            .await
17302            .unwrap_err();
17303        let (status, active): (String, bool) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
17304            "SELECT status, active FROM {schema}.execution_intent"
17305        )))
17306        .fetch_one(&admin_pool)
17307        .await
17308        .unwrap();
17309
17310        assert!(
17311            error
17312                .to_string()
17313                .contains("failed authenticated payload validation"),
17314            "was: {error}"
17315        );
17316        assert_eq!(status, "broadcast");
17317        assert!(active);
17318
17319        drop_execution_schema(&admin_pool, &schema).await;
17320    }
17321
17322    #[tokio::test]
17323    async fn execution_transaction_constraints_reject_conflicting_identity() {
17324        const TRANSACTION_HASH: &str = "0xduplicate-transaction-hash";
17325        const OTHER_WALLET: &str = "0x0000000000000000000000000000000000000001";
17326        let Some((admin_pool, schema, client, _)) = execution_client_with_unprotected_database(
17327            "execution_duplicate_record_test",
17328            ready_rpc_state(),
17329        )
17330        .await
17331        else {
17332            return;
17333        };
17334        let database = client.cache.database.as_ref().unwrap();
17335        let operator = database
17336            .reserve_execution_intent(&ExecutionIntentInsert {
17337                chain_id: 42161,
17338                wallet_address: WALLET.to_string(),
17339                purpose: "wrap".to_string(),
17340                client_order_id: None,
17341                trader_id: None,
17342                strategy_id: None,
17343                account_id: None,
17344                instrument_id: None,
17345                pool_address: None,
17346                transaction_to: WETH_ADDRESS.to_string(),
17347                transaction_input: "0xd0e30db0".to_string(),
17348                transaction_value: "1".to_string(),
17349                amount_in: None,
17350                created_block: FIXTURE_BLOCK,
17351            })
17352            .await
17353            .unwrap();
17354        database
17355            .assign_execution_intent_nonce(operator.id, 7)
17356            .await
17357            .unwrap();
17358        database
17359            .add_execution_transaction_hash(operator.id, 42161, TRANSACTION_HASH, &[1, 2, 3])
17360            .await
17361            .unwrap();
17362        database
17363            .add_execution_transaction_hash(operator.id, 42161, TRANSACTION_HASH, &[1, 2, 3])
17364            .await
17365            .unwrap();
17366
17367        let signer_conflict = database
17368            .reserve_execution_intent(&ExecutionIntentInsert {
17369                chain_id: 42161,
17370                wallet_address: WALLET.to_string(),
17371                purpose: "approve".to_string(),
17372                client_order_id: None,
17373                trader_id: None,
17374                strategy_id: None,
17375                account_id: None,
17376                instrument_id: None,
17377                pool_address: None,
17378                transaction_to: ROUTER_ADDRESS.to_string(),
17379                transaction_input: "0x01".to_string(),
17380                transaction_value: "0".to_string(),
17381                amount_in: None,
17382                created_block: FIXTURE_BLOCK,
17383            })
17384            .await
17385            .unwrap_err();
17386        let other = database
17387            .reserve_execution_intent(&ExecutionIntentInsert {
17388                chain_id: 42161,
17389                wallet_address: OTHER_WALLET.to_string(),
17390                purpose: "approve".to_string(),
17391                client_order_id: None,
17392                trader_id: None,
17393                strategy_id: None,
17394                account_id: None,
17395                instrument_id: None,
17396                pool_address: None,
17397                transaction_to: ROUTER_ADDRESS.to_string(),
17398                transaction_input: "0x01".to_string(),
17399                transaction_value: "0".to_string(),
17400                amount_in: None,
17401                created_block: FIXTURE_BLOCK,
17402            })
17403            .await
17404            .unwrap();
17405        database
17406            .assign_execution_intent_nonce(other.id, 7)
17407            .await
17408            .unwrap();
17409        let hash_conflict = database
17410            .add_execution_transaction_hash(other.id, 42161, TRANSACTION_HASH, &[4, 5, 6])
17411            .await
17412            .unwrap_err();
17413
17414        let record = database
17415            .get_execution_transaction(42161, TRANSACTION_HASH)
17416            .await
17417            .unwrap()
17418            .unwrap();
17419        let count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
17420            "SELECT COUNT(*) FROM {schema}.execution_intent"
17421        )))
17422        .fetch_one(&admin_pool)
17423        .await
17424        .unwrap();
17425
17426        assert_eq!(record.wallet_address.as_deref(), Some(WALLET));
17427        assert_eq!(record.nonce, 7);
17428        assert_eq!(record.transaction_hash, TRANSACTION_HASH);
17429        assert_eq!(record.purpose, "wrap");
17430        assert_eq!(record.status, "signed");
17431        assert_eq!(record.client_order_id, None);
17432        assert!(
17433            hash_conflict
17434                .to_string()
17435                .contains("conflicts with its persisted identity"),
17436            "was: {hash_conflict}"
17437        );
17438        assert_eq!(
17439            signer_conflict.to_string(),
17440            "Execution intent reservation failed before commit"
17441        );
17442        assert!(
17443            signer_conflict.chain().any(|cause| cause
17444                .to_string()
17445                .contains("execution_intent_active_signer_key")),
17446            "was: {signer_conflict:#}"
17447        );
17448        assert_eq!(count, 2);
17449
17450        drop_execution_schema(&admin_pool, &schema).await;
17451    }
17452
17453    #[tokio::test]
17454    async fn execution_status_transitions_are_idempotent() {
17455        const TRANSACTION_HASH: &str =
17456            "0x5555555555555555555555555555555555555555555555555555555555555555";
17457        let Some((admin_pool, schema, client, _)) = execution_client_with_unprotected_database(
17458            "execution_transition_test",
17459            ready_rpc_state(),
17460        )
17461        .await
17462        else {
17463            return;
17464        };
17465        let database = client.cache.database.as_ref().unwrap();
17466        let intent = database
17467            .reserve_execution_intent(&ExecutionIntentInsert {
17468                chain_id: 42161,
17469                wallet_address: WALLET.to_string(),
17470                purpose: "wrap".to_string(),
17471                client_order_id: None,
17472                trader_id: None,
17473                strategy_id: None,
17474                account_id: None,
17475                instrument_id: None,
17476                pool_address: None,
17477                transaction_to: WETH_ADDRESS.to_string(),
17478                transaction_input: "0xd0e30db0".to_string(),
17479                transaction_value: "1".to_string(),
17480                amount_in: None,
17481                created_block: FIXTURE_BLOCK,
17482            })
17483            .await
17484            .unwrap();
17485        database
17486            .assign_execution_intent_nonce(intent.id, 7)
17487            .await
17488            .unwrap();
17489        database
17490            .add_execution_transaction_hash(intent.id, 42161, TRANSACTION_HASH, &[1, 2, 3])
17491            .await
17492            .unwrap();
17493
17494        for _ in 0..2 {
17495            database
17496                .record_execution_status(
17497                    intent.id,
17498                    TRANSACTION_HASH,
17499                    TransactionStatus::Broadcast,
17500                    None,
17501                    None,
17502                    None,
17503                    None,
17504                    None,
17505                )
17506                .await
17507                .unwrap();
17508        }
17509
17510        for status in [TransactionStatus::Included, TransactionStatus::Included] {
17511            database
17512                .record_execution_status(
17513                    intent.id,
17514                    TRANSACTION_HASH,
17515                    status,
17516                    Some(FIXTURE_BLOCK + 1),
17517                    Some("0x2222222222222222222222222222222222222222222222222222222222222222"),
17518                    Some(true),
17519                    Some(50_112),
17520                    Some("100000000"),
17521                )
17522                .await
17523                .unwrap();
17524        }
17525
17526        for _ in 0..2 {
17527            database
17528                .record_execution_status(
17529                    intent.id,
17530                    TRANSACTION_HASH,
17531                    TransactionStatus::Finalized,
17532                    Some(FIXTURE_BLOCK + 1),
17533                    Some("0x2222222222222222222222222222222222222222222222222222222222222222"),
17534                    Some(true),
17535                    Some(50_112),
17536                    Some("100000000"),
17537                )
17538                .await
17539                .unwrap();
17540        }
17541
17542        let transitions: Vec<String> = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
17543            "SELECT to_status FROM {schema}.execution_transaction_transition ORDER BY id"
17544        )))
17545        .fetch_all(&admin_pool)
17546        .await
17547        .unwrap();
17548        let (status, active): (String, bool) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
17549            "SELECT status, active FROM {schema}.execution_intent"
17550        )))
17551        .fetch_one(&admin_pool)
17552        .await
17553        .unwrap();
17554        let append_only_error = sqlx::query(sqlx::AssertSqlSafe(format!(
17555            "DELETE FROM {schema}.execution_transaction_transition WHERE intent_id = {}",
17556            intent.id
17557        )))
17558        .execute(&admin_pool)
17559        .await
17560        .unwrap_err();
17561
17562        assert_eq!(
17563            transitions,
17564            ["prepared", "signed", "broadcast", "included", "finalized"]
17565        );
17566        assert_eq!(status, "finalized");
17567        assert!(active);
17568        assert!(
17569            append_only_error
17570                .to_string()
17571                .contains("Execution transitions are append-only"),
17572            "was: {append_only_error}"
17573        );
17574
17575        drop_execution_schema(&admin_pool, &schema).await;
17576    }
17577
17578    #[tokio::test]
17579    async fn wrap_then_approve_persists_records_and_clears_in_flight() {
17580        let state = execution_rpc_state()
17581            .with_parameter_response_sequence(
17582                "eth_getBlockByNumber",
17583                "latest",
17584                &[
17585                    BLOCK_BY_NUMBER,
17586                    BLOCK_BY_NUMBER,
17587                    BLOCK_BY_NUMBER,
17588                    BLOCK_FINALIZED,
17589                    BLOCK_FINALIZED,
17590                    BLOCK_FINALIZED,
17591                ],
17592            )
17593            .with_response_sequence(
17594                "eth_call",
17595                &[
17596                    CALL_BALANCE,
17597                    CALL_BALANCE,
17598                    CALL_BALANCE,
17599                    CALL_BALANCE,
17600                    CALL_BALANCE,
17601                    CALL_BALANCE,
17602                    CALL_BALANCE_AFTER_WRAP,
17603                    CALL_BALANCE_AFTER_WRAP,
17604                    CALL_BALANCE_AFTER_WRAP,
17605                    CALL_BOOL_TRUE,
17606                    CALL_BOOL_TRUE,
17607                    CALL_BOOL_TRUE,
17608                ],
17609            )
17610            .with_call_response_sequence(
17611                ALLOWANCE_SELECTOR,
17612                &[
17613                    CALL_ZERO,
17614                    CALL_ZERO,
17615                    CALL_ZERO,
17616                    CALL_ALLOWANCE_MAX,
17617                    CALL_ALLOWANCE_MAX,
17618                    CALL_ALLOWANCE_MAX,
17619                ],
17620            )
17621            .with_response_sequence(
17622                "eth_getTransactionCount",
17623                &[
17624                    TRANSACTION_COUNT,
17625                    TRANSACTION_COUNT,
17626                    TRANSACTION_COUNT,
17627                    TRANSACTION_COUNT,
17628                    TRANSACTION_COUNT,
17629                    TRANSACTION_COUNT,
17630                    TRANSACTION_COUNT_NEXT,
17631                    TRANSACTION_COUNT_NEXT,
17632                    TRANSACTION_COUNT_NEXT,
17633                    TRANSACTION_COUNT_NEXT,
17634                    TRANSACTION_COUNT_NEXT,
17635                    TRANSACTION_COUNT_NEXT,
17636                ],
17637            )
17638            .with_response("eth_estimateGas", ESTIMATE_GAS)
17639            .with_response_sequence(
17640                "eth_getTransactionReceipt",
17641                &[
17642                    RECEIPT_SUCCESS,
17643                    RECEIPT_SUCCESS,
17644                    RECEIPT_SUCCESS,
17645                    RECEIPT_SUCCESS,
17646                    RECEIPT_SUCCESS,
17647                    RECEIPT_SUCCESS,
17648                ],
17649            )
17650            .with_send_raw_transaction_echo();
17651        let Some((admin_pool, schema, mut client, state)) =
17652            execution_client_with_database("execution_client_test", state).await
17653        else {
17654            return;
17655        };
17656        client.config.unlimited_approval = true;
17657        client.transaction_limits.receipt_timeout_secs = 3;
17658
17659        let wrap_hash = client
17660            .wrap(U256::from(1_000_000_000_000_000u64))
17661            .await
17662            .unwrap();
17663
17664        let record = client
17665            .cache
17666            .get_execution_transaction(42161, &wrap_hash.to_string())
17667            .await
17668            .unwrap()
17669            .unwrap();
17670        assert_eq!(record.nonce, 7);
17671        assert_eq!(record.purpose, "wrap");
17672        assert_eq!(record.status, "finalized");
17673        assert_eq!(
17674            execution_intent_markers(&admin_pool, &schema).await,
17675            vec![("wrap".into(), "finalized".into(), true, false)]
17676        );
17677
17678        let approve_hash = client
17679            .approve(WETH_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
17680            .await
17681            .unwrap();
17682
17683        let record = client
17684            .cache
17685            .get_execution_transaction(42161, &approve_hash.to_string())
17686            .await
17687            .unwrap()
17688            .unwrap();
17689        assert_eq!(record.nonce, 8);
17690        assert_eq!(record.purpose, "approve");
17691        assert_eq!(record.status, "finalized");
17692        assert_eq!(
17693            execution_intent_markers(&admin_pool, &schema).await,
17694            vec![
17695                ("wrap".into(), "finalized".into(), true, false),
17696                ("approve".into(), "finalized".into(), true, false),
17697            ]
17698        );
17699
17700        let requests = state.recorded_requests();
17701        let broadcasts: Vec<_> = requests
17702            .iter()
17703            .filter(|request| request["method"] == "eth_sendRawTransaction")
17704            .collect();
17705        assert_eq!(broadcasts.len(), 2);
17706        for broadcast in &broadcasts {
17707            let payload = broadcast["params"][0].as_str().unwrap();
17708            assert!(payload.starts_with("0x02"), "was: {payload}");
17709        }
17710        let broadcast_indexes = requests
17711            .iter()
17712            .enumerate()
17713            .filter_map(|(index, request)| {
17714                (request["method"] == "eth_sendRawTransaction").then_some(index)
17715            })
17716            .collect::<Vec<_>>();
17717
17718        for (index, expected_block) in broadcast_indexes
17719            .into_iter()
17720            .zip([FIXTURE_BLOCK_PARAM, "0x1cf0d42"])
17721        {
17722            let nonce_fence = &requests[index - 6..index];
17723            assert!(nonce_fence[..3].iter().all(|request| {
17724                request["method"] == "eth_getTransactionCount"
17725                    && request["params"][1] == expected_block
17726            }));
17727            assert!(nonce_fence[3..].iter().all(|request| {
17728                request["method"] == "eth_getTransactionCount" && request["params"][1] == "pending"
17729            }));
17730        }
17731        let receipt_polls = requests
17732            .iter()
17733            .filter(|request| request["method"] == "eth_getTransactionReceipt")
17734            .count();
17735        assert_eq!(receipt_polls, 6);
17736        let allowance_calls: Vec<_> = requests
17737            .iter()
17738            .filter(|request| {
17739                request["method"] == "eth_call"
17740                    && request["params"][0]["data"]
17741                        .as_str()
17742                        .is_some_and(|data| data.starts_with(ALLOWANCE_SELECTOR))
17743            })
17744            .collect();
17745        assert_eq!(allowance_calls.len(), 6);
17746        assert!(
17747            allowance_calls[..3]
17748                .iter()
17749                .all(|request| request["params"][1] == "0x1cf0d42")
17750        );
17751        assert!(
17752            allowance_calls[3..]
17753                .iter()
17754                .all(|request| request["params"][1] == "0x1cf0d41")
17755        );
17756
17757        // Unlimited approval policy encoded U256::MAX in the approve calldata
17758        let estimates: Vec<_> = requests
17759            .iter()
17760            .filter(|request| request["method"] == "eth_estimateGas")
17761            .collect();
17762        assert_eq!(estimates.len(), 6);
17763        assert!(estimates[..3].iter().all(|request| {
17764            request["params"].as_array().unwrap().len() == 2
17765                && request["params"][1] == FIXTURE_BLOCK_PARAM
17766        }));
17767        assert!(estimates[3..].iter().all(|request| {
17768            request["params"].as_array().unwrap().len() == 2 && request["params"][1] == "0x1cf0d42"
17769        }));
17770        let approve_data = estimates[3]["params"][0]["data"].as_str().unwrap();
17771        assert!(
17772            approve_data.starts_with("0x095ea7b3"),
17773            "was: {approve_data}"
17774        );
17775        assert!(
17776            approve_data.ends_with(&"f".repeat(64)),
17777            "was: {approve_data}"
17778        );
17779
17780        for selector in [FACTORY_SELECTOR, WETH9_SELECTOR] {
17781            let calls: Vec<_> = requests
17782                .iter()
17783                .filter(|request| {
17784                    request["method"] == "eth_call"
17785                        && request["params"][0]["data"]
17786                            .as_str()
17787                            .is_some_and(|data| data.starts_with(selector))
17788                })
17789                .collect();
17790            assert!(!calls.is_empty(), "selector {selector}");
17791            assert!(
17792                calls.iter().all(|request| request["params"][1]
17793                    .as_str()
17794                    .is_some_and(|block| block.starts_with("0x"))),
17795                "selector {selector}: {calls:?}"
17796            );
17797        }
17798        let latest_blocks = requests
17799            .iter()
17800            .filter(|request| {
17801                request["method"] == "eth_getBlockByNumber"
17802                    && request["params"] == serde_json::json!(["latest", false])
17803            })
17804            .count();
17805        assert_eq!(latest_blocks, 6);
17806
17807        drop_execution_schema(&admin_pool, &schema).await;
17808    }
17809
17810    #[tokio::test]
17811    async fn reverted_receipt_marks_record_reverted_and_errors() {
17812        let state = signing_rpc_state()
17813            .with_response("eth_getTransactionReceipt", RECEIPT_REVERTED)
17814            .with_send_raw_transaction_echo();
17815        let Some((admin_pool, schema, mut client, _)) =
17816            execution_client_with_database("execution_reverted_test", state).await
17817        else {
17818            return;
17819        };
17820
17821        let error = client
17822            .wrap(U256::from(1_000_000_000_000_000u64))
17823            .await
17824            .unwrap_err();
17825
17826        assert!(
17827            error.to_string().contains("reverted on-chain"),
17828            "was: {error}"
17829        );
17830        assert!(client.in_flight.lock().is_none());
17831
17832        let expected_hash = expected_wrap_tx_hash(U256::from(1_000_000_000_000_000u64)).await;
17833
17834        let record = client
17835            .cache
17836            .get_execution_transaction(42161, &expected_hash.to_string())
17837            .await
17838            .unwrap()
17839            .unwrap();
17840        assert_eq!(record.status, "reverted");
17841        assert_eq!(
17842            execution_intent_markers(&admin_pool, &schema).await,
17843            vec![("wrap".into(), "reverted".into(), true, false)]
17844        );
17845
17846        drop_execution_schema(&admin_pool, &schema).await;
17847    }
17848
17849    fn market_sell_order_with_id(instrument_id: InstrumentId, client_order_id: &str) -> OrderAny {
17850        OrderTestBuilder::new(OrderType::Market)
17851            .trader_id(TraderId::from("TRADER-001"))
17852            .strategy_id(StrategyId::from("S-001"))
17853            .instrument_id(instrument_id)
17854            .client_order_id(ClientOrderId::from(client_order_id))
17855            .side(OrderSide::Sell)
17856            .quantity(Quantity::from("0.001"))
17857            .build()
17858    }
17859
17860    fn market_buy_order_with_id(instrument_id: InstrumentId, client_order_id: &str) -> OrderAny {
17861        OrderTestBuilder::new(OrderType::Market)
17862            .trader_id(TraderId::from("TRADER-001"))
17863            .strategy_id(StrategyId::from("S-001"))
17864            .instrument_id(instrument_id)
17865            .client_order_id(ClientOrderId::from(client_order_id))
17866            .side(OrderSide::Buy)
17867            .quantity(Quantity::from("0.001"))
17868            .build()
17869    }
17870
17871    fn submit_order_list_cmd(orders: &[OrderAny]) -> SubmitOrderList {
17872        let order_list = OrderList::new(
17873            OrderListId::from("OL-001"),
17874            orders[0].instrument_id(),
17875            orders[0].strategy_id(),
17876            orders.iter().map(|order| order.client_order_id()).collect(),
17877            UnixNanos::default(),
17878        );
17879        SubmitOrderList::new(
17880            TraderId::from("TRADER-001"),
17881            Some(ClientId::from("BLOCKCHAIN-001")),
17882            orders[0].strategy_id(),
17883            order_list,
17884            orders
17885                .iter()
17886                .map(|order| order.init_event().clone())
17887                .collect(),
17888            None,
17889            None,
17890            None,
17891            UUID4::new(),
17892            UnixNanos::default(),
17893            None,
17894        )
17895    }
17896
17897    fn modify_order_cmd(
17898        instrument_id: InstrumentId,
17899        client_order_id: ClientOrderId,
17900    ) -> ModifyOrder {
17901        ModifyOrder::new(
17902            TraderId::from("TRADER-001"),
17903            Some(ClientId::from("BLOCKCHAIN-001")),
17904            StrategyId::from("S-001"),
17905            instrument_id,
17906            client_order_id,
17907            None,
17908            Some(Quantity::from("0.002")),
17909            None,
17910            None,
17911            UUID4::new(),
17912            UnixNanos::default(),
17913            None,
17914            None,
17915        )
17916    }
17917
17918    fn cancel_order_cmd(
17919        instrument_id: InstrumentId,
17920        client_order_id: ClientOrderId,
17921    ) -> CancelOrder {
17922        CancelOrder::new(
17923            TraderId::from("TRADER-001"),
17924            Some(ClientId::from("BLOCKCHAIN-001")),
17925            StrategyId::from("S-001"),
17926            instrument_id,
17927            client_order_id,
17928            None,
17929            UUID4::new(),
17930            UnixNanos::default(),
17931            None,
17932            None,
17933        )
17934    }
17935
17936    fn cancel_all_orders_cmd(instrument_id: InstrumentId) -> CancelAllOrders {
17937        CancelAllOrders::new(
17938            TraderId::from("TRADER-001"),
17939            Some(ClientId::from("BLOCKCHAIN-001")),
17940            StrategyId::from("S-001"),
17941            instrument_id,
17942            Some(OrderSide::Sell),
17943            UUID4::new(),
17944            UnixNanos::default(),
17945            None,
17946            None,
17947        )
17948    }
17949
17950    fn batch_cancel_orders_cmd(cancels: Vec<CancelOrder>) -> BatchCancelOrders {
17951        BatchCancelOrders::new(
17952            TraderId::from("TRADER-001"),
17953            Some(ClientId::from("BLOCKCHAIN-001")),
17954            StrategyId::from("S-001"),
17955            cancels[0].instrument_id,
17956            cancels,
17957            UUID4::new(),
17958            UnixNanos::default(),
17959            None,
17960            None,
17961        )
17962    }
17963
17964    fn query_order_cmd(instrument_id: InstrumentId, client_order_id: ClientOrderId) -> QueryOrder {
17965        QueryOrder::new(
17966            TraderId::from("TRADER-001"),
17967            Some(ClientId::from("BLOCKCHAIN-001")),
17968            StrategyId::from("S-001"),
17969            instrument_id,
17970            client_order_id,
17971            None,
17972            UUID4::new(),
17973            UnixNanos::default(),
17974            None,
17975            None,
17976        )
17977    }
17978
17979    async fn unsupported_client_with_mock_rpc()
17980    -> (BlockchainExecutionClient, MockRpcState, Rc<RefCell<Cache>>) {
17981        let state = ready_rpc_state();
17982        let addr = start_mock_rpc_server(state.clone()).await;
17983        let (client, cache) = swap_client_with_cache(test_config(format!("http://{addr}")));
17984        (client, state, cache)
17985    }
17986
17987    #[tokio::test]
17988    async fn submit_order_denies_reduce_only_without_side_effects() {
17989        let (mut client, state, cache) = unsupported_client_with_mock_rpc().await;
17990        let order = OrderTestBuilder::new(OrderType::Market)
17991            .trader_id(TraderId::from("TRADER-001"))
17992            .strategy_id(StrategyId::from("S-001"))
17993            .instrument_id(test_pool().instrument_id)
17994            .client_order_id(ClientOrderId::from("O-REDUCE-ONLY"))
17995            .side(OrderSide::Sell)
17996            .quantity(Quantity::from("0.001"))
17997            .reduce_only(true)
17998            .build();
17999        cache
18000            .borrow_mut()
18001            .add_order(order.clone(), None, None, false)
18002            .unwrap();
18003        let mut receiver = start_with_events(&mut client);
18004
18005        client.submit_order(submit_order_cmd(&order)).unwrap();
18006
18007        let events = collect_order_events(&mut receiver);
18008        assert_eq!(events.len(), 1, "was: {events:?}");
18009        let OrderEventAny::Denied(denied) = &events[0] else {
18010            panic!("expected OrderDenied, was {:?}", events[0]);
18011        };
18012        assert_eq!(denied.client_order_id, order.client_order_id());
18013        assert_eq!(denied.reason, "UNSUPPORTED_REDUCE_ONLY");
18014        assert!(state.recorded_requests().is_empty());
18015        assert!(client.in_flight.lock().is_none());
18016    }
18017
18018    #[tokio::test]
18019    async fn submit_order_list_denies_every_order_without_side_effects() {
18020        let (mut client, state, cache) = unsupported_client_with_mock_rpc().await;
18021        let pool = test_pool();
18022        let first = test_market_sell_order(pool.instrument_id);
18023        let second = market_sell_order_with_id(pool.instrument_id, "O-SWAP-002");
18024        cache
18025            .borrow_mut()
18026            .add_order(second.clone(), None, None, false)
18027            .unwrap();
18028        let orders = [first.clone(), second.clone()];
18029        let mut receiver = start_with_events(&mut client);
18030
18031        client
18032            .submit_order_list(submit_order_list_cmd(&orders))
18033            .unwrap();
18034
18035        let events = collect_order_events(&mut receiver);
18036        let mut denied_ids = Vec::new();
18037
18038        for event in &events {
18039            let OrderEventAny::Denied(denied) = event else {
18040                panic!("expected OrderDenied, was {event:?}");
18041            };
18042            assert_eq!(denied.reason, ORDER_LIST_UNSUPPORTED);
18043            denied_ids.push(denied.client_order_id);
18044        }
18045        denied_ids.sort();
18046        assert_eq!(
18047            denied_ids,
18048            [first.client_order_id(), second.client_order_id()]
18049        );
18050        assert!(state.recorded_requests().is_empty());
18051        assert!(client.in_flight.lock().is_none());
18052
18053        for order in &orders {
18054            let cache_ref = cache.borrow();
18055            let cached = cache_ref.order(&order.client_order_id()).unwrap();
18056            assert_eq!(cached.status(), OrderStatus::Initialized);
18057        }
18058    }
18059
18060    #[tokio::test]
18061    async fn modify_order_rejects_without_side_effects() {
18062        let (mut client, state, cache) = unsupported_client_with_mock_rpc().await;
18063        let order = test_market_sell_order(test_pool().instrument_id);
18064        let mut receiver = start_with_events(&mut client);
18065
18066        client
18067            .modify_order(modify_order_cmd(
18068                order.instrument_id(),
18069                order.client_order_id(),
18070            ))
18071            .unwrap();
18072
18073        let events = collect_order_events(&mut receiver);
18074        assert_eq!(events.len(), 1, "was: {events:?}");
18075        let OrderEventAny::ModifyRejected(rejected) = &events[0] else {
18076            panic!("expected OrderModifyRejected, was {:?}", events[0]);
18077        };
18078        assert_eq!(rejected.client_order_id, order.client_order_id());
18079        assert_eq!(rejected.reason, ORDER_MODIFY_UNSUPPORTED);
18080        assert!(state.recorded_requests().is_empty());
18081        assert!(client.in_flight.lock().is_none());
18082        let cache_ref = cache.borrow();
18083        let cached = cache_ref.order(&order.client_order_id()).unwrap();
18084        assert_eq!(cached.status(), OrderStatus::Initialized);
18085    }
18086
18087    #[tokio::test]
18088    async fn cancel_order_rejects_without_side_effects() {
18089        let (mut client, state, cache) = unsupported_client_with_mock_rpc().await;
18090        let order = test_market_sell_order(test_pool().instrument_id);
18091        let mut receiver = start_with_events(&mut client);
18092
18093        client
18094            .cancel_order(cancel_order_cmd(
18095                order.instrument_id(),
18096                order.client_order_id(),
18097            ))
18098            .unwrap();
18099
18100        let events = collect_order_events(&mut receiver);
18101        assert_eq!(events.len(), 1, "was: {events:?}");
18102        let OrderEventAny::CancelRejected(rejected) = &events[0] else {
18103            panic!("expected OrderCancelRejected, was {:?}", events[0]);
18104        };
18105        assert_eq!(rejected.client_order_id, order.client_order_id());
18106        assert_eq!(rejected.reason, ORDER_CANCEL_UNSUPPORTED);
18107        assert!(state.recorded_requests().is_empty());
18108        assert!(client.in_flight.lock().is_none());
18109        let cache_ref = cache.borrow();
18110        let cached = cache_ref.order(&order.client_order_id()).unwrap();
18111        assert_eq!(cached.status(), OrderStatus::Initialized);
18112    }
18113
18114    #[tokio::test]
18115    async fn batch_cancel_orders_rejects_each_order_without_side_effects() {
18116        let (mut client, state, cache) = unsupported_client_with_mock_rpc().await;
18117        let pool = test_pool();
18118        let first = test_market_sell_order(pool.instrument_id);
18119        let second = market_sell_order_with_id(pool.instrument_id, "O-SWAP-002");
18120        cache
18121            .borrow_mut()
18122            .add_order(second.clone(), None, None, false)
18123            .unwrap();
18124        let orders = [first.clone(), second.clone()];
18125        let cancels = orders
18126            .iter()
18127            .map(|order| cancel_order_cmd(order.instrument_id(), order.client_order_id()))
18128            .collect();
18129        let mut receiver = start_with_events(&mut client);
18130
18131        client
18132            .batch_cancel_orders(batch_cancel_orders_cmd(cancels))
18133            .unwrap();
18134
18135        let events = collect_order_events(&mut receiver);
18136        let mut rejected_ids = Vec::new();
18137
18138        for event in &events {
18139            let OrderEventAny::CancelRejected(rejected) = event else {
18140                panic!("expected OrderCancelRejected, was {event:?}");
18141            };
18142            assert_eq!(rejected.reason, ORDER_CANCEL_UNSUPPORTED);
18143            rejected_ids.push(rejected.client_order_id);
18144        }
18145        rejected_ids.sort();
18146        assert_eq!(
18147            rejected_ids,
18148            [first.client_order_id(), second.client_order_id()]
18149        );
18150        assert!(state.recorded_requests().is_empty());
18151        assert!(client.in_flight.lock().is_none());
18152    }
18153
18154    #[tokio::test]
18155    async fn cancel_all_and_query_order_log_unsupported_without_side_effects() {
18156        let (mut client, state, cache) = unsupported_client_with_mock_rpc().await;
18157        let order = test_market_sell_order(test_pool().instrument_id);
18158        let mut receiver = start_with_events(&mut client);
18159
18160        client
18161            .cancel_all_orders(cancel_all_orders_cmd(order.instrument_id()))
18162            .unwrap();
18163        client
18164            .query_order(query_order_cmd(
18165                order.instrument_id(),
18166                order.client_order_id(),
18167            ))
18168            .unwrap();
18169
18170        let events = collect_order_events(&mut receiver);
18171        assert!(events.is_empty(), "was: {events:?}");
18172        assert!(state.recorded_requests().is_empty());
18173        assert!(client.in_flight.lock().is_none());
18174        let cache_ref = cache.borrow();
18175        let cached = cache_ref.order(&order.client_order_id()).unwrap();
18176        assert_eq!(cached.status(), OrderStatus::Initialized);
18177    }
18178
18179    #[tokio::test]
18180    async fn unsupported_commands_handle_unknown_orders_without_panic() {
18181        let (mut client, state, _) = unsupported_client_with_mock_rpc().await;
18182        let instrument_id = test_pool().instrument_id;
18183        let unknown = ClientOrderId::from("O-UNKNOWN");
18184        let mut receiver = start_with_events(&mut client);
18185
18186        client
18187            .modify_order(modify_order_cmd(instrument_id, unknown))
18188            .unwrap();
18189        client
18190            .cancel_order(cancel_order_cmd(instrument_id, unknown))
18191            .unwrap();
18192        client
18193            .batch_cancel_orders(batch_cancel_orders_cmd(vec![cancel_order_cmd(
18194                instrument_id,
18195                unknown,
18196            )]))
18197            .unwrap();
18198        client
18199            .query_order(query_order_cmd(instrument_id, unknown))
18200            .unwrap();
18201
18202        let events = collect_order_events(&mut receiver);
18203        assert!(events.is_empty(), "was: {events:?}");
18204        assert!(state.recorded_requests().is_empty());
18205        assert!(client.in_flight.lock().is_none());
18206    }
18207
18208    #[tokio::test]
18209    async fn report_generators_error_except_mass_status_without_side_effects() {
18210        let (client, state, _) = unsupported_client_with_mock_rpc().await;
18211
18212        let report = client
18213            .generate_order_status_report(&GenerateOrderStatusReport::new(
18214                UUID4::new(),
18215                UnixNanos::default(),
18216                None,
18217                None,
18218                None,
18219                None,
18220                None,
18221            ))
18222            .await
18223            .unwrap_err();
18224        assert_eq!(report.to_string(), VENUE_EXECUTION_REPORTS_UNSUPPORTED);
18225
18226        let reports = client
18227            .generate_order_status_reports(&GenerateOrderStatusReports::new(
18228                UUID4::new(),
18229                UnixNanos::default(),
18230                false,
18231                None,
18232                None,
18233                None,
18234                None,
18235                None,
18236            ))
18237            .await
18238            .unwrap_err();
18239        assert_eq!(reports.to_string(), VENUE_EXECUTION_REPORTS_UNSUPPORTED);
18240
18241        let fills = client
18242            .generate_fill_reports(GenerateFillReports::new(
18243                UUID4::new(),
18244                UnixNanos::default(),
18245                None,
18246                None,
18247                None,
18248                None,
18249                None,
18250                None,
18251            ))
18252            .await
18253            .unwrap_err();
18254        assert_eq!(fills.to_string(), VENUE_EXECUTION_REPORTS_UNSUPPORTED);
18255
18256        let positions = client
18257            .generate_position_status_reports(&GeneratePositionStatusReports::new(
18258                UUID4::new(),
18259                UnixNanos::default(),
18260                None,
18261                None,
18262                None,
18263                None,
18264                None,
18265            ))
18266            .await
18267            .unwrap_err();
18268        assert_eq!(positions.to_string(), VENUE_EXECUTION_REPORTS_UNSUPPORTED);
18269
18270        let mass_status = client.generate_mass_status(None).await.unwrap();
18271        assert!(mass_status.is_none());
18272
18273        let mass_status = client.generate_mass_status(Some(60)).await.unwrap();
18274        assert!(mass_status.is_none());
18275
18276        assert!(state.recorded_requests().is_empty());
18277        assert!(client.in_flight.lock().is_none());
18278    }
18279
18280    async fn connect_test_postgres(
18281        test_name: &str,
18282    ) -> Option<(sqlx::PgPool, PostgresConnectOptions)> {
18283        let pg_config = get_postgres_connect_options(None, None, None, None, None);
18284        let admin_options: sqlx::postgres::PgConnectOptions = pg_config.clone().into();
18285        let admin_pool = match PgPoolOptions::new()
18286            .max_connections(1)
18287            .connect_with(admin_options)
18288            .await
18289        {
18290            Ok(pool) => pool,
18291            Err(e) => {
18292                eprintln!("Postgres unavailable; skipping {test_name} test: {e}");
18293                return None;
18294            }
18295        };
18296
18297        Some((admin_pool, pg_config))
18298    }
18299
18300    async fn execution_client_with_database(
18301        test_name: &str,
18302        state: MockRpcState,
18303    ) -> Option<(
18304        sqlx::PgPool,
18305        String,
18306        BlockchainExecutionClient,
18307        MockRpcState,
18308    )> {
18309        let (admin_pool, schema, mut client, state) =
18310            execution_client_with_unprotected_database(test_name, state).await?;
18311        protect_test_storage(&mut client, &schema).await;
18312        Some((admin_pool, schema, client, state))
18313    }
18314
18315    async fn execution_client_with_unprotected_database(
18316        test_name: &str,
18317        state: MockRpcState,
18318    ) -> Option<(
18319        sqlx::PgPool,
18320        String,
18321        BlockchainExecutionClient,
18322        MockRpcState,
18323    )> {
18324        let (admin_pool, pg_config) = connect_test_postgres(test_name).await?;
18325        let schema = format!("{test_name}_{}", std::process::id());
18326        setup_execution_schema(&admin_pool, &schema).await;
18327
18328        let db_options: sqlx::postgres::PgConnectOptions = pg_config.into();
18329        let db_options = db_options.options([("search_path", schema.clone())]);
18330        let database = connect_test_database(db_options).await.unwrap();
18331        let addr = start_mock_rpc_server(state.clone()).await;
18332        let mut client = test_client(format!("http://{addr}"));
18333        client.cache.database = Some(database);
18334        // Mirror the connect-time migration: tests create the pre-submission table shape
18335        client
18336            .cache
18337            .ensure_execution_transaction_schema()
18338            .await
18339            .unwrap();
18340        initialize_test_verification_ledger(&client).await;
18341        client.signer = Some(Arc::new(
18342            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
18343        ));
18344        client.core.set_connected();
18345
18346        Some((admin_pool, schema, client, state))
18347    }
18348
18349    async fn install_reservation_commit_rejection(admin_pool: &sqlx::PgPool, schema: &str) {
18350        for statement in [
18351            format!(
18352                "CREATE FUNCTION {schema}.reject_execution_reservation_commit() RETURNS trigger \
18353                 LANGUAGE plpgsql AS 'BEGIN RAISE EXCEPTION ''test reservation commit rejection''; \
18354                 RETURN NEW; END'"
18355            ),
18356            format!(
18357                "CREATE CONSTRAINT TRIGGER reject_execution_reservation_commit AFTER INSERT ON \
18358                 {schema}.execution_transaction_transition DEFERRABLE INITIALLY DEFERRED \
18359                 FOR EACH ROW EXECUTE FUNCTION {schema}.reject_execution_reservation_commit()"
18360            ),
18361        ] {
18362            sqlx::query(sqlx::AssertSqlSafe(statement))
18363                .execute(admin_pool)
18364                .await
18365                .unwrap();
18366        }
18367    }
18368
18369    async fn install_recoverable_commit_rejection(admin_pool: &sqlx::PgPool, schema: &str) {
18370        for statement in [
18371            format!(
18372                "CREATE FUNCTION {schema}.reject_recoverable_commit() RETURNS trigger \
18373                 LANGUAGE plpgsql AS 'BEGIN IF NEW.transition_key = ''recoverable'' THEN \
18374                 RAISE EXCEPTION ''test recoverable commit rejection''; END IF; \
18375                 RETURN NEW; END'"
18376            ),
18377            format!(
18378                "CREATE CONSTRAINT TRIGGER reject_recoverable_commit AFTER INSERT ON \
18379                 {schema}.execution_transaction_transition DEFERRABLE INITIALLY DEFERRED \
18380                 FOR EACH ROW EXECUTE FUNCTION {schema}.reject_recoverable_commit()"
18381            ),
18382        ] {
18383            sqlx::query(sqlx::AssertSqlSafe(statement))
18384                .execute(admin_pool)
18385                .await
18386                .unwrap();
18387        }
18388    }
18389
18390    async fn drop_execution_schema(admin_pool: &sqlx::PgPool, schema: &str) {
18391        sqlx::query(sqlx::AssertSqlSafe(format!("DROP SCHEMA {schema} CASCADE")))
18392            .execute(admin_pool)
18393            .await
18394            .unwrap();
18395    }
18396
18397    async fn setup_execution_schema(admin_pool: &sqlx::PgPool, schema: &str) {
18398        for statement in [
18399            format!("CREATE SCHEMA {schema}"),
18400            format!(
18401                r#"CREATE TABLE {schema}."chain" (chain_id INTEGER PRIMARY KEY, name TEXT NOT NULL)"#
18402            ),
18403            format!(r#"INSERT INTO {schema}."chain" (chain_id, name) VALUES (42161, 'Arbitrum')"#),
18404            format!(
18405                r#"CREATE TABLE {schema}."execution_transaction" (
18406                    id BIGSERIAL PRIMARY KEY,
18407                    chain_id INTEGER NOT NULL REFERENCES {schema}."chain"(chain_id) ON DELETE CASCADE,
18408                    nonce BIGINT NOT NULL,
18409                    transaction_hash TEXT NOT NULL,
18410                    purpose TEXT NOT NULL,
18411                    status TEXT NOT NULL,
18412                    UNIQUE (chain_id, transaction_hash)
18413                )"#
18414            ),
18415        ] {
18416            sqlx::query(sqlx::AssertSqlSafe(statement))
18417                .execute(admin_pool)
18418                .await
18419                .unwrap();
18420        }
18421    }
18422
18423    fn execution_transaction_create_sql() -> &'static str {
18424        const TABLES_SQL: &str = include_str!("../../../../../schema/sql/tables.sql");
18425        const START: &str = "CREATE TABLE IF NOT EXISTS \"execution_transaction\"";
18426        let start = TABLES_SQL
18427            .find(START)
18428            .expect("execution_transaction table is missing from tables.sql");
18429        let statement = &TABLES_SQL[start..];
18430        let end = statement
18431            .find(";\n")
18432            .expect("execution_transaction CREATE TABLE is unterminated")
18433            + 1;
18434        &statement[..end]
18435    }
18436}