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 selections contain duplicates, transaction limits are missing or
165    /// invalid, independent verification is missing or conflicts with the configured chain,
166    /// the deployment manifest, provider topology, a configured address, or a token pair is invalid,
167    /// the router allowlist is 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        config.validate()?;
173
174        let transaction_limits = Self::transaction_limits(&config)?;
175        let chain = Arc::new(config.chain.clone());
176        let cache = BlockchainCache::new(Arc::clone(&chain));
177        let http_rpc_client = Arc::new(BlockchainHttpRpcClient::new(
178            config.http_rpc_url.clone().into_inner(),
179            config.rpc_requests_per_second,
180            None,
181        )?);
182
183        let verification_config = config.verification.as_ref().ok_or_else(|| {
184            anyhow::anyhow!("Independent Blockchain execution verification is required")
185        })?;
186        anyhow::ensure!(
187            verification_config.chain_anchor.chain_id == config.chain.chain_id,
188            "Verification chain anchor ID does not match the configured chain"
189        );
190        anyhow::ensure!(
191            verification_config.chain_anchor.chain_name == config.chain.name.to_string(),
192            "Verification chain anchor name does not match the configured chain"
193        );
194        let verification = VerificationCoordinator::new(
195            Arc::clone(&http_rpc_client),
196            config.http_rpc_url.expose_secret(),
197            verification_config,
198            config.rpc_requests_per_second,
199        )?;
200        let wallet_address = validate_address(config.wallet_address.as_str())?;
201        let erc20_contract = Erc20Contract::new_with_timeout(
202            Arc::clone(&http_rpc_client),
203            Some(EXECUTION_RPC_TIMEOUT_SECS),
204            true,
205        );
206
207        let router_addresses = config
208            .router_addresses
209            .iter()
210            .map(|address| validate_address(address.as_str()))
211            .collect::<anyhow::Result<Vec<_>>>()?;
212        if router_addresses.is_empty() {
213            anyhow::bail!("`router_addresses` must contain at least one router address");
214        }
215        let weth_address = validate_address(config.weth_address.as_str())?;
216        Self::validate_manifest_contracts(&config, &router_addresses, weth_address)?;
217
218        let mut token_universe = HashSet::new();
219
220        if let Some(specified_tokens) = &config.tokens {
221            for token in specified_tokens {
222                let token_address = validate_address(token.as_str())?;
223                token_universe.insert(token_address);
224            }
225        }
226        let wallet_balance = WalletBalance::new(token_universe);
227        let emitter = ExecutionEventEmitter::new(
228            get_atomic_clock_realtime(),
229            core_client.trader_id,
230            core_client.account_id,
231            core_client.account_type,
232            core_client.base_currency,
233        );
234
235        let pending_tasks = TaskGroup::new();
236
237        Ok(Self {
238            core: core_client,
239            emitter,
240            wallet_balance: Arc::new(Mutex::new(wallet_balance)),
241            chain,
242            cache,
243            config,
244            signer: None,
245            payload_keys: None,
246            router_addresses,
247            transaction_limits,
248            weth_address,
249            in_flight: Arc::new(Mutex::new(None)),
250            erc20_contract,
251            http_rpc_client,
252            verification,
253            wallet_address,
254            pending_tasks,
255        })
256    }
257
258    fn transaction_limits(
259        config: &BlockchainExecutionClientConfig,
260    ) -> anyhow::Result<TransactionLimits> {
261        let (
262            Some(allowed_token_pairs),
263            Some(slippage_bps),
264            Some(max_slippage_bps),
265            Some(max_order_amount),
266            Some(deadline_seconds),
267            Some(max_quote_age_blocks),
268            Some(receipt_timeout_secs),
269        ) = (
270            &config.allowed_token_pairs,
271            config.slippage_bps,
272            config.max_slippage_bps,
273            config.max_order_amount,
274            config.deadline_seconds,
275            config.max_quote_age_blocks,
276            config.receipt_timeout_secs,
277        )
278        else {
279            anyhow::bail!(
280                "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"
281            );
282        };
283
284        let mut parsed_pairs = HashSet::with_capacity(allowed_token_pairs.len());
285        for (token_in, token_out) in allowed_token_pairs {
286            parsed_pairs.insert((
287                validate_address(token_in.as_str())?,
288                validate_address(token_out.as_str())?,
289            ));
290        }
291
292        let quote_spend_limits = config.quote_spend_limits.as_deref().unwrap_or_default();
293        let mut parsed_quote_spend_limits = HashMap::with_capacity(quote_spend_limits.len());
294        for limit in quote_spend_limits {
295            let token_in = validate_address(limit.token_in.as_str())?;
296            let token_out = validate_address(limit.token_out.as_str())?;
297            let spend_token = validate_address(limit.spend_token.as_str())?;
298
299            if !parsed_pairs.contains(&(token_in, token_out)) {
300                anyhow::bail!(
301                    "Quote spend limit pair {token_in} -> {token_out} is not in the `allowed_token_pairs` allowlist"
302                );
303            }
304
305            if spend_token != token_in {
306                anyhow::bail!(
307                    "Quote spend limit for {token_in} -> {token_out} is denominated in {spend_token}; `spend_token` must match `token_in`"
308                );
309            }
310
311            if limit.max_amount.is_empty()
312                || !limit.max_amount.bytes().all(|byte| byte.is_ascii_digit())
313            {
314                anyhow::bail!(
315                    "Quote spend limit `max_amount` '{}' must be a base-10 unsigned integer string",
316                    limit.max_amount
317                );
318            }
319            let max_amount = U256::from_str(&limit.max_amount).map_err(|_| {
320                anyhow::anyhow!(
321                    "Quote spend limit `max_amount` '{}' exceeds the U256 range",
322                    limit.max_amount
323                )
324            })?;
325            let ceiling = QuoteSpendCeiling {
326                spend_token,
327                spend_token_decimals: limit.spend_token_decimals,
328                max_amount,
329            };
330
331            if parsed_quote_spend_limits
332                .insert((token_in, token_out), ceiling)
333                .is_some()
334            {
335                anyhow::bail!(
336                    "Duplicate quote spend limit for token pair {token_in} -> {token_out}"
337                );
338            }
339        }
340
341        if slippage_bps > max_slippage_bps {
342            anyhow::bail!(
343                "`slippage_bps` {slippage_bps} exceeds `max_slippage_bps` {max_slippage_bps}"
344            );
345        }
346
347        if max_slippage_bps >= BPS_DENOMINATOR {
348            anyhow::bail!("`max_slippage_bps` {max_slippage_bps} must be below {BPS_DENOMINATOR}");
349        }
350
351        if !(1..=4_095).contains(&max_quote_age_blocks) {
352            anyhow::bail!("`max_quote_age_blocks` must be in 1..=4095");
353        }
354
355        Ok(TransactionLimits {
356            allowed_token_pairs: parsed_pairs,
357            quote_spend_limits: parsed_quote_spend_limits,
358            slippage_bps,
359            max_slippage_bps,
360            max_order_amount,
361            deadline_seconds,
362            max_quote_age_blocks,
363            receipt_timeout_secs,
364        })
365    }
366
367    fn validate_manifest_contracts(
368        config: &BlockchainExecutionClientConfig,
369        routers: &[Address],
370        weth: Address,
371    ) -> anyhow::Result<()> {
372        let verification = config.verification.as_ref().ok_or_else(|| {
373            anyhow::anyhow!("Independent Blockchain execution verification is required")
374        })?;
375        let manifest = &verification.deployment_manifest;
376        let role_addresses = |role| {
377            manifest
378                .contracts
379                .iter()
380                .filter(|contract| contract.role == role)
381                .map(|contract| {
382                    Address::from_str(&contract.address)
383                        .map_err(|_| anyhow::anyhow!("Deployment manifest address is invalid"))
384                })
385                .collect::<anyhow::Result<HashSet<_>>>()
386        };
387        let singleton = |role, description: &str| {
388            let addresses = role_addresses(role)?;
389            anyhow::ensure!(
390                addresses.len() == 1,
391                "Deployment manifest must contain exactly one {description} contract"
392            );
393            Ok(*addresses.iter().next().expect("singleton role address"))
394        };
395
396        let configured_routers = routers.iter().copied().collect::<HashSet<_>>();
397        anyhow::ensure!(
398            role_addresses(BlockchainContractRole::Router)? == configured_routers,
399            "Deployment manifest router set does not match `router_addresses`"
400        );
401        anyhow::ensure!(
402            singleton(BlockchainContractRole::WrappedNative, "wrapped native")? == weth,
403            "Deployment manifest wrapped native contract does not match `weth_address`"
404        );
405        let factory = singleton(BlockchainContractRole::Factory, "factory")?;
406        let registered_factory =
407            crate::exchanges::get_dex_extended(config.chain.name, &DexType::UniswapV3)
408                .map(|dex| dex.factory)
409                .ok_or_else(|| {
410                    anyhow::anyhow!(
411                        "No registered Uniswap V3 deployment for chain {}",
412                        config.chain.name
413                    )
414                })?;
415        anyhow::ensure!(
416            factory == registered_factory,
417            "Deployment manifest factory does not match the registered Uniswap V3 factory"
418        );
419        let quote_contract = singleton(BlockchainContractRole::Quote, "quote")?;
420
421        let mut token_decimals = HashMap::new();
422
423        for token in &manifest.tokens {
424            let address = Address::from_str(&token.address)
425                .map_err(|_| anyhow::anyhow!("Deployment manifest token address is invalid"))?;
426            anyhow::ensure!(
427                token_decimals.insert(address, token.decimals).is_none(),
428                "Deployment manifest contains a duplicate token identity"
429            );
430        }
431        anyhow::ensure!(
432            token_decimals.contains_key(&weth),
433            "Deployment manifest has no wrapped native token identity"
434        );
435
436        if let Some(tokens) = &config.tokens {
437            for token in tokens {
438                let address = validate_address(token)?;
439                anyhow::ensure!(
440                    token_decimals.contains_key(&address),
441                    "Configured token {address} has no deployment manifest identity"
442                );
443            }
444        }
445
446        for (token_in, token_out) in config.allowed_token_pairs.as_deref().unwrap_or_default() {
447            let token_in = validate_address(token_in)?;
448            let token_out = validate_address(token_out)?;
449            anyhow::ensure!(
450                token_in != token_out
451                    && token_decimals.contains_key(&token_in)
452                    && token_decimals.contains_key(&token_out),
453                "Allowed token pair {token_in} -> {token_out} is not fully pinned by the deployment manifest"
454            );
455        }
456
457        for limit in config.quote_spend_limits.as_deref().unwrap_or_default() {
458            let spend_token = validate_address(&limit.spend_token)?;
459            anyhow::ensure!(
460                token_decimals.get(&spend_token) == Some(&limit.spend_token_decimals),
461                "Quote spend limit decimals do not match the deployment manifest"
462            );
463        }
464
465        let pool_contracts = role_addresses(BlockchainContractRole::Pool)?;
466
467        for pool in &manifest.pools {
468            let pool_address = Address::from_str(&pool.address)
469                .map_err(|_| anyhow::anyhow!("Deployment manifest pool address is invalid"))?;
470            let pool_factory = Address::from_str(&pool.factory)
471                .map_err(|_| anyhow::anyhow!("Deployment manifest pool factory is invalid"))?;
472            let pool_quote = Address::from_str(&pool.quote_contract).map_err(|_| {
473                anyhow::anyhow!("Deployment manifest pool quote contract is invalid")
474            })?;
475            anyhow::ensure!(
476                pool_contracts.contains(&pool_address)
477                    && pool_factory == factory
478                    && pool_quote == quote_contract,
479                "Deployment manifest pool does not use the pinned pool, factory, and quote identities"
480            );
481        }
482        Ok(())
483    }
484
485    async fn fetch_native_currency_balance(&self) -> anyhow::Result<Money> {
486        let balance_u256 = self
487            .http_rpc_client
488            .get_balance_with_timeout(&self.wallet_address, None, Some(EXECUTION_RPC_TIMEOUT_SECS))
489            .await?;
490
491        let native_currency = self.chain.native_currency();
492
493        Money::from_u256(balance_u256, native_currency).map_err(Into::into)
494    }
495
496    async fn fetch_token_balance(
497        &mut self,
498        token_address: &Address,
499    ) -> anyhow::Result<TokenBalance> {
500        let token = if let Some(token) = self.cache.get_token(token_address) {
501            token.to_owned()
502        } else {
503            let token_info = self.erc20_contract.fetch_token_info(token_address).await?;
504            let token = Token::new(
505                Arc::clone(&self.chain),
506                *token_address,
507                token_info.name,
508                token_info.symbol,
509                token_info.decimals,
510            );
511            self.cache.add_token(token.clone()).await?;
512            token
513        };
514
515        let amount = self
516            .erc20_contract
517            .balance_of(token_address, &self.wallet_address)
518            .await?;
519        let token_balance = TokenBalance::new(amount, token);
520
521        // TODO: Use price oracle here and cache, to get the latest price then convert to USD
522        // then use token_balance.set_amount_usd(amount_usd) to set the amount_usd value.
523
524        Ok(token_balance)
525    }
526
527    /// Refreshes and publishes all native currency and tracked ERC-20 balances.
528    async fn refresh_wallet_balances(&mut self) -> anyhow::Result<()> {
529        let (wallet_balance, balances) = self.fetch_wallet_balances().await?;
530        self.generate_account_state(
531            balances,
532            vec![],
533            true,
534            get_atomic_clock_realtime().get_time_ns(),
535            None,
536        )?;
537        *self.wallet_balance.lock() = wallet_balance;
538        Ok(())
539    }
540
541    async fn fetch_wallet_balances(
542        &mut self,
543    ) -> anyhow::Result<(WalletBalance, Vec<AccountBalance>)> {
544        let native_currency_balance = self.fetch_native_currency_balance().await?;
545        let token_universe = self.wallet_balance.lock().token_universe.clone();
546        let mut token_addresses = token_universe.iter().copied().collect::<Vec<_>>();
547        token_addresses.sort_unstable();
548
549        let mut token_balances = Vec::with_capacity(token_addresses.len());
550        for token_address in token_addresses {
551            let token_balance = self
552                .fetch_token_balance(&token_address)
553                .await
554                .with_context(|| format!("failed to fetch token balance for {token_address}"))?;
555            token_balances.push(token_balance);
556        }
557
558        let mut wallet_balance = WalletBalance::new(token_universe);
559        let balances = wallet_balance.replace_balances(native_currency_balance, token_balances)?;
560        log::debug!(
561            "Refreshed wallet balance with {} account balances",
562            balances.len()
563        );
564        Ok((wallet_balance, balances))
565    }
566
567    /// Runs a read-only execution preflight for the pool selected by `instrument_id`.
568    ///
569    /// Verifies the RPC chain ID against configuration, deployed bytecode at the router, pool,
570    /// and token addresses, wallet native and token balances, exact router allowance of the
571    /// pool's base (input) token, and current fee conditions. Changes no state.
572    ///
573    /// # Errors
574    ///
575    /// Returns an error if the instrument ID cannot be resolved to a cached Uniswap V3 pool
576    /// on the client's chain, or if any RPC call fails. On-chain readiness failures are
577    /// reported in the returned [`BlockchainPreflightReport`], not as errors.
578    pub async fn preflight(
579        &self,
580        instrument_id: &InstrumentId,
581    ) -> anyhow::Result<BlockchainPreflightReport> {
582        let pool = self.resolve_pool(instrument_id)?;
583        let base_token = pool.get_base_token().clone();
584        let quote_token = pool.get_quote_token().clone();
585
586        let actual_chain_id = self.http_rpc_client.chain_id().await?;
587
588        let pool_code = self.http_rpc_client.get_code(&pool.address).await?;
589        let pool_check = PoolPreflightCheck {
590            instrument_id: pool.instrument_id,
591            address: pool.address,
592            has_deployed_code: !pool_code.is_empty(),
593            fee: pool.fee,
594            base_token: base_token.address,
595            quote_token: quote_token.address,
596        };
597
598        let mut routers = Vec::with_capacity(self.router_addresses.len());
599        for router in &self.router_addresses {
600            let code = self.http_rpc_client.get_code(router).await?;
601            routers.push(ContractCodeCheck {
602                address: *router,
603                has_deployed_code: !code.is_empty(),
604            });
605        }
606
607        let mut tokens = Vec::with_capacity(2);
608
609        for token in [&base_token, &quote_token] {
610            let code = self.http_rpc_client.get_code(&token.address).await?;
611            let wallet_balance = self
612                .erc20_contract
613                .balance_of(&token.address, &self.wallet_address)
614                .await?;
615
616            let mut router_allowances = Vec::new();
617            if token.address == base_token.address {
618                router_allowances.reserve(self.router_addresses.len());
619                for router in &self.router_addresses {
620                    let amount = self
621                        .erc20_contract
622                        .allowance(&token.address, &self.wallet_address, router)
623                        .await?;
624                    router_allowances.push((*router, amount));
625                }
626            }
627
628            tokens.push(TokenPreflightCheck {
629                address: token.address,
630                symbol: token.symbol.clone(),
631                has_deployed_code: !code.is_empty(),
632                wallet_balance,
633                router_allowances,
634            });
635        }
636
637        let native_balance_wei = self
638            .http_rpc_client
639            .get_balance_with_timeout(&self.wallet_address, None, Some(EXECUTION_RPC_TIMEOUT_SECS))
640            .await?;
641
642        let latest_block = self.http_rpc_client.latest_block().await?;
643        let base_fee_per_gas_wei = latest_block.base_fee_per_gas.ok_or_else(|| {
644            anyhow::anyhow!("Latest block {} has no base fee", latest_block.number)
645        })?;
646        let max_priority_fee_per_gas_wei = self.http_rpc_client.max_priority_fee_per_gas().await?;
647        let derived_max_fee_per_gas_wei = compute_max_fee(
648            base_fee_per_gas_wei,
649            max_priority_fee_per_gas_wei,
650            self.config.base_fee_buffer_bps,
651        )?;
652
653        Ok(BlockchainPreflightReport::new(
654            u64::from(self.chain.chain_id),
655            actual_chain_id,
656            pool_check,
657            routers,
658            tokens,
659            native_balance_wei,
660            base_fee_per_gas_wei,
661            max_priority_fee_per_gas_wei,
662            derived_max_fee_per_gas_wei,
663            u128::from(self.config.max_fee_per_gas_wei),
664        ))
665    }
666
667    /// Wraps native currency into the wrapped native token via a WETH `deposit()`
668    /// transaction carrying `amount_wei` of native value.
669    ///
670    /// This is an explicit operator operation; it never runs inside `submit_order`.
671    ///
672    /// # Errors
673    ///
674    /// Returns an error if the amount is zero, the WETH target is not a deployed ERC-20 contract,
675    /// the client is not connected, another transaction is in flight, no durable store is
676    /// configured, the WETH balance does not increase by `amount_wei`, or any RPC, policy, signing,
677    /// persistence, or broadcast step fails. A persistence failure after signing, or a failed
678    /// postcondition after finality, leaves the in-flight slot occupied.
679    #[allow(
680        clippy::large_futures,
681        reason = "keeps transaction verification futures inline without additional boxing"
682    )]
683    pub async fn wrap(&mut self, amount_wei: U256) -> anyhow::Result<B256> {
684        if amount_wei.is_zero() {
685            anyhow::bail!("Wrap amount must be positive");
686        }
687
688        self.ensure_transaction_ready(TransactionPurpose::Wrap)?;
689
690        let calldata = WETH9::depositCall {}.abi_encode();
691        let executor = self.transaction_executor()?;
692        let included = executor
693            .transact(
694                self.weth_address,
695                amount_wei,
696                Bytes::from(calldata),
697                TransactionPurpose::Wrap,
698                None,
699                TransactionAuthorization::Wrap {
700                    weth: self.weth_address,
701                },
702            )
703            .await?;
704        let postconditions =
705            verify_wrap_balance_increase(&executor, &self.weth_address, amount_wei, &included)
706                .await?;
707        executor
708            .commit_verified_finality(&included, TransactionStatus::Finalized, &postconditions)
709            .await?;
710        executor
711            .database
712            .mark_execution_event_emitted(included.intent_id, "terminal")
713            .await?;
714        executor.release_slot();
715
716        Ok(included.tx_hash)
717    }
718
719    /// Approves an allowlisted SwapRouter to spend `amount` of `token` via an ERC-20
720    /// `approve` transaction. Zero always revokes the allowance. For nonzero requests,
721    /// `unlimited_approval` changes the target to `U256::MAX`.
722    ///
723    /// This is an explicit operator operation; it never runs inside `submit_order`.
724    ///
725    /// # Errors
726    ///
727    /// Returns an error if the router or token fails policy and deployment checks, a nonzero
728    /// allowance was not cleared first, approval simulation returns false or malformed data, the
729    /// client is not connected, another transaction is in flight, no durable store is configured,
730    /// the resulting allowance differs from the target, or any RPC, signing, persistence, or
731    /// broadcast step fails. A persistence failure after signing, or a failed postcondition after
732    /// finality, leaves the in-flight slot occupied.
733    #[allow(
734        clippy::large_futures,
735        reason = "keeps transaction verification futures inline without additional boxing"
736    )]
737    pub async fn approve(
738        &mut self,
739        token: Address,
740        amount: U256,
741        router: Address,
742    ) -> anyhow::Result<B256> {
743        if !self.router_addresses.contains(&router) {
744            anyhow::bail!("Router {router} is not in the configured `router_addresses` allowlist");
745        }
746
747        if !amount.is_zero()
748            && !self
749                .transaction_limits
750                .allowed_token_pairs
751                .iter()
752                .any(|(token_in, _)| *token_in == token)
753        {
754            anyhow::bail!(
755                "Token {token} is not an input token in the configured `allowed_token_pairs`"
756            );
757        }
758
759        self.ensure_transaction_ready(TransactionPurpose::Approve)?;
760
761        let approval_amount = if amount.is_zero() {
762            U256::ZERO
763        } else if self.config.unlimited_approval {
764            U256::MAX
765        } else {
766            amount
767        };
768        let calldata = ERC20::approveCall {
769            spender: router,
770            amount: approval_amount,
771        }
772        .abi_encode();
773
774        let executor = self.transaction_executor()?;
775        let included = executor
776            .transact(
777                token,
778                U256::ZERO,
779                Bytes::from(calldata),
780                TransactionPurpose::Approve,
781                None,
782                TransactionAuthorization::Approve {
783                    token,
784                    router,
785                    amount: approval_amount,
786                },
787            )
788            .await?;
789        let postconditions =
790            verify_approve_allowance(&executor, &token, &router, approval_amount, &included)
791                .await?;
792        executor
793            .commit_verified_finality(&included, TransactionStatus::Finalized, &postconditions)
794            .await?;
795        executor
796            .database
797            .mark_execution_event_emitted(included.intent_id, "terminal")
798            .await?;
799        executor.release_slot();
800
801        Ok(included.tx_hash)
802    }
803
804    fn uniswap_v3_factory(&self) -> anyhow::Result<Address> {
805        crate::exchanges::get_dex_extended(self.chain.name, &DexType::UniswapV3)
806            .map(|dex| dex.factory)
807            .ok_or_else(|| {
808                anyhow::anyhow!(
809                    "No registered Uniswap V3 deployment for chain {}",
810                    self.chain.name
811                )
812            })
813    }
814
815    fn resolve_pool(&self, instrument_id: &InstrumentId) -> anyhow::Result<Pool> {
816        let (blockchain, dex_type) = instrument_id.venue.parse_dex()?;
817        if blockchain != self.chain.name {
818            anyhow::bail!(
819                "Pool venue chain {blockchain} does not match the client chain {}",
820                self.chain.name
821            );
822        }
823
824        if dex_type != DexType::UniswapV3 {
825            anyhow::bail!("Unsupported DEX type {dex_type}; only UniswapV3 is supported");
826        }
827
828        let pool_identifier = PoolIdentifier::new_checked(instrument_id.symbol.as_str())?;
829        if !pool_identifier.is_address() {
830            anyhow::bail!(
831                "Pool identifier {pool_identifier} is a pool ID; only address identifiers are supported"
832            );
833        }
834
835        let pool = self
836            .core
837            .cache()
838            .pool(instrument_id)
839            .cloned()
840            .ok_or_else(|| {
841                anyhow::anyhow!(
842                    "Unknown pool {instrument_id}; not found in the shared engine cache"
843                )
844            })?;
845
846        if pool.token0.get_token_priority() == pool.token1.get_token_priority() {
847            anyhow::bail!(
848                "Pool {instrument_id} tokens share a token priority; base and quote orientation is ambiguous"
849            );
850        }
851
852        Ok(pool)
853    }
854
855    /// Authenticates every persisted signed transaction in this execution database.
856    ///
857    /// Run this while the execution client is disconnected. The check takes a stable table lock,
858    /// reads in bounded batches, and returns counts, deployment identity, key IDs, and database
859    /// roles with direct ownership or `SELECT` grants.
860    ///
861    /// # Errors
862    ///
863    /// Returns an error if storage state is inconsistent, keys are missing, or a payload cannot be
864    /// opened and authenticated against its durable intent.
865    pub async fn check_payload_storage(
866        &self,
867        batch_size: usize,
868    ) -> anyhow::Result<PayloadStorageCheck> {
869        let batch_size = validate_payload_operation_batch_size(batch_size)?;
870        let database = self.payload_operation_database().await?;
871        let keys = self.load_payload_keys()?;
872        database
873            .check_execution_payload_storage(keys.as_ref(), None, batch_size)
874            .await
875            .map(Into::into)
876    }
877
878    /// Activates or resumes protected signed-transaction storage for this execution database.
879    ///
880    /// Run this while the execution client is disconnected. A full payload check must succeed
881    /// before a later execution connection is permitted.
882    ///
883    /// # Errors
884    ///
885    /// Returns an error if the client is connected, an active key or deployment identity is
886    /// unavailable, or any stored payload cannot be migrated and authenticated.
887    pub async fn protect_payload_storage(&self) -> anyhow::Result<()> {
888        let database = self.payload_operation_database().await?;
889        database.ensure_execution_transaction_schema().await?;
890        let keys = self
891            .load_payload_keys()?
892            .ok_or_else(|| anyhow::anyhow!("Payload protection requires an active payload key"))?;
893        database.ensure_execution_payload_storage(&keys).await
894    }
895
896    /// Rewraps all protected payloads in this execution database with the configured active key.
897    ///
898    /// The prior active key must remain configured as a retired key until this method and a
899    /// subsequent full check both succeed.
900    ///
901    /// # Errors
902    ///
903    /// Returns an error if the client is connected, storage is not protected, required keys are
904    /// unavailable, or any bounded rewrap batch fails authentication.
905    pub async fn rewrap_payload_storage(&self, batch_size: usize) -> anyhow::Result<()> {
906        let batch_size = validate_payload_operation_batch_size(batch_size)?;
907        let database = self.payload_operation_database().await?;
908        let keys = self
909            .load_payload_keys()?
910            .ok_or_else(|| anyhow::anyhow!("Payload rewrap requires an active payload key"))?;
911        database
912            .rewrap_execution_payload_storage(&keys, batch_size)
913            .await
914    }
915
916    /// Restores authenticated plaintext payloads and removes protection from this database.
917    ///
918    /// This incident-only operation is resumable. Keep the complete key set configured until it
919    /// succeeds and the unprotected database passes a full payload check.
920    ///
921    /// # Errors
922    ///
923    /// Returns an error if the client is connected, storage is not protected, required keys are
924    /// unavailable, or any bounded rollback batch fails authentication.
925    pub async fn rollback_payload_storage(&self, batch_size: usize) -> anyhow::Result<()> {
926        let batch_size = validate_payload_operation_batch_size(batch_size)?;
927        let database = self.payload_operation_database().await?;
928        let keys = self
929            .load_payload_keys()?
930            .ok_or_else(|| anyhow::anyhow!("Payload rollback requires an active payload key"))?;
931        database
932            .rollback_execution_payload_storage(&keys, batch_size)
933            .await
934    }
935
936    async fn payload_operation_database(&self) -> anyhow::Result<BlockchainCacheDatabase> {
937        anyhow::ensure!(
938            !self.core.is_connected(),
939            "Disconnect the execution client before payload storage operations"
940        );
941
942        if let Some(database) = &self.cache.database {
943            return Ok(database.clone());
944        }
945        let options = self
946            .config
947            .postgres_cache_database_config
948            .as_ref()
949            .ok_or_else(|| anyhow::anyhow!("No Postgres cache database is configured"))?;
950        BlockchainCacheDatabase::connect(options.clone().into())
951            .await
952            .context("failed to connect to the execution database")
953    }
954
955    fn load_payload_keys(&self) -> anyhow::Result<Option<PayloadKeySet>> {
956        PayloadKeySet::load(
957            self.config.payload_key_env.as_deref(),
958            &self.config.payload_key_retired_env,
959            self.config.payload_deployment_id.as_deref(),
960        )
961    }
962
963    fn payload_policy(&self) -> PayloadPolicy {
964        PayloadPolicy {
965            chain_id: self.chain.chain_id,
966            signer: self.wallet_address,
967            gas_limit: self.config.gas_limit,
968            max_fee_per_gas: self.config.max_fee_per_gas_wei,
969        }
970    }
971
972    fn transaction_executor(&self) -> anyhow::Result<TransactionExecutor> {
973        let database = self.cache.database.clone().ok_or_else(|| {
974            anyhow::anyhow!("No durable store configured; refusing to submit a transaction")
975        })?;
976        let signer = self
977            .signer
978            .clone()
979            .ok_or_else(|| anyhow::anyhow!("Signer not initialized; connect the client first"))?;
980        let payload_keys = self.payload_keys.clone().ok_or_else(|| {
981            anyhow::anyhow!("Protected payload keys are not initialized; connect the client first")
982        })?;
983        let verification_config = self
984            .config
985            .verification
986            .as_ref()
987            .expect("verification config validated at construction");
988        let identities = std::iter::once(&verification_config.authoritative)
989            .chain(
990                verification_config
991                    .verifiers
992                    .iter()
993                    .map(|provider| &provider.identity),
994            )
995            .collect::<Vec<_>>();
996
997        Ok(TransactionExecutor {
998            http_rpc_client: Arc::clone(&self.http_rpc_client),
999            verification: self.verification.clone(),
1000            manifest_version: verification_config.manifest_version.clone(),
1001            manifest_digest: verification_config.manifest_digest.clone(),
1002            deployment_manifest: Arc::new(verification_config.deployment_manifest.clone()),
1003            provider_ids: identities
1004                .iter()
1005                .map(|identity| identity.provider_id.clone())
1006                .collect(),
1007            operator_ids: identities
1008                .iter()
1009                .map(|identity| identity.operator_id.clone())
1010                .collect(),
1011            failure_domain_ids: identities
1012                .iter()
1013                .flat_map(|identity| identity.failure_domain_ids.iter().cloned())
1014                .collect(),
1015            database,
1016            signer,
1017            payload_keys,
1018            in_flight: Arc::clone(&self.in_flight),
1019            wallet_balance: Arc::clone(&self.wallet_balance),
1020            account_id: self.core.account_id,
1021            wallet_address: self.wallet_address,
1022            chain_id: self.chain.chain_id,
1023            max_fee_per_gas_wei: self.config.max_fee_per_gas_wei,
1024            base_fee_buffer_bps: self.config.base_fee_buffer_bps,
1025            gas_limit: self.config.gas_limit,
1026            gas_buffer_bps: self.config.gas_buffer_bps,
1027            receipt_timeout: receipt_timeout(self.transaction_limits.receipt_timeout_secs),
1028            receipt_max_polls: receipt_max_polls(self.transaction_limits.receipt_timeout_secs),
1029        })
1030    }
1031
1032    fn restore_swap_plan(&self, intent: &ExecutionIntentRow) -> anyhow::Result<SwapPlan> {
1033        let client_order_id = ClientOrderId::new_checked(
1034            intent
1035                .client_order_id
1036                .as_deref()
1037                .ok_or_else(|| anyhow::anyhow!("Persisted swap intent has no client order ID"))?,
1038        )?;
1039        let order = self
1040            .core
1041            .cache()
1042            .try_order_owned(&client_order_id)
1043            .with_context(|| {
1044                format!(
1045                    "Cannot reconcile swap intent {} because order {client_order_id} is not restored",
1046                    intent.id
1047                )
1048            })?;
1049        let instrument_id = InstrumentId::from_str(
1050            intent
1051                .instrument_id
1052                .as_deref()
1053                .ok_or_else(|| anyhow::anyhow!("Persisted swap intent has no instrument ID"))?,
1054        )?;
1055        anyhow::ensure!(
1056            order.instrument_id() == instrument_id,
1057            "Persisted swap instrument {instrument_id} does not match restored order instrument {}",
1058            order.instrument_id()
1059        );
1060        anyhow::ensure!(
1061            intent.trader_id.as_deref() == Some(order.trader_id().as_str()),
1062            "Persisted swap trader does not match restored order"
1063        );
1064        anyhow::ensure!(
1065            intent.strategy_id.as_deref() == Some(order.strategy_id().as_str()),
1066            "Persisted swap strategy does not match restored order"
1067        );
1068        anyhow::ensure!(
1069            intent.account_id.as_deref() == Some(self.core.account_id.as_str()),
1070            "Persisted swap account does not match execution client account"
1071        );
1072
1073        let pool = self.resolve_pool(&instrument_id)?;
1074        let pool_address = Address::from_str(
1075            intent
1076                .pool_address
1077                .as_deref()
1078                .ok_or_else(|| anyhow::anyhow!("Persisted swap intent has no pool address"))?,
1079        )?;
1080        anyhow::ensure!(
1081            pool.address == pool_address,
1082            "Persisted pool {pool_address} does not match restored pool {}",
1083            pool.address
1084        );
1085        let amount_in = U256::from_str(
1086            intent
1087                .amount_in
1088                .as_deref()
1089                .ok_or_else(|| anyhow::anyhow!("Persisted swap intent has no input amount"))?,
1090        )?;
1091        let fee = U24::try_from(
1092            pool.fee
1093                .ok_or_else(|| anyhow::anyhow!("Restored pool {instrument_id} has no fee"))?,
1094        )?;
1095        let quote_token = pool.get_quote_token();
1096        let quote_currency = Currency::new_checked(
1097            &quote_token.symbol,
1098            quote_token.decimals,
1099            0,
1100            &quote_token.name,
1101            CurrencyType::Crypto,
1102        )?;
1103        let (token_in, token_out) = swap_token_pair(
1104            order.order_side(),
1105            pool.get_base_token().address,
1106            quote_token.address,
1107        );
1108        let factory = self.uniswap_v3_factory()?;
1109        anyhow::ensure!(
1110            pool.dex.factory == factory,
1111            "Restored pool {instrument_id} references factory {}, expected registered factory {factory}",
1112            pool.dex.factory
1113        );
1114
1115        Ok(SwapPlan {
1116            order,
1117            quote_currency,
1118            pool,
1119            instrument_id,
1120            pool_address,
1121            router: Address::from_str(&intent.transaction_to)?,
1122            factory,
1123            weth: self.weth_address,
1124            token_in,
1125            token_out,
1126            fee,
1127            amount_in,
1128            min_amount_out: U256::ZERO,
1129            slippage_bps: 0,
1130            quote_spend_ceiling: None,
1131            profiler_position: None,
1132        })
1133    }
1134
1135    #[allow(
1136        clippy::large_futures,
1137        reason = "keeps transaction verification futures inline without additional boxing"
1138    )]
1139    async fn reconcile_unresolved_execution(&self) -> anyhow::Result<()> {
1140        let database = self.cache.database.clone().ok_or_else(|| {
1141            anyhow::anyhow!("No durable store configured for execution reconciliation")
1142        })?;
1143        let _payload_lease = database
1144            .acquire_execution_payload_lease(self.payload_keys.as_deref().ok_or_else(|| {
1145                anyhow::anyhow!("Protected payload keys are required for execution recovery")
1146            })?)
1147            .await?;
1148        let wallet_address = self.wallet_address.to_string();
1149        anyhow::ensure!(
1150            !database
1151                .has_recoverable_signed_execution(self.chain.chain_id, &wallet_address)
1152                .await?,
1153            "A recoverable execution for wallet {} retains signed transaction bytes; refusing to reuse its nonce without explicit recovery",
1154            self.wallet_address
1155        );
1156        let Some(intent) = database
1157            .get_active_execution_intent(self.chain.chain_id, &wallet_address)
1158            .await?
1159        else {
1160            return Ok(());
1161        };
1162        anyhow::ensure!(
1163            intent.schema_version == crate::execution::transaction::EXECUTION_SCHEMA_VERSION,
1164            "Execution intent {} uses unsupported schema version {}",
1165            intent.id,
1166            intent.schema_version
1167        );
1168
1169        if intent.status == "prepared" {
1170            database
1171                .mark_execution_intent_recoverable(intent.id)
1172                .await?;
1173            release_preparing_slot(&self.in_flight);
1174            return Ok(());
1175        }
1176
1177        let purpose = TransactionPurpose::parse(&intent.purpose).ok_or_else(|| {
1178            anyhow::anyhow!(
1179                "Execution intent {} has unknown purpose {}",
1180                intent.id,
1181                intent.purpose
1182            )
1183        })?;
1184        let nonce = intent
1185            .nonce
1186            .ok_or_else(|| anyhow::anyhow!("Active execution intent {} has no nonce", intent.id))?;
1187        *self.in_flight.lock() = Some(InFlightSlot::Recovering(RecoveryTransaction {
1188            intent_id: intent.id,
1189            nonce,
1190            purpose,
1191        }));
1192        let hashes = database.get_execution_transaction_hashes(intent.id).await?;
1193        let current = current_execution_hash(intent.id, &hashes)?;
1194        let tx_hash = B256::from_str(&current.transaction_hash).with_context(|| {
1195            format!(
1196                "Execution intent {} has invalid transaction hash {}",
1197                intent.id, current.transaction_hash
1198            )
1199        })?;
1200        let policy = PayloadPolicy {
1201            chain_id: self.chain.chain_id,
1202            signer: self.wallet_address,
1203            gas_limit: self.config.gas_limit,
1204            max_fee_per_gas: self.config.max_fee_per_gas_wei,
1205        };
1206        let mut authenticated_payloads = HashMap::new();
1207        let mut current_payload = None;
1208
1209        for hash in &hashes {
1210            anyhow::ensure!(
1211                hash.intent_id == intent.id,
1212                "Persisted transaction row references intent {}, expected {}",
1213                hash.intent_id,
1214                intent.id
1215            );
1216            anyhow::ensure!(
1217                hash.chain_id == self.chain.chain_id,
1218                "Persisted transaction row chain ID {} does not match configured chain ID {}",
1219                hash.chain_id,
1220                self.chain.chain_id
1221            );
1222
1223            if !hash.payload_expected {
1224                anyhow::ensure!(
1225                    hash.raw_transaction.is_none() && hash.sealed_transaction.is_none(),
1226                    "Replacement transaction {} unexpectedly retains signed bytes",
1227                    hash.transaction_hash
1228                );
1229                continue;
1230            }
1231            let raw_transaction = open_execution_payload(
1232                self.payload_keys
1233                    .as_deref()
1234                    .expect("payload keys checked above"),
1235                policy,
1236                &intent,
1237                hash,
1238                "recovery",
1239            )
1240            .map_err(|e| {
1241                anyhow::anyhow!(
1242                    "Execution intent {} signed transaction {} failed authentication: {e}",
1243                    intent.id,
1244                    hash.transaction_hash
1245                )
1246            })?;
1247            let authenticated_hash = B256::from_str(&hash.transaction_hash).with_context(|| {
1248                format!(
1249                    "Execution intent {} has invalid transaction hash {}",
1250                    intent.id, hash.transaction_hash
1251                )
1252            })?;
1253            anyhow::ensure!(
1254                authenticated_payloads
1255                    .insert(authenticated_hash, raw_transaction.clone())
1256                    .is_none(),
1257                "Execution intent {} has duplicate authenticated transaction hash {}",
1258                intent.id,
1259                hash.transaction_hash
1260            );
1261
1262            if hash.id == current.id {
1263                current_payload = Some(raw_transaction);
1264            }
1265        }
1266        anyhow::ensure!(
1267            !authenticated_payloads.is_empty(),
1268            "Execution intent {} has no persisted signed transaction bytes",
1269            intent.id
1270        );
1271
1272        if intent.status == "broadcast" {
1273            anyhow::ensure!(
1274                current_payload.is_some(),
1275                "Broadcast execution intent {} has no persisted signed transaction bytes",
1276                intent.id
1277            );
1278        }
1279
1280        if intent.status == "signed" {
1281            anyhow::bail!(
1282                "Execution intent {} has a signed transaction {} that was not authorized for broadcast; its nonce remains reserved pending explicit recovery",
1283                intent.id,
1284                tx_hash
1285            );
1286        }
1287
1288        *self.in_flight.lock() = Some(InFlightSlot::AwaitingFinality(InFlightTransaction {
1289            intent_id: intent.id,
1290            nonce,
1291            tx_hash,
1292            purpose,
1293        }));
1294
1295        let plan = if purpose == TransactionPurpose::Swap {
1296            Some(self.restore_swap_plan(&intent)?)
1297        } else {
1298            None
1299        };
1300
1301        if let Some(plan) = &plan
1302            && !intent.acknowledgement_emitted
1303        {
1304            if plan.order.ts_submitted().is_none() {
1305                self.emitter.emit_order_submitted(&plan.order);
1306            }
1307            database
1308                .mark_execution_event_emitted(intent.id, "acknowledgement")
1309                .await?;
1310        }
1311
1312        let executor = self.transaction_executor()?;
1313        let mut prepared = PreparedTransaction {
1314            intent_id: intent.id,
1315            created_block: intent.created_block,
1316            nonce,
1317            tx_hash,
1318            raw_tx: current_payload.unwrap_or_default(),
1319            payload_lease: None,
1320        };
1321
1322        let finality_already_committed = matches!(intent.status.as_str(), "finalized" | "reverted");
1323        if !finality_already_committed {
1324            match executor
1325                .authorize_rebroadcast(&prepared, &intent, purpose)
1326                .await?
1327            {
1328                ReconciliationAuthorization::Rebroadcast => {
1329                    match executor.broadcast(&prepared).await? {
1330                        BroadcastOutcome::Accepted => {}
1331                        BroadcastOutcome::Ambiguous(message) => log::warn!("{message}"),
1332                    }
1333                }
1334                ReconciliationAuthorization::Retain => {
1335                    log::warn!(
1336                        "Rebroadcast of transaction {} was suppressed by verified reconciliation state",
1337                        prepared.tx_hash
1338                    );
1339                }
1340                ReconciliationAuthorization::ScanReplacement(head) => {
1341                    let Some((replacement_hash, replacement_payload)) = executor
1342                        .scan_canonical_replacement(&intent, nonce, head, &authenticated_payloads)
1343                        .await?
1344                    else {
1345                        log::warn!(
1346                            "Canonical replacement scan for intent {} reached its bounded verified window",
1347                            intent.id
1348                        );
1349                        return Ok(());
1350                    };
1351                    prepared.tx_hash = replacement_hash;
1352                    prepared.raw_tx = replacement_payload;
1353                    *self.in_flight.lock() =
1354                        Some(InFlightSlot::AwaitingFinality(InFlightTransaction {
1355                            intent_id: intent.id,
1356                            nonce,
1357                            tx_hash: replacement_hash,
1358                            purpose,
1359                        }));
1360                }
1361            }
1362        }
1363        let outcome = if finality_already_committed {
1364            let receipt = verified_value(
1365                executor.verification.verify_receipt(&tx_hash).await,
1366                "persisted terminal receipt",
1367            )?;
1368            let finality = executor
1369                .receipt_is_stably_finalized(&receipt)
1370                .await?
1371                .ok_or_else(|| {
1372                    anyhow::anyhow!(
1373                        "Persisted terminal transaction {tx_hash} is not stable at the finalized boundary"
1374                    )
1375                })?;
1376            let included = IncludedTransaction {
1377                intent_id: intent.id,
1378                nonce,
1379                tx_hash,
1380                block_number: receipt.block_number,
1381                receipt,
1382                finality,
1383            };
1384
1385            if intent.status == "finalized" {
1386                InclusionOutcome::Finalized(included)
1387            } else {
1388                InclusionOutcome::Reverted(included)
1389            }
1390        } else {
1391            executor.await_finality(&prepared).await?
1392        };
1393
1394        match outcome {
1395            InclusionOutcome::Finalized(mut included) => {
1396                let trace_purpose = match (&plan, purpose) {
1397                    (Some(plan), TransactionPurpose::Swap) => match plan.order.order_side() {
1398                        OrderSide::Sell => "swap_sell",
1399                        OrderSide::Buy => "swap_buy",
1400                    },
1401                    (None, TransactionPurpose::Wrap) => "wrap",
1402                    (None, TransactionPurpose::Approve) => "approve",
1403                    _ => anyhow::bail!("Restored transaction purpose is inconsistent"),
1404                };
1405                included.finality.decisions.extend(
1406                    verify_finalized_transaction(
1407                        &included,
1408                        &intent,
1409                        nonce,
1410                        &prepared.raw_tx,
1411                        &executor,
1412                        trace_purpose,
1413                    )
1414                    .await?,
1415                );
1416
1417                if let Some(plan) = plan {
1418                    let fill = validate_finalized_swap_fill(&plan, &included)?;
1419                    let wallet =
1420                        load_verified_wallet_after_fill(&plan, &included, &executor).await?;
1421                    if !finality_already_committed {
1422                        executor
1423                            .commit_verified_finality(
1424                                &included,
1425                                TransactionStatus::Finalized,
1426                                &wallet.decisions,
1427                            )
1428                            .await?;
1429                    }
1430                    complete_finalized_swap(
1431                        &plan,
1432                        intent.id,
1433                        included.tx_hash,
1434                        fill,
1435                        wallet,
1436                        &executor,
1437                        &self.emitter,
1438                    )
1439                    .await?;
1440                    executor.release_slot();
1441                } else {
1442                    let postconditions = self
1443                        .verify_recovered_operator_transaction(
1444                            &intent, purpose, &included, &executor,
1445                        )
1446                        .await?;
1447
1448                    if !finality_already_committed {
1449                        executor
1450                            .commit_verified_finality(
1451                                &included,
1452                                TransactionStatus::Finalized,
1453                                &postconditions,
1454                            )
1455                            .await?;
1456                    }
1457                    database
1458                        .mark_execution_event_emitted(intent.id, "terminal")
1459                        .await?;
1460                    executor.release_slot();
1461                }
1462            }
1463            InclusionOutcome::Reverted(mut included) => {
1464                let trace_purpose = match (&plan, purpose) {
1465                    (Some(plan), TransactionPurpose::Swap) => match plan.order.order_side() {
1466                        OrderSide::Sell => "swap_sell",
1467                        OrderSide::Buy => "swap_buy",
1468                    },
1469                    (None, TransactionPurpose::Wrap) => "wrap",
1470                    (None, TransactionPurpose::Approve) => "approve",
1471                    _ => anyhow::bail!("Restored transaction purpose is inconsistent"),
1472                };
1473                included.finality.decisions.extend(
1474                    verify_finalized_transaction(
1475                        &included,
1476                        &intent,
1477                        nonce,
1478                        &prepared.raw_tx,
1479                        &executor,
1480                        trace_purpose,
1481                    )
1482                    .await?,
1483                );
1484
1485                if !finality_already_committed {
1486                    executor
1487                        .commit_verified_finality(&included, TransactionStatus::Reverted, &[])
1488                        .await?;
1489                }
1490
1491                if let Some(plan) = plan
1492                    && plan.order.status() != OrderStatus::Rejected
1493                {
1494                    send_reverted_order(&self.emitter, &plan.order, &included)?;
1495                }
1496                database
1497                    .mark_execution_event_emitted(intent.id, "terminal")
1498                    .await?;
1499                executor.release_slot();
1500            }
1501            InclusionOutcome::Pending(message) => log::warn!("{message}"),
1502        }
1503        Ok(())
1504    }
1505
1506    /// Revalidates a restored wrap or approve against the finalized signer-nonce transaction
1507    /// and reruns the live postcondition before the restored intent may report success.
1508    ///
1509    /// Fails closed: any identity mismatch or unproven postcondition returns an error, which
1510    /// keeps the in-flight signer slot occupied, leaves the intent active, and fails connect.
1511    async fn verify_recovered_operator_transaction(
1512        &self,
1513        intent: &ExecutionIntentRow,
1514        purpose: TransactionPurpose,
1515        included: &IncludedTransaction,
1516        executor: &TransactionExecutor,
1517    ) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
1518        let (to, input, value) = persisted_call_fields(intent)?;
1519
1520        match purpose {
1521            TransactionPurpose::Wrap => {
1522                verify_wrap_balance_increase(executor, &to, value, included).await
1523            }
1524            TransactionPurpose::Approve => {
1525                let call = ERC20::approveCall::abi_decode(&input)
1526                    .with_context(|| "persisted approve calldata is invalid")?;
1527                verify_approve_allowance(executor, &to, &call.spender, call.amount, included).await
1528            }
1529            TransactionPurpose::Swap => {
1530                unreachable!("swap intents restore a swap plan")
1531            }
1532        }
1533    }
1534
1535    fn ensure_transaction_ready(&self, purpose: TransactionPurpose) -> anyhow::Result<()> {
1536        if !self.core.is_connected() {
1537            anyhow::bail!("Blockchain execution client is not connected");
1538        }
1539
1540        {
1541            let slot = self.in_flight.lock();
1542            if let Some(in_flight) = *slot {
1543                return Err(in_flight_limit_error(&in_flight));
1544            }
1545        }
1546
1547        if !self.cache.has_database() {
1548            anyhow::bail!(
1549                "No durable store configured; refusing to submit a {} transaction",
1550                purpose.as_str()
1551            );
1552        }
1553        Ok(())
1554    }
1555
1556    /// Validates a submit-order command against the configured policy and builds the swap plan.
1557    ///
1558    /// Local checks run first: pool resolution, order semantics, allowlists, amount and
1559    /// slippage limits, and the quote derived from the live pool profiler. Infrastructure
1560    /// readiness (connection, in-flight slot, durable store, signer) follows. Chain state is
1561    /// verified in the spawned task before signing.
1562    fn prepare_swap(&self, cmd: &SubmitOrder, order: &OrderAny) -> anyhow::Result<SwapPlan> {
1563        let instrument_id = order.instrument_id();
1564        let pool = self.resolve_pool(&instrument_id)?;
1565
1566        if order.order_type() != OrderType::Market {
1567            anyhow::bail!(
1568                "Unsupported order type {}; only Market is supported",
1569                order.order_type()
1570            );
1571        }
1572
1573        if !matches!(order.order_side(), OrderSide::Buy | OrderSide::Sell) {
1574            anyhow::bail!(
1575                "Unsupported order side {}; only Buy and Sell are supported",
1576                order.order_side()
1577            );
1578        }
1579
1580        if order.is_quote_quantity() {
1581            anyhow::bail!(OrderDeniedReason::unsupported_quote_quantity());
1582        }
1583
1584        let fee = pool
1585            .fee
1586            .ok_or_else(|| anyhow::anyhow!("Pool {instrument_id} has no fee tier"))?;
1587        let fee = U24::try_from(fee)
1588            .map_err(|_| anyhow::anyhow!("Pool {instrument_id} fee {fee} exceeds uint24"))?;
1589
1590        let base_token = pool.get_base_token();
1591        let quote_token = pool.get_quote_token();
1592        let quote_currency = Currency::new_checked(
1593            &quote_token.symbol,
1594            quote_token.decimals,
1595            0,
1596            &quote_token.name,
1597            CurrencyType::Crypto,
1598        )?;
1599        let (token_in, token_out) =
1600            swap_token_pair(order.order_side(), base_token.address, quote_token.address);
1601
1602        if !self
1603            .transaction_limits
1604            .allowed_token_pairs
1605            .contains(&(token_in, token_out))
1606        {
1607            anyhow::bail!(
1608                "Token pair {token_in} -> {token_out} is not in the `allowed_token_pairs` allowlist"
1609            );
1610        }
1611
1612        let base_amount = quantity_to_raw_amount(order.quantity(), base_token.decimals)?;
1613        if base_amount > U256::from(self.transaction_limits.max_order_amount) {
1614            anyhow::bail!(
1615                "Order amount {base_amount} exceeds the configured `max_order_amount` {}",
1616                self.transaction_limits.max_order_amount
1617            );
1618        }
1619
1620        let slippage_bps = match cmd
1621            .params
1622            .as_ref()
1623            .and_then(|params| params.get_u64("slippage_bps"))
1624        {
1625            Some(value) => u32::try_from(value).map_err(|_| {
1626                anyhow::anyhow!("slippage_bps parameter {value} exceeds the u32 range")
1627            })?,
1628            None => self.transaction_limits.slippage_bps,
1629        };
1630
1631        if slippage_bps > self.transaction_limits.max_slippage_bps {
1632            anyhow::bail!(
1633                "Slippage {slippage_bps} bps exceeds the configured `max_slippage_bps` {}",
1634                self.transaction_limits.max_slippage_bps
1635            );
1636        }
1637
1638        let quote_spend_ceiling = if order.order_side() == OrderSide::Buy {
1639            let ceiling = self
1640                .transaction_limits
1641                .quote_spend_limits
1642                .get(&(token_in, token_out))
1643                .ok_or_else(|| {
1644                    anyhow::anyhow!(
1645                        "No `quote_spend_limits` entry for BUY token pair {token_in} -> {token_out}"
1646                    )
1647                })?;
1648            anyhow::ensure!(
1649                ceiling.spend_token == quote_token.address,
1650                "Quote spend limit for {token_in} -> {token_out} is denominated in {}, expected quote token {}",
1651                ceiling.spend_token,
1652                quote_token.address
1653            );
1654            anyhow::ensure!(
1655                ceiling.spend_token_decimals == quote_token.decimals,
1656                "Quote spend limit for token {} uses {} decimals, expected pool quote-token decimals {}",
1657                ceiling.spend_token,
1658                ceiling.spend_token_decimals,
1659                quote_token.decimals
1660            );
1661            Some(ceiling)
1662        } else {
1663            None
1664        };
1665
1666        let profiler = self
1667            .core
1668            .cache()
1669            .pool_profiler(&instrument_id)
1670            .cloned()
1671            .ok_or_else(|| {
1672                anyhow::anyhow!(
1673                    "No pool profiler for {instrument_id}; an active data subscription is required to quote the swap"
1674                )
1675            })?;
1676
1677        if !profiler.is_initialized {
1678            anyhow::bail!("Pool profiler for {instrument_id} is not initialized");
1679        }
1680        let profiler_position = profiler.last_processed_event.clone().ok_or_else(|| {
1681            anyhow::anyhow!("Pool profiler for {instrument_id} has processed no events")
1682        })?;
1683
1684        let zero_for_one = token_in == pool.token0.address;
1685        let (amount_in, quoted_amount_out) = match order.order_side() {
1686            OrderSide::Sell => {
1687                let quote = profiler
1688                    .swap_exact_in(base_amount, zero_for_one, None)
1689                    .map_err(|e| anyhow::anyhow!("Swap quote failed for {instrument_id}: {e}"))?;
1690                let amount_filled = if zero_for_one {
1691                    quote.amount0
1692                } else {
1693                    quote.amount1
1694                };
1695
1696                if amount_filled != I256::from(base_amount) {
1697                    anyhow::bail!(
1698                        "Local quote for {instrument_id} filled {amount_filled} of the {base_amount} order amount; pool liquidity cannot fill the order"
1699                    );
1700                }
1701                (base_amount, exact_output_amount(&quote, zero_for_one)?)
1702            }
1703            OrderSide::Buy => {
1704                let quote = profiler
1705                    .swap_exact_out(base_amount, zero_for_one, None)
1706                    .map_err(|e| anyhow::anyhow!("Swap quote failed for {instrument_id}: {e}"))?;
1707                let amount_in = quote.get_input_amount();
1708                if amount_in.is_zero() {
1709                    anyhow::bail!("Local quote for {instrument_id} produced a zero quote input");
1710                }
1711                let ceiling = quote_spend_ceiling.ok_or_else(|| {
1712                    anyhow::anyhow!(
1713                        "No `quote_spend_limits` entry for BUY token pair {token_in} -> {token_out}"
1714                    )
1715                })?;
1716
1717                if amount_in > ceiling.max_amount {
1718                    anyhow::bail!(
1719                        "BUY quote amount {amount_in} exceeds the configured `quote_spend_limits` maximum {} for {token_in} -> {token_out}",
1720                        ceiling.max_amount
1721                    );
1722                }
1723                (amount_in, base_amount)
1724            }
1725        };
1726        let min_amount_out = derive_min_amount_out(quoted_amount_out, slippage_bps)?;
1727
1728        self.ensure_transaction_ready(TransactionPurpose::Swap)?;
1729
1730        if self.signer.is_none() {
1731            anyhow::bail!("Signer not initialized; connect the client first");
1732        }
1733
1734        let pool_address = pool.address;
1735        let factory = self.uniswap_v3_factory()?;
1736        anyhow::ensure!(
1737            pool.dex.factory == factory,
1738            "Pool {instrument_id} references factory {}, expected registered factory {factory}",
1739            pool.dex.factory
1740        );
1741
1742        Ok(SwapPlan {
1743            order: order.clone(),
1744            pool,
1745            quote_currency,
1746            instrument_id,
1747            pool_address,
1748            router: self.router_addresses[0],
1749            factory,
1750            weth: self.weth_address,
1751            token_in,
1752            token_out,
1753            fee,
1754            amount_in,
1755            min_amount_out,
1756            slippage_bps,
1757            quote_spend_ceiling: quote_spend_ceiling.copied(),
1758            profiler_position: Some(profiler_position),
1759        })
1760    }
1761}
1762
1763/// Result of authenticating every persisted signed transaction in one execution database.
1764#[derive(Debug, Clone, PartialEq, Eq)]
1765pub struct PayloadStorageCheck {
1766    /// Whether payload protection is active.
1767    pub protected: bool,
1768    /// Durable deployment identity when protection is active.
1769    pub deployment_id: Option<String>,
1770    /// Rows which still contain plaintext signed transaction bytes.
1771    pub plaintext_rows: u64,
1772    /// Signed transaction rows which require a payload.
1773    pub original_rows: u64,
1774    /// Canonical replacement rows whose original bytes are unavailable.
1775    pub replacement_rows: u64,
1776    /// Payload rows successfully opened and authenticated.
1777    pub authenticated_rows: u64,
1778    /// Key IDs referenced by protected payloads.
1779    pub key_ids: Vec<String>,
1780    /// Database roles with direct table ownership or `SELECT` grants.
1781    pub read_roles: Vec<String>,
1782}
1783
1784impl From<ExecutionPayloadCheck> for PayloadStorageCheck {
1785    fn from(value: ExecutionPayloadCheck) -> Self {
1786        Self {
1787            protected: value.protected,
1788            deployment_id: value.deployment_id,
1789            plaintext_rows: value.plaintext_rows,
1790            original_rows: value.original_rows,
1791            replacement_rows: value.replacement_rows,
1792            authenticated_rows: value.authenticated_rows,
1793            key_ids: value.key_ids,
1794            read_roles: value.read_roles,
1795        }
1796    }
1797}
1798
1799// A broadcast transaction awaiting finality, occupying the single in-flight slot.
1800#[derive(Debug, Clone, Copy)]
1801struct InFlightTransaction {
1802    intent_id: i64,
1803    nonce: u64,
1804    tx_hash: B256,
1805    purpose: TransactionPurpose,
1806}
1807
1808#[derive(Debug, Clone, Copy)]
1809struct RecoveryTransaction {
1810    intent_id: i64,
1811    nonce: u64,
1812    purpose: TransactionPurpose,
1813}
1814
1815/// The single in-flight transaction slot.
1816///
1817/// The slot is claimed before any preparation RPC call so the `pending` nonce read stays
1818/// authoritative: a second transaction is rejected before it can sign. A claim is released
1819/// only when preparation fails before signing; from persistence onward the slot is never
1820/// released on failure, because the database may have committed before its acknowledgement
1821/// was lost.
1822#[derive(Debug, Clone, Copy)]
1823enum InFlightSlot {
1824    /// Claimed before preparation; no signed transaction exists yet.
1825    Preparing(TransactionPurpose),
1826    /// Restored durable ownership retained while persisted transaction data is authenticated.
1827    Recovering(RecoveryTransaction),
1828    /// Signed, persisted, and awaiting finality.
1829    AwaitingFinality(InFlightTransaction),
1830}
1831
1832#[derive(Debug, Clone)]
1833struct IncludedTransaction {
1834    intent_id: i64,
1835    nonce: u64,
1836    tx_hash: B256,
1837    block_number: u64,
1838    receipt: RpcTransactionReceipt,
1839    finality: StableFinality,
1840}
1841
1842#[derive(Debug, Clone)]
1843struct StableFinality {
1844    decisions: Vec<ExecutionVerificationDecision>,
1845    inclusion_header: ExecutionVerifiedHeader,
1846    finalized_headers: Vec<ExecutionVerifiedHeader>,
1847}
1848
1849#[derive(Debug, Clone, Copy)]
1850enum TransactionAuthorization {
1851    Wrap {
1852        weth: Address,
1853    },
1854    Approve {
1855        token: Address,
1856        router: Address,
1857        amount: U256,
1858    },
1859}
1860
1861/// The single-in-flight limit error naming the transaction currently occupying the slot.
1862fn in_flight_limit_error(slot: &InFlightSlot) -> anyhow::Error {
1863    match slot {
1864        InFlightSlot::Preparing(purpose) => anyhow::anyhow!(
1865            "A {} transaction is being prepared; at most one transaction can be in flight",
1866            purpose.as_str()
1867        ),
1868        InFlightSlot::Recovering(recovery) => anyhow::anyhow!(
1869            "Execution intent {} ({}, nonce {}) retains signer ownership pending recovery; at most one transaction can be in flight",
1870            recovery.intent_id,
1871            recovery.purpose.as_str(),
1872            recovery.nonce
1873        ),
1874        InFlightSlot::AwaitingFinality(in_flight) => anyhow::anyhow!(
1875            "Transaction {} (intent {}, {}, nonce {}) is still awaiting finality; at most one transaction can be in flight",
1876            in_flight.tx_hash,
1877            in_flight.intent_id,
1878            in_flight.purpose.as_str(),
1879            in_flight.nonce
1880        ),
1881    }
1882}
1883
1884/// Releases a pre-signature slot claim when the slot is still in the preparing state.
1885///
1886/// Aborted or failed preparation can leave a claim behind; because no signed transaction
1887/// exists for a preparing slot, releasing it cannot strand a broadcastable signature.
1888fn release_preparing_slot(in_flight: &Mutex<Option<InFlightSlot>>) {
1889    let mut slot = in_flight.lock();
1890    if matches!(*slot, Some(InFlightSlot::Preparing(_))) {
1891        *slot = None;
1892    }
1893}
1894
1895fn release_preparing_if_reservation_not_committed(
1896    in_flight: &Mutex<Option<InFlightSlot>>,
1897    error: &anyhow::Error,
1898) {
1899    if reservation_failure_proven_not_committed(error) {
1900        release_preparing_slot(in_flight);
1901    }
1902}
1903
1904#[derive(Debug)]
1905struct TransactionLimits {
1906    allowed_token_pairs: HashSet<(Address, Address)>,
1907    quote_spend_limits: HashMap<(Address, Address), QuoteSpendCeiling>,
1908    slippage_bps: u32,
1909    max_slippage_bps: u32,
1910    max_order_amount: u64,
1911    deadline_seconds: u64,
1912    max_quote_age_blocks: u64,
1913    receipt_timeout_secs: u64,
1914}
1915
1916#[derive(Debug, Clone, Copy)]
1917struct QuoteSpendCeiling {
1918    spend_token: Address,
1919    spend_token_decimals: u8,
1920    max_amount: U256,
1921}
1922
1923/// A locally signed EIP-1559 transaction ready for persist-before-broadcast.
1924struct PreparedTransaction {
1925    intent_id: i64,
1926    created_block: u64,
1927    nonce: u64,
1928    tx_hash: B256,
1929    raw_tx: Vec<u8>,
1930    payload_lease: Option<ExecutionPayloadLease>,
1931}
1932
1933impl Debug for PreparedTransaction {
1934    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1935        f.debug_struct(stringify!(PreparedTransaction))
1936            .field("intent_id", &self.intent_id)
1937            .field("created_block", &self.created_block)
1938            .field("nonce", &self.nonce)
1939            .field("tx_hash", &self.tx_hash)
1940            .field("raw_tx", &"<redacted>")
1941            .field(
1942                "payload_lease",
1943                &self.payload_lease.as_ref().map(|_| "held"),
1944            )
1945            .finish()
1946    }
1947}
1948
1949/// The classified outcome of a broadcast attempt.
1950#[derive(Debug)]
1951enum BroadcastOutcome {
1952    /// The node accepted the transaction (or reported it already known).
1953    Accepted,
1954    /// Acceptance is uncertain; the persisted record stays active and the slot occupied.
1955    Ambiguous(String),
1956}
1957
1958/// The classified outcome of awaiting transaction finality.
1959#[derive(Debug)]
1960enum InclusionOutcome {
1961    /// Canonical successful receipt observed through a stable finalized boundary.
1962    Finalized(IncludedTransaction),
1963    /// Canonical failed receipt observed through a stable finalized boundary.
1964    Reverted(IncludedTransaction),
1965    /// No receipt arrived within the poll budget or observation failed; the record stays
1966    /// pending and the in-flight slot occupied.
1967    Pending(String),
1968}
1969
1970enum ReconciliationAuthorization {
1971    Rebroadcast,
1972    Retain,
1973    ScanReplacement(VerifiedBlockHeader),
1974}
1975
1976fn verified_value<T>(outcome: VerificationOutcome<T>, context: &str) -> anyhow::Result<T> {
1977    required_verification(outcome, context).map(|verified| verified.value)
1978}
1979
1980fn required_verification<T>(
1981    outcome: VerificationOutcome<T>,
1982    context: &str,
1983) -> anyhow::Result<Verified<T>> {
1984    match outcome {
1985        VerificationOutcome::Verified(verified) => Ok(verified),
1986        VerificationOutcome::Disagreement(_) => {
1987            anyhow::bail!("{context} verification disagreed")
1988        }
1989        VerificationOutcome::Unavailable(_) => {
1990            anyhow::bail!("{context} verification is unavailable")
1991        }
1992        VerificationOutcome::Retryable(_) => {
1993            anyhow::bail!("{context} verification is retryable")
1994        }
1995        VerificationOutcome::LocallyInvalid(_) => {
1996            anyhow::bail!("{context} verification is locally invalid")
1997        }
1998    }
1999}
2000
2001fn validate_transaction_authorization(
2002    authorization: Option<&TransactionAuthorization>,
2003    to: Address,
2004    value: U256,
2005    input: &[u8],
2006) -> anyhow::Result<()> {
2007    match authorization {
2008        None => Ok(()),
2009        Some(TransactionAuthorization::Wrap { weth }) => {
2010            anyhow::ensure!(
2011                to == *weth && !value.is_zero() && input == WETH9::depositCall::SELECTOR,
2012                "Wrap authorization does not match the transaction call"
2013            );
2014            Ok(())
2015        }
2016        Some(TransactionAuthorization::Approve {
2017            token,
2018            router,
2019            amount,
2020        }) => {
2021            let expected = ERC20::approveCall {
2022                spender: *router,
2023                amount: *amount,
2024            }
2025            .abi_encode();
2026            anyhow::ensure!(
2027                to == *token && value.is_zero() && input == expected,
2028                "Approve authorization does not match the transaction call"
2029            );
2030            Ok(())
2031        }
2032    }
2033}
2034
2035fn verification_decision<T>(
2036    verified: &Verified<T>,
2037    height_start: Option<u64>,
2038    height_end: Option<u64>,
2039) -> ExecutionVerificationDecision {
2040    ExecutionVerificationDecision {
2041        read_class: verified.read.as_str(),
2042        height_start,
2043        height_end,
2044        normalized_value_digest: verified.normalized_value_digest.to_string(),
2045    }
2046}
2047
2048fn parse_verified_header(header: &ExecutionVerifiedHeader) -> anyhow::Result<VerifiedBlockHeader> {
2049    Ok(VerifiedBlockHeader {
2050        number: header.number,
2051        hash: B256::from_str(&header.hash).context("Durable finalized header hash is invalid")?,
2052        parent_hash: B256::from_str(&header.parent_hash)
2053            .context("Durable finalized parent hash is invalid")?,
2054        timestamp: header.timestamp,
2055        base_fee_per_gas: header.base_fee_per_gas,
2056    })
2057}
2058
2059fn durable_verified_header(header: &VerifiedBlockHeader) -> ExecutionVerifiedHeader {
2060    ExecutionVerifiedHeader {
2061        number: header.number,
2062        hash: header.hash.to_string(),
2063        parent_hash: header.parent_hash.to_string(),
2064        timestamp: header.timestamp,
2065        base_fee_per_gas: header.base_fee_per_gas,
2066    }
2067}
2068
2069/// Shared execution context driving the EIP-1559 transaction pipeline.
2070///
2071/// Wrap, approve, and swap submissions share one implementation so the chain, fee, gas,
2072/// signing, persistence, and broadcast policy cannot drift between operator transactions
2073/// and order flow. Carries only `Send` state so order submission can run on a spawned task
2074/// after the synchronous trait method returns.
2075#[derive(Debug, Clone)]
2076struct TransactionExecutor {
2077    http_rpc_client: Arc<BlockchainHttpRpcClient>,
2078    verification: VerificationCoordinator,
2079    manifest_version: String,
2080    manifest_digest: String,
2081    deployment_manifest: Arc<BlockchainDeploymentManifest>,
2082    provider_ids: Vec<String>,
2083    operator_ids: Vec<String>,
2084    failure_domain_ids: Vec<String>,
2085    database: BlockchainCacheDatabase,
2086    signer: Arc<PrivateKeySigner>,
2087    payload_keys: Arc<PayloadKeySet>,
2088    in_flight: Arc<Mutex<Option<InFlightSlot>>>,
2089    wallet_balance: Arc<Mutex<WalletBalance>>,
2090    account_id: AccountId,
2091    wallet_address: Address,
2092    chain_id: u32,
2093    max_fee_per_gas_wei: u64,
2094    base_fee_buffer_bps: u32,
2095    gas_limit: u64,
2096    gas_buffer_bps: u32,
2097    receipt_timeout: Duration,
2098    receipt_max_polls: u32,
2099}
2100
2101impl TransactionExecutor {
2102    /// Builds, signs, persists, and broadcasts an EIP-1559 transaction, then awaits finality.
2103    ///
2104    /// Order of operations: claim the in-flight slot, chain ID verification, nonce and fee and
2105    /// gas policy checks, local signing, persist-before-broadcast, broadcast, inclusion
2106    /// observation. The persisted record guarantees a signed transaction is never forgotten.
2107    #[allow(
2108        clippy::large_futures,
2109        reason = "keeps transaction verification futures inline without additional boxing"
2110    )]
2111    async fn transact(
2112        &self,
2113        to: Address,
2114        value: U256,
2115        input: Bytes,
2116        purpose: TransactionPurpose,
2117        client_order_id: Option<ClientOrderId>,
2118        authorization: TransactionAuthorization,
2119    ) -> anyhow::Result<IncludedTransaction> {
2120        self.claim_slot(purpose)?;
2121        let now_unix_secs = current_unix_secs()?;
2122        let decision_header = match required_verification(
2123            self.verification
2124                .verify_decision_header(now_unix_secs)
2125                .await,
2126            "operator decision header",
2127        ) {
2128            Ok(header) => header,
2129            Err(e) => {
2130                release_preparing_slot(&self.in_flight);
2131                return Err(e);
2132            }
2133        };
2134        let created_block = decision_header.value.number;
2135        let intent = ExecutionIntentInsert {
2136            chain_id: self.chain_id,
2137            wallet_address: self.wallet_address.to_string(),
2138            purpose: purpose.as_str().to_string(),
2139            client_order_id: client_order_id.map(|id| id.to_string()),
2140            trader_id: None,
2141            strategy_id: None,
2142            account_id: None,
2143            instrument_id: None,
2144            pool_address: None,
2145            transaction_to: to.to_string(),
2146            transaction_input: hex::encode_prefixed(&input),
2147            transaction_value: value.to_string(),
2148            amount_in: None,
2149            created_block,
2150        };
2151        let intent = match self.database.reserve_execution_intent(&intent).await {
2152            Ok(intent) => intent,
2153            Err(e) => {
2154                release_preparing_if_reservation_not_committed(&self.in_flight, &e);
2155                return Err(e);
2156            }
2157        };
2158        let prepared = match self
2159            .prepare_and_sign(
2160                intent.id,
2161                intent.created_block,
2162                to,
2163                value,
2164                input,
2165                &authorization,
2166                decision_header,
2167            )
2168            .await
2169        {
2170            Ok(prepared) => prepared,
2171            Err(e) => {
2172                if self
2173                    .database
2174                    .mark_execution_intent_recoverable(intent.id)
2175                    .await
2176                    .is_ok()
2177                {
2178                    release_preparing_slot(&self.in_flight);
2179                }
2180                return Err(e);
2181            }
2182        };
2183        self.fill_and_persist(&prepared, purpose).await?;
2184
2185        match self.broadcast(&prepared).await? {
2186            BroadcastOutcome::Accepted => {}
2187            BroadcastOutcome::Ambiguous(message) => log::warn!("{message}"),
2188        }
2189
2190        match self.await_finality(&prepared).await? {
2191            InclusionOutcome::Finalized(mut included) => {
2192                included.finality.decisions.extend(
2193                    verify_finalized_transaction(
2194                        &included,
2195                        &intent,
2196                        prepared.nonce,
2197                        &prepared.raw_tx,
2198                        self,
2199                        purpose.as_str(),
2200                    )
2201                    .await?,
2202                );
2203                Ok(included)
2204            }
2205            InclusionOutcome::Reverted(mut included) => {
2206                included.finality.decisions.extend(
2207                    verify_finalized_transaction(
2208                        &included,
2209                        &intent,
2210                        prepared.nonce,
2211                        &prepared.raw_tx,
2212                        self,
2213                        purpose.as_str(),
2214                    )
2215                    .await?,
2216                );
2217                self.commit_verified_finality(&included, TransactionStatus::Reverted, &[])
2218                    .await?;
2219                self.database
2220                    .mark_execution_event_emitted(prepared.intent_id, "terminal")
2221                    .await?;
2222                self.release_slot();
2223                anyhow::bail!("Transaction {} reverted on-chain", included.tx_hash)
2224            }
2225            InclusionOutcome::Pending(message) => anyhow::bail!(message),
2226        }
2227    }
2228
2229    /// Claims the single in-flight slot before any preparation RPC call, so the `pending`
2230    /// nonce read stays authoritative: a second transaction is rejected before it can sign.
2231    fn claim_slot(&self, purpose: TransactionPurpose) -> anyhow::Result<()> {
2232        let mut slot = self.in_flight.lock();
2233        if let Some(in_flight) = *slot {
2234            return Err(in_flight_limit_error(&in_flight));
2235        }
2236        *slot = Some(InFlightSlot::Preparing(purpose));
2237        Ok(())
2238    }
2239
2240    /// Runs the read-only pre-signing pipeline: chain ID verification, nonce selection, fee
2241    /// and gas policy checks, transaction building, and local signing.
2242    #[expect(
2243        clippy::too_many_arguments,
2244        reason = "Security-critical transaction fields stay explicit at the signing boundary"
2245    )]
2246    #[allow(
2247        clippy::large_futures,
2248        reason = "keeps transaction verification futures inline without additional boxing"
2249    )]
2250    async fn prepare_and_sign(
2251        &self,
2252        intent_id: i64,
2253        created_block: u64,
2254        to: Address,
2255        value: U256,
2256        input: Bytes,
2257        authorization: &TransactionAuthorization,
2258        decision_header: Verified<VerifiedBlockHeader>,
2259    ) -> anyhow::Result<PreparedTransaction> {
2260        self.prepare_and_sign_with_anchors(
2261            intent_id,
2262            created_block,
2263            to,
2264            value,
2265            input,
2266            None,
2267            Some(authorization),
2268            Some(decision_header),
2269        )
2270        .await
2271    }
2272
2273    #[allow(
2274        clippy::large_futures,
2275        reason = "keeps transaction verification futures inline without additional boxing"
2276    )]
2277    async fn prepare_and_sign_swap(
2278        &self,
2279        intent_id: i64,
2280        created_block: u64,
2281        to: Address,
2282        value: U256,
2283        input: Bytes,
2284        anchors: &SwapQuoteAnchors,
2285    ) -> anyhow::Result<PreparedTransaction> {
2286        self.prepare_and_sign_with_anchors(
2287            intent_id,
2288            created_block,
2289            to,
2290            value,
2291            input,
2292            Some(anchors),
2293            None,
2294            None,
2295        )
2296        .await
2297    }
2298
2299    #[expect(
2300        clippy::too_many_arguments,
2301        reason = "Security-critical transaction fields and verification anchors stay explicit"
2302    )]
2303    #[allow(
2304        clippy::large_futures,
2305        reason = "keeps transaction verification futures inline without additional boxing"
2306    )]
2307    async fn prepare_and_sign_with_anchors(
2308        &self,
2309        intent_id: i64,
2310        created_block: u64,
2311        to: Address,
2312        value: U256,
2313        input: Bytes,
2314        swap_anchors: Option<&SwapQuoteAnchors>,
2315        authorization: Option<&TransactionAuthorization>,
2316        decision_header: Option<Verified<VerifiedBlockHeader>>,
2317    ) -> anyhow::Result<PreparedTransaction> {
2318        let expected_chain_id = u64::from(self.chain_id);
2319        let chain_id_verification = required_verification(
2320            self.verification.verify_chain_id().await,
2321            "pre-sign chain ID",
2322        )?;
2323        let actual_chain_id = chain_id_verification.value;
2324        anyhow::ensure!(
2325            actual_chain_id == expected_chain_id,
2326            "Verified chain ID does not match the transaction chain"
2327        );
2328        let decision_header_verification = if let Some(anchors) = swap_anchors {
2329            required_verification(
2330                self.verification.verify_block(anchors.state.number).await,
2331                "pre-sign swap decision header reread",
2332            )?
2333        } else {
2334            match decision_header {
2335                Some(verified) => verified,
2336                None => {
2337                    let now_unix_secs = current_unix_secs()?;
2338                    required_verification(
2339                        self.verification
2340                            .verify_decision_header(now_unix_secs)
2341                            .await,
2342                        "pre-sign decision header",
2343                    )?
2344                }
2345            }
2346        };
2347        let decision_header = decision_header_verification.value;
2348
2349        if let Some(anchors) = swap_anchors {
2350            anyhow::ensure!(
2351                decision_header == anchors.state,
2352                "Verified swap decision header changed before signing"
2353            );
2354        }
2355        let decision_ancestry = self.verify_decision_ancestry(decision_header).await?;
2356        validate_transaction_authorization(authorization, to, value, &input)?;
2357        let deployment_verification = required_verification(
2358            self.verification
2359                .verify_deployment_manifest(&self.deployment_manifest, decision_header.number)
2360                .await,
2361            "pre-sign deployment manifest",
2362        )?;
2363        let authorization_decisions = match authorization {
2364            Some(authorization) => {
2365                self.verify_transaction_authorization(authorization, decision_header.number)
2366                    .await?
2367            }
2368            None => Vec::new(),
2369        };
2370        let base_fee_per_gas_wei = decision_header.base_fee_per_gas.ok_or_else(|| {
2371            anyhow::anyhow!(
2372                "Verified decision block {} has no base fee",
2373                decision_header.number
2374            )
2375        })?;
2376        let priority_fee_verification = required_verification(
2377            self.verification.verify_priority_fee().await,
2378            "pre-sign priority fee",
2379        )?;
2380        let priority_fee_per_gas_wei = priority_fee_verification.value;
2381        let (max_fee_per_gas, max_priority_fee_per_gas) = derive_fees(
2382            base_fee_per_gas_wei,
2383            priority_fee_per_gas_wei,
2384            self.base_fee_buffer_bps,
2385            u128::from(self.max_fee_per_gas_wei),
2386        )?;
2387        let gas_estimate_verification = required_verification(
2388            self.verification
2389                .verify_gas_estimate(
2390                    &self.wallet_address,
2391                    &to,
2392                    value,
2393                    &input,
2394                    decision_header.number,
2395                )
2396                .await,
2397            "pre-sign gas estimate",
2398        )?;
2399        let gas_estimate = gas_estimate_verification.value;
2400        let gas_limit = derive_gas_limit(gas_estimate, self.gas_buffer_bps, self.gas_limit)?;
2401        let max_gas_cost = U256::from(gas_limit)
2402            .checked_mul(U256::from(max_fee_per_gas))
2403            .ok_or_else(|| anyhow::anyhow!("Maximum gas cost overflow"))?;
2404        let max_transaction_cost = value
2405            .checked_add(max_gas_cost)
2406            .ok_or_else(|| anyhow::anyhow!("Maximum transaction cost overflow"))?;
2407        let native_balance_verification = required_verification(
2408            self.verification
2409                .verify_balance(&self.wallet_address, decision_header.number)
2410                .await,
2411            "pre-sign native balance",
2412        )?;
2413        let native_balance = native_balance_verification.value;
2414
2415        if native_balance < max_transaction_cost {
2416            anyhow::bail!(
2417                "Native currency balance {native_balance} wei is below maximum transaction cost {max_transaction_cost} wei"
2418            );
2419        }
2420        let decision_height = Some(decision_header.number);
2421        let mut decisions = vec![
2422            verification_decision(&chain_id_verification, None, None),
2423            verification_decision(
2424                &decision_header_verification,
2425                decision_height,
2426                decision_height,
2427            ),
2428            verification_decision(&deployment_verification, decision_height, decision_height),
2429            verification_decision(&priority_fee_verification, None, None),
2430            verification_decision(&gas_estimate_verification, decision_height, decision_height),
2431            verification_decision(
2432                &native_balance_verification,
2433                decision_height,
2434                decision_height,
2435            ),
2436        ];
2437        decisions.extend(decision_ancestry);
2438        decisions.extend(authorization_decisions);
2439        if let Some(anchors) = swap_anchors {
2440            decisions.extend(self.verify_swap_anchors_before_sign(anchors).await?);
2441            decisions.extend(anchors.precondition_decisions.iter().cloned());
2442        } else {
2443            decisions.extend(self.verify_pre_sign_header_fence(decision_header).await?);
2444        }
2445        let canonical_nonce_verification = required_verification(
2446            self.verification
2447                .verify_transaction_count(&self.wallet_address, decision_header.number)
2448                .await,
2449            "pre-sign canonical nonce reread",
2450        )?;
2451        let pending_nonce_verification = required_verification(
2452            self.verification
2453                .verify_pending_transaction_count(&self.wallet_address)
2454                .await,
2455            "pre-sign pending nonce reread",
2456        )?;
2457        anyhow::ensure!(
2458            pending_nonce_verification.value == canonical_nonce_verification.value,
2459            "Pending nonce does not match the verified canonical nonce"
2460        );
2461        let nonce = canonical_nonce_verification.value;
2462        decisions.push(verification_decision(
2463            &canonical_nonce_verification,
2464            decision_height,
2465            decision_height,
2466        ));
2467        decisions.push(verification_decision(
2468            &pending_nonce_verification,
2469            None,
2470            None,
2471        ));
2472        let tx = build_eip1559_transaction(
2473            expected_chain_id,
2474            nonce,
2475            gas_limit,
2476            max_fee_per_gas,
2477            max_priority_fee_per_gas,
2478            to,
2479            value,
2480            input,
2481        );
2482        let wallet_address = self.wallet_address.to_string();
2483        self.database
2484            .assign_execution_intent_nonce_verified(&ExecutionNonceAssignment {
2485                intent_id,
2486                chain_id: self.chain_id,
2487                wallet_address: &wallet_address,
2488                nonce,
2489                manifest_version: &self.manifest_version,
2490                manifest_digest: &self.manifest_digest,
2491                provider_ids: &self.provider_ids,
2492                operator_ids: &self.operator_ids,
2493                failure_domain_ids: &self.failure_domain_ids,
2494                decisions: &decisions,
2495            })
2496            .await?;
2497        let payload_lease = self
2498            .database
2499            .acquire_execution_payload_lease(&self.payload_keys)
2500            .await?;
2501        let (tx_hash, raw_tx) = sign_eip1559_transaction(tx, &self.signer).await?;
2502
2503        Ok(PreparedTransaction {
2504            intent_id,
2505            created_block,
2506            nonce,
2507            tx_hash,
2508            raw_tx,
2509            payload_lease: Some(payload_lease),
2510        })
2511    }
2512
2513    async fn verify_pre_sign_header_fence(
2514        &self,
2515        target: VerifiedBlockHeader,
2516    ) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
2517        let checkpoint = required_verification(
2518            self.verification.verify_checkpoint().await,
2519            "pre-sign checkpoint reread",
2520        )?;
2521        let header = required_verification(
2522            self.verification.verify_block(target.number).await,
2523            "pre-sign decision header reread",
2524        )?;
2525        anyhow::ensure!(
2526            header.value == target,
2527            "Decision header changed before signing"
2528        );
2529        Ok(vec![
2530            verification_decision(
2531                &checkpoint,
2532                Some(checkpoint.value.number),
2533                Some(checkpoint.value.number),
2534            ),
2535            verification_decision(&header, Some(target.number), Some(target.number)),
2536        ])
2537    }
2538
2539    async fn verify_decision_ancestry(
2540        &self,
2541        target: VerifiedBlockHeader,
2542    ) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
2543        let checkpoint = required_verification(
2544            self.verification.verify_checkpoint().await,
2545            "pre-sign checkpoint",
2546        )?;
2547        anyhow::ensure!(
2548            checkpoint.value.number <= target.number,
2549            "Pre-sign decision header precedes the trusted checkpoint"
2550        );
2551        let mut decisions = vec![verification_decision(
2552            &checkpoint,
2553            Some(checkpoint.value.number),
2554            Some(checkpoint.value.number),
2555        )];
2556        let wallet_address = self.wallet_address.to_string();
2557        let position = self
2558            .database
2559            .load_execution_verification_position(
2560                self.chain_id,
2561                &wallet_address,
2562                &self.manifest_version,
2563                &self.manifest_digest,
2564            )
2565            .await?
2566            .ok_or_else(|| anyhow::anyhow!("Execution verification ledger is not initialized"))?;
2567        let durable_tip = parse_verified_header(&position.finalized_tip)?;
2568        anyhow::ensure!(
2569            durable_tip.number >= checkpoint.value.number && durable_tip.number <= target.number,
2570            "Pre-sign decision header does not extend the durable finalized header tip"
2571        );
2572        let durable_tip_verification = required_verification(
2573            self.verification.verify_block(durable_tip.number).await,
2574            "pre-sign durable finalized tip",
2575        )?;
2576        anyhow::ensure!(
2577            durable_tip_verification.value == durable_tip,
2578            "Durable finalized header tip conflicts with independent sources"
2579        );
2580
2581        if durable_tip != checkpoint.value {
2582            decisions.push(verification_decision(
2583                &durable_tip_verification,
2584                Some(durable_tip.number),
2585                Some(durable_tip.number),
2586            ));
2587        }
2588        let mut cursor = durable_tip;
2589        while cursor.number < target.number {
2590            let end = cursor.number.saturating_add(4_096).min(target.number);
2591            let start = cursor.number.saturating_add(1);
2592            let ancestry = required_verification(
2593                self.verification.verify_header_window(cursor, end).await,
2594                "pre-sign decision ancestry",
2595            )?;
2596            cursor = *ancestry
2597                .value
2598                .last()
2599                .expect("nonempty decision ancestry advances the cursor");
2600            decisions.push(verification_decision(&ancestry, Some(start), Some(end)));
2601        }
2602        anyhow::ensure!(
2603            cursor == target,
2604            "Pre-sign decision header conflicts with its trusted ancestry"
2605        );
2606        Ok(decisions)
2607    }
2608
2609    async fn verify_transaction_authorization(
2610        &self,
2611        authorization: &TransactionAuthorization,
2612        block: u64,
2613    ) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
2614        match *authorization {
2615            TransactionAuthorization::Wrap { weth } => {
2616                let call = ERC20::balanceOfCall {
2617                    account: self.wallet_address,
2618                }
2619                .abi_encode();
2620                let balance = required_verification(
2621                    self.verification
2622                        .verify_decoded_call(None, &weth, U256::ZERO, &call, block, |result| {
2623                            ERC20::balanceOfCall::abi_decode_returns(result).map_err(Into::into)
2624                        })
2625                        .await,
2626                    "pre-sign wrapped token probe",
2627                )?;
2628                Ok(vec![verification_decision(
2629                    &balance,
2630                    Some(block),
2631                    Some(block),
2632                )])
2633            }
2634            TransactionAuthorization::Approve {
2635                token,
2636                router,
2637                amount,
2638            } => {
2639                let allowance_call = ERC20::allowanceCall {
2640                    owner: self.wallet_address,
2641                    spender: router,
2642                }
2643                .abi_encode();
2644                let allowance = required_verification(
2645                    self.verification
2646                        .verify_decoded_call(
2647                            None,
2648                            &token,
2649                            U256::ZERO,
2650                            &allowance_call,
2651                            block,
2652                            |result| {
2653                                ERC20::allowanceCall::abi_decode_returns(result).map_err(Into::into)
2654                            },
2655                        )
2656                        .await,
2657                    "pre-sign router allowance",
2658                )?;
2659                anyhow::ensure!(
2660                    allowance.value.is_zero() || amount.is_zero(),
2661                    "Router allowance for token {token} is already {}; approve zero before setting a new nonzero allowance",
2662                    allowance.value
2663                );
2664
2665                let approve_call = ERC20::approveCall {
2666                    spender: router,
2667                    amount,
2668                }
2669                .abi_encode();
2670                let simulation = required_verification(
2671                    self.verification
2672                        .verify_decoded_simulation(
2673                            &self.wallet_address,
2674                            &token,
2675                            U256::ZERO,
2676                            &approve_call,
2677                            block,
2678                            |result| {
2679                                if result.is_empty() {
2680                                    Ok(true)
2681                                } else {
2682                                    ERC20::approveCall::abi_decode_returns_validate(result)
2683                                        .map_err(Into::into)
2684                                }
2685                            },
2686                        )
2687                        .await,
2688                    "pre-sign approval simulation",
2689                )?;
2690
2691                match &simulation.value {
2692                    VerifiedSimulation::Succeeded(true) => {}
2693                    VerifiedSimulation::Succeeded(false) => {
2694                        anyhow::bail!("ERC-20 approve returned false for token {token}")
2695                    }
2696                    VerifiedSimulation::Denied => {
2697                        anyhow::bail!("ERC-20 approve simulation reverted for token {token}")
2698                    }
2699                }
2700                Ok(vec![
2701                    verification_decision(&allowance, Some(block), Some(block)),
2702                    verification_decision(&simulation, Some(block), Some(block)),
2703                ])
2704            }
2705        }
2706    }
2707
2708    async fn verify_swap_anchors_before_sign(
2709        &self,
2710        anchors: &SwapQuoteAnchors,
2711    ) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
2712        let checkpoint = required_verification(
2713            self.verification.verify_checkpoint().await,
2714            "pre-sign checkpoint reread",
2715        )?;
2716        let watermark = required_verification(
2717            self.verification
2718                .verify_block(anchors.watermark.number)
2719                .await,
2720            "pre-sign profiler watermark reread",
2721        )?;
2722        anyhow::ensure!(
2723            watermark.value == anchors.watermark,
2724            "Pool state header changed before signing"
2725        );
2726        let ancestry = required_verification(
2727            self.verification
2728                .verify_header_window(watermark.value, anchors.state.number)
2729                .await,
2730            "pre-sign profiler ancestry reread",
2731        )?;
2732
2733        if let Some(last) = ancestry.value.last() {
2734            anyhow::ensure!(
2735                *last == anchors.state,
2736                "Swap decision header changed in the pre-sign ancestry reread"
2737            );
2738        } else {
2739            anyhow::ensure!(
2740                watermark.value == anchors.state,
2741                "Empty pre-sign ancestry does not end at the swap decision header"
2742            );
2743        }
2744        let quote = match anchors.quote_kind {
2745            SwapQuoteKind::ExactInput(amount_in) => required_verification(
2746                self.verification
2747                    .verify_quote_exact_input_single(
2748                        &anchors.quote_contract,
2749                        anchors.token_in,
2750                        anchors.token_out,
2751                        amount_in,
2752                        anchors.fee,
2753                        anchors.state.number,
2754                    )
2755                    .await,
2756                "pre-sign exact-input quote reread",
2757            )?,
2758            SwapQuoteKind::ExactOutput(amount_out) => required_verification(
2759                self.verification
2760                    .verify_quote_exact_output_single(
2761                        &anchors.quote_contract,
2762                        anchors.token_in,
2763                        anchors.token_out,
2764                        amount_out,
2765                        anchors.fee,
2766                        anchors.state.number,
2767                    )
2768                    .await,
2769                "pre-sign exact-output quote reread",
2770            )?,
2771        };
2772        anyhow::ensure!(
2773            quote.value == anchors.quote,
2774            "Independent swap quote changed before signing"
2775        );
2776        Ok(vec![
2777            verification_decision(
2778                &checkpoint,
2779                Some(checkpoint.value.number),
2780                Some(checkpoint.value.number),
2781            ),
2782            verification_decision(
2783                &watermark,
2784                Some(anchors.watermark.number),
2785                Some(anchors.watermark.number),
2786            ),
2787            verification_decision(
2788                &ancestry,
2789                Some(anchors.watermark.number),
2790                Some(anchors.state.number),
2791            ),
2792            verification_decision(
2793                &quote,
2794                Some(anchors.state.number),
2795                Some(anchors.state.number),
2796            ),
2797        ])
2798    }
2799
2800    /// Fills the claimed slot with the signed transaction and persists the pending record
2801    /// before broadcast.
2802    ///
2803    /// The slot is filled before the cancellable write: PostgreSQL may commit before this
2804    /// future resumes, so cancellation cannot safely restore the pre-transaction state. A
2805    /// persistence failure leaves the slot occupied because commit acknowledgement is
2806    /// ambiguous.
2807    async fn fill_and_persist(
2808        &self,
2809        prepared: &PreparedTransaction,
2810        purpose: TransactionPurpose,
2811    ) -> anyhow::Result<()> {
2812        {
2813            let mut slot = self.in_flight.lock();
2814            *slot = Some(InFlightSlot::AwaitingFinality(InFlightTransaction {
2815                intent_id: prepared.intent_id,
2816                nonce: prepared.nonce,
2817                tx_hash: prepared.tx_hash,
2818                purpose,
2819            }));
2820        }
2821
2822        let tx_hash = prepared.tx_hash;
2823        let transaction_hash = tx_hash.to_string();
2824        let policy = self.payload_policy();
2825        let intent = self
2826            .database
2827            .get_execution_intent(prepared.intent_id)
2828            .await?;
2829        authenticate_payload_identity(
2830            &prepared.raw_tx,
2831            &intent,
2832            &transaction_hash,
2833            self.chain_id,
2834            policy,
2835        )
2836        .with_context(|| format!("Newly signed transaction {tx_hash} failed authentication"))?;
2837
2838        self.database
2839            .reserve_execution_payload_seal(&self.payload_keys)
2840            .await?;
2841        let context = payload_context_identity(
2842            &intent,
2843            &transaction_hash,
2844            self.chain_id,
2845            self.payload_keys.deployment_id(),
2846        )?;
2847        let envelope = self.payload_keys.seal(&prepared.raw_tx, &context)?;
2848        let row = self
2849            .database
2850            .add_execution_transaction_envelope(
2851                prepared.intent_id,
2852                self.chain_id,
2853                &transaction_hash,
2854                &envelope,
2855            )
2856            .await
2857        .map_err(|e| {
2858            anyhow::anyhow!(
2859                "Failed to persist transaction {tx_hash}: {e}; the in-flight slot stays occupied"
2860            )
2861        })?;
2862        let stored = open_execution_payload(
2863            &self.payload_keys,
2864            policy,
2865            &intent,
2866            &row,
2867            "initial persistence",
2868        )?;
2869        anyhow::ensure!(
2870            stored == prepared.raw_tx,
2871            "Persisted transaction {tx_hash} does not match the signed bytes"
2872        );
2873        Ok(())
2874    }
2875
2876    fn payload_policy(&self) -> PayloadPolicy {
2877        PayloadPolicy {
2878            chain_id: self.chain_id,
2879            signer: self.wallet_address,
2880            gas_limit: self.gas_limit,
2881            max_fee_per_gas: self.max_fee_per_gas_wei,
2882        }
2883    }
2884
2885    #[allow(
2886        clippy::large_futures,
2887        reason = "keeps transaction verification futures inline without additional boxing"
2888    )]
2889    async fn authorize_rebroadcast(
2890        &self,
2891        prepared: &PreparedTransaction,
2892        intent: &ExecutionIntentRow,
2893        purpose: TransactionPurpose,
2894    ) -> anyhow::Result<ReconciliationAuthorization> {
2895        let now_unix_secs = current_unix_secs()?;
2896        let decision_header = required_verification(
2897            self.verification
2898                .verify_decision_header(now_unix_secs)
2899                .await,
2900            "rebroadcast decision header",
2901        )?;
2902        let block = decision_header.value.number;
2903        let mut decisions = vec![verification_decision(
2904            &decision_header,
2905            Some(block),
2906            Some(block),
2907        )];
2908        decisions.extend(self.verify_decision_ancestry(decision_header.value).await?);
2909        let deployment = required_verification(
2910            self.verification
2911                .verify_deployment_manifest(&self.deployment_manifest, block)
2912                .await,
2913            "rebroadcast deployment manifest",
2914        )?;
2915        decisions.push(verification_decision(&deployment, Some(block), Some(block)));
2916        let canonical_nonce = required_verification(
2917            self.verification
2918                .verify_transaction_count(&self.wallet_address, block)
2919                .await,
2920            "rebroadcast canonical nonce",
2921        )?;
2922        decisions.push(verification_decision(
2923            &canonical_nonce,
2924            Some(block),
2925            Some(block),
2926        ));
2927        let pending_nonce = required_verification(
2928            self.verification
2929                .verify_reconciliation_pending_transaction_count(
2930                    &self.wallet_address,
2931                    prepared.nonce,
2932                )
2933                .await,
2934            "rebroadcast pending nonce",
2935        )?;
2936        decisions.push(verification_decision(&pending_nonce, None, None));
2937        let receipt_absence = required_verification(
2938            self.verification
2939                .verify_receipt_absence(&prepared.tx_hash)
2940                .await,
2941            "rebroadcast receipt absence",
2942        )?;
2943        decisions.push(verification_decision(&receipt_absence, None, None));
2944
2945        let next_nonce = prepared
2946            .nonce
2947            .checked_add(1)
2948            .ok_or_else(|| anyhow::anyhow!("Owned signer nonce overflow"))?;
2949        anyhow::ensure!(
2950            (prepared.nonce..=next_nonce).contains(&pending_nonce.value),
2951            "Pending nonce {} is outside the owned reconciliation range {}..={next_nonce}",
2952            pending_nonce.value,
2953            prepared.nonce
2954        );
2955        anyhow::ensure!(
2956            (prepared.nonce..=next_nonce).contains(&canonical_nonce.value),
2957            "Canonical nonce {} is outside the owned reconciliation range {}..={next_nonce}",
2958            canonical_nonce.value,
2959            prepared.nonce
2960        );
2961
2962        if !receipt_absence.value {
2963            self.persist_rebroadcast_decisions(intent.id, prepared.nonce, &decisions)
2964                .await?;
2965            return Ok(ReconciliationAuthorization::Retain);
2966        }
2967
2968        if canonical_nonce.value == next_nonce {
2969            self.persist_rebroadcast_decisions(intent.id, prepared.nonce, &decisions)
2970                .await?;
2971            return Ok(ReconciliationAuthorization::ScanReplacement(
2972                decision_header.value,
2973            ));
2974        }
2975
2976        let (to, input, value) = persisted_call_fields(intent)?;
2977        let authorized = match purpose {
2978            TransactionPurpose::Wrap => {
2979                let simulation = required_verification(
2980                    self.verification
2981                        .verify_decoded_simulation(
2982                            &self.wallet_address,
2983                            &to,
2984                            value,
2985                            &input,
2986                            block,
2987                            |result| Ok(result.is_empty()),
2988                        )
2989                        .await,
2990                    "rebroadcast wrap simulation",
2991                )?;
2992                let authorized = matches!(&simulation.value, VerifiedSimulation::Succeeded(true));
2993                decisions.push(verification_decision(&simulation, Some(block), Some(block)));
2994                authorized
2995            }
2996            TransactionPurpose::Approve => {
2997                let simulation = required_verification(
2998                    self.verification
2999                        .verify_decoded_simulation(
3000                            &self.wallet_address,
3001                            &to,
3002                            value,
3003                            &input,
3004                            block,
3005                            |result| {
3006                                if result.is_empty() {
3007                                    Ok(true)
3008                                } else {
3009                                    ERC20::approveCall::abi_decode_returns_validate(result)
3010                                        .map_err(Into::into)
3011                                }
3012                            },
3013                        )
3014                        .await,
3015                    "rebroadcast approve simulation",
3016                )?;
3017                let authorized = matches!(&simulation.value, VerifiedSimulation::Succeeded(true));
3018                decisions.push(verification_decision(&simulation, Some(block), Some(block)));
3019                authorized
3020            }
3021            TransactionPurpose::Swap => {
3022                let call = UniswapV3SwapRouter::exactInputSingleCall::abi_decode(&input)
3023                    .with_context(|| "persisted swap calldata is invalid")?;
3024
3025                if U256::from(decision_header.value.timestamp) > call.params.deadline {
3026                    false
3027                } else {
3028                    let simulation = required_verification(
3029                        self.verification
3030                            .verify_decoded_simulation(
3031                                &self.wallet_address,
3032                                &to,
3033                                value,
3034                                &input,
3035                                block,
3036                                |result| {
3037                                    UniswapV3SwapRouter::exactInputSingleCall::abi_decode_returns(
3038                                        result,
3039                                    )
3040                                    .map_err(Into::into)
3041                                },
3042                            )
3043                            .await,
3044                        "rebroadcast swap simulation",
3045                    )?;
3046                    let authorized = match &simulation.value {
3047                        VerifiedSimulation::Succeeded(amount_out) => {
3048                            *amount_out >= call.params.amountOutMinimum
3049                        }
3050                        VerifiedSimulation::Denied => false,
3051                    };
3052                    decisions.push(verification_decision(&simulation, Some(block), Some(block)));
3053                    authorized
3054                }
3055            }
3056        };
3057        self.persist_rebroadcast_decisions(intent.id, prepared.nonce, &decisions)
3058            .await?;
3059        Ok(if authorized {
3060            ReconciliationAuthorization::Rebroadcast
3061        } else {
3062            ReconciliationAuthorization::Retain
3063        })
3064    }
3065
3066    async fn scan_canonical_replacement(
3067        &self,
3068        intent: &ExecutionIntentRow,
3069        nonce: u64,
3070        head: VerifiedBlockHeader,
3071        authenticated_payloads: &HashMap<B256, Vec<u8>>,
3072    ) -> anyhow::Result<Option<(B256, Vec<u8>)>> {
3073        let wallet_address = self.wallet_address.to_string();
3074        let cursor = self
3075            .database
3076            .load_execution_replacement_cursor(
3077                intent.id,
3078                self.chain_id,
3079                &wallet_address,
3080                nonce,
3081                &self.manifest_digest,
3082            )
3083            .await?;
3084        let start = cursor.as_ref().map_or(intent.created_block, |header| {
3085            header.number.saturating_add(1)
3086        });
3087        anyhow::ensure!(
3088            start <= head.number,
3089            "Canonical nonce advanced without an authenticated signer transaction in the scanned canonical range"
3090        );
3091        let scan_range = replacement_scan_range(start, head.number)?;
3092        let end = *scan_range.end();
3093        let mut decisions = Vec::new();
3094        let mut blocks = Vec::new();
3095
3096        if let Some(cursor) = cursor.as_ref() {
3097            let parent = parse_verified_header(cursor)?;
3098            let window = required_verification(
3099                self.verification
3100                    .verify_replacement_window(parent, end)
3101                    .await,
3102                "canonical replacement window",
3103            )?;
3104            decisions.push(verification_decision(&window, Some(start), Some(end)));
3105            blocks = window.value;
3106        } else {
3107            let start_header = required_verification(
3108                self.verification.verify_block(start).await,
3109                "canonical replacement start header",
3110            )?;
3111            decisions.push(verification_decision(
3112                &start_header,
3113                Some(start),
3114                Some(start),
3115            ));
3116            let start_block = required_verification(
3117                self.verification.verify_replacement_block(start).await,
3118                "canonical replacement start block",
3119            )?;
3120            anyhow::ensure!(
3121                VerifiedBlockHeader::from(start_block.value.clone()) == start_header.value,
3122                "Replacement block conflicts with its canonical header"
3123            );
3124            decisions.push(verification_decision(
3125                &start_block,
3126                Some(start),
3127                Some(start),
3128            ));
3129            blocks.push(start_block.value);
3130
3131            if end > start {
3132                let window = required_verification(
3133                    self.verification
3134                        .verify_replacement_window(start_header.value, end)
3135                        .await,
3136                    "canonical replacement window",
3137                )?;
3138                decisions.push(verification_decision(&window, Some(start + 1), Some(end)));
3139                blocks.extend(window.value);
3140            }
3141        }
3142
3143        let scanned_tip = blocks
3144            .last()
3145            .map(|block| VerifiedBlockHeader::from(block.clone()))
3146            .ok_or_else(|| anyhow::anyhow!("Verified replacement scan returned no blocks"))?;
3147        if end == head.number {
3148            anyhow::ensure!(
3149                scanned_tip == head,
3150                "Replacement scan tip conflicts with the verified canonical head"
3151            );
3152        }
3153        let mut candidates = blocks
3154            .iter()
3155            .flat_map(|block| block.transactions.iter())
3156            .filter(|transaction| {
3157                transaction.from == self.wallet_address && transaction.nonce == nonce
3158            });
3159        let candidate = candidates.next();
3160        anyhow::ensure!(
3161            candidates.next().is_none(),
3162            "Canonical replacement scan found duplicate signer-nonce transactions"
3163        );
3164
3165        let finalized_cursor = self
3166            .database
3167            .load_execution_verified_header(
3168                self.chain_id,
3169                &wallet_address,
3170                end,
3171                &self.manifest_digest,
3172            )
3173            .await?;
3174
3175        if let Some(cursor) = finalized_cursor.as_ref() {
3176            anyhow::ensure!(
3177                parse_verified_header(cursor)? == scanned_tip,
3178                "Replacement scan conflicts with the durable finalized header ledger"
3179            );
3180        }
3181
3182        let mut mismatch = None;
3183        let matched = candidate.and_then(|transaction| {
3184            let Some(raw_transaction) = authenticated_payloads.get(&transaction.hash).cloned()
3185            else {
3186                mismatch = Some(anyhow::anyhow!(
3187                    "Canonical signer-nonce transaction {} has no authenticated retained payload",
3188                    transaction.hash
3189                ));
3190                return None;
3191            };
3192
3193            if let Err(e) = validate_rpc_transaction_matches_payload(transaction, &raw_transaction)
3194            {
3195                mismatch = Some(e.context(format!(
3196                    "Canonical signer-nonce transaction {} failed authenticated payload validation",
3197                    transaction.hash
3198                )));
3199                return None;
3200            }
3201            Some((transaction.hash, raw_transaction))
3202        });
3203        let matched_hash = matched.as_ref().map(|(hash, _)| hash.to_string());
3204        self.database
3205            .record_execution_replacement_scan(&ExecutionReplacementScan {
3206                intent_id: intent.id,
3207                chain_id: self.chain_id,
3208                wallet_address: &wallet_address,
3209                nonce,
3210                finalized_cursor: finalized_cursor.as_ref(),
3211                matched_transaction_hash: matched_hash.as_deref(),
3212                manifest_version: &self.manifest_version,
3213                manifest_digest: &self.manifest_digest,
3214                provider_ids: &self.provider_ids,
3215                operator_ids: &self.operator_ids,
3216                failure_domain_ids: &self.failure_domain_ids,
3217                decisions: &decisions,
3218            })
3219            .await?;
3220
3221        if let Some(e) = mismatch {
3222            return Err(e);
3223        }
3224
3225        if matched.is_none() && end == head.number {
3226            anyhow::bail!(
3227                "Canonical nonce advanced without an authenticated signer transaction in the canonical range"
3228            );
3229        }
3230        Ok(matched)
3231    }
3232
3233    async fn persist_rebroadcast_decisions(
3234        &self,
3235        intent_id: i64,
3236        nonce: u64,
3237        decisions: &[ExecutionVerificationDecision],
3238    ) -> anyhow::Result<()> {
3239        let wallet_address = self.wallet_address.to_string();
3240        self.database
3241            .record_execution_verification_batch(&ExecutionVerificationBatch {
3242                intent_id,
3243                chain_id: self.chain_id,
3244                wallet_address: &wallet_address,
3245                nonce,
3246                decision_class: "rebroadcast",
3247                manifest_version: &self.manifest_version,
3248                manifest_digest: &self.manifest_digest,
3249                provider_ids: &self.provider_ids,
3250                operator_ids: &self.operator_ids,
3251                failure_domain_ids: &self.failure_domain_ids,
3252                decisions,
3253            })
3254            .await
3255    }
3256
3257    async fn broadcast(&self, prepared: &PreparedTransaction) -> anyhow::Result<BroadcastOutcome> {
3258        let tx_hash = prepared.tx_hash;
3259
3260        self.database
3261            .record_execution_status(
3262                prepared.intent_id,
3263                &tx_hash.to_string(),
3264                TransactionStatus::Broadcast,
3265                None,
3266                None,
3267                None,
3268                None,
3269                None,
3270            )
3271            .await
3272            .map_err(|e| {
3273                anyhow::anyhow!(
3274                    "Failed to persist broadcast attempt for transaction {tx_hash}: {e}; the in-flight slot stays occupied"
3275                )
3276            })?;
3277
3278        match self
3279            .http_rpc_client
3280            .send_raw_transaction(&prepared.raw_tx, &tx_hash)
3281            .await
3282        {
3283            Ok(broadcast_hash) => {
3284                if broadcast_hash != tx_hash {
3285                    // The node acknowledged a different hash: acceptance of the signed
3286                    // transaction is unverified, so reconcile through the persisted record
3287                    // rather than poll a hash that cannot match the chain
3288                    return Ok(BroadcastOutcome::Ambiguous(format!(
3289                        "Broadcast of transaction {tx_hash} returned a differing hash {broadcast_hash}; the persisted record reconciles instead of rebroadcasting"
3290                    )));
3291                }
3292                Ok(BroadcastOutcome::Accepted)
3293            }
3294            Err(BroadcastError::TimeoutAfterSend) => Ok(BroadcastOutcome::Ambiguous(format!(
3295                "Broadcast of transaction {tx_hash} timed out after send; the persisted record reconciles instead of rebroadcasting"
3296            ))),
3297            Err(BroadcastError::Failed(message)) => {
3298                // Transport or response failure: acceptance is ambiguous (the failure may also
3299                // predate dispatch; see BroadcastError::Failed), so occupy the slot and
3300                // reconcile rather than risk a rebroadcast
3301                Ok(BroadcastOutcome::Ambiguous(format!(
3302                    "Broadcast of transaction {tx_hash} failed ambiguously ({message}); the persisted record reconciles instead of rebroadcasting"
3303                )))
3304            }
3305            Err(error @ BroadcastError::Rejected { .. }) => {
3306                Ok(BroadcastOutcome::Ambiguous(format!(
3307                    "Broadcast of transaction {tx_hash} was rejected ({error}); the signed hash remains occupied until canonical nonce reconciliation"
3308                )))
3309            }
3310        }
3311    }
3312
3313    /// Polls until the receipt is canonical at a stable finalized boundary.
3314    #[allow(
3315        clippy::large_futures,
3316        reason = "keeps transaction verification futures inline without additional boxing"
3317    )]
3318    async fn await_finality(
3319        &self,
3320        prepared: &PreparedTransaction,
3321    ) -> anyhow::Result<InclusionOutcome> {
3322        let tx_hash = prepared.tx_hash;
3323        let deadline = tokio::time::Instant::now() + self.receipt_timeout;
3324
3325        for attempt in 0..self.receipt_max_polls {
3326            if tokio::time::Instant::now() >= deadline {
3327                break;
3328            }
3329
3330            if attempt > 0 {
3331                tokio::time::sleep(RECEIPT_POLL_INTERVAL).await;
3332            }
3333
3334            let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
3335            let receipt_result =
3336                match tokio::time::timeout(remaining, self.verification.verify_receipt(&tx_hash))
3337                    .await
3338                {
3339                    Ok(result) => result,
3340                    Err(_) => break,
3341                };
3342
3343            match receipt_result {
3344                VerificationOutcome::Verified(verified_receipt) => {
3345                    let receipt = &verified_receipt.value;
3346                    let canonical_verification = required_verification(
3347                        self.verification.verify_block(receipt.block_number).await,
3348                        "receipt inclusion header",
3349                    )?;
3350                    let canonical = canonical_verification.value;
3351
3352                    if canonical.hash != receipt.block_hash {
3353                        continue;
3354                    }
3355
3356                    if let Some(mut finality) = self.receipt_is_stably_finalized(receipt).await? {
3357                        finality.decisions.insert(
3358                            0,
3359                            verification_decision(
3360                                &canonical_verification,
3361                                Some(receipt.block_number),
3362                                Some(receipt.block_number),
3363                            ),
3364                        );
3365                        finality.decisions.insert(
3366                            0,
3367                            verification_decision(
3368                                &verified_receipt,
3369                                Some(receipt.block_number),
3370                                Some(receipt.block_number),
3371                            ),
3372                        );
3373                        let included = IncludedTransaction {
3374                            intent_id: prepared.intent_id,
3375                            nonce: prepared.nonce,
3376                            tx_hash,
3377                            block_number: receipt.block_number,
3378                            receipt: receipt.clone(),
3379                            finality,
3380                        };
3381                        return if receipt.status {
3382                            Ok(InclusionOutcome::Finalized(included))
3383                        } else {
3384                            Ok(InclusionOutcome::Reverted(included))
3385                        };
3386                    }
3387                }
3388                VerificationOutcome::Retryable(_) => {}
3389                VerificationOutcome::Disagreement(_) => {
3390                    return Ok(InclusionOutcome::Pending(format!(
3391                        "Receipt verification disagreed for transaction {tx_hash}; the intent stays occupied for reconciliation"
3392                    )));
3393                }
3394                VerificationOutcome::Unavailable(_) => {
3395                    log::warn!(
3396                        "Finality poll {}/{} for transaction {tx_hash} was unavailable",
3397                        attempt + 1,
3398                        self.receipt_max_polls
3399                    );
3400                }
3401                VerificationOutcome::LocallyInvalid(_) => {
3402                    anyhow::bail!(
3403                        "Receipt verification is locally invalid for transaction {tx_hash}"
3404                    );
3405                }
3406            }
3407        }
3408
3409        self.database
3410            .record_execution_status(
3411                prepared.intent_id,
3412                &tx_hash.to_string(),
3413                TransactionStatus::Dropped,
3414                None,
3415                None,
3416                None,
3417                None,
3418                None,
3419            )
3420            .await?;
3421        Ok(InclusionOutcome::Pending(format!(
3422            "Timed out awaiting finality of transaction {tx_hash}; the intent stays occupied for reconciliation"
3423        )))
3424    }
3425
3426    async fn receipt_is_stably_finalized(
3427        &self,
3428        receipt: &RpcTransactionReceipt,
3429    ) -> anyhow::Result<Option<StableFinality>> {
3430        let finalized_verification = match self.verification.verify_finalized_header().await {
3431            VerificationOutcome::Verified(verified) => verified,
3432            VerificationOutcome::Retryable(_) | VerificationOutcome::Unavailable(_) => {
3433                return Ok(None);
3434            }
3435            VerificationOutcome::Disagreement(_) => {
3436                anyhow::bail!("Finalized header verification disagreed")
3437            }
3438            VerificationOutcome::LocallyInvalid(_) => {
3439                anyhow::bail!("Finalized header verification is locally invalid")
3440            }
3441        };
3442        let finalized = finalized_verification.value;
3443        if finalized.number < receipt.block_number {
3444            return Ok(None);
3445        }
3446
3447        let checkpoint_verification = required_verification(
3448            self.verification.verify_checkpoint().await,
3449            "finality checkpoint reread",
3450        )?;
3451        let checkpoint = checkpoint_verification.value;
3452        let mut decisions = vec![verification_decision(
3453            &checkpoint_verification,
3454            Some(checkpoint.number),
3455            Some(checkpoint.number),
3456        )];
3457        let position = self
3458            .database
3459            .load_execution_verification_position(
3460                self.chain_id,
3461                &self.wallet_address.to_string(),
3462                &self.manifest_version,
3463                &self.manifest_digest,
3464            )
3465            .await?
3466            .ok_or_else(|| anyhow::anyhow!("Execution verification ledger is not initialized"))?;
3467        let durable_tip = parse_verified_header(&position.finalized_tip)?;
3468        anyhow::ensure!(
3469            durable_tip.number >= checkpoint.number,
3470            "Durable finalized header tip precedes the trusted checkpoint"
3471        );
3472        let mut finalized_headers = vec![durable_tip];
3473        let durable_tip_verification = required_verification(
3474            self.verification.verify_block(durable_tip.number).await,
3475            "finality durable header tip",
3476        )?;
3477        anyhow::ensure!(
3478            durable_tip_verification.value == durable_tip,
3479            "Durable finalized header tip conflicts with independent sources"
3480        );
3481        decisions.push(verification_decision(
3482            &durable_tip_verification,
3483            Some(durable_tip.number),
3484            Some(durable_tip.number),
3485        ));
3486        anyhow::ensure!(
3487            finalized.number >= durable_tip.number,
3488            "Verified finalized height regressed below the durable finalized header tip"
3489        );
3490        let mut ancestry_cursor = durable_tip;
3491        while ancestry_cursor.number < finalized.number {
3492            let end = ancestry_cursor
3493                .number
3494                .saturating_add(4_096)
3495                .min(finalized.number);
3496            let start = ancestry_cursor.number.saturating_add(1);
3497            let ancestry_verification = required_verification(
3498                self.verification
3499                    .verify_header_window(ancestry_cursor, end)
3500                    .await,
3501                "finality ancestry",
3502            )?;
3503            let ancestry = &ancestry_verification.value;
3504            ancestry_cursor = *ancestry
3505                .last()
3506                .expect("nonempty finality ancestry advances the cursor");
3507            decisions.push(verification_decision(
3508                &ancestry_verification,
3509                Some(start),
3510                Some(end),
3511            ));
3512            finalized_headers.extend(ancestry.iter().copied());
3513        }
3514        anyhow::ensure!(
3515            ancestry_cursor == finalized,
3516            "Finalized header conflicts with its verified ancestry"
3517        );
3518
3519        let canonical_again_verification = required_verification(
3520            self.verification.verify_block(receipt.block_number).await,
3521            "finality inclusion header reread",
3522        )?;
3523        let canonical_again = canonical_again_verification.value;
3524        let finalized_again_verification = required_verification(
3525            self.verification.verify_block(finalized.number).await,
3526            "finalized header reread",
3527        )?;
3528        let finalized_again = finalized_again_verification.value;
3529        anyhow::ensure!(
3530            canonical_again.hash == receipt.block_hash && finalized_again == finalized,
3531            "Finality verification disagreed with the receipt or finalized header"
3532        );
3533        decisions.extend([
3534            verification_decision(
3535                &finalized_verification,
3536                Some(finalized.number),
3537                Some(finalized.number),
3538            ),
3539            verification_decision(
3540                &canonical_again_verification,
3541                Some(receipt.block_number),
3542                Some(receipt.block_number),
3543            ),
3544            verification_decision(
3545                &finalized_again_verification,
3546                Some(finalized.number),
3547                Some(finalized.number),
3548            ),
3549        ]);
3550        Ok(Some(StableFinality {
3551            decisions,
3552            inclusion_header: durable_verified_header(&canonical_again),
3553            finalized_headers: finalized_headers
3554                .iter()
3555                .map(durable_verified_header)
3556                .collect(),
3557        }))
3558    }
3559
3560    async fn commit_verified_finality(
3561        &self,
3562        included: &IncludedTransaction,
3563        status: TransactionStatus,
3564        verified_postconditions: &[ExecutionVerificationDecision],
3565    ) -> anyhow::Result<()> {
3566        anyhow::ensure!(
3567            matches!(
3568                status,
3569                TransactionStatus::Finalized | TransactionStatus::Reverted
3570            ) && included.receipt.status == (status == TransactionStatus::Finalized),
3571            "Finality status conflicts with the verified transaction receipt"
3572        );
3573        let mut decisions = included.finality.decisions.clone();
3574        decisions.extend_from_slice(verified_postconditions);
3575        let wallet_address = self.wallet_address.to_string();
3576        let transaction_hash = included.tx_hash.to_string();
3577        let block_hash = included.receipt.block_hash.to_string();
3578        let effective_gas_price = included.receipt.effective_gas_price.to_string();
3579        self.database
3580            .record_execution_finality_verified(&ExecutionFinalityTransition {
3581                intent_id: included.intent_id,
3582                chain_id: self.chain_id,
3583                wallet_address: &wallet_address,
3584                nonce: included.nonce,
3585                transaction_hash: &transaction_hash,
3586                status,
3587                block_number: included.block_number,
3588                block_hash: &block_hash,
3589                receipt_success: included.receipt.status,
3590                gas_used: included.receipt.gas_used,
3591                effective_gas_price: &effective_gas_price,
3592                manifest_version: &self.manifest_version,
3593                manifest_digest: &self.manifest_digest,
3594                provider_ids: &self.provider_ids,
3595                operator_ids: &self.operator_ids,
3596                failure_domain_ids: &self.failure_domain_ids,
3597                decisions: &decisions,
3598                finalized_headers: &included.finality.finalized_headers,
3599            })
3600            .await
3601    }
3602
3603    fn release_slot(&self) {
3604        *self.in_flight.lock() = None;
3605    }
3606}
3607
3608fn replacement_scan_range(from_block: u64, head_block: u64) -> anyhow::Result<RangeInclusive<u64>> {
3609    anyhow::ensure!(
3610        head_block >= from_block,
3611        "Canonical head {head_block} is behind execution creation block {from_block}"
3612    );
3613    let max_end = from_block.saturating_add(MAX_REPLACEMENT_SCAN_BLOCKS - 1);
3614    Ok(from_block..=head_block.min(max_end))
3615}
3616
3617fn current_unix_secs() -> anyhow::Result<u64> {
3618    SystemTime::now()
3619        .duration_since(UNIX_EPOCH)
3620        .map_err(|_| anyhow::anyhow!("Trusted host clock precedes the Unix epoch"))
3621        .map(|duration| duration.as_secs())
3622}
3623
3624fn validate_payload_operation_batch_size(batch_size: usize) -> anyhow::Result<i64> {
3625    anyhow::ensure!(
3626        (1..=MAX_PAYLOAD_OPERATION_BATCH_SIZE).contains(&batch_size),
3627        "Payload operation batch size must be between 1 and {MAX_PAYLOAD_OPERATION_BATCH_SIZE}"
3628    );
3629    Ok(i64::try_from(batch_size).expect("bounded payload batch size fits i64"))
3630}
3631
3632fn current_execution_hash(
3633    intent_id: i64,
3634    hashes: &[ExecutionTransactionHashRow],
3635) -> anyhow::Result<&ExecutionTransactionHashRow> {
3636    let mut current = hashes.iter().filter(|row| row.current);
3637    let row = current
3638        .next()
3639        .ok_or_else(|| anyhow::anyhow!("Execution intent {intent_id} has no current hash"))?;
3640    anyhow::ensure!(
3641        current.next().is_none(),
3642        "Execution intent {intent_id} has more than one current hash"
3643    );
3644    Ok(row)
3645}
3646
3647fn open_execution_payload(
3648    keys: &PayloadKeySet,
3649    policy: PayloadPolicy,
3650    intent: &ExecutionIntentRow,
3651    hash: &ExecutionTransactionHashRow,
3652    reason: &str,
3653) -> anyhow::Result<Vec<u8>> {
3654    anyhow::ensure!(
3655        hash.payload_expected,
3656        "Execution transaction {} has no signed payload",
3657        hash.transaction_hash
3658    );
3659    anyhow::ensure!(
3660        hash.raw_transaction.is_none(),
3661        "Protected execution transaction {} contains plaintext",
3662        hash.transaction_hash
3663    );
3664    let envelope = hash.sealed_transaction.as_deref().ok_or_else(|| {
3665        anyhow::anyhow!(
3666            "Protected execution transaction {} has no sealed payload",
3667            hash.transaction_hash
3668        )
3669    })?;
3670    let context = payload_context(intent, hash, keys.deployment_id())?;
3671    let raw_transaction = keys.unseal(envelope, &context)?;
3672    log::info!(
3673        "Unsealed execution payload for intent {} transaction {} during {reason}",
3674        intent.id,
3675        hash.transaction_hash
3676    );
3677    authenticate_retained_payload(&raw_transaction, intent, hash, keys.deployment_id())?;
3678    if retained_payload_requires_policy(intent, hash, policy)? {
3679        authenticate_payload(&raw_transaction, intent, hash, policy, keys.deployment_id())
3680            .with_context(|| {
3681                format!(
3682                    "execution intent {} transaction {} violates current execution policy",
3683                    intent.id, hash.transaction_hash
3684                )
3685            })?;
3686    }
3687    Ok(raw_transaction)
3688}
3689
3690fn receipt_max_polls(receipt_timeout_secs: u64) -> u32 {
3691    u32::try_from(receipt_timeout_secs.max(1)).unwrap_or(u32::MAX)
3692}
3693
3694fn receipt_timeout(receipt_timeout_secs: u64) -> Duration {
3695    Duration::from_secs(receipt_timeout_secs.clamp(1, u64::from(u32::MAX)))
3696}
3697
3698/// A fully validated swap ready for asynchronous dispatch.
3699#[derive(Debug)]
3700struct SwapPlan {
3701    order: OrderAny,
3702    pool: Pool,
3703    quote_currency: Currency,
3704    instrument_id: InstrumentId,
3705    pool_address: Address,
3706    router: Address,
3707    factory: Address,
3708    weth: Address,
3709    token_in: Address,
3710    token_out: Address,
3711    fee: U24,
3712    amount_in: U256,
3713    min_amount_out: U256,
3714    slippage_bps: u32,
3715    quote_spend_ceiling: Option<QuoteSpendCeiling>,
3716    profiler_position: Option<BlockPosition>,
3717}
3718
3719#[derive(Debug, Clone)]
3720struct SwapQuoteAnchors {
3721    watermark: VerifiedBlockHeader,
3722    state: VerifiedBlockHeader,
3723    quote_contract: Address,
3724    token_in: Address,
3725    token_out: Address,
3726    fee: U24,
3727    quote_kind: SwapQuoteKind,
3728    quote: UniswapV3Quote,
3729    precondition_decisions: Vec<ExecutionVerificationDecision>,
3730}
3731
3732#[derive(Debug, Clone, Copy)]
3733enum SwapQuoteKind {
3734    ExactInput(U256),
3735    ExactOutput(U256),
3736}
3737
3738/// Executes a validated swap plan: read-only pre-trade checks, signing, persistence,
3739/// broadcast, and finality observation.
3740///
3741/// Emits only events justified by known transaction state: `OrderDenied` before the
3742/// transaction exists, `OrderSubmitted` after broadcast acceptance, and `OrderRejected` on
3743/// a definitive node rejection or an on-chain revert. No fill is emitted at broadcast or
3744/// first inclusion; fills arrive with finality reconciliation. Ambiguous outcomes keep the
3745/// order submitted untouched while the persisted record reconciles. The client retains
3746/// unresolved submissions, so an exhausted in-flight check leaves the order submitted.
3747#[allow(
3748    clippy::large_futures,
3749    reason = "keeps transaction verification futures inline without additional boxing"
3750)]
3751async fn execute_swap(
3752    mut plan: SwapPlan,
3753    executor: TransactionExecutor,
3754    emitter: ExecutionEventEmitter,
3755    max_quote_age_blocks: u64,
3756    deadline_seconds: u64,
3757) -> anyhow::Result<()> {
3758    let order = &plan.order;
3759
3760    if let Err(e) = executor.claim_slot(TransactionPurpose::Swap) {
3761        emitter.emit_order_denied(order, &e.to_string());
3762        return Ok(());
3763    }
3764
3765    let Some(profiler_position) = plan.profiler_position.as_ref() else {
3766        release_preparing_slot(&executor.in_flight);
3767        emitter.emit_order_denied(order, "Pool profiler has no quote provenance");
3768        return Ok(());
3769    };
3770    let mut swap_anchors = match validate_swap_quote(
3771        profiler_position,
3772        &plan,
3773        max_quote_age_blocks,
3774        &executor.verification,
3775        &executor.deployment_manifest,
3776    )
3777    .await
3778    {
3779        Ok(anchors) => anchors,
3780        Err(e) => {
3781            release_preparing_slot(&executor.in_flight);
3782            emitter.emit_order_denied(order, &e.to_string());
3783            return Ok(());
3784        }
3785    };
3786    let (amount_in, min_amount_out) = match verified_swap_amounts(&plan, swap_anchors.quote) {
3787        Ok(amounts) => amounts,
3788        Err(e) => {
3789            release_preparing_slot(&executor.in_flight);
3790            emitter.emit_order_denied(order, &e.to_string());
3791            return Ok(());
3792        }
3793    };
3794    plan.amount_in = amount_in;
3795    plan.min_amount_out = min_amount_out;
3796    let deadline = match swap_anchors.state.timestamp.checked_add(deadline_seconds) {
3797        Some(deadline) => deadline,
3798        None => {
3799            release_preparing_slot(&executor.in_flight);
3800            emitter.emit_order_denied(
3801                order,
3802                &format!(
3803                    "Swap deadline overflow: anchor timestamp {} plus `deadline_seconds` {deadline_seconds} exceeds u64",
3804                    swap_anchors.state.timestamp
3805                ),
3806            );
3807            return Ok(());
3808        }
3809    };
3810
3811    swap_anchors.precondition_decisions =
3812        match check_swap_preconditions(&plan, swap_anchors.state.number, &executor).await {
3813            Ok(decisions) => decisions,
3814            Err(e) => {
3815                release_preparing_slot(&executor.in_flight);
3816                emitter.emit_order_denied(order, &e.to_string());
3817                return Ok(());
3818            }
3819        };
3820
3821    let calldata = UniswapV3SwapRouter::exactInputSingleCall {
3822        params: UniswapV3SwapRouter::ExactInputSingleParams {
3823            tokenIn: plan.token_in,
3824            tokenOut: plan.token_out,
3825            fee: plan.fee,
3826            recipient: executor.wallet_address,
3827            deadline: U256::from(deadline),
3828            amountIn: plan.amount_in,
3829            amountOutMinimum: plan.min_amount_out,
3830            sqrtPriceLimitX96: U160::ZERO,
3831        },
3832    }
3833    .abi_encode();
3834
3835    let calldata = Bytes::from(calldata);
3836    let intent = ExecutionIntentInsert {
3837        chain_id: executor.chain_id,
3838        wallet_address: executor.wallet_address.to_string(),
3839        purpose: TransactionPurpose::Swap.as_str().to_string(),
3840        client_order_id: Some(order.client_order_id().to_string()),
3841        trader_id: Some(order.trader_id().to_string()),
3842        strategy_id: Some(order.strategy_id().to_string()),
3843        account_id: Some(executor.account_id.to_string()),
3844        instrument_id: Some(plan.instrument_id.to_string()),
3845        pool_address: Some(plan.pool_address.to_string()),
3846        transaction_to: plan.router.to_string(),
3847        transaction_input: hex::encode_prefixed(&calldata),
3848        transaction_value: U256::ZERO.to_string(),
3849        amount_in: Some(plan.amount_in.to_string()),
3850        created_block: swap_anchors.state.number,
3851    };
3852    let intent = match executor.database.reserve_execution_intent(&intent).await {
3853        Ok(intent) => intent,
3854        Err(e) => {
3855            release_preparing_if_reservation_not_committed(&executor.in_flight, &e);
3856            emitter.emit_order_denied(order, &e.to_string());
3857            return Ok(());
3858        }
3859    };
3860    let prepared = match executor
3861        .prepare_and_sign_swap(
3862            intent.id,
3863            intent.created_block,
3864            plan.router,
3865            U256::ZERO,
3866            calldata,
3867            &swap_anchors,
3868        )
3869        .await
3870    {
3871        Ok(prepared) => prepared,
3872        Err(e) => {
3873            if executor
3874                .database
3875                .mark_execution_intent_recoverable(intent.id)
3876                .await
3877                .is_ok()
3878            {
3879                release_preparing_slot(&executor.in_flight);
3880            }
3881            emitter.emit_order_denied(order, &e.to_string());
3882            return Ok(());
3883        }
3884    };
3885
3886    if let Err(e) = executor
3887        .fill_and_persist(&prepared, TransactionPurpose::Swap)
3888        .await
3889    {
3890        emitter.emit_order_denied(order, &e.to_string());
3891        return Ok(());
3892    }
3893
3894    let broadcast = executor.broadcast(&prepared).await?;
3895    emitter.emit_order_submitted(order);
3896    executor
3897        .database
3898        .mark_execution_event_emitted(intent.id, "acknowledgement")
3899        .await?;
3900
3901    if let BroadcastOutcome::Ambiguous(message) = broadcast {
3902        log::warn!("{message}");
3903    }
3904
3905    let trace_purpose = match plan.order.order_side() {
3906        OrderSide::Sell => "swap_sell",
3907        OrderSide::Buy => "swap_buy",
3908    };
3909
3910    match executor.await_finality(&prepared).await? {
3911        InclusionOutcome::Finalized(mut included) => {
3912            included.finality.decisions.extend(
3913                verify_finalized_transaction(
3914                    &included,
3915                    &intent,
3916                    prepared.nonce,
3917                    &prepared.raw_tx,
3918                    &executor,
3919                    trace_purpose,
3920                )
3921                .await?,
3922            );
3923            let fill = validate_finalized_swap_fill(&plan, &included)?;
3924            let wallet = load_verified_wallet_after_fill(&plan, &included, &executor).await?;
3925            executor
3926                .commit_verified_finality(
3927                    &included,
3928                    TransactionStatus::Finalized,
3929                    &wallet.decisions,
3930                )
3931                .await?;
3932            complete_finalized_swap(
3933                &plan,
3934                intent.id,
3935                included.tx_hash,
3936                fill,
3937                wallet,
3938                &executor,
3939                &emitter,
3940            )
3941            .await?;
3942            executor.release_slot();
3943            Ok(())
3944        }
3945        InclusionOutcome::Reverted(mut included) => {
3946            included.finality.decisions.extend(
3947                verify_finalized_transaction(
3948                    &included,
3949                    &intent,
3950                    prepared.nonce,
3951                    &prepared.raw_tx,
3952                    &executor,
3953                    trace_purpose,
3954                )
3955                .await?,
3956            );
3957            executor
3958                .commit_verified_finality(&included, TransactionStatus::Reverted, &[])
3959                .await?;
3960            send_reverted_order(&emitter, order, &included)?;
3961            executor
3962                .database
3963                .mark_execution_event_emitted(intent.id, "terminal")
3964                .await?;
3965            executor.release_slot();
3966            Ok(())
3967        }
3968        InclusionOutcome::Pending(message) => anyhow::bail!(message),
3969    }
3970}
3971
3972async fn validate_swap_quote(
3973    position: &BlockPosition,
3974    plan: &SwapPlan,
3975    max_age_blocks: u64,
3976    verification: &VerificationCoordinator,
3977    manifest: &BlockchainDeploymentManifest,
3978) -> anyhow::Result<SwapQuoteAnchors> {
3979    let block_hash = position.block_hash.as_deref().ok_or_else(|| {
3980        anyhow::anyhow!(
3981            "Pool state at block {} has no ingestion-time block hash; refresh the profiler before execution",
3982            position.number
3983        )
3984    })?;
3985    let expected_block_hash = B256::from_str(block_hash)
3986        .with_context(|| format!("Invalid profiler block hash {block_hash}"))?;
3987    let now_unix_secs = current_unix_secs()?;
3988    let head = verified_value(
3989        verification.verify_decision_header(now_unix_secs).await,
3990        "swap decision header",
3991    )?;
3992    validate_quote_age(position.number, head.number, max_age_blocks)?;
3993    let canonical_block = verified_value(
3994        verification.verify_block(position.number).await,
3995        "profiler watermark header",
3996    )?;
3997    anyhow::ensure!(
3998        canonical_block.hash == expected_block_hash,
3999        "Pool state block {} changed from {} to {}; refresh the profiler before execution",
4000        position.number,
4001        expected_block_hash,
4002        canonical_block.hash
4003    );
4004
4005    let snapshot_transaction = position.transaction_index == BLOCK_SCOPED_SNAPSHOT_INDEX;
4006    let snapshot_log = position.log_index == BLOCK_SCOPED_SNAPSHOT_INDEX;
4007    anyhow::ensure!(
4008        snapshot_transaction == snapshot_log,
4009        "Pool state at block {} has an invalid partial snapshot watermark",
4010        position.number
4011    );
4012
4013    if snapshot_transaction {
4014        let snapshot_hash = B256::from_str(&position.transaction_hash)
4015            .with_context(|| "Invalid block-scoped snapshot hash")?;
4016        anyhow::ensure!(
4017            snapshot_hash == expected_block_hash,
4018            "Block-scoped snapshot hash {snapshot_hash} does not match ingestion hash {expected_block_hash}"
4019        );
4020    } else {
4021        validate_profiler_event_verified(
4022            position,
4023            expected_block_hash,
4024            plan.pool_address,
4025            &plan.pool,
4026            verification,
4027        )
4028        .await?;
4029    }
4030
4031    let ancestry = verified_value(
4032        verification
4033            .verify_header_window(canonical_block, head.number)
4034            .await,
4035        "profiler-to-decision ancestry",
4036    )?;
4037
4038    if let Some(last) = ancestry.last() {
4039        anyhow::ensure!(
4040            *last == head,
4041            "Swap decision header conflicts with profiler ancestry"
4042        );
4043    } else {
4044        anyhow::ensure!(
4045            canonical_block == head,
4046            "Empty profiler ancestry does not end at the decision header"
4047        );
4048    }
4049    verified_value(
4050        verification
4051            .verify_deployment_manifest(manifest, head.number)
4052            .await,
4053        "swap deployment manifest",
4054    )?;
4055    let quote_contract = validate_manifest_pool(plan, manifest)?;
4056    let quote_kind = match plan.order.order_side() {
4057        OrderSide::Sell => SwapQuoteKind::ExactInput(quantity_to_raw_amount(
4058            plan.order.quantity(),
4059            plan.pool.get_base_token().decimals,
4060        )?),
4061        OrderSide::Buy => SwapQuoteKind::ExactOutput(quantity_to_raw_amount(
4062            plan.order.quantity(),
4063            plan.pool.get_base_token().decimals,
4064        )?),
4065    };
4066    let quote = verified_value(
4067        verify_swap_quote(verification, quote_contract, plan, quote_kind, head.number).await,
4068        "independent swap quote",
4069    )?;
4070
4071    Ok(SwapQuoteAnchors {
4072        watermark: canonical_block,
4073        state: head,
4074        quote_contract,
4075        token_in: plan.token_in,
4076        token_out: plan.token_out,
4077        fee: plan.fee,
4078        quote_kind,
4079        quote,
4080        precondition_decisions: Vec::new(),
4081    })
4082}
4083
4084async fn verify_swap_quote(
4085    verification: &VerificationCoordinator,
4086    quote_contract: Address,
4087    plan: &SwapPlan,
4088    kind: SwapQuoteKind,
4089    block: u64,
4090) -> VerificationOutcome<UniswapV3Quote> {
4091    match kind {
4092        SwapQuoteKind::ExactInput(amount_in) => {
4093            verification
4094                .verify_quote_exact_input_single(
4095                    &quote_contract,
4096                    plan.token_in,
4097                    plan.token_out,
4098                    amount_in,
4099                    plan.fee,
4100                    block,
4101                )
4102                .await
4103        }
4104        SwapQuoteKind::ExactOutput(amount_out) => {
4105            verification
4106                .verify_quote_exact_output_single(
4107                    &quote_contract,
4108                    plan.token_in,
4109                    plan.token_out,
4110                    amount_out,
4111                    plan.fee,
4112                    block,
4113                )
4114                .await
4115        }
4116    }
4117}
4118
4119fn verified_swap_amounts(plan: &SwapPlan, quote: UniswapV3Quote) -> anyhow::Result<(U256, U256)> {
4120    anyhow::ensure!(
4121        !quote.amount.is_zero(),
4122        "Independent swap quote returned zero"
4123    );
4124    let base_amount =
4125        quantity_to_raw_amount(plan.order.quantity(), plan.pool.get_base_token().decimals)?;
4126    let slippage_bps = plan.slippage_bps;
4127    match plan.order.order_side() {
4128        OrderSide::Sell => Ok((
4129            base_amount,
4130            derive_min_amount_out(quote.amount, slippage_bps)?,
4131        )),
4132        OrderSide::Buy => {
4133            let ceiling = plan.quote_spend_ceiling.ok_or_else(|| {
4134                anyhow::anyhow!(
4135                    "No quote spend ceiling for BUY token pair {} -> {}",
4136                    plan.token_in,
4137                    plan.token_out
4138                )
4139            })?;
4140            anyhow::ensure!(
4141                quote.amount <= ceiling.max_amount,
4142                "BUY quote amount {} exceeds the configured quote-spend maximum {} for {} -> {}",
4143                quote.amount,
4144                ceiling.max_amount,
4145                plan.token_in,
4146                plan.token_out
4147            );
4148            Ok((
4149                quote.amount,
4150                derive_min_amount_out(base_amount, slippage_bps)?,
4151            ))
4152        }
4153    }
4154}
4155
4156fn validate_manifest_pool(
4157    plan: &SwapPlan,
4158    manifest: &BlockchainDeploymentManifest,
4159) -> anyhow::Result<Address> {
4160    let matching = manifest
4161        .pools
4162        .iter()
4163        .filter(|pool| Address::from_str(&pool.address).ok() == Some(plan.pool_address))
4164        .collect::<Vec<_>>();
4165    anyhow::ensure!(
4166        matching.len() == 1,
4167        "Pool {} does not have exactly one deployment manifest definition",
4168        plan.pool_address
4169    );
4170    let pool = matching[0];
4171    let token0 = Address::from_str(&pool.token0)?;
4172    let token1 = Address::from_str(&pool.token1)?;
4173    let factory = Address::from_str(&pool.factory)?;
4174    let quote_contract = Address::from_str(&pool.quote_contract)?;
4175    anyhow::ensure!(
4176        token0 == plan.pool.token0.address
4177            && token1 == plan.pool.token1.address
4178            && pool.fee == plan.pool.fee.expect("validated pool fee")
4179            && factory == plan.factory,
4180        "Cached pool {} does not match its deployment manifest identity",
4181        plan.pool_address
4182    );
4183
4184    for token in [&plan.pool.token0, &plan.pool.token1] {
4185        let identities = manifest
4186            .tokens
4187            .iter()
4188            .filter(|identity| Address::from_str(&identity.address).ok() == Some(token.address))
4189            .collect::<Vec<_>>();
4190        anyhow::ensure!(
4191            identities.len() == 1,
4192            "Token {} does not have exactly one deployment manifest identity",
4193            token.address
4194        );
4195        let identity = identities[0];
4196        anyhow::ensure!(
4197            identity.name == token.name
4198                && identity.symbol == token.symbol
4199                && identity.decimals == token.decimals,
4200            "Cached token {} does not match its deployment manifest identity",
4201            token.address
4202        );
4203        let expected_role = if token.address == plan.pool.get_base_token().address {
4204            "base"
4205        } else {
4206            "quote"
4207        };
4208        anyhow::ensure!(
4209            matches!(identity.asset_role.as_str(), "both") || identity.asset_role == expected_role,
4210            "Token {} is not permitted as the pool {expected_role} asset",
4211            token.address
4212        );
4213    }
4214    Ok(quote_contract)
4215}
4216
4217async fn validate_profiler_event_verified(
4218    position: &BlockPosition,
4219    expected_block_hash: B256,
4220    pool_address: Address,
4221    pool: &Pool,
4222    verification: &VerificationCoordinator,
4223) -> anyhow::Result<()> {
4224    let transaction_hash = B256::from_str(&position.transaction_hash).with_context(|| {
4225        format!(
4226            "Invalid profiler transaction hash {}",
4227            position.transaction_hash
4228        )
4229    })?;
4230    let receipt = verified_value(
4231        verification.verify_receipt(&transaction_hash).await,
4232        "profiler watermark receipt",
4233    )?;
4234    anyhow::ensure!(
4235        receipt.status,
4236        "Profiler transaction did not execute successfully"
4237    );
4238    anyhow::ensure!(
4239        receipt.transaction_hash == transaction_hash,
4240        "Profiler receipt transaction hash does not match its ingestion watermark"
4241    );
4242    anyhow::ensure!(
4243        receipt.block_number == position.number
4244            && receipt.block_hash == expected_block_hash
4245            && receipt.transaction_index == u64::from(position.transaction_index),
4246        "Profiler receipt position does not match its ingestion watermark"
4247    );
4248    let matching_logs = receipt
4249        .logs
4250        .iter()
4251        .filter(|log| rpc_log::extract_log_index(log).ok() == Some(position.log_index))
4252        .collect::<Vec<_>>();
4253    anyhow::ensure!(
4254        matching_logs.len() == 1,
4255        "Profiler receipt contains {} logs at global index {}; expected exactly one",
4256        matching_logs.len(),
4257        position.log_index
4258    );
4259    let log = matching_logs[0];
4260    let log_transaction_hash = B256::from_str(&rpc_log::extract_transaction_hash(log)?)
4261        .with_context(|| "Invalid profiler log transaction hash")?;
4262    let log_block_hash = log
4263        .block_hash
4264        .as_deref()
4265        .ok_or_else(|| anyhow::anyhow!("Profiler log has no block hash"))?;
4266    anyhow::ensure!(
4267        !log.removed
4268            && log_transaction_hash == transaction_hash
4269            && rpc_log::extract_block_number(log)? == position.number
4270            && rpc_log::extract_transaction_index(log)? == position.transaction_index
4271            && B256::from_str(log_block_hash)? == expected_block_hash,
4272        "Profiler log position does not match its ingestion watermark"
4273    );
4274    anyhow::ensure!(
4275        rpc_log::extract_address(log)? == pool_address,
4276        "Profiler watermark log did not come from expected pool {pool_address}"
4277    );
4278    let signature = log
4279        .topics
4280        .first()
4281        .ok_or_else(|| anyhow::anyhow!("Profiler watermark log has no event signature"))?;
4282    let supported =
4283        profiler_event_signatures(pool).any(|expected| expected.eq_ignore_ascii_case(signature));
4284    anyhow::ensure!(
4285        supported,
4286        "Profiler watermark log has an unsupported event signature"
4287    );
4288    Ok(())
4289}
4290
4291fn profiler_event_signatures(pool: &Pool) -> impl Iterator<Item = &str> {
4292    [
4293        Some(pool.dex.swap_created_event.as_ref()),
4294        Some(pool.dex.mint_created_event.as_ref()),
4295        Some(pool.dex.burn_created_event.as_ref()),
4296        Some(pool.dex.collect_created_event.as_ref()),
4297        pool.dex.flash_created_event.as_deref(),
4298        pool.dex.fee_protocol_update_event.as_deref(),
4299        pool.dex.fee_protocol_collect_event.as_deref(),
4300    ]
4301    .into_iter()
4302    .flatten()
4303}
4304
4305fn validate_quote_age(
4306    profiler_block: u64,
4307    latest_block: u64,
4308    max_age_blocks: u64,
4309) -> anyhow::Result<()> {
4310    anyhow::ensure!(
4311        profiler_block <= latest_block,
4312        "Pool state at block {profiler_block} is ahead of the latest block {latest_block}; the execution RPC endpoint lags the data feed"
4313    );
4314    let quote_age = latest_block - profiler_block;
4315    anyhow::ensure!(
4316        quote_age <= max_age_blocks,
4317        "Stale quote: pool state at block {profiler_block}, latest block {latest_block}, exceeds `max_quote_age_blocks` {max_age_blocks}"
4318    );
4319    Ok(())
4320}
4321
4322fn validate_rpc_transaction_matches_payload(
4323    transaction: &RpcTransaction,
4324    raw_transaction: &[u8],
4325) -> anyhow::Result<()> {
4326    let signed = decode_signed_transaction(raw_transaction)?;
4327    anyhow::ensure!(
4328        transaction.hash == signed.hash
4329            && transaction.from == signed.signer
4330            && transaction.nonce == signed.nonce
4331            && transaction.chain_id == Some(signed.chain_id)
4332            && transaction.transaction_type == Some(2)
4333            && transaction.to == Some(signed.to)
4334            && transaction.input == signed.input
4335            && transaction.value == signed.value
4336            && transaction.gas == Some(signed.gas_limit)
4337            && transaction.max_fee_per_gas == Some(U256::from(signed.max_fee_per_gas))
4338            && transaction.max_priority_fee_per_gas
4339                == Some(U256::from(signed.max_priority_fee_per_gas)),
4340        "Verified transaction fields differ from the authenticated signed payload"
4341    );
4342    Ok(())
4343}
4344
4345#[allow(
4346    clippy::large_futures,
4347    reason = "keeps transaction verification futures inline without additional boxing"
4348)]
4349async fn verify_finalized_transaction(
4350    included: &IncludedTransaction,
4351    intent: &ExecutionIntentRow,
4352    nonce: u64,
4353    raw_transaction: &[u8],
4354    executor: &TransactionExecutor,
4355    trace_purpose: &str,
4356) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
4357    verify_finalized_transaction_identity(
4358        included,
4359        intent,
4360        nonce,
4361        raw_transaction,
4362        &executor.verification,
4363        executor.wallet_address,
4364        executor.chain_id,
4365        &executor.deployment_manifest,
4366        trace_purpose,
4367    )
4368    .await
4369}
4370
4371#[expect(clippy::too_many_arguments)]
4372async fn verify_finalized_transaction_identity(
4373    included: &IncludedTransaction,
4374    intent: &ExecutionIntentRow,
4375    nonce: u64,
4376    raw_transaction: &[u8],
4377    verification: &VerificationCoordinator,
4378    wallet_address: Address,
4379    chain_id: u32,
4380    deployment_manifest: &BlockchainDeploymentManifest,
4381    trace_purpose: &str,
4382) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
4383    let transaction_verification = required_verification(
4384        verification.verify_transaction(&included.tx_hash).await,
4385        "finalized transaction",
4386    )?;
4387    let transaction = &transaction_verification.value;
4388    let signed = decode_signed_transaction(raw_transaction)?;
4389    let (expected_to, expected_input, expected_value) = persisted_call_fields(intent)?;
4390    anyhow::ensure!(
4391        included.receipt.transaction_hash == included.tx_hash
4392            && signed.hash == included.tx_hash
4393            && signed.signer == wallet_address
4394            && signed.chain_id == u64::from(chain_id)
4395            && signed.nonce == nonce
4396            && signed.to == expected_to
4397            && signed.input == expected_input
4398            && signed.value == expected_value,
4399        "Finalized transaction does not match the authenticated signed payload and persisted intent"
4400    );
4401    validate_rpc_transaction_matches_payload(transaction, raw_transaction)
4402        .context("finalized transaction identity mismatch")?;
4403
4404    let trace_verification = required_verification(
4405        verification.verify_call_trace(&included.tx_hash).await,
4406        "finalized call trace",
4407    )?;
4408    validate_call_trace(
4409        &trace_verification.value,
4410        &signed,
4411        included.receipt.status,
4412        trace_purpose,
4413        deployment_manifest,
4414    )?;
4415    let deployment_verification = required_verification(
4416        verification
4417            .verify_deployment_manifest(deployment_manifest, included.block_number)
4418            .await,
4419        "inclusion deployment manifest",
4420    )?;
4421
4422    Ok(vec![
4423        verification_decision(
4424            &transaction_verification,
4425            Some(included.block_number),
4426            Some(included.block_number),
4427        ),
4428        verification_decision(
4429            &trace_verification,
4430            Some(included.block_number),
4431            Some(included.block_number),
4432        ),
4433        verification_decision(
4434            &deployment_verification,
4435            Some(included.block_number),
4436            Some(included.block_number),
4437        ),
4438    ])
4439}
4440
4441fn validate_call_trace(
4442    trace: &VerifiedCallTrace,
4443    signed: &crate::execution::transaction::DecodedSignedTransaction,
4444    receipt_success: bool,
4445    purpose: &str,
4446    manifest: &BlockchainDeploymentManifest,
4447) -> anyhow::Result<()> {
4448    anyhow::ensure!(
4449        trace.call_type == RpcCallType::Call
4450            && trace.from == signed.signer
4451            && trace.to == Some(signed.to)
4452            && trace.value == signed.value
4453            && trace.input_digest == keccak256(&signed.input)
4454            && trace.success == receipt_success,
4455        "Verified call-trace root differs from the authenticated transaction"
4456    );
4457    validate_internal_calls(&trace.calls, signed.to, purpose, manifest)
4458}
4459
4460fn validate_internal_calls(
4461    calls: &[VerifiedCallTrace],
4462    caller_context: Address,
4463    purpose: &str,
4464    manifest: &BlockchainDeploymentManifest,
4465) -> anyhow::Result<()> {
4466    for call in calls {
4467        anyhow::ensure!(
4468            call.from == caller_context,
4469            "Verified call trace child has an invalid caller context"
4470        );
4471        let target = call.to.ok_or_else(|| {
4472            anyhow::anyhow!("Verified call trace contains an operation without a target")
4473        })?;
4474        let call_type = match call.call_type {
4475            RpcCallType::Call => "call",
4476            RpcCallType::Callcode => "callcode",
4477            RpcCallType::Delegatecall => "delegatecall",
4478            RpcCallType::Staticcall => "staticcall",
4479            RpcCallType::Create | RpcCallType::Create2 | RpcCallType::Selfdestruct => {
4480                anyhow::bail!("Verified call trace contains a forbidden state-changing operation")
4481            }
4482        };
4483        let permitted = manifest.call_edges.iter().any(|edge| {
4484            edge.purpose == purpose
4485                && edge.call_type.eq_ignore_ascii_case(call_type)
4486                && Address::from_str(&edge.caller).ok() == Some(call.from)
4487                && Address::from_str(&edge.target).ok() == Some(target)
4488        });
4489        anyhow::ensure!(
4490            permitted,
4491            "Verified call trace contains an unreviewed {call_type} edge {} -> {target} for {purpose}",
4492            call.from
4493        );
4494        let child_context = match call.call_type {
4495            RpcCallType::Call | RpcCallType::Staticcall => target,
4496            RpcCallType::Callcode | RpcCallType::Delegatecall => caller_context,
4497            RpcCallType::Create | RpcCallType::Create2 | RpcCallType::Selfdestruct => {
4498                unreachable!("forbidden operations return before child traversal")
4499            }
4500        };
4501        validate_internal_calls(&call.calls, child_context, purpose, manifest)?;
4502    }
4503    Ok(())
4504}
4505
4506async fn verify_wrap_balance_increase(
4507    executor: &TransactionExecutor,
4508    weth_address: &Address,
4509    amount_wei: U256,
4510    included: &IncludedTransaction,
4511) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
4512    let previous_block = included.block_number.checked_sub(1).ok_or_else(|| {
4513        anyhow::anyhow!(
4514            "Included wrap transaction {} has invalid block number 0",
4515            included.tx_hash
4516        )
4517    })?;
4518    let call = ERC20::balanceOfCall {
4519        account: executor.wallet_address,
4520    }
4521    .abi_encode();
4522    let balance_before = required_verification(
4523        executor
4524            .verification
4525            .verify_decoded_call(
4526                None,
4527                weth_address,
4528                U256::ZERO,
4529                &call,
4530                previous_block,
4531                |result| ERC20::balanceOfCall::abi_decode_returns(result).map_err(Into::into),
4532            )
4533            .await,
4534        "wrapped balance before finality",
4535    )
4536    .with_context(|| {
4537        format!(
4538            "failed to verify WETH balance before included transaction {} at block {previous_block}",
4539            included.tx_hash
4540        )
4541    })?;
4542    let balance_after = required_verification(
4543        executor
4544            .verification
4545            .verify_decoded_call(
4546                None,
4547                weth_address,
4548                U256::ZERO,
4549                &call,
4550                included.block_number,
4551                |result| ERC20::balanceOfCall::abi_decode_returns(result).map_err(Into::into),
4552            )
4553            .await,
4554        "wrapped balance after finality",
4555    )
4556    .with_context(|| {
4557        format!(
4558            "failed to verify WETH balance after included transaction {} at block {}",
4559            included.tx_hash, included.block_number
4560        )
4561    })?;
4562    let expected_balance = balance_before
4563        .value
4564        .checked_add(amount_wei)
4565        .ok_or_else(|| {
4566            anyhow::anyhow!(
4567                "WETH balance overflow for included transaction {} at block {}",
4568                included.tx_hash,
4569                included.block_number
4570            )
4571        })?;
4572    anyhow::ensure!(
4573        balance_after.value == expected_balance,
4574        "WETH balance after transaction {} did not increase by {amount_wei}: expected {expected_balance}, was {}",
4575        included.tx_hash,
4576        balance_after.value
4577    );
4578
4579    Ok(vec![
4580        verification_decision(&balance_before, Some(previous_block), Some(previous_block)),
4581        verification_decision(
4582            &balance_after,
4583            Some(included.block_number),
4584            Some(included.block_number),
4585        ),
4586    ])
4587}
4588
4589async fn verify_approve_allowance(
4590    executor: &TransactionExecutor,
4591    token: &Address,
4592    router: &Address,
4593    amount: U256,
4594    included: &IncludedTransaction,
4595) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
4596    let call = ERC20::allowanceCall {
4597        owner: executor.wallet_address,
4598        spender: *router,
4599    }
4600    .abi_encode();
4601    let allowance = required_verification(
4602        executor
4603            .verification
4604            .verify_decoded_call(
4605                None,
4606                token,
4607                U256::ZERO,
4608                &call,
4609                included.block_number,
4610                |result| ERC20::allowanceCall::abi_decode_returns(result).map_err(Into::into),
4611            )
4612            .await,
4613        "router allowance after finality",
4614    )
4615    .with_context(|| {
4616        format!(
4617            "failed to verify router allowance after included transaction {} at block {}",
4618            included.tx_hash, included.block_number
4619        )
4620    })?;
4621    anyhow::ensure!(
4622        allowance.value == amount,
4623        "Router allowance after transaction {} does not equal the requested amount {amount}: was {}",
4624        included.tx_hash,
4625        allowance.value
4626    );
4627
4628    Ok(vec![verification_decision(
4629        &allowance,
4630        Some(included.block_number),
4631        Some(included.block_number),
4632    )])
4633}
4634
4635async fn complete_finalized_swap(
4636    plan: &SwapPlan,
4637    intent_id: i64,
4638    tx_hash: B256,
4639    fill: Option<FinalizedSwapFill>,
4640    wallet: VerifiedWalletRefresh,
4641    executor: &TransactionExecutor,
4642    emitter: &ExecutionEventEmitter,
4643) -> anyhow::Result<()> {
4644    if let Some(fill) = fill {
4645        let filled = OrderFilled::new(
4646            emitter.trader_id(),
4647            plan.order.strategy_id(),
4648            plan.order.instrument_id(),
4649            plan.order.client_order_id(),
4650            fill.venue_order_id,
4651            emitter.account_id(),
4652            fill.trade_id,
4653            plan.order.order_side(),
4654            plan.order.order_type(),
4655            fill.last_qty,
4656            fill.last_px,
4657            plan.quote_currency,
4658            LiquiditySide::Taker,
4659            execution_event_id(tx_hash, b"fill"),
4660            fill.ts_event,
4661            fill.ts_event,
4662            false,
4663            None,
4664            Some(fill.commission),
4665            None,
4666        );
4667        emitter.try_send_order_event(OrderEventAny::Filled(filled))?;
4668
4669        if fill.last_qty < plan.order.quantity() {
4670            let canceled = OrderCanceled::new(
4671                emitter.trader_id(),
4672                plan.order.strategy_id(),
4673                plan.order.instrument_id(),
4674                plan.order.client_order_id(),
4675                execution_event_id(tx_hash, b"partial_cancel"),
4676                fill.ts_event,
4677                fill.ts_event,
4678                false,
4679                Some(fill.venue_order_id),
4680                Some(emitter.account_id()),
4681                None,
4682            );
4683            emitter.try_send_order_event(OrderEventAny::Canceled(canceled))?;
4684        }
4685    }
4686
4687    *executor.wallet_balance.lock() = wallet.wallet_balance;
4688    emitter.try_emit_account_state(
4689        wallet.balances,
4690        vec![],
4691        true,
4692        get_atomic_clock_realtime().get_time_ns(),
4693        None,
4694    )?;
4695    executor
4696        .database
4697        .mark_execution_event_emitted(intent_id, "fill")
4698        .await
4699}
4700
4701fn send_reverted_order(
4702    emitter: &ExecutionEventEmitter,
4703    order: &OrderAny,
4704    included: &IncludedTransaction,
4705) -> anyhow::Result<()> {
4706    let ts_event = finalized_inclusion_time(included)?;
4707    let rejected = OrderRejected::new(
4708        emitter.trader_id(),
4709        order.strategy_id(),
4710        order.instrument_id(),
4711        order.client_order_id(),
4712        emitter.account_id(),
4713        format!("Transaction {} reverted on-chain", included.tx_hash).into(),
4714        execution_event_id(included.tx_hash, b"reverted"),
4715        ts_event,
4716        ts_event,
4717        false,
4718        false,
4719    );
4720    emitter.try_send_order_event(OrderEventAny::Rejected(rejected))
4721}
4722
4723fn finalized_inclusion_time(included: &IncludedTransaction) -> anyhow::Result<UnixNanos> {
4724    let inclusion = &included.finality.inclusion_header;
4725    anyhow::ensure!(
4726        inclusion.number == included.block_number
4727            && inclusion.hash == included.receipt.block_hash.to_string(),
4728        "Verified inclusion header does not match the finalized receipt"
4729    );
4730    let timestamp = inclusion.timestamp;
4731    let nanos = timestamp
4732        .checked_mul(NANOSECONDS_IN_SECOND)
4733        .ok_or_else(|| anyhow::anyhow!("Verified inclusion timestamp exceeds nanoseconds"))?;
4734    Ok(UnixNanos::from(nanos))
4735}
4736
4737fn execution_event_id(tx_hash: B256, event: &[u8]) -> UUID4 {
4738    let mut identity = Vec::with_capacity(tx_hash.len() + event.len());
4739    identity.extend_from_slice(tx_hash.as_slice());
4740    identity.extend_from_slice(event);
4741    let digest = keccak256(identity);
4742    let mut bytes = [0u8; 16];
4743    bytes.copy_from_slice(&digest[..16]);
4744    UUID4::from_bytes(bytes)
4745}
4746
4747struct FinalizedSwapFill {
4748    venue_order_id: VenueOrderId,
4749    trade_id: TradeId,
4750    last_qty: Quantity,
4751    last_px: Price,
4752    commission: Money,
4753    ts_event: UnixNanos,
4754}
4755
4756struct VerifiedWalletRefresh {
4757    wallet_balance: WalletBalance,
4758    balances: Vec<AccountBalance>,
4759    decisions: Vec<ExecutionVerificationDecision>,
4760}
4761
4762fn validate_finalized_swap_fill(
4763    plan: &SwapPlan,
4764    included: &IncludedTransaction,
4765) -> anyhow::Result<Option<FinalizedSwapFill>> {
4766    let signature =
4767        keccak256("Swap(address,address,int256,int256,uint160,uint128,int24)").to_string();
4768    let swap_logs = included
4769        .receipt
4770        .logs
4771        .iter()
4772        .filter(|log| {
4773            !log.removed
4774                && log.topics.first().is_some_and(|topic| topic == &signature)
4775                && Address::from_str(&log.address).ok() == Some(plan.pool_address)
4776        })
4777        .collect::<Vec<_>>();
4778    anyhow::ensure!(
4779        swap_logs.len() == 1,
4780        "Finalized transaction {} emitted {} Swap logs from expected pool {}; expected exactly one",
4781        included.tx_hash,
4782        swap_logs.len(),
4783        plan.pool_address
4784    );
4785    let log = swap_logs[0];
4786    let log_transaction_hash = B256::from_str(&rpc_log::extract_transaction_hash(log)?)
4787        .with_context(|| "Invalid finalized Swap log transaction hash")?;
4788    let log_block_hash = log
4789        .block_hash
4790        .as_deref()
4791        .ok_or_else(|| anyhow::anyhow!("Finalized Swap log has no block hash"))?;
4792    anyhow::ensure!(
4793        log_transaction_hash == included.tx_hash
4794            && rpc_log::extract_block_number(log)? == included.block_number
4795            && u64::from(rpc_log::extract_transaction_index(log)?)
4796                == included.receipt.transaction_index
4797            && B256::from_str(log_block_hash)
4798                .with_context(|| "Invalid finalized Swap log block hash")?
4799                == included.receipt.block_hash,
4800        "Finalized Swap log position does not match transaction {}",
4801        included.tx_hash
4802    );
4803
4804    let dex = crate::exchanges::get_dex_extended(plan.pool.chain.name, &plan.pool.dex.name)
4805        .ok_or_else(|| {
4806            anyhow::anyhow!(
4807                "No RPC Swap decoder for {}:{}",
4808                plan.pool.chain.name,
4809                plan.pool.dex.name
4810            )
4811        })?;
4812    let event = dex.parse_swap_event_rpc(log)?;
4813    let (base_amount, quote_amount) =
4814        if plan.pool.get_base_token().address == plan.pool.token0.address {
4815            (event.amount0, event.amount1)
4816        } else {
4817            (event.amount1, event.amount0)
4818        };
4819    let last_qty = match plan.order.order_side() {
4820        OrderSide::Sell => {
4821            anyhow::ensure!(
4822                base_amount.is_positive() && base_amount.unsigned_abs() == plan.amount_in,
4823                "Finalized Swap input {base_amount} does not match the persisted amount {}",
4824                plan.amount_in
4825            );
4826            plan.order.quantity()
4827        }
4828        OrderSide::Buy => {
4829            anyhow::ensure!(
4830                quote_amount.is_positive() && quote_amount.unsigned_abs() == plan.amount_in,
4831                "Finalized Swap input {quote_amount} does not match the persisted amount {}",
4832                plan.amount_in
4833            );
4834            anyhow::ensure!(
4835                base_amount.is_negative(),
4836                "Finalized Swap base amount {base_amount} is not a BUY output"
4837            );
4838            raw_amount_to_quantity(
4839                base_amount.unsigned_abs(),
4840                plan.pool.get_base_token().decimals,
4841            )?
4842        }
4843    };
4844
4845    let block = &included.finality.inclusion_header;
4846    anyhow::ensure!(
4847        block.number == included.block_number
4848            && block.hash == included.receipt.block_hash.to_string(),
4849        "Verified inclusion header {} does not match receipt hash {}",
4850        included.block_number,
4851        included.receipt.block_hash
4852    );
4853    let timestamp_ns = block
4854        .timestamp
4855        .checked_mul(NANOSECONDS_IN_SECOND)
4856        .ok_or_else(|| anyhow::anyhow!("Finalized block timestamp overflows nanoseconds"))?;
4857    let mut swap = event.to_pool_swap(
4858        Arc::clone(&plan.pool.chain),
4859        plan.instrument_id,
4860        plan.pool.pool_identifier,
4861        UnixNanos::from(timestamp_ns),
4862    );
4863    swap.calculate_trade_info(&plan.pool.token0, &plan.pool.token1, None)?;
4864    let trade = swap
4865        .trade_info
4866        .as_ref()
4867        .ok_or_else(|| anyhow::anyhow!("Finalized Swap has no calculated trade information"))?;
4868    anyhow::ensure!(
4869        trade.order_side == plan.order.order_side(),
4870        "Finalized Swap side {} does not match {} order",
4871        trade.order_side,
4872        plan.order.order_side()
4873    );
4874    let gas_cost = included
4875        .receipt
4876        .effective_gas_price
4877        .checked_mul(U256::from(included.receipt.gas_used))
4878        .ok_or_else(|| anyhow::anyhow!("Finalized transaction gas commission overflow"))?;
4879    let commission = Money::from_u256(gas_cost, plan.pool.chain.native_currency())?;
4880    let trade_digest = keccak256(format!("{}:{}", included.tx_hash, swap.log_index));
4881    let trade_digest = trade_digest.to_string();
4882    let trade_id = TradeId::new_checked(&trade_digest[2..38])?;
4883
4884    if plan
4885        .order
4886        .trade_ids()
4887        .iter()
4888        .any(|existing| **existing == trade_id)
4889    {
4890        return Ok(None);
4891    }
4892
4893    let venue_order_id = VenueOrderId::new_checked(included.tx_hash.to_string())?;
4894    let ts_event = UnixNanos::from(timestamp_ns);
4895    let last_px = match plan.order.order_side() {
4896        OrderSide::Buy => fill_price_from_quote(last_qty, plan.amount_in, plan.quote_currency)?,
4897        _ => trade.execution_price,
4898    };
4899    Ok(Some(FinalizedSwapFill {
4900        venue_order_id,
4901        trade_id,
4902        last_qty,
4903        last_px,
4904        commission,
4905        ts_event,
4906    }))
4907}
4908
4909async fn load_verified_wallet_after_fill(
4910    plan: &SwapPlan,
4911    included: &IncludedTransaction,
4912    executor: &TransactionExecutor,
4913) -> anyhow::Result<VerifiedWalletRefresh> {
4914    let mut token_universe = executor.wallet_balance.lock().token_universe.clone();
4915    token_universe.insert(plan.pool.token0.address);
4916    token_universe.insert(plan.pool.token1.address);
4917
4918    let native_amount = required_verification(
4919        executor
4920            .verification
4921            .verify_balance(&executor.wallet_address, included.block_number)
4922            .await,
4923        "finalized native balance",
4924    )?;
4925    let native_balance = Money::from_u256(native_amount.value, plan.pool.chain.native_currency())?;
4926    let mut decisions = vec![verification_decision(
4927        &native_amount,
4928        Some(included.block_number),
4929        Some(included.block_number),
4930    )];
4931    let mut token_addresses = token_universe.iter().copied().collect::<Vec<_>>();
4932    token_addresses.sort_unstable();
4933    let mut token_balances = Vec::with_capacity(token_addresses.len());
4934    for address in token_addresses {
4935        let identities = executor
4936            .deployment_manifest
4937            .tokens
4938            .iter()
4939            .filter(|identity| Address::from_str(&identity.address).ok() == Some(address))
4940            .collect::<Vec<_>>();
4941        anyhow::ensure!(
4942            identities.len() == 1,
4943            "Wallet token {address} does not have exactly one deployment manifest identity"
4944        );
4945        let identity = identities[0];
4946        let token = Token::new(
4947            Arc::clone(&plan.pool.chain),
4948            address,
4949            identity.name.clone(),
4950            identity.symbol.clone(),
4951            identity.decimals,
4952        );
4953        let call = ERC20::balanceOfCall {
4954            account: executor.wallet_address,
4955        }
4956        .abi_encode();
4957        let amount = required_verification(
4958            executor
4959                .verification
4960                .verify_decoded_call(
4961                    None,
4962                    &address,
4963                    U256::ZERO,
4964                    &call,
4965                    included.block_number,
4966                    |result| ERC20::balanceOfCall::abi_decode_returns(result).map_err(Into::into),
4967                )
4968                .await,
4969            "finalized token balance",
4970        )?;
4971        decisions.push(verification_decision(
4972            &amount,
4973            Some(included.block_number),
4974            Some(included.block_number),
4975        ));
4976        token_balances.push(TokenBalance::new(amount.value, token));
4977    }
4978
4979    let mut wallet_balance = WalletBalance::new(token_universe);
4980    let balances = wallet_balance.replace_balances(native_balance, token_balances)?;
4981    Ok(VerifiedWalletRefresh {
4982        wallet_balance,
4983        balances,
4984        decisions,
4985    })
4986}
4987
4988async fn verify_connect_capabilities(
4989    verification: &VerificationCoordinator,
4990    manifest: &BlockchainDeploymentManifest,
4991    wallet: Address,
4992    weth: Address,
4993    block: u64,
4994) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
4995    let contract = manifest
4996        .contracts
4997        .first()
4998        .ok_or_else(|| anyhow::anyhow!("Deployment manifest has no capability probe target"))?;
4999    let contract_address = Address::from_str(&contract.address)
5000        .with_context(|| "Deployment manifest capability probe target is invalid")?;
5001    let storage = required_verification(
5002        verification
5003            .verify_storage(&contract_address, &B256::ZERO, block)
5004            .await,
5005        "Blockchain explicit-height storage capability",
5006    )?;
5007
5008    let balance_call = ERC20::balanceOfCall { account: wallet }.abi_encode();
5009    let gas = required_verification(
5010        verification
5011            .verify_gas_estimate(&wallet, &weth, U256::ZERO, &balance_call, block)
5012            .await,
5013        "Blockchain explicit-height gas capability",
5014    )?;
5015
5016    let mut decisions = vec![
5017        verification_decision(&storage, Some(block), Some(block)),
5018        verification_decision(&gas, Some(block), Some(block)),
5019    ];
5020
5021    for pool in &manifest.pools {
5022        let quote_contract = Address::from_str(&pool.quote_contract)
5023            .with_context(|| "Deployment manifest quote capability target is invalid")?;
5024        let token_in = Address::from_str(&pool.token0)
5025            .with_context(|| "Deployment manifest quote input token is invalid")?;
5026        let token_out = Address::from_str(&pool.token1)
5027            .with_context(|| "Deployment manifest quote output token is invalid")?;
5028        let fee = U24::try_from(pool.fee)
5029            .map_err(|_| anyhow::anyhow!("Deployment manifest pool fee is invalid"))?;
5030        let quote = required_verification(
5031            verification
5032                .verify_quote_exact_input_single(
5033                    &quote_contract,
5034                    token_in,
5035                    token_out,
5036                    U256::from(1u64),
5037                    fee,
5038                    block,
5039                )
5040                .await,
5041            "Blockchain explicit-height quote capability",
5042        )?;
5043        decisions.push(verification_decision(&quote, Some(block), Some(block)));
5044    }
5045
5046    let trace = required_verification(
5047        verification.verify_call_trace_capability().await,
5048        "Blockchain call trace capability",
5049    )?;
5050    decisions.push(verification_decision(&trace, None, None));
5051    Ok(decisions)
5052}
5053
5054/// Runs the read-only pre-trade checks for a swap: deployed bytecode at the pool, router,
5055/// and token addresses, and an operator-prepared router allowance and input-token balance
5056/// covering the amount. The shared signing pipeline checks the exact maximum native cost.
5057/// Never wraps or approves.
5058async fn check_swap_preconditions(
5059    plan: &SwapPlan,
5060    block: u64,
5061    executor: &TransactionExecutor,
5062) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
5063    let mut decisions = Vec::new();
5064    let factory_call = UniswapV3RouterState::factoryCall.abi_encode();
5065    let router_factory = required_verification(
5066        executor
5067            .verification
5068            .verify_decoded_call(
5069                None,
5070                &plan.router,
5071                U256::ZERO,
5072                &factory_call,
5073                block,
5074                |result| {
5075                    UniswapV3RouterState::factoryCall::abi_decode_returns(result)
5076                        .map_err(Into::into)
5077                },
5078            )
5079            .await,
5080        "swap router factory",
5081    )?;
5082    anyhow::ensure!(
5083        router_factory.value == plan.factory,
5084        "Router {} reports an unexpected factory",
5085        plan.router,
5086    );
5087    decisions.push(verification_decision(
5088        &router_factory,
5089        Some(block),
5090        Some(block),
5091    ));
5092
5093    let weth_call = UniswapV3RouterState::WETH9Call.abi_encode();
5094    let router_weth = required_verification(
5095        executor
5096            .verification
5097            .verify_decoded_call(
5098                None,
5099                &plan.router,
5100                U256::ZERO,
5101                &weth_call,
5102                block,
5103                |result| {
5104                    UniswapV3RouterState::WETH9Call::abi_decode_returns(result).map_err(Into::into)
5105                },
5106            )
5107            .await,
5108        "swap router wrapped native",
5109    )?;
5110    anyhow::ensure!(
5111        router_weth.value == plan.weth,
5112        "Router {} reports an unexpected wrapped native contract",
5113        plan.router,
5114    );
5115    decisions.push(verification_decision(
5116        &router_weth,
5117        Some(block),
5118        Some(block),
5119    ));
5120
5121    let pool_call = UniswapV3Factory::getPoolCall {
5122        tokenA: plan.token_in,
5123        tokenB: plan.token_out,
5124        fee: plan.fee,
5125    }
5126    .abi_encode();
5127    let registered_pool = required_verification(
5128        executor
5129            .verification
5130            .verify_decoded_call(
5131                None,
5132                &plan.factory,
5133                U256::ZERO,
5134                &pool_call,
5135                block,
5136                |result| {
5137                    UniswapV3Factory::getPoolCall::abi_decode_returns(result).map_err(Into::into)
5138                },
5139            )
5140            .await,
5141        "swap factory pool",
5142    )?;
5143    anyhow::ensure!(
5144        registered_pool.value == plan.pool_address,
5145        "Factory resolves an unexpected pool for the swap token pair and fee"
5146    );
5147    decisions.push(verification_decision(
5148        &registered_pool,
5149        Some(block),
5150        Some(block),
5151    ));
5152
5153    for token in [&plan.pool.token0, &plan.pool.token1] {
5154        let decimals_call = ERC20::decimalsCall.abi_encode();
5155        let decimals = required_verification(
5156            executor
5157                .verification
5158                .verify_decoded_call(
5159                    None,
5160                    &token.address,
5161                    U256::ZERO,
5162                    &decimals_call,
5163                    block,
5164                    |result| ERC20::decimalsCall::abi_decode_returns(result).map_err(Into::into),
5165                )
5166                .await,
5167            "swap token decimals",
5168        )?;
5169        anyhow::ensure!(
5170            decimals.value == token.decimals,
5171            "Token {} reports unexpected decimals",
5172            token.address,
5173        );
5174        decisions.push(verification_decision(&decimals, Some(block), Some(block)));
5175    }
5176
5177    let allowance_call = ERC20::allowanceCall {
5178        owner: executor.wallet_address,
5179        spender: plan.router,
5180    }
5181    .abi_encode();
5182    let allowance = required_verification(
5183        executor
5184            .verification
5185            .verify_decoded_call(
5186                None,
5187                &plan.token_in,
5188                U256::ZERO,
5189                &allowance_call,
5190                block,
5191                |result| ERC20::allowanceCall::abi_decode_returns(result).map_err(Into::into),
5192            )
5193            .await,
5194        "swap input allowance",
5195    )?;
5196
5197    if allowance.value < plan.amount_in {
5198        anyhow::bail!(
5199            "Router allowance {} is below the swap amount {} for input token {}; approve the router explicitly before submitting",
5200            allowance.value,
5201            plan.amount_in,
5202            plan.token_in
5203        );
5204    }
5205    decisions.push(verification_decision(&allowance, Some(block), Some(block)));
5206
5207    let balance_call = ERC20::balanceOfCall {
5208        account: executor.wallet_address,
5209    }
5210    .abi_encode();
5211    let balance = required_verification(
5212        executor
5213            .verification
5214            .verify_decoded_call(
5215                None,
5216                &plan.token_in,
5217                U256::ZERO,
5218                &balance_call,
5219                block,
5220                |result| ERC20::balanceOfCall::abi_decode_returns(result).map_err(Into::into),
5221            )
5222            .await,
5223        "swap input balance",
5224    )?;
5225
5226    if balance.value < plan.amount_in {
5227        anyhow::bail!(
5228            "Input token {} balance {} is below the swap amount {}",
5229            plan.token_in,
5230            balance.value,
5231            plan.amount_in
5232        );
5233    }
5234    decisions.push(verification_decision(&balance, Some(block), Some(block)));
5235
5236    Ok(decisions)
5237}
5238
5239/// Converts a base-denominated order quantity into raw token units with exact integer
5240/// scaling by the base token decimals.
5241///
5242/// `Quantity::raw` is scaled to the greater of its declared precision and `FIXED_PRECISION`;
5243/// token amounts are scaled to the token's decimals. Quantities not exactly representable in
5244/// token units are rejected rather than rounded.
5245fn quantity_to_raw_amount(quantity: Quantity, decimals: u8) -> anyhow::Result<U256> {
5246    if quantity.is_zero() {
5247        anyhow::bail!("Order quantity must be positive");
5248    }
5249
5250    let raw = U256::from(quantity.raw());
5251    let raw_precision = quantity.precision.max(FIXED_PRECISION);
5252    if decimals >= raw_precision {
5253        let scale = U256::from(10u64)
5254            .checked_pow(U256::from(decimals - raw_precision))
5255            .ok_or_else(|| anyhow::anyhow!("Order amount scaling overflow"))?;
5256        raw.checked_mul(scale).ok_or_else(|| {
5257            anyhow::anyhow!("Order amount overflow scaling quantity to raw token units")
5258        })
5259    } else {
5260        let divisor = U256::from(10u64)
5261            .checked_pow(U256::from(raw_precision - decimals))
5262            .ok_or_else(|| anyhow::anyhow!("Order amount scaling overflow"))?;
5263        if !(raw % divisor).is_zero() {
5264            anyhow::bail!(
5265                "Order quantity {quantity} is not exactly representable in {decimals} base token decimals"
5266            );
5267        }
5268        Ok(raw / divisor)
5269    }
5270}
5271
5272fn swap_token_pair(side: OrderSide, base: Address, quote: Address) -> (Address, Address) {
5273    match side {
5274        OrderSide::Sell => (base, quote),
5275        OrderSide::Buy => (quote, base),
5276    }
5277}
5278
5279fn fill_price_from_quote(
5280    last_qty: Quantity,
5281    quote_amount: U256,
5282    quote_currency: Currency,
5283) -> anyhow::Result<Price> {
5284    let quote = Money::from_u256(quote_amount, quote_currency)?;
5285    Price::from_decimal_dp(quote.as_decimal() / last_qty.as_decimal(), FIXED_PRECISION)
5286        .map_err(anyhow::Error::from)
5287}
5288
5289fn raw_amount_to_quantity(amount: U256, decimals: u8) -> anyhow::Result<Quantity> {
5290    if amount.is_zero() {
5291        anyhow::bail!("Executed amount must be positive");
5292    }
5293    let quantity = if decimals >= FIXED_PRECISION {
5294        let scale = U256::from(10u64)
5295            .checked_pow(U256::from(decimals - FIXED_PRECISION))
5296            .ok_or_else(|| anyhow::anyhow!("Executed amount scaling overflow"))?;
5297        Quantity::from_u256(amount / scale, FIXED_PRECISION).map_err(anyhow::Error::from)?
5298    } else {
5299        Quantity::from_u256(amount, decimals).map_err(anyhow::Error::from)?
5300    };
5301
5302    if quantity.is_zero() {
5303        anyhow::bail!(
5304            "Executed amount {amount} is below representable quantity precision {FIXED_PRECISION}"
5305        );
5306    }
5307    Ok(quantity)
5308}
5309
5310fn exact_output_amount(quote: &SwapQuote, zero_for_one: bool) -> anyhow::Result<U256> {
5311    let amount = if zero_for_one {
5312        quote.amount1
5313    } else {
5314        quote.amount0
5315    };
5316
5317    if !amount.is_negative() {
5318        anyhow::bail!("Swap quote output amount {amount} is not a positive output");
5319    }
5320    Ok(amount.unsigned_abs())
5321}
5322
5323/// Derives the minimum output accepted for the swap: the quoted output reduced by
5324/// `slippage_bps`, in exact integer arithmetic. Rejects a zero minimum, which would leave
5325/// the swap without slippage protection.
5326fn derive_min_amount_out(quoted_amount_out: U256, slippage_bps: u32) -> anyhow::Result<U256> {
5327    if slippage_bps >= BPS_DENOMINATOR {
5328        anyhow::bail!("Slippage {slippage_bps} bps must be below {BPS_DENOMINATOR}");
5329    }
5330    let min_amount_out = quoted_amount_out
5331        .checked_mul(U256::from(BPS_DENOMINATOR - slippage_bps))
5332        .and_then(|scaled| scaled.checked_div(U256::from(BPS_DENOMINATOR)))
5333        .ok_or_else(|| anyhow::anyhow!("Minimum output derivation overflow"))?;
5334    if min_amount_out.is_zero() {
5335        anyhow::bail!(
5336            "Derived minimum output is zero for quoted output {quoted_amount_out} at {slippage_bps} bps slippage"
5337        );
5338    }
5339    Ok(min_amount_out)
5340}
5341
5342impl BlockchainExecutionClient {
5343    #[allow(
5344        clippy::large_futures,
5345        reason = "keeps transaction verification futures inline without additional boxing"
5346    )]
5347    async fn build_execution_verification_migration(
5348        &self,
5349        snapshot: ExecutionVerificationMigrationSnapshot,
5350        finalized: VerifiedBlockHeader,
5351        finalized_headers: &[VerifiedBlockHeader],
5352        nonce_verification: &Verified<u64>,
5353    ) -> anyhow::Result<ExecutionVerificationMigration> {
5354        let next_canonical_nonce = nonce_verification.value;
5355        let mut hashes_by_intent: HashMap<i64, Vec<&ExecutionTransactionHashRow>> = HashMap::new();
5356        for hash in &snapshot.hashes {
5357            hashes_by_intent
5358                .entry(hash.intent_id)
5359                .or_default()
5360                .push(hash);
5361        }
5362        let active_count = snapshot
5363            .intents
5364            .iter()
5365            .filter(|intent| intent.active)
5366            .count();
5367        anyhow::ensure!(
5368            active_count <= 1,
5369            "Retained execution history has multiple active signer owners"
5370        );
5371
5372        let mut nonce_owners = HashMap::new();
5373        let mut records = Vec::with_capacity(snapshot.intents.len());
5374        let finalized_headers = finalized_headers
5375            .iter()
5376            .map(durable_verified_header)
5377            .collect::<Vec<_>>();
5378
5379        for intent in &snapshot.intents {
5380            anyhow::ensure!(
5381                intent.chain_id == self.chain.chain_id
5382                    && intent.wallet_address == self.config.wallet_address,
5383                "Retained execution intent belongs to another signer"
5384            );
5385            let purpose = TransactionPurpose::parse(&intent.purpose).ok_or_else(|| {
5386                anyhow::anyhow!("Retained execution intent has an unsupported purpose")
5387            })?;
5388            let hashes = hashes_by_intent
5389                .get(&intent.id)
5390                .map(Vec::as_slice)
5391                .unwrap_or_default();
5392            let current = hashes
5393                .iter()
5394                .copied()
5395                .filter(|hash| hash.current)
5396                .collect::<Vec<_>>();
5397            anyhow::ensure!(
5398                current.len() <= 1,
5399                "Retained execution intent {} has multiple current hashes",
5400                intent.id
5401            );
5402            let current = current.first().copied();
5403            let mut authenticated = HashMap::new();
5404
5405            for hash in hashes {
5406                if hash.payload_expected {
5407                    let raw = open_execution_payload(
5408                        self.payload_keys
5409                            .as_deref()
5410                            .expect("Postgres execution requires payload keys"),
5411                        self.payload_policy(),
5412                        intent,
5413                        hash,
5414                        "verification migration",
5415                    )?;
5416                    authenticated.insert(hash.id, raw);
5417                } else {
5418                    anyhow::ensure!(
5419                        hash.raw_transaction.is_none() && hash.sealed_transaction.is_none(),
5420                        "Unowned replacement hash retains signed bytes"
5421                    );
5422                }
5423            }
5424
5425            if let Some(nonce) = intent.nonce {
5426                anyhow::ensure!(
5427                    nonce_owners.insert(nonce, intent.id).is_none(),
5428                    "Retained execution history has duplicate signer nonce ownership"
5429                );
5430            }
5431
5432            let base_decision = verification_decision(
5433                nonce_verification,
5434                Some(finalized.number),
5435                Some(finalized.number),
5436            );
5437
5438            if !intent.active {
5439                if matches!(intent.status.as_str(), "finalized" | "reverted") {
5440                    let expected_marker =
5441                        if purpose == TransactionPurpose::Swap && intent.status == "finalized" {
5442                            intent.fill_emitted
5443                        } else {
5444                            intent.terminal_emitted
5445                        };
5446                    anyhow::ensure!(
5447                        expected_marker,
5448                        "Released terminal intent {} has no durable event marker",
5449                        intent.id
5450                    );
5451                } else {
5452                    anyhow::ensure!(
5453                        matches!(intent.status.as_str(), "dropped" | "recoverable")
5454                            && authenticated.is_empty(),
5455                        "Released nonterminal intent {} retains signed ownership",
5456                        intent.id
5457                    );
5458                    records.push(ExecutionVerificationMigrationRecord {
5459                        intent_id: intent.id,
5460                        nonce: intent.nonce,
5461                        transaction_hash: None,
5462                        terminal_status: None,
5463                        block_number: None,
5464                        block_hash: None,
5465                        receipt_success: None,
5466                        gas_used: None,
5467                        effective_gas_price: None,
5468                        recover_prepared: false,
5469                        decisions: vec![base_decision],
5470                    });
5471                    continue;
5472                }
5473            }
5474
5475            if intent.active && intent.nonce.is_none() {
5476                anyhow::ensure!(
5477                    intent.status == "prepared" && hashes.is_empty(),
5478                    "Unassigned active intent {} is not an unsigned preparation",
5479                    intent.id
5480                );
5481                records.push(ExecutionVerificationMigrationRecord {
5482                    intent_id: intent.id,
5483                    nonce: None,
5484                    transaction_hash: None,
5485                    terminal_status: None,
5486                    block_number: None,
5487                    block_hash: None,
5488                    receipt_success: None,
5489                    gas_used: None,
5490                    effective_gas_price: None,
5491                    recover_prepared: true,
5492                    decisions: vec![base_decision],
5493                });
5494                continue;
5495            }
5496
5497            let nonce = intent.nonce.ok_or_else(|| {
5498                anyhow::anyhow!("Retained signed intent {} has no nonce", intent.id)
5499            })?;
5500            let current = current.ok_or_else(|| {
5501                anyhow::anyhow!("Retained signed intent {} has no current hash", intent.id)
5502            })?;
5503            let raw_transaction = authenticated.get(&current.id).ok_or_else(|| {
5504                anyhow::anyhow!(
5505                    "Retained signed intent {} has no authenticated current payload",
5506                    intent.id
5507                )
5508            })?;
5509
5510            if intent.active && nonce == next_canonical_nonce {
5511                anyhow::ensure!(
5512                    !matches!(intent.status.as_str(), "finalized" | "reverted"),
5513                    "Active terminal intent conflicts with the canonical nonce ledger"
5514                );
5515                records.push(ExecutionVerificationMigrationRecord {
5516                    intent_id: intent.id,
5517                    nonce: Some(nonce),
5518                    transaction_hash: Some(current.transaction_hash.clone()),
5519                    terminal_status: None,
5520                    block_number: None,
5521                    block_hash: None,
5522                    receipt_success: None,
5523                    gas_used: None,
5524                    effective_gas_price: None,
5525                    recover_prepared: false,
5526                    decisions: vec![base_decision],
5527                });
5528                continue;
5529            }
5530            anyhow::ensure!(
5531                nonce < next_canonical_nonce,
5532                "Retained active nonce {nonce} is above canonical nonce {next_canonical_nonce}"
5533            );
5534
5535            let receipt_verification = required_verification(
5536                self.verification
5537                    .verify_receipt(&B256::from_str(&current.transaction_hash).with_context(
5538                        || {
5539                            format!(
5540                                "Retained transaction hash {} is invalid",
5541                                current.transaction_hash
5542                            )
5543                        },
5544                    )?)
5545                    .await,
5546                "migration receipt",
5547            )?;
5548            let receipt = receipt_verification.value.clone();
5549            anyhow::ensure!(
5550                receipt.block_number <= finalized.number,
5551                "Retained terminal receipt is above the verified finalized boundary"
5552            );
5553            let inclusion_verification = required_verification(
5554                self.verification.verify_block(receipt.block_number).await,
5555                "migration inclusion header",
5556            )?;
5557            anyhow::ensure!(
5558                inclusion_verification.value.hash == receipt.block_hash
5559                    && finalized_headers.iter().any(|header| {
5560                        header.number == receipt.block_number
5561                            && header.hash == receipt.block_hash.to_string()
5562                    }),
5563                "Retained terminal receipt is not on the verified finalized ancestry"
5564            );
5565            let tx_hash = B256::from_str(&current.transaction_hash)
5566                .context("Retained transaction hash is invalid")?;
5567            let included = IncludedTransaction {
5568                intent_id: intent.id,
5569                nonce,
5570                tx_hash,
5571                block_number: receipt.block_number,
5572                receipt: receipt.clone(),
5573                finality: StableFinality {
5574                    decisions: Vec::new(),
5575                    inclusion_header: durable_verified_header(&inclusion_verification.value),
5576                    finalized_headers: finalized_headers.clone(),
5577                },
5578            };
5579            let trace_purpose = match purpose {
5580                TransactionPurpose::Wrap => "wrap",
5581                TransactionPurpose::Approve => "approve",
5582                TransactionPurpose::Swap => {
5583                    match self.restore_swap_plan(intent)?.order.order_side() {
5584                        OrderSide::Sell => "swap_sell",
5585                        OrderSide::Buy => "swap_buy",
5586                    }
5587                }
5588            };
5589            let mut decisions = vec![
5590                verification_decision(
5591                    &receipt_verification,
5592                    Some(receipt.block_number),
5593                    Some(receipt.block_number),
5594                ),
5595                verification_decision(
5596                    &inclusion_verification,
5597                    Some(receipt.block_number),
5598                    Some(receipt.block_number),
5599                ),
5600            ];
5601            decisions.extend(
5602                verify_finalized_transaction_identity(
5603                    &included,
5604                    intent,
5605                    nonce,
5606                    raw_transaction,
5607                    &self.verification,
5608                    self.wallet_address,
5609                    self.chain.chain_id,
5610                    &self
5611                        .config
5612                        .verification
5613                        .as_ref()
5614                        .expect("verification config validated")
5615                        .deployment_manifest,
5616                    trace_purpose,
5617                )
5618                .await?,
5619            );
5620            let terminal_status = if receipt.status {
5621                TransactionStatus::Finalized
5622            } else {
5623                TransactionStatus::Reverted
5624            };
5625
5626            if !intent.active {
5627                anyhow::ensure!(
5628                    intent.status == terminal_status.as_str(),
5629                    "Released terminal intent status conflicts with its verified receipt"
5630                );
5631            }
5632            records.push(ExecutionVerificationMigrationRecord {
5633                intent_id: intent.id,
5634                nonce: Some(nonce),
5635                transaction_hash: Some(current.transaction_hash.clone()),
5636                terminal_status: Some(terminal_status),
5637                block_number: Some(receipt.block_number),
5638                block_hash: Some(receipt.block_hash.to_string()),
5639                receipt_success: Some(receipt.status),
5640                gas_used: Some(receipt.gas_used),
5641                effective_gas_price: Some(receipt.effective_gas_price.to_string()),
5642                recover_prepared: false,
5643                decisions,
5644            });
5645        }
5646
5647        Ok(ExecutionVerificationMigration { snapshot, records })
5648    }
5649}
5650
5651#[async_trait(?Send)]
5652impl ExecutionClient for BlockchainExecutionClient {
5653    fn is_connected(&self) -> bool {
5654        self.core.is_connected()
5655    }
5656
5657    fn client_id(&self) -> ClientId {
5658        self.core.client_id
5659    }
5660
5661    fn account_id(&self) -> AccountId {
5662        self.core.account_id
5663    }
5664
5665    fn venue(&self) -> Venue {
5666        self.core.venue
5667    }
5668
5669    fn handles_order_venue(&self, venue: Venue) -> bool {
5670        venue.parse_dex().is_ok_and(|(blockchain, dex_type)| {
5671            blockchain == self.chain.name && dex_type == DexType::UniswapV3
5672        })
5673    }
5674
5675    fn oms_type(&self) -> OmsType {
5676        self.core.oms_type
5677    }
5678
5679    fn get_account(&self) -> Option<AccountAny> {
5680        self.core.cache().account_owned(&self.core.account_id)
5681    }
5682
5683    fn generate_account_state(
5684        &self,
5685        balances: Vec<AccountBalance>,
5686        margins: Vec<MarginBalance>,
5687        reported: bool,
5688        ts_event: UnixNanos,
5689        info: Option<Params>,
5690    ) -> anyhow::Result<()> {
5691        self.emitter
5692            .try_emit_account_state(balances, margins, reported, ts_event, info)
5693    }
5694
5695    fn start(&mut self) -> anyhow::Result<()> {
5696        if self.core.is_started() {
5697            return Ok(());
5698        }
5699
5700        self.emitter.set_sender(get_exec_event_sender());
5701        self.core.set_started();
5702        log::info!(
5703            "Started: client_id={}, account_id={}",
5704            self.core.client_id,
5705            self.core.account_id
5706        );
5707        Ok(())
5708    }
5709
5710    fn stop(&mut self) -> anyhow::Result<()> {
5711        if self.core.is_stopped() {
5712            return Ok(());
5713        }
5714
5715        self.pending_tasks.begin_shutdown();
5716        self.signer = None;
5717        self.core.set_stopped();
5718        self.core.set_disconnected();
5719        log::info!("Stopped: client_id={}", self.core.client_id);
5720        Ok(())
5721    }
5722
5723    #[allow(
5724        clippy::large_futures,
5725        reason = "keeps transaction verification futures inline without additional boxing"
5726    )]
5727    fn submit_order(&self, cmd: SubmitOrder) -> anyhow::Result<()> {
5728        let order = self.core.cache().try_order_owned(&cmd.client_order_id)?;
5729
5730        if order.is_closed() {
5731            log::warn!("Cannot submit closed order {}", order.client_order_id());
5732            return Ok(());
5733        }
5734
5735        if let Err(reason) = validate_order(&order) {
5736            self.emitter.emit_order_denied(&order, &reason.to_string());
5737            return Ok(());
5738        }
5739
5740        if !self.pending_tasks.is_open() {
5741            self.emitter
5742                .emit_order_denied(&order, "Blockchain execution client is shutting down");
5743            return Ok(());
5744        }
5745
5746        let plan = match self.prepare_swap(&cmd, &order) {
5747            Ok(plan) => plan,
5748            Err(e) => {
5749                self.emitter.emit_order_denied(&order, &e.to_string());
5750                return Ok(());
5751            }
5752        };
5753
5754        let executor = match self.transaction_executor() {
5755            Ok(executor) => executor,
5756            Err(e) => {
5757                self.emitter.emit_order_denied(&order, &e.to_string());
5758                return Ok(());
5759            }
5760        };
5761
5762        let emitter = self.emitter.clone();
5763        let max_quote_age_blocks = self.transaction_limits.max_quote_age_blocks;
5764        let deadline_seconds = self.transaction_limits.deadline_seconds;
5765        let client_order_id = order.client_order_id();
5766
5767        let future = async move {
5768            if let Err(e) = execute_swap(
5769                plan,
5770                executor,
5771                emitter,
5772                max_quote_age_blocks,
5773                deadline_seconds,
5774            )
5775            .await
5776            {
5777                log::warn!("Swap execution for order {client_order_id} failed: {e:?}");
5778            }
5779        };
5780
5781        if let Err(e) = self.pending_tasks.spawn(future) {
5782            release_preparing_slot(&self.in_flight);
5783            log::warn!("Skipping blockchain swap after shutdown began: {e}");
5784        }
5785
5786        Ok(())
5787    }
5788
5789    fn submit_order_list(&self, cmd: SubmitOrderList) -> anyhow::Result<()> {
5790        let orders = self
5791            .core
5792            .cache()
5793            .orders_for_ids(&cmd.order_list.client_order_ids, &cmd);
5794
5795        for order in &orders {
5796            if order.is_closed() {
5797                log::warn!("Cannot submit closed order {}", order.client_order_id());
5798                continue;
5799            }
5800
5801            self.emitter
5802                .emit_order_denied(order, ORDER_LIST_UNSUPPORTED);
5803        }
5804
5805        Ok(())
5806    }
5807
5808    fn modify_order(&self, cmd: ModifyOrder) -> anyhow::Result<()> {
5809        let Ok(order) = self.core.cache().try_order_owned(&cmd.client_order_id) else {
5810            log::warn!("Cannot modify unknown order {}", cmd.client_order_id);
5811            return Ok(());
5812        };
5813
5814        self.emitter.emit_order_modify_rejected(
5815            &order,
5816            cmd.venue_order_id,
5817            ORDER_MODIFY_UNSUPPORTED,
5818            get_atomic_clock_realtime().get_time_ns(),
5819        );
5820        Ok(())
5821    }
5822
5823    fn cancel_order(&self, cmd: CancelOrder) -> anyhow::Result<()> {
5824        let Ok(order) = self.core.cache().try_order_owned(&cmd.client_order_id) else {
5825            log::warn!("Cannot cancel unknown order {}", cmd.client_order_id);
5826            return Ok(());
5827        };
5828
5829        self.emitter.emit_order_cancel_rejected(
5830            &order,
5831            cmd.venue_order_id,
5832            ORDER_CANCEL_UNSUPPORTED,
5833            get_atomic_clock_realtime().get_time_ns(),
5834        );
5835        Ok(())
5836    }
5837
5838    fn cancel_all_orders(&self, cmd: CancelAllOrders) -> anyhow::Result<()> {
5839        log::warn!(
5840            "Cancel-all for {} is not supported on the blockchain execution client",
5841            cmd.instrument_id
5842        );
5843        Ok(())
5844    }
5845
5846    fn batch_cancel_orders(&self, cmd: BatchCancelOrders) -> anyhow::Result<()> {
5847        for cancel in cmd.cancels {
5848            self.cancel_order(cancel)?;
5849        }
5850        Ok(())
5851    }
5852
5853    fn query_account(&self, cmd: QueryAccount) -> anyhow::Result<()> {
5854        anyhow::ensure!(
5855            cmd.account_id == self.core.account_id,
5856            "Query account ID {} does not match client account ID {}",
5857            cmd.account_id,
5858            self.core.account_id
5859        );
5860        anyhow::ensure!(self.core.is_started(), "Execution client is not started");
5861
5862        let balances = self.wallet_balance.lock().as_account_balances()?;
5863        self.generate_account_state(
5864            balances,
5865            vec![],
5866            true,
5867            get_atomic_clock_realtime().get_time_ns(),
5868            None,
5869        )
5870    }
5871
5872    fn query_order(&self, cmd: QueryOrder) -> anyhow::Result<()> {
5873        log::debug!(
5874            "Order queries are not supported on the blockchain execution client; cannot query {}",
5875            cmd.client_order_id
5876        );
5877        Ok(())
5878    }
5879
5880    fn retain_unresolved_submissions(&self) -> bool {
5881        true
5882    }
5883
5884    #[allow(
5885        clippy::large_futures,
5886        reason = "keeps transaction verification futures inline without additional boxing"
5887    )]
5888    async fn connect(&mut self) -> anyhow::Result<()> {
5889        if self.core.is_connected() {
5890            log::warn!("Blockchain execution client already connected");
5891            return Ok(());
5892        }
5893
5894        log::info!(
5895            "Connecting to blockchain execution client on chain {}",
5896            self.chain.name
5897        );
5898
5899        if !self.pending_tasks.is_open() || !self.pending_tasks.is_empty() {
5900            self.pending_tasks.begin_shutdown();
5901            self.pending_tasks
5902                .finish_shutdown(Duration::from_secs(5), Duration::from_secs(2))
5903                .await
5904                .map_err(|e| anyhow::anyhow!("Failed to terminate blockchain submissions: {e}"))?;
5905            self.signer = None;
5906            self.pending_tasks
5907                .start_generation()
5908                .map_err(|e| anyhow::anyhow!("Failed to start blockchain task generation: {e}"))?;
5909        }
5910        release_preparing_slot(&self.in_flight);
5911
5912        let setup_guard = TaskGroupGuard::new(&[&self.pending_tasks], || {});
5913
5914        let payload_keys = PayloadKeySet::load(
5915            self.config.payload_key_env.as_deref(),
5916            &self.config.payload_key_retired_env,
5917            self.config.payload_deployment_id.as_deref(),
5918        )?
5919        .map(Arc::new);
5920
5921        if self.cache.database.is_some() || self.config.postgres_cache_database_config.is_some() {
5922            let keys = payload_keys.as_deref().ok_or_else(|| {
5923                anyhow::anyhow!(
5924                    "Postgres execution requires an active payload key and deployment identity"
5925                )
5926            })?;
5927
5928            if self.cache.database.is_none() {
5929                let pg_options = self
5930                    .config
5931                    .postgres_cache_database_config
5932                    .as_ref()
5933                    .expect("Postgres configuration checked above");
5934                let database = crate::cache::database::BlockchainCacheDatabase::connect(
5935                    pg_options.clone().into(),
5936                )
5937                .await
5938                .map_err(|e| {
5939                    anyhow::anyhow!("Failed to connect to the Postgres cache database: {e}")
5940                })?;
5941                self.cache.database = Some(database);
5942            }
5943            self.cache
5944                .database
5945                .as_ref()
5946                .expect("database was attached")
5947                .require_execution_payload_storage_ready(keys)
5948                .await?;
5949            self.cache.initialize_chain().await;
5950            self.cache.ensure_execution_transaction_schema().await?;
5951            let check = self
5952                .cache
5953                .database
5954                .as_ref()
5955                .expect("database was attached")
5956                .check_execution_payload_storage(
5957                    Some(keys),
5958                    Some(PayloadPolicy {
5959                        chain_id: self.chain.chain_id,
5960                        signer: self.wallet_address,
5961                        gas_limit: self.config.gas_limit,
5962                        max_fee_per_gas: self.config.max_fee_per_gas_wei,
5963                    }),
5964                    100,
5965                )
5966                .await?;
5967            anyhow::ensure!(
5968                check.protected,
5969                "Postgres execution requires protected payload storage"
5970            );
5971        } else {
5972            log::warn!(
5973                "No Postgres cache database configured; transactions will be refused (no durable store)"
5974            );
5975        }
5976        self.payload_keys = payload_keys;
5977
5978        let verification = self
5979            .config
5980            .verification
5981            .as_ref()
5982            .expect("verification config validated at construction");
5983        let position = if let Some(database) = self.cache.database.as_ref() {
5984            database
5985                .load_execution_verification_position(
5986                    self.chain.chain_id,
5987                    &self.config.wallet_address,
5988                    &verification.manifest_version,
5989                    &verification.manifest_digest,
5990                )
5991                .await?
5992        } else {
5993            None
5994        };
5995        let migration_snapshot = if position.is_none() {
5996            if let Some(database) = self.cache.database.as_ref() {
5997                let snapshot = database
5998                    .load_execution_verification_migration_snapshot(
5999                        self.chain.chain_id,
6000                        &self.config.wallet_address,
6001                    )
6002                    .await?;
6003
6004                if snapshot.intents.is_empty() {
6005                    None
6006                } else {
6007                    Some(snapshot)
6008                }
6009            } else {
6010                None
6011            }
6012        } else {
6013            None
6014        };
6015
6016        let chain_id_verification = required_verification(
6017            self.verification.verify_chain_id().await,
6018            "Blockchain chain ID",
6019        )?;
6020        let checkpoint_verification = required_verification(
6021            self.verification.verify_checkpoint().await,
6022            "Blockchain checkpoint",
6023        )?;
6024        let checkpoint = checkpoint_verification.value;
6025        let finalized_verification = required_verification(
6026            self.verification.verify_finalized_header().await,
6027            "Blockchain finalized header",
6028        )?;
6029        let finalized = finalized_verification.value;
6030        let mut connect_decisions = vec![
6031            verification_decision(&chain_id_verification, None, None),
6032            verification_decision(
6033                &checkpoint_verification,
6034                Some(checkpoint.number),
6035                Some(checkpoint.number),
6036            ),
6037            verification_decision(
6038                &finalized_verification,
6039                Some(finalized.number),
6040                Some(finalized.number),
6041            ),
6042        ];
6043        let mut finalized_headers = if let Some(position) = position.as_ref() {
6044            let durable_tip = parse_verified_header(&position.finalized_tip)?;
6045            anyhow::ensure!(
6046                durable_tip.number >= checkpoint.number,
6047                "Durable finalized header tip precedes the trusted checkpoint"
6048            );
6049            let durable_tip_verification = required_verification(
6050                self.verification.verify_block(durable_tip.number).await,
6051                "Blockchain durable finalized tip",
6052            )?;
6053            anyhow::ensure!(
6054                durable_tip_verification.value == durable_tip,
6055                "Durable finalized header tip conflicts with independent sources"
6056            );
6057            connect_decisions.push(verification_decision(
6058                &durable_tip_verification,
6059                Some(durable_tip.number),
6060                Some(durable_tip.number),
6061            ));
6062            vec![durable_tip]
6063        } else {
6064            vec![checkpoint]
6065        };
6066        let mut ancestry_cursor = *finalized_headers
6067            .last()
6068            .expect("finalized header ledger is nonempty");
6069        anyhow::ensure!(
6070            finalized.number >= ancestry_cursor.number,
6071            "Verified finalized height regressed below the durable finalized header tip"
6072        );
6073
6074        while ancestry_cursor.number < finalized.number {
6075            let end = ancestry_cursor
6076                .number
6077                .saturating_add(4_096)
6078                .min(finalized.number);
6079            let start = ancestry_cursor.number.saturating_add(1);
6080            let headers_verification = required_verification(
6081                self.verification
6082                    .verify_header_window(ancestry_cursor, end)
6083                    .await,
6084                "Blockchain finalized ancestry",
6085            )?;
6086            let headers = &headers_verification.value;
6087            ancestry_cursor = *headers
6088                .last()
6089                .expect("nonempty ancestry window advances the cursor");
6090            finalized_headers.extend(headers.iter().copied());
6091            connect_decisions.push(verification_decision(
6092                &headers_verification,
6093                Some(start),
6094                Some(end),
6095            ));
6096        }
6097        anyhow::ensure!(
6098            ancestry_cursor == finalized,
6099            "Verified finalized header conflicts with its ancestry window"
6100        );
6101        let nonce_verification = required_verification(
6102            self.verification
6103                .verify_transaction_count(&self.wallet_address, finalized.number)
6104                .await,
6105            "Blockchain finalized transaction count",
6106        )?;
6107        let observed_canonical_nonce = nonce_verification.value;
6108        let next_canonical_nonce = position
6109            .as_ref()
6110            .map_or(observed_canonical_nonce, |position| {
6111                position.next_canonical_nonce
6112            });
6113
6114        if let Some(position) = position.as_ref() {
6115            log::debug!(
6116                "Resumed execution verification ledger at nonce revision {} with observed finalized nonce {}",
6117                position.revision,
6118                observed_canonical_nonce,
6119            );
6120        }
6121        connect_decisions.push(verification_decision(
6122            &nonce_verification,
6123            Some(finalized.number),
6124            Some(finalized.number),
6125        ));
6126        let deployment_verification = required_verification(
6127            self.verification
6128                .verify_deployment_manifest(&verification.deployment_manifest, finalized.number)
6129                .await,
6130            "Blockchain deployment manifest",
6131        )?;
6132        connect_decisions.push(verification_decision(
6133            &deployment_verification,
6134            Some(finalized.number),
6135            Some(finalized.number),
6136        ));
6137        connect_decisions.extend(
6138            verify_connect_capabilities(
6139                &self.verification,
6140                &verification.deployment_manifest,
6141                self.wallet_address,
6142                self.weth_address,
6143                finalized.number,
6144            )
6145            .await?,
6146        );
6147        let migration = if let Some(snapshot) = migration_snapshot {
6148            Some(
6149                self.build_execution_verification_migration(
6150                    snapshot,
6151                    finalized,
6152                    &finalized_headers,
6153                    &nonce_verification,
6154                )
6155                .await?,
6156            )
6157        } else {
6158            None
6159        };
6160
6161        if let Some(database) = self.cache.database.as_ref() {
6162            let identities = std::iter::once(&verification.authoritative)
6163                .chain(
6164                    verification
6165                        .verifiers
6166                        .iter()
6167                        .map(|provider| &provider.identity),
6168                )
6169                .collect::<Vec<_>>();
6170            let provider_ids = identities
6171                .iter()
6172                .map(|identity| identity.provider_id.clone())
6173                .collect::<Vec<_>>();
6174            let operator_ids = identities
6175                .iter()
6176                .map(|identity| identity.operator_id.clone())
6177                .collect::<Vec<_>>();
6178            let failure_domain_ids = identities
6179                .iter()
6180                .flat_map(|identity| identity.failure_domain_ids.iter().cloned())
6181                .collect::<Vec<_>>();
6182            let finalized_headers = finalized_headers
6183                .iter()
6184                .map(durable_verified_header)
6185                .collect::<Vec<_>>();
6186            database
6187                .ensure_execution_verification_schema(&ExecutionVerificationBootstrap {
6188                    chain_id: self.chain.chain_id,
6189                    wallet_address: &self.config.wallet_address,
6190                    manifest_version: &verification.manifest_version,
6191                    manifest_digest: &verification.manifest_digest,
6192                    checkpoint_number: checkpoint.number,
6193                    checkpoint_hash: &checkpoint.hash.to_string(),
6194                    checkpoint_parent_hash: &checkpoint.parent_hash.to_string(),
6195                    checkpoint_timestamp: checkpoint.timestamp,
6196                    checkpoint_base_fee_per_gas: checkpoint.base_fee_per_gas,
6197                    finalized_headers: &finalized_headers,
6198                    next_canonical_nonce,
6199                    observed_canonical_nonce,
6200                    provider_ids: &provider_ids,
6201                    operator_ids: &operator_ids,
6202                    failure_domain_ids: &failure_domain_ids,
6203                    decisions: &connect_decisions,
6204                    migration: migration.as_ref(),
6205                })
6206                .await?;
6207        }
6208
6209        let payload_connect_lease = if let (Some(database), Some(keys)) =
6210            (self.cache.database.as_ref(), self.payload_keys.as_deref())
6211        {
6212            Some(
6213                database
6214                    .require_execution_payload_storage(keys, self.payload_policy(), 100)
6215                    .await?,
6216            )
6217        } else {
6218            None
6219        };
6220
6221        // Load the signer key from the configured environment variable; the key is never
6222        // logged, serialized, or stored in configuration
6223        let private_key = Zeroizing::new(
6224            std::env::var(&self.config.signer_private_key_env).map_err(|_| {
6225                anyhow::anyhow!(
6226                    "Signer private key environment variable '{}' is not set",
6227                    self.config.signer_private_key_env
6228                )
6229            })?,
6230        );
6231        let encoded_key = private_key.trim();
6232        let encoded_key = encoded_key.strip_prefix("0x").unwrap_or(encoded_key);
6233        let key_bytes = Zeroizing::new(hex::decode_array::<32>(encoded_key).map_err(|_| {
6234            anyhow::anyhow!(
6235                "Signer private key in '{}' is not a valid hex private key",
6236                self.config.signer_private_key_env
6237            )
6238        })?);
6239        let signer = PrivateKeySigner::from_slice(&key_bytes[..]).map_err(|_| {
6240            anyhow::anyhow!(
6241                "Signer private key in '{}' is not a valid secp256k1 private key",
6242                self.config.signer_private_key_env
6243            )
6244        })?;
6245
6246        if signer.address() != self.wallet_address {
6247            anyhow::bail!(
6248                "Signer address {} derived from '{}' does not match configured wallet address {}",
6249                signer.address(),
6250                self.config.signer_private_key_env,
6251                self.wallet_address
6252            );
6253        }
6254
6255        self.signer = Some(Arc::new(signer));
6256        drop(payload_connect_lease);
6257
6258        if self.cache.has_database()
6259            && let Err(e) = self.reconcile_unresolved_execution().await
6260        {
6261            self.signer = None;
6262            return Err(e);
6263        }
6264
6265        if let Err(e) = self.refresh_wallet_balances().await {
6266            self.signer = None;
6267            return Err(e);
6268        }
6269        self.core.set_connected();
6270        setup_guard.disarm();
6271        log::info!(
6272            "Blockchain execution client connected on chain {}",
6273            self.chain.name
6274        );
6275        Ok(())
6276    }
6277
6278    async fn disconnect(&mut self) -> anyhow::Result<()> {
6279        self.pending_tasks.begin_shutdown();
6280        let tasks_result = self
6281            .pending_tasks
6282            .finish_shutdown(Duration::from_secs(5), Duration::from_secs(2))
6283            .await;
6284        self.signer = None;
6285        self.core.set_disconnected();
6286        tasks_result
6287            .map_err(|e| anyhow::anyhow!("Failed to terminate blockchain submissions: {e}"))?;
6288        Ok(())
6289    }
6290
6291    async fn generate_order_status_report(
6292        &self,
6293        _cmd: &GenerateOrderStatusReport,
6294    ) -> anyhow::Result<Option<OrderStatusReport>> {
6295        anyhow::bail!("{VENUE_EXECUTION_REPORTS_UNSUPPORTED}");
6296    }
6297
6298    async fn generate_order_status_reports(
6299        &self,
6300        _cmd: &GenerateOrderStatusReports,
6301    ) -> anyhow::Result<Vec<OrderStatusReport>> {
6302        anyhow::bail!("{VENUE_EXECUTION_REPORTS_UNSUPPORTED}");
6303    }
6304
6305    async fn generate_fill_reports(
6306        &self,
6307        _cmd: GenerateFillReports,
6308    ) -> anyhow::Result<Vec<FillReport>> {
6309        anyhow::bail!("{VENUE_EXECUTION_REPORTS_UNSUPPORTED}");
6310    }
6311
6312    async fn generate_position_status_reports(
6313        &self,
6314        _cmd: &GeneratePositionStatusReports,
6315    ) -> anyhow::Result<Vec<PositionStatusReport>> {
6316        anyhow::bail!("{VENUE_EXECUTION_REPORTS_UNSUPPORTED}");
6317    }
6318
6319    async fn generate_mass_status(
6320        &self,
6321        _lookback_mins: Option<u64>,
6322    ) -> anyhow::Result<Option<ExecutionMassStatus>> {
6323        // Venue mass status is unsupported; durable intent reconciliation at connect
6324        // covers restart recovery, and Ok(None) keeps LiveNode startup safe
6325        log::info!(
6326            "Mass status is not supported on the blockchain execution client; skipping venue reconciliation"
6327        );
6328        Ok(None)
6329    }
6330}
6331
6332fn validate_order(order: &impl Order) -> Result<(), OrderDeniedReason> {
6333    if order.is_reduce_only() {
6334        return Err(OrderDeniedReason::UnsupportedReduceOnly);
6335    }
6336
6337    Ok(())
6338}
6339
6340#[cfg(test)]
6341mod tests {
6342    #![allow(
6343        clippy::large_futures,
6344        reason = "exercises inline transaction futures without changing their allocation behavior"
6345    )]
6346
6347    use std::{
6348        cell::RefCell,
6349        rc::Rc,
6350        sync::atomic::{AtomicU64, Ordering},
6351    };
6352
6353    use alloy::{
6354        primitives::{address, aliases::I24},
6355        sol_types::SolValue,
6356    };
6357    use nautilus_common::{
6358        cache::Cache,
6359        clock::{Clock, VirtualClock},
6360        live::runner::replace_exec_event_sender,
6361        messages::{ExecutionEvent, execution::TradingCommand},
6362        testing::wait_until_async,
6363    };
6364    use nautilus_core::UUID4;
6365    use nautilus_infrastructure::sql::pg::{PostgresConnectOptions, get_postgres_connect_options};
6366    use nautilus_live::{
6367        execution::{
6368            config::ExecutionManagerConfig,
6369            submission::{SubmissionRecoveryExhausted, SubmissionRecoverySource},
6370        },
6371        manager::ExecutionManager,
6372    };
6373    use nautilus_model::{
6374        defi::{
6375            PoolProfiler,
6376            chain::chains,
6377            data::block::BlockPosition,
6378            pool_analysis::{
6379                position::PoolPosition,
6380                snapshot::{PoolAnalytics, PoolSnapshot, PoolState},
6381            },
6382            tick_map::{tick::PoolTick, tick_math::get_tick_at_sqrt_ratio},
6383        },
6384        enums::AccountType,
6385        events::OrderEventAny,
6386        identifiers::{OrderListId, StrategyId, TraderId},
6387        orders::{OrderList, OrderTestBuilder},
6388        types::Price,
6389    };
6390    use rstest::rstest;
6391    use sqlx::postgres::{PgAdvisoryLock, PgAdvisoryLockKey, PgPoolOptions};
6392
6393    use super::*;
6394    use crate::{
6395        cache::database::tests::connect_test_database,
6396        config::{
6397            BlockchainCallEdgeManifest, BlockchainChainAnchorConfig, BlockchainContractManifest,
6398            BlockchainContractProbe, BlockchainContractRole, BlockchainDeploymentManifest,
6399            BlockchainPoolManifest, BlockchainProviderIdentity, BlockchainTokenManifest,
6400            BlockchainVerificationConfig, BlockchainVerificationProviderConfig, QuoteSpendLimit,
6401        },
6402        constants::BLOCKCHAIN_VENUE,
6403        exchanges::arbitrum::UNISWAP_V3,
6404        rpc::http::{
6405            EXECUTION_RPC_TIMEOUT_SECS,
6406            tests::mock::{MockRpcState, start_mock_rpc_server},
6407        },
6408    };
6409
6410    /// Polls for the receipt of a broadcast transaction until it exists or the poll bound
6411    /// is exhausted. A `null` receipt result is a legitimate pending response.
6412    async fn poll_for_receipt(
6413        http_rpc_client: &BlockchainHttpRpcClient,
6414        tx_hash: &B256,
6415        max_polls: u32,
6416        interval: Duration,
6417    ) -> anyhow::Result<Option<RpcTransactionReceipt>> {
6418        let mut last_error = None;
6419        let mut observed_pending = false;
6420
6421        for attempt in 0..max_polls {
6422            if attempt > 0 {
6423                tokio::time::sleep(interval).await;
6424            }
6425
6426            match http_rpc_client.get_transaction_receipt(tx_hash).await {
6427                Ok(Some(receipt)) => return Ok(Some(receipt)),
6428                Ok(None) => observed_pending = true,
6429                Err(e) => {
6430                    log::warn!(
6431                        "Receipt poll {}/{} for transaction {tx_hash} failed: {e}",
6432                        attempt + 1,
6433                        max_polls
6434                    );
6435                    last_error = Some(e);
6436                }
6437            }
6438        }
6439
6440        if !observed_pending && let Some(e) = last_error {
6441            return Err(e);
6442        }
6443
6444        Ok(None)
6445    }
6446
6447    const CHAIN_ID_ARBITRUM: &str =
6448        include_str!("../../test_data/execution/rpc_eth_chain_id_arbitrum.json");
6449    const CHAIN_ID_ETHEREUM: &str =
6450        include_str!("../../test_data/execution/rpc_eth_chain_id_ethereum.json");
6451    const GET_CODE_DEPLOYED: &str =
6452        include_str!("../../test_data/execution/rpc_eth_get_code_deployed.json");
6453    const GET_CODE_EMPTY: &str =
6454        include_str!("../../test_data/execution/rpc_eth_get_code_empty.json");
6455    const GET_BALANCE: &str = include_str!("../../test_data/execution/rpc_eth_get_balance.json");
6456    const GET_BALANCE_ZERO: &str =
6457        include_str!("../../test_data/execution/rpc_eth_get_balance_zero.json");
6458    const GET_BALANCE_INSUFFICIENT: &str =
6459        include_str!("../../test_data/execution/rpc_eth_get_balance_insufficient.json");
6460    const CALL_BALANCE: &str = include_str!("../../test_data/execution/rpc_eth_call_balance.json");
6461    const CALL_BALANCE_AFTER_WRAP: &str =
6462        include_str!("../../test_data/execution/rpc_eth_call_balance_after_wrap.json");
6463    const CALL_BALANCE_WETH: &str =
6464        include_str!("../../test_data/execution/rpc_eth_call_balance_weth.json");
6465    const CALL_BALANCE_USDC: &str =
6466        include_str!("../../test_data/execution/rpc_eth_call_balance_usdc.json");
6467    const CALL_BALANCE_WETH_UPDATED: &str =
6468        include_str!("../../test_data/execution/rpc_eth_call_balance_weth_updated.json");
6469    const CALL_BALANCE_USDC_UPDATED: &str =
6470        include_str!("../../test_data/execution/rpc_eth_call_balance_usdc_updated.json");
6471    const CALL_BOOL_TRUE: &str =
6472        include_str!("../../test_data/execution/rpc_eth_call_bool_true.json");
6473    const CALL_EMPTY: &str = include_str!("../../test_data/execution/rpc_eth_call_empty.json");
6474    const CALL_ZERO: &str = include_str!("../../test_data/execution/rpc_eth_call_zero.json");
6475    const CALL_ALLOWANCE: &str =
6476        include_str!("../../test_data/execution/rpc_eth_call_allowance.json");
6477    const CALL_ALLOWANCE_1000: &str = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0x00000000000000000000000000000000000000000000000000000000000003e8\"}";
6478    const CALL_ALLOWANCE_MAX: &str = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\"}";
6479    const CALL_FACTORY: &str = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0x0000000000000000000000001f98431c8ad98523631ae4a59f267346ea31f984\"}";
6480    const CALL_WETH: &str = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0x00000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab1\"}";
6481    const CALL_USDC: &str = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0x000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e5831\"}";
6482    const CALL_FEE_500: &str = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0x00000000000000000000000000000000000000000000000000000000000001f4\"}";
6483    const CALL_POOL: &str = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0x000000000000000000000000c6962004f452be9203591991d15f6b388e09e8d0\"}";
6484    const CALL_REVERTED: &str =
6485        r#"{"jsonrpc":"2.0","id":1,"error":{"code":3,"message":"execution reverted"}}"#;
6486    const STORAGE_ZERO: &str = r#"{"jsonrpc":"2.0","id":1,"result":"0x0000000000000000000000000000000000000000000000000000000000000000"}"#;
6487    const TRACE_UNKNOWN_TRANSACTION: &str =
6488        r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32000,"message":"transaction not found"}}"#;
6489    const CALL_DECIMALS_18: &str = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0x0000000000000000000000000000000000000000000000000000000000000012\"}";
6490    const CALL_DECIMALS_6: &str = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0x0000000000000000000000000000000000000000000000000000000000000006\"}";
6491    const TRANSACTION_COUNT: &str =
6492        include_str!("../../test_data/execution/rpc_eth_get_transaction_count.json");
6493    const TRANSACTION_COUNT_NEXT: &str =
6494        include_str!("../../test_data/execution/rpc_eth_get_transaction_count_next.json");
6495    const ESTIMATE_GAS: &str = include_str!("../../test_data/execution/rpc_eth_estimate_gas.json");
6496    const MAX_PRIORITY_FEE: &str =
6497        include_str!("../../test_data/execution/rpc_eth_max_priority_fee_per_gas.json");
6498    const BLOCK_BY_NUMBER: &str =
6499        include_str!("../../test_data/execution/rpc_eth_get_block_by_number.json");
6500    const BLOCK_CANONICAL: &str =
6501        include_str!("../../test_data/execution/rpc_eth_get_block_canonical.json");
6502    const BLOCK_FINALIZED: &str =
6503        include_str!("../../test_data/execution/rpc_eth_get_block_finalized.json");
6504    const RECEIPT_SUCCESS: &str =
6505        include_str!("../../test_data/execution/rpc_eth_get_transaction_receipt_success.json");
6506    const RECEIPT_REVERTED: &str =
6507        include_str!("../../test_data/execution/rpc_eth_get_transaction_receipt_reverted.json");
6508    const RECEIPT_NULL: &str =
6509        include_str!("../../test_data/execution/rpc_eth_get_transaction_receipt_null.json");
6510    const SEND_RAW_TRANSACTION: &str =
6511        include_str!("../../test_data/execution/rpc_eth_send_raw_transaction.json");
6512    const SEND_RAW_TRANSACTION_REJECTED: &str =
6513        include_str!("../../test_data/execution/rpc_eth_send_raw_transaction_rejected.json");
6514    const SEND_RAW_TRANSACTION_NONCE_TOO_LOW: &str =
6515        include_str!("../../test_data/execution/rpc_eth_send_raw_transaction_nonce_too_low.json");
6516    const RPC_METHOD_NOT_FOUND: &str =
6517        include_str!("../../test_data/execution/rpc_error_method_not_found.json");
6518
6519    const WALLET: &str = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266";
6520    const ROUTER: &str = "0xE592427A0AEce92De3Edee1F18E0157C05861564";
6521    const WETH: &str = "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1";
6522    const USDC: &str = "0xaf88d065e77c8cC2239327C5EDb3A432268e5831";
6523    const TEST_TIMEOUT: Duration = Duration::from_secs(10);
6524
6525    const WETH_ADDRESS: Address = address!("82aF49447D8a07e3bd95BD0d56f35241523fBab1");
6526    const USDC_ADDRESS: Address = address!("af88d065e77c8cC2239327C5EDb3A432268e5831");
6527    const ROUTER_ADDRESS: Address = address!("E592427A0AEce92De3Edee1F18E0157C05861564");
6528
6529    // Anvil development key for WALLET (public, test-only)
6530    const TEST_PRIVATE_KEY: &str =
6531        "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
6532
6533    const BALANCE_OF_SELECTOR: &str = "0x70a08231";
6534    const ALLOWANCE_SELECTOR: &str = "0xdd62ed3e";
6535    const POOL_TOKEN0_SELECTOR: &str = "0x0dfe1681";
6536    const POOL_TOKEN1_SELECTOR: &str = "0xd21220a7";
6537    const POOL_FEE_SELECTOR: &str = "0xddca3f43";
6538    const DECIMALS_SELECTOR: &str = "0x313ce567";
6539    const FACTORY_SELECTOR: &str = "0xc45a0155";
6540    const WETH9_SELECTOR: &str = "0x4aa4a4fc";
6541    const GET_POOL_SELECTOR: &str = "0x1698ee82";
6542    const QUOTE_EXACT_INPUT_SELECTOR: &str = "0xc6a5026a";
6543    const QUOTE_EXACT_OUTPUT_SELECTOR: &str = "0xbd21704a";
6544
6545    fn test_pool() -> Pool {
6546        let chain = Arc::new(chains::ARBITRUM.clone());
6547        let dex = Arc::clone(&UNISWAP_V3.dex);
6548        let weth = Token::new(
6549            Arc::clone(&chain),
6550            address!("82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
6551            "Wrapped Ether".to_string(),
6552            "WETH".to_string(),
6553            18,
6554        );
6555        let usdc = Token::new(
6556            Arc::clone(&chain),
6557            address!("af88d065e77c8cC2239327C5EDb3A432268e5831"),
6558            "USD Coin".to_string(),
6559            "USDC".to_string(),
6560            6,
6561        );
6562
6563        Pool::new(
6564            chain,
6565            dex,
6566            address!("C6962004f452bE9203591991D15f6b388e09E8D0"),
6567            PoolIdentifier::from_address(address!("C6962004f452bE9203591991D15f6b388e09E8D0")),
6568            55_000_000,
6569            weth,
6570            usdc,
6571            Some(500),
6572            Some(10),
6573            UnixNanos::default(),
6574        )
6575    }
6576
6577    fn test_config(http_rpc_url: String) -> BlockchainExecutionClientConfig {
6578        test_config_with_signer_env(http_rpc_url, "BLOCKCHAIN_TEST_PRIVATE_KEY")
6579    }
6580
6581    fn test_config_with_signer_env(
6582        http_rpc_url: String,
6583        signer_env: &str,
6584    ) -> BlockchainExecutionClientConfig {
6585        let verifier_separator = if http_rpc_url.contains('?') { '&' } else { '?' };
6586        let code_hash = keccak256(
6587            hex::decode("6080604052348015600e575f5ffd5b5060").expect("valid test bytecode"),
6588        )
6589        .to_string();
6590        let result = |response: &str| {
6591            serde_json::from_str::<serde_json::Value>(response).unwrap()["result"]
6592                .as_str()
6593                .unwrap()
6594                .to_string()
6595        };
6596        let probe = |call_data: &str, expected_output: String| BlockchainContractProbe {
6597            call_data: call_data.to_string(),
6598            expected_output,
6599        };
6600        let contract = |address: &str, role| {
6601            let probes = match role {
6602                BlockchainContractRole::Router => vec![
6603                    probe(FACTORY_SELECTOR, result(CALL_FACTORY)),
6604                    probe(WETH9_SELECTOR, result(CALL_WETH)),
6605                ],
6606                BlockchainContractRole::Factory => vec![probe(
6607                    &hex::encode_prefixed(
6608                        UniswapV3Factory::getPoolCall {
6609                            tokenA: WETH_ADDRESS,
6610                            tokenB: USDC_ADDRESS,
6611                            fee: U24::try_from(500u32).unwrap(),
6612                        }
6613                        .abi_encode(),
6614                    ),
6615                    result(CALL_POOL),
6616                )],
6617                BlockchainContractRole::WrappedNative => {
6618                    vec![probe(DECIMALS_SELECTOR, result(CALL_DECIMALS_18))]
6619                }
6620                BlockchainContractRole::Quote => {
6621                    vec![probe(FACTORY_SELECTOR, result(CALL_FACTORY))]
6622                }
6623                BlockchainContractRole::Token => {
6624                    vec![probe(DECIMALS_SELECTOR, result(CALL_DECIMALS_6))]
6625                }
6626                BlockchainContractRole::Pool => vec![
6627                    probe(POOL_TOKEN0_SELECTOR, result(CALL_WETH)),
6628                    probe(POOL_TOKEN1_SELECTOR, result(CALL_USDC)),
6629                    probe(POOL_FEE_SELECTOR, result(CALL_FEE_500)),
6630                ],
6631                BlockchainContractRole::Implementation => Vec::new(),
6632            };
6633            BlockchainContractManifest {
6634                address: address.to_string(),
6635                role,
6636                runtime_code_hash: code_hash.clone(),
6637                proxy: None,
6638                probes,
6639            }
6640        };
6641        let deployment_manifest = BlockchainDeploymentManifest {
6642            version: "test-v1".to_string(),
6643            chain_id: chains::ARBITRUM.chain_id,
6644            chain_name: chains::ARBITRUM.name.to_string(),
6645            contracts: vec![
6646                contract(ROUTER, BlockchainContractRole::Router),
6647                contract(
6648                    "0x1F98431c8aD98523631AE4a59f267346ea31F984",
6649                    BlockchainContractRole::Factory,
6650                ),
6651                contract(WETH, BlockchainContractRole::WrappedNative),
6652                contract(
6653                    "0x61fFE014bA17989E743c5F6cB21bF9697530B21e",
6654                    BlockchainContractRole::Quote,
6655                ),
6656                contract(USDC, BlockchainContractRole::Token),
6657                contract(
6658                    "0xC6962004f452bE9203591991D15f6b388e09E8D0",
6659                    BlockchainContractRole::Pool,
6660                ),
6661            ],
6662            tokens: vec![
6663                BlockchainTokenManifest {
6664                    address: WETH.to_string(),
6665                    name: "Wrapped Ether".to_string(),
6666                    symbol: "WETH".to_string(),
6667                    decimals: 18,
6668                    asset_role: "both".to_string(),
6669                },
6670                BlockchainTokenManifest {
6671                    address: USDC.to_string(),
6672                    name: "USD Coin".to_string(),
6673                    symbol: "USDC".to_string(),
6674                    decimals: 6,
6675                    asset_role: "both".to_string(),
6676                },
6677            ],
6678            pools: vec![BlockchainPoolManifest {
6679                address: "0xC6962004f452bE9203591991D15f6b388e09E8D0".to_string(),
6680                token0: WETH.to_string(),
6681                token1: USDC.to_string(),
6682                fee: 500,
6683                factory: "0x1F98431c8aD98523631AE4a59f267346ea31F984".to_string(),
6684                quote_contract: "0x61fFE014bA17989E743c5F6cB21bF9697530B21e".to_string(),
6685            }],
6686            call_edges: ["swap_sell", "swap_buy"]
6687                .into_iter()
6688                .map(|purpose| BlockchainCallEdgeManifest {
6689                    purpose: purpose.to_string(),
6690                    caller: ROUTER.to_string(),
6691                    target: "0xC6962004f452bE9203591991D15f6b388e09E8D0".to_string(),
6692                    call_type: "call".to_string(),
6693                })
6694                .collect(),
6695        };
6696        let manifest_digest =
6697            keccak256(serde_json::to_vec(&deployment_manifest).unwrap()).to_string();
6698        let verification = BlockchainVerificationConfig {
6699            authoritative: BlockchainProviderIdentity {
6700                provider_id: "authoritative".to_string(),
6701                operator_id: "operator-a".to_string(),
6702                failure_domain_ids: vec!["domain-a".to_string()],
6703            },
6704            verifiers: vec![
6705                BlockchainVerificationProviderConfig {
6706                    identity: BlockchainProviderIdentity {
6707                        provider_id: "verifier-a".to_string(),
6708                        operator_id: "operator-b".to_string(),
6709                        failure_domain_ids: vec!["domain-b".to_string()],
6710                    },
6711                    http_rpc_url: format!("{http_rpc_url}{verifier_separator}source=verifier-a")
6712                        .into(),
6713                },
6714                BlockchainVerificationProviderConfig {
6715                    identity: BlockchainProviderIdentity {
6716                        provider_id: "verifier-b".to_string(),
6717                        operator_id: "operator-c".to_string(),
6718                        failure_domain_ids: vec!["domain-c".to_string()],
6719                    },
6720                    http_rpc_url: format!("{http_rpc_url}{verifier_separator}source=verifier-b")
6721                        .into(),
6722                },
6723            ],
6724            chain_anchor: BlockchainChainAnchorConfig {
6725                chain_id: chains::ARBITRUM.chain_id,
6726                chain_name: chains::ARBITRUM.name.to_string(),
6727                checkpoint_height: 30_346_560,
6728                checkpoint_hash:
6729                    "0x1111111111111111111111111111111111111111111111111111111111111111".to_string(),
6730                checkpoint_timestamp: 1_761_888_800,
6731                max_head_skew_blocks: 3,
6732                max_head_age_secs: u64::MAX,
6733                max_future_drift_secs: u64::MAX,
6734            },
6735            manifest_version: "test-v1".to_string(),
6736            manifest_digest,
6737            deployment_manifest,
6738        };
6739        BlockchainExecutionClientConfig::builder()
6740            .client_id(AccountId::from("BLOCKCHAIN-001"))
6741            .chain(chains::ARBITRUM.clone())
6742            .wallet_address(WALLET.to_string())
6743            .http_rpc_url(http_rpc_url.into())
6744            .verification(verification)
6745            .signer_private_key_env(signer_env.to_string())
6746            .router_addresses(vec![ROUTER.to_string()])
6747            .weth_address(WETH.to_string())
6748            .max_fee_per_gas_wei(1_000_000_000)
6749            .base_fee_buffer_bps(2_000)
6750            .gas_limit(1_000_000)
6751            .gas_buffer_bps(2_000)
6752            .allowed_token_pairs(vec![(WETH.to_string(), USDC.to_string())])
6753            .slippage_bps(50)
6754            .max_slippage_bps(200)
6755            .max_order_amount(1_000_000_000_000_000_000)
6756            .deadline_seconds(300)
6757            .max_quote_age_blocks(100)
6758            .receipt_timeout_secs(1)
6759            .build()
6760    }
6761
6762    fn buy_test_config(http_rpc_url: String) -> BlockchainExecutionClientConfig {
6763        let max_amount = expected_buy_amount_in().to_string();
6764        let mut config = test_config(http_rpc_url);
6765        config.allowed_token_pairs = Some(vec![
6766            (WETH.to_string(), USDC.to_string()),
6767            (USDC.to_string(), WETH.to_string()),
6768        ]);
6769        config.quote_spend_limits = Some(vec![quote_spend_limit(USDC, WETH, 6, &max_amount)]);
6770        config
6771    }
6772
6773    fn refresh_test_manifest_digest(config: &mut BlockchainExecutionClientConfig) {
6774        let verification = config.verification.as_mut().unwrap();
6775        verification.manifest_digest =
6776            keccak256(serde_json::to_vec(&verification.deployment_manifest).unwrap()).to_string();
6777    }
6778
6779    fn quote_spend_limit(
6780        token_in: &str,
6781        token_out: &str,
6782        spend_token_decimals: u8,
6783        max_amount: &str,
6784    ) -> QuoteSpendLimit {
6785        QuoteSpendLimit::builder()
6786            .token_in(token_in.to_string())
6787            .token_out(token_out.to_string())
6788            .spend_token(token_in.to_string())
6789            .spend_token_decimals(spend_token_decimals)
6790            .max_amount(max_amount.to_string())
6791            .build()
6792    }
6793
6794    fn test_client_from_config(
6795        config: BlockchainExecutionClientConfig,
6796        pool: Pool,
6797    ) -> BlockchainExecutionClient {
6798        test_client_result(config, pool).unwrap()
6799    }
6800
6801    fn test_client_result(
6802        config: BlockchainExecutionClientConfig,
6803        pool: Pool,
6804    ) -> anyhow::Result<BlockchainExecutionClient> {
6805        test_client_and_cache(config, pool).map(|(client, _)| client)
6806    }
6807
6808    fn test_client_and_cache(
6809        config: BlockchainExecutionClientConfig,
6810        pool: Pool,
6811    ) -> anyhow::Result<(BlockchainExecutionClient, Rc<RefCell<Cache>>)> {
6812        let cache = Rc::new(RefCell::new(Cache::default()));
6813        cache.borrow_mut().add_pool(pool).unwrap();
6814        let core = ExecutionClientCore::new(
6815            TraderId::from("TRADER-001"),
6816            ClientId::from("BLOCKCHAIN-001"),
6817            *BLOCKCHAIN_VENUE,
6818            OmsType::Netting,
6819            AccountId::from("BLOCKCHAIN-001"),
6820            AccountType::Wallet,
6821            None,
6822            Rc::clone(&cache),
6823        );
6824
6825        let client = BlockchainExecutionClient::new(core, config)?;
6826        Ok((client, cache))
6827    }
6828
6829    fn test_client(http_rpc_url: String) -> BlockchainExecutionClient {
6830        test_client_from_config(test_config(http_rpc_url), test_pool())
6831    }
6832
6833    async fn client_with_mock_rpc(
6834        state: MockRpcState,
6835    ) -> (BlockchainExecutionClient, MockRpcState) {
6836        let addr = start_mock_rpc_server(state.clone()).await;
6837        (test_client(format!("http://{addr}")), state)
6838    }
6839
6840    async fn client_with_token_mock_rpc(
6841        state: MockRpcState,
6842        signer_env: &str,
6843    ) -> (BlockchainExecutionClient, MockRpcState, Rc<RefCell<Cache>>) {
6844        let state = with_connect_capabilities(state);
6845        let addr = start_mock_rpc_server(state.clone()).await;
6846        let pool = test_pool();
6847        let tokens = [pool.token0.clone(), pool.token1.clone()];
6848        let mut config = test_config_with_signer_env(format!("http://{addr}"), signer_env);
6849        config.tokens = Some(vec![WETH.to_string(), USDC.to_string()]);
6850        let (mut client, cache) = test_client_and_cache(config, pool).unwrap();
6851        for token in tokens {
6852            client.cache.add_token(token).await.unwrap();
6853        }
6854        (client, state, cache)
6855    }
6856
6857    fn execution_rpc_state() -> MockRpcState {
6858        MockRpcState::default()
6859            .with_receipt_hash_from_request()
6860            .with_response("eth_chainId", CHAIN_ID_ARBITRUM)
6861            .with_response("eth_getCode", GET_CODE_DEPLOYED)
6862            .with_response("eth_getBalance", GET_BALANCE)
6863            .with_response("eth_getBlockByNumber", BLOCK_BY_NUMBER)
6864            .with_response("eth_getTransactionCount", TRANSACTION_COUNT)
6865            .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", BLOCK_CANONICAL)
6866            .with_parameter_response("eth_getBlockByNumber", "finalized", BLOCK_FINALIZED)
6867            .with_parameter_response("eth_getBlockByNumber", "0x1cf0d42", BLOCK_FINALIZED)
6868            .with_response("eth_maxPriorityFeePerGas", MAX_PRIORITY_FEE)
6869            .with_call_response(FACTORY_SELECTOR, CALL_FACTORY)
6870            .with_call_response(WETH9_SELECTOR, CALL_WETH)
6871            .with_call_response(GET_POOL_SELECTOR, CALL_POOL)
6872            .with_call_response(POOL_TOKEN0_SELECTOR, CALL_WETH)
6873            .with_call_response(POOL_TOKEN1_SELECTOR, CALL_USDC)
6874            .with_call_response(POOL_FEE_SELECTOR, CALL_FEE_500)
6875            .with_contract_call_response(WETH, DECIMALS_SELECTOR, CALL_DECIMALS_18)
6876            .with_contract_call_response(USDC, DECIMALS_SELECTOR, CALL_DECIMALS_6)
6877            .with_call_response(
6878                QUOTE_EXACT_INPUT_SELECTOR,
6879                &quote_response(expected_sell_quote_amount()),
6880            )
6881            .with_call_response(
6882                QUOTE_EXACT_OUTPUT_SELECTOR,
6883                &quote_response(expected_buy_amount_in()),
6884            )
6885    }
6886
6887    fn ready_rpc_state() -> MockRpcState {
6888        with_connect_capabilities(execution_rpc_state())
6889            .with_call_response(BALANCE_OF_SELECTOR, CALL_BALANCE)
6890            .with_call_response(ALLOWANCE_SELECTOR, CALL_ALLOWANCE)
6891    }
6892
6893    fn with_connect_capabilities(state: MockRpcState) -> MockRpcState {
6894        state
6895            .with_response("eth_getStorageAt", STORAGE_ZERO)
6896            .with_response("eth_estimateGas", ESTIMATE_GAS)
6897            .with_parameter_response(
6898                "debug_traceTransaction",
6899                &B256::ZERO.to_string(),
6900                TRACE_UNKNOWN_TRANSACTION,
6901            )
6902    }
6903
6904    fn signing_rpc_state() -> MockRpcState {
6905        ready_rpc_state()
6906            .with_response("eth_getTransactionCount", TRANSACTION_COUNT)
6907            .with_response("eth_estimateGas", ESTIMATE_GAS)
6908    }
6909
6910    fn broadcast_rpc_state() -> MockRpcState {
6911        execution_rpc_state()
6912            .with_response("eth_getTransactionCount", TRANSACTION_COUNT)
6913            .with_response("eth_estimateGas", ESTIMATE_GAS)
6914            .with_response("eth_getTransactionReceipt", RECEIPT_SUCCESS)
6915            .with_send_raw_transaction_echo()
6916            .with_call_response_sequence(ALLOWANCE_SELECTOR, &[CALL_ZERO, CALL_ALLOWANCE_1000])
6917    }
6918
6919    async fn expected_wrap_tx_hash(value: U256) -> B256 {
6920        expected_tx_hash(
6921            WETH_ADDRESS,
6922            value,
6923            Bytes::from(nautilus_core::hex::decode("d0e30db0").unwrap()),
6924        )
6925        .await
6926    }
6927
6928    async fn expected_approve_tx_hash(amount: U256) -> B256 {
6929        let calldata = ERC20::approveCall {
6930            spender: ROUTER_ADDRESS,
6931            amount,
6932        }
6933        .abi_encode();
6934        expected_tx_hash(WETH_ADDRESS, U256::ZERO, Bytes::from(calldata)).await
6935    }
6936
6937    async fn expected_tx_hash(to: Address, value: U256, input: Bytes) -> B256 {
6938        // The orchestration's policy math is deterministic: nonce 7 from the fixture,
6939        // buffered gas 78000, buffered max fee 130000000, priority fee 10000000
6940        let expected_tx =
6941            build_eip1559_transaction(42161, 7, 78_000, 130_000_000, 10_000_000, to, value, input);
6942        let (expected_hash, _) = sign_eip1559_transaction(
6943            expected_tx,
6944            &PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
6945        )
6946        .await
6947        .unwrap();
6948        expected_hash
6949    }
6950
6951    async fn await_recorded_requests(state: &MockRpcState, method: &str, expected: usize) {
6952        wait_until_async(
6953            || async {
6954                state
6955                    .recorded_requests()
6956                    .iter()
6957                    .filter(|request| request["method"] == method)
6958                    .count()
6959                    >= expected
6960            },
6961            TEST_TIMEOUT,
6962        )
6963        .await;
6964    }
6965
6966    /// The block number served by the `eth_getBlockByNumber` fixture; swap quotes pin their
6967    /// profiler state to it so the freshness check passes.
6968    const FIXTURE_BLOCK: u64 = 30_346_560;
6969    const FIXTURE_BLOCK_PARAM: &str = "0x1cf0d40";
6970    const FIXTURE_BLOCK_HASH: &str =
6971        "0x1111111111111111111111111111111111111111111111111111111111111111";
6972    /// The timestamp served by the `eth_getBlockByNumber` fixture.
6973    const FIXTURE_BLOCK_TIMESTAMP: u64 = 1_761_888_800;
6974    /// Synthetic full-range liquidity for the test pool profiler.
6975    const TEST_LIQUIDITY: u128 = 1_000_000_000_000_000_000_000;
6976
6977    fn test_profiler(pool: &Pool, block_number: u64) -> PoolProfiler {
6978        test_profiler_at_block(pool, block_number, FIXTURE_BLOCK_HASH)
6979    }
6980
6981    fn test_profiler_at_block(pool: &Pool, block_number: u64, block_hash: &str) -> PoolProfiler {
6982        test_profiler_with_range(
6983            pool,
6984            block_number,
6985            block_hash,
6986            U160::from(1u128 << 96),
6987            -887_220,
6988            887_220,
6989            TEST_LIQUIDITY,
6990        )
6991    }
6992
6993    fn test_profiler_with_state(
6994        pool: &Pool,
6995        block_number: u64,
6996        sqrt_price_x96: U160,
6997        liquidity: u128,
6998    ) -> PoolProfiler {
6999        test_profiler_with_range(
7000            pool,
7001            block_number,
7002            FIXTURE_BLOCK_HASH,
7003            sqrt_price_x96,
7004            -887_220,
7005            887_220,
7006            liquidity,
7007        )
7008    }
7009
7010    /// Builds a profiler from a synthetic snapshot with a single position over
7011    /// `[tick_lower, tick_upper]`: exact-input quotes within the range match the
7012    /// constant-product curve, and a tiny test swap never crosses a tick.
7013    fn test_profiler_with_range(
7014        pool: &Pool,
7015        block_number: u64,
7016        block_hash: &str,
7017        sqrt_price_x96: U160,
7018        tick_lower: i32,
7019        tick_upper: i32,
7020        liquidity: u128,
7021    ) -> PoolProfiler {
7022        let snapshot = PoolSnapshot::new(
7023            pool.instrument_id,
7024            PoolState {
7025                current_tick: get_tick_at_sqrt_ratio(sqrt_price_x96),
7026                price_sqrt_ratio_x96: sqrt_price_x96,
7027                liquidity,
7028                protocol_fees_token0: U256::ZERO,
7029                protocol_fees_token1: U256::ZERO,
7030                fee_protocol: 0,
7031                fee_protocol0_basis_points: None,
7032                fee_protocol1_basis_points: None,
7033                fee_growth_global_0: U256::ZERO,
7034                fee_growth_global_1: U256::ZERO,
7035            },
7036            vec![PoolPosition::new(
7037                address!("DeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF"),
7038                tick_lower,
7039                tick_upper,
7040                liquidity as i128,
7041            )],
7042            vec![
7043                PoolTick::new(
7044                    tick_lower,
7045                    liquidity,
7046                    liquidity as i128,
7047                    U256::ZERO,
7048                    U256::ZERO,
7049                    true,
7050                    0,
7051                ),
7052                PoolTick::new(
7053                    tick_upper,
7054                    liquidity,
7055                    -(liquidity as i128),
7056                    U256::ZERO,
7057                    U256::ZERO,
7058                    true,
7059                    0,
7060                ),
7061            ],
7062            PoolAnalytics::default(),
7063            BlockPosition::new(
7064                block_number,
7065                block_hash.to_string(),
7066                BLOCK_SCOPED_SNAPSHOT_INDEX,
7067                BLOCK_SCOPED_SNAPSHOT_INDEX,
7068            )
7069            .with_block_hash(Some(block_hash.to_string())),
7070            UnixNanos::default(),
7071            UnixNanos::default(),
7072        );
7073        let mut profiler = PoolProfiler::new(Arc::new(pool.clone()));
7074        profiler.restore_from_snapshot(snapshot).unwrap();
7075        profiler
7076    }
7077
7078    fn test_market_sell_order(instrument_id: InstrumentId) -> OrderAny {
7079        market_sell_order_with_id(instrument_id, "O-SWAP-001")
7080    }
7081
7082    fn test_market_buy_order(instrument_id: InstrumentId) -> OrderAny {
7083        market_buy_order_with_id(instrument_id, "O-SWAP-BUY-001")
7084    }
7085
7086    fn submit_order_cmd(order: &OrderAny) -> SubmitOrder {
7087        SubmitOrder::new(
7088            TraderId::from("TRADER-001"),
7089            Some(ClientId::from("BLOCKCHAIN-001")),
7090            order.strategy_id(),
7091            order.instrument_id(),
7092            order.client_order_id(),
7093            order.init_event().clone(),
7094            None,
7095            None,
7096            None,
7097            UUID4::new(),
7098            UnixNanos::default(),
7099            None,
7100        )
7101    }
7102
7103    /// Builds a client whose shared cache holds the test pool, a live profiler at
7104    /// `FIXTURE_BLOCK`, and a SELL market order, without a durable store.
7105    fn swap_client_with_cache(
7106        config: BlockchainExecutionClientConfig,
7107    ) -> (BlockchainExecutionClient, Rc<RefCell<Cache>>) {
7108        let cache = Rc::new(RefCell::new(Cache::default()));
7109        let pool = test_pool();
7110        cache.borrow_mut().add_pool(pool.clone()).unwrap();
7111        cache
7112            .borrow_mut()
7113            .add_order(
7114                test_market_sell_order(pool.instrument_id),
7115                None,
7116                None,
7117                false,
7118            )
7119            .unwrap();
7120        cache
7121            .borrow_mut()
7122            .add_pool_profiler(test_profiler_at_block(
7123                &pool,
7124                FIXTURE_BLOCK,
7125                FIXTURE_BLOCK_HASH,
7126            ))
7127            .unwrap();
7128        let core = ExecutionClientCore::new(
7129            TraderId::from("TRADER-001"),
7130            ClientId::from("BLOCKCHAIN-001"),
7131            *BLOCKCHAIN_VENUE,
7132            OmsType::Netting,
7133            AccountId::from("BLOCKCHAIN-001"),
7134            AccountType::Wallet,
7135            None,
7136            Rc::clone(&cache),
7137        );
7138
7139        let client = BlockchainExecutionClient::new(core, config).unwrap();
7140        (client, cache)
7141    }
7142
7143    /// Builds a connected, signer-equipped swap client backed by a fresh Postgres schema.
7144    async fn swap_client_with_database(
7145        test_name: &str,
7146        state: MockRpcState,
7147    ) -> Option<(
7148        sqlx::PgPool,
7149        String,
7150        BlockchainExecutionClient,
7151        MockRpcState,
7152        Rc<RefCell<Cache>>,
7153    )> {
7154        swap_client_with_database_config(test_name, state, test_config).await
7155    }
7156
7157    async fn swap_client_with_buy_database(
7158        test_name: &str,
7159        state: MockRpcState,
7160    ) -> Option<(
7161        sqlx::PgPool,
7162        String,
7163        BlockchainExecutionClient,
7164        MockRpcState,
7165        Rc<RefCell<Cache>>,
7166    )> {
7167        swap_client_with_database_config(test_name, state, buy_test_config).await
7168    }
7169
7170    async fn swap_client_with_database_config<F>(
7171        test_name: &str,
7172        state: MockRpcState,
7173        config: F,
7174    ) -> Option<(
7175        sqlx::PgPool,
7176        String,
7177        BlockchainExecutionClient,
7178        MockRpcState,
7179        Rc<RefCell<Cache>>,
7180    )>
7181    where
7182        F: FnOnce(String) -> BlockchainExecutionClientConfig,
7183    {
7184        let (admin_pool, pg_config) = connect_test_postgres(test_name).await?;
7185        let schema = format!("{test_name}_{}", std::process::id());
7186        setup_execution_schema(&admin_pool, &schema).await;
7187
7188        let db_options: sqlx::postgres::PgConnectOptions = pg_config.into();
7189        let db_options = db_options.options([("search_path", schema.clone())]);
7190        let database = connect_test_database(db_options).await.unwrap();
7191        let addr = start_mock_rpc_server(state.clone()).await;
7192        let (mut client, cache) = swap_client_with_cache(config(format!("http://{addr}")));
7193        client.cache.database = Some(database);
7194        // Mirror the connect-time migration: tests create the pre-submission table shape
7195        client
7196            .cache
7197            .ensure_execution_transaction_schema()
7198            .await
7199            .unwrap();
7200        protect_test_storage(&mut client, &schema).await;
7201        initialize_test_verification_ledger(&client).await;
7202        client.signer = Some(Arc::new(
7203            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
7204        ));
7205        client.core.set_connected();
7206
7207        Some((admin_pool, schema, client, state, cache))
7208    }
7209
7210    async fn initialize_test_verification_ledger(client: &BlockchainExecutionClient) {
7211        initialize_test_verification_ledger_with_headers(
7212            client,
7213            &[ExecutionVerifiedHeader {
7214                number: FIXTURE_BLOCK,
7215                hash: FIXTURE_BLOCK_HASH.to_string(),
7216                parent_hash: "0x0000000000000000000000000000000000000000000000000000000000000001"
7217                    .to_string(),
7218                timestamp: FIXTURE_BLOCK_TIMESTAMP,
7219                base_fee_per_gas: Some(100_000_000),
7220            }],
7221        )
7222        .await;
7223    }
7224
7225    async fn initialize_test_verification_ledger_with_headers(
7226        client: &BlockchainExecutionClient,
7227        finalized_headers: &[ExecutionVerifiedHeader],
7228    ) {
7229        ensure_test_verification_ledger(client, finalized_headers, 7, 7)
7230            .await
7231            .unwrap();
7232    }
7233
7234    async fn ensure_test_verification_ledger(
7235        client: &BlockchainExecutionClient,
7236        finalized_headers: &[ExecutionVerifiedHeader],
7237        next_canonical_nonce: u64,
7238        observed_canonical_nonce: u64,
7239    ) -> anyhow::Result<()> {
7240        let verification = client.config.verification.as_ref().unwrap();
7241        let provider_ids = vec![
7242            "authoritative".to_string(),
7243            "verifier-a".to_string(),
7244            "verifier-b".to_string(),
7245        ];
7246        let operator_ids = vec![
7247            "operator-a".to_string(),
7248            "operator-b".to_string(),
7249            "operator-c".to_string(),
7250        ];
7251        let failure_domain_ids = vec![
7252            "domain-a".to_string(),
7253            "domain-b".to_string(),
7254            "domain-c".to_string(),
7255        ];
7256        let decisions = [ExecutionVerificationDecision {
7257            read_class: "numbered_block",
7258            height_start: Some(FIXTURE_BLOCK),
7259            height_end: Some(FIXTURE_BLOCK),
7260            normalized_value_digest: B256::ZERO.to_string(),
7261        }];
7262        client
7263            .cache
7264            .database
7265            .as_ref()
7266            .unwrap()
7267            .ensure_execution_verification_schema(&ExecutionVerificationBootstrap {
7268                chain_id: 42_161,
7269                wallet_address: WALLET,
7270                manifest_version: &verification.manifest_version,
7271                manifest_digest: &verification.manifest_digest,
7272                checkpoint_number: FIXTURE_BLOCK,
7273                checkpoint_hash: FIXTURE_BLOCK_HASH,
7274                checkpoint_parent_hash:
7275                    "0x0000000000000000000000000000000000000000000000000000000000000001",
7276                checkpoint_timestamp: FIXTURE_BLOCK_TIMESTAMP,
7277                checkpoint_base_fee_per_gas: Some(100_000_000),
7278                finalized_headers,
7279                next_canonical_nonce,
7280                observed_canonical_nonce,
7281                provider_ids: &provider_ids,
7282                operator_ids: &operator_ids,
7283                failure_domain_ids: &failure_domain_ids,
7284                decisions: &decisions,
7285                migration: None,
7286            })
7287            .await
7288    }
7289
7290    async fn initialize_test_verification_migration(
7291        client: &BlockchainExecutionClient,
7292        finalized_headers: &[ExecutionVerifiedHeader],
7293        next_canonical_nonce: u64,
7294        decisions: &[ExecutionVerificationDecision],
7295        migration: &ExecutionVerificationMigration,
7296    ) {
7297        let verification = client.config.verification.as_ref().unwrap();
7298        let provider_ids = vec![
7299            "authoritative".to_string(),
7300            "verifier-a".to_string(),
7301            "verifier-b".to_string(),
7302        ];
7303        let operator_ids = vec![
7304            "operator-a".to_string(),
7305            "operator-b".to_string(),
7306            "operator-c".to_string(),
7307        ];
7308        let failure_domain_ids = vec![
7309            "domain-a".to_string(),
7310            "domain-b".to_string(),
7311            "domain-c".to_string(),
7312        ];
7313        client
7314            .cache
7315            .database
7316            .as_ref()
7317            .unwrap()
7318            .ensure_execution_verification_schema(&ExecutionVerificationBootstrap {
7319                chain_id: 42_161,
7320                wallet_address: WALLET,
7321                manifest_version: &verification.manifest_version,
7322                manifest_digest: &verification.manifest_digest,
7323                checkpoint_number: FIXTURE_BLOCK,
7324                checkpoint_hash: FIXTURE_BLOCK_HASH,
7325                checkpoint_parent_hash:
7326                    "0x0000000000000000000000000000000000000000000000000000000000000001",
7327                checkpoint_timestamp: FIXTURE_BLOCK_TIMESTAMP,
7328                checkpoint_base_fee_per_gas: Some(100_000_000),
7329                finalized_headers,
7330                next_canonical_nonce,
7331                observed_canonical_nonce: next_canonical_nonce,
7332                provider_ids: &provider_ids,
7333                operator_ids: &operator_ids,
7334                failure_domain_ids: &failure_domain_ids,
7335                decisions,
7336                migration: Some(migration),
7337            })
7338            .await
7339            .unwrap();
7340    }
7341
7342    async fn swap_rpc_state() -> MockRpcState {
7343        let min_amount_out = expected_min_amount_out(50);
7344        swap_rpc_state_with_min_amount_out(min_amount_out).await
7345    }
7346
7347    async fn swap_rpc_state_with_min_amount_out(min_amount_out: U256) -> MockRpcState {
7348        let (tx_hash, _) = expected_swap_tx(min_amount_out).await;
7349        finalized_swap_rpc_state(tx_hash, min_amount_out)
7350    }
7351
7352    async fn swap_rpc_state_for_mismatch() -> MockRpcState {
7353        swap_rpc_state()
7354            .await
7355            .with_response("eth_sendRawTransaction", SEND_RAW_TRANSACTION)
7356    }
7357
7358    fn awaiting_in_flight(client: &BlockchainExecutionClient) -> InFlightTransaction {
7359        let slot = *client.in_flight.lock();
7360        let Some(InFlightSlot::AwaitingFinality(in_flight)) = slot else {
7361            panic!("expected an awaiting-finality transaction, was {slot:?}");
7362        };
7363        in_flight
7364    }
7365
7366    fn recovering_in_flight(client: &BlockchainExecutionClient) -> RecoveryTransaction {
7367        let slot = *client.in_flight.lock();
7368        let Some(InFlightSlot::Recovering(recovery)) = slot else {
7369            panic!("expected a recovery transaction, was {slot:?}");
7370        };
7371        recovery
7372    }
7373
7374    async fn execution_intent_markers(
7375        admin_pool: &sqlx::PgPool,
7376        schema: &str,
7377    ) -> Vec<(String, String, bool, bool)> {
7378        sqlx::query_as(sqlx::AssertSqlSafe(format!(
7379            "SELECT purpose, status, terminal_emitted, active \
7380             FROM {schema}.execution_intent ORDER BY id"
7381        )))
7382        .fetch_all(admin_pool)
7383        .await
7384        .unwrap()
7385    }
7386
7387    #[allow(unsafe_code)] // env-var mutation in tests; unique names avoid cross-test races
7388    fn payload_test_keys(
7389        active: [u8; 32],
7390        retired: Vec<[u8; 32]>,
7391        deployment_id: &str,
7392    ) -> PayloadKeySet {
7393        static NEXT_ENV_ID: AtomicU64 = AtomicU64::new(0);
7394
7395        let env_id = NEXT_ENV_ID.fetch_add(1, Ordering::Relaxed);
7396        let active_env = format!("BLOCKCHAIN_TEST_PAYLOAD_KEY_{env_id}_ACTIVE");
7397        let retired_envs = retired
7398            .iter()
7399            .enumerate()
7400            .map(|(index, _)| format!("BLOCKCHAIN_TEST_PAYLOAD_KEY_{env_id}_RETIRED_{index}"))
7401            .collect::<Vec<_>>();
7402        // SAFETY: each invocation uses unique variable names and removes them before returning
7403        unsafe { std::env::set_var(&active_env, hex::encode(active)) };
7404
7405        for (env, key) in retired_envs.iter().zip(retired) {
7406            // SAFETY: the retired variable name is unique to this invocation
7407            unsafe { std::env::set_var(env, hex::encode(key)) };
7408        }
7409
7410        let keys = PayloadKeySet::load(Some(&active_env), &retired_envs, Some(deployment_id))
7411            .unwrap()
7412            .unwrap();
7413
7414        // SAFETY: no other test or thread uses these unique variable names
7415        unsafe { std::env::remove_var(active_env) };
7416        for env in retired_envs {
7417            // SAFETY: the retired variable name is unique to this invocation
7418            unsafe { std::env::remove_var(env) };
7419        }
7420        keys
7421    }
7422
7423    #[allow(unsafe_code)] // env-var mutation in tests; unique names avoid cross-test races
7424    fn set_test_payload_key(
7425        client: &mut BlockchainExecutionClient,
7426        key: [u8; 32],
7427        deployment_id: &str,
7428    ) -> PayloadKeySet {
7429        static NEXT_ENV_ID: AtomicU64 = AtomicU64::new(0);
7430
7431        let env_id = NEXT_ENV_ID.fetch_add(1, Ordering::Relaxed);
7432        let active_env = format!("BLOCKCHAIN_TEST_EXECUTION_KEY_{env_id}");
7433        // SAFETY: each invocation uses a unique variable name retained for reconnect tests
7434        unsafe { std::env::set_var(&active_env, hex::encode(key)) };
7435        client.config.payload_key_env = Some(active_env);
7436        client.config.payload_deployment_id = Some(deployment_id.to_string());
7437        client.load_payload_keys().unwrap().unwrap()
7438    }
7439
7440    async fn protect_test_storage(client: &mut BlockchainExecutionClient, deployment_id: &str) {
7441        let keys = set_test_payload_key(client, [0xa5; 32], deployment_id);
7442        client
7443            .cache
7444            .database
7445            .as_ref()
7446            .unwrap()
7447            .ensure_execution_payload_storage(&keys)
7448            .await
7449            .unwrap();
7450        client.payload_keys = Some(Arc::new(keys));
7451    }
7452
7453    async fn reserve_test_wrap_intent(database: &BlockchainCacheDatabase) -> ExecutionIntentRow {
7454        reserve_test_wrap_intent_for_wallet(database, WALLET).await
7455    }
7456
7457    async fn reserve_test_wrap_intent_for_wallet(
7458        database: &BlockchainCacheDatabase,
7459        wallet: &str,
7460    ) -> ExecutionIntentRow {
7461        database
7462            .reserve_execution_intent(&ExecutionIntentInsert {
7463                chain_id: 42161,
7464                wallet_address: wallet.to_string(),
7465                purpose: "wrap".to_string(),
7466                client_order_id: None,
7467                trader_id: None,
7468                strategy_id: None,
7469                account_id: None,
7470                instrument_id: None,
7471                pool_address: None,
7472                transaction_to: WETH_ADDRESS.to_string(),
7473                transaction_input: "0xd0e30db0".to_string(),
7474                transaction_value: "1".to_string(),
7475                amount_in: None,
7476                created_block: FIXTURE_BLOCK,
7477            })
7478            .await
7479            .unwrap()
7480    }
7481
7482    async fn persist_test_wrap_broadcast(
7483        database: &BlockchainCacheDatabase,
7484        keys: Option<&PayloadKeySet>,
7485    ) -> (ExecutionIntentRow, B256, Vec<u8>) {
7486        let intent = reserve_test_wrap_intent(database).await;
7487        database
7488            .assign_execution_intent_nonce(intent.id, 7)
7489            .await
7490            .unwrap();
7491        let intent = database.get_execution_intent(intent.id).await.unwrap();
7492        let transaction = build_eip1559_transaction(
7493            42161,
7494            7,
7495            78_000,
7496            130_000_000,
7497            10_000_000,
7498            WETH_ADDRESS,
7499            U256::from(1u64),
7500            Bytes::from(nautilus_core::hex::decode("d0e30db0").unwrap()),
7501        );
7502        let (tx_hash, raw_tx) = sign_eip1559_transaction(
7503            transaction,
7504            &PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
7505        )
7506        .await
7507        .unwrap();
7508        persist_test_payload(database, keys, &intent, tx_hash, &raw_tx).await;
7509        database
7510            .record_execution_status(
7511                intent.id,
7512                &tx_hash.to_string(),
7513                TransactionStatus::Broadcast,
7514                None,
7515                None,
7516                None,
7517                None,
7518                None,
7519            )
7520            .await
7521            .unwrap();
7522        (intent, tx_hash, raw_tx)
7523    }
7524
7525    async fn reserve_test_swap_intent(database: &BlockchainCacheDatabase) -> ExecutionIntentRow {
7526        let pool = test_pool();
7527        let order = test_market_sell_order(pool.instrument_id);
7528        let calldata = expected_swap_calldata(expected_min_amount_out(50));
7529
7530        database
7531            .reserve_execution_intent(&ExecutionIntentInsert {
7532                chain_id: 42161,
7533                wallet_address: WALLET.to_string(),
7534                purpose: "swap".to_string(),
7535                client_order_id: Some(order.client_order_id().to_string()),
7536                trader_id: Some(order.trader_id().to_string()),
7537                strategy_id: Some(order.strategy_id().to_string()),
7538                account_id: Some("BLOCKCHAIN-001".to_string()),
7539                instrument_id: Some(pool.instrument_id.to_string()),
7540                pool_address: Some(pool.address.to_string()),
7541                transaction_to: ROUTER_ADDRESS.to_string(),
7542                transaction_input: hex::encode_prefixed(&calldata),
7543                transaction_value: U256::ZERO.to_string(),
7544                amount_in: Some("1000000000000000".to_string()),
7545                created_block: FIXTURE_BLOCK,
7546            })
7547            .await
7548            .unwrap()
7549    }
7550
7551    async fn persist_invalid_test_swap(
7552        database: &BlockchainCacheDatabase,
7553        keys: Option<&PayloadKeySet>,
7554    ) -> (ExecutionIntentRow, B256) {
7555        let intent = reserve_test_swap_intent(database).await;
7556        database
7557            .assign_execution_intent_nonce(intent.id, 7)
7558            .await
7559            .unwrap();
7560        let intent = database.get_execution_intent(intent.id).await.unwrap();
7561        let (tx_hash, raw_transaction) = expected_swap_tx(expected_min_amount_out(50)).await;
7562        let mut raw_transaction = hex::decode(raw_transaction.strip_prefix("0x").unwrap()).unwrap();
7563        raw_transaction.push(0xff);
7564        persist_test_payload(database, keys, &intent, tx_hash, &raw_transaction).await;
7565        database
7566            .record_execution_status(
7567                intent.id,
7568                &tx_hash.to_string(),
7569                TransactionStatus::Broadcast,
7570                None,
7571                None,
7572                None,
7573                None,
7574                None,
7575            )
7576            .await
7577            .unwrap();
7578
7579        (intent, tx_hash)
7580    }
7581
7582    async fn persist_test_swap_broadcast(
7583        database: &BlockchainCacheDatabase,
7584        keys: Option<&PayloadKeySet>,
7585    ) -> (ExecutionIntentRow, B256, Vec<u8>) {
7586        let intent = reserve_test_swap_intent(database).await;
7587        database
7588            .assign_execution_intent_nonce(intent.id, 7)
7589            .await
7590            .unwrap();
7591        let intent = database.get_execution_intent(intent.id).await.unwrap();
7592        let (tx_hash, raw_transaction) = expected_swap_tx(expected_min_amount_out(50)).await;
7593        let raw_transaction = hex::decode(raw_transaction.strip_prefix("0x").unwrap()).unwrap();
7594        persist_test_payload(database, keys, &intent, tx_hash, &raw_transaction).await;
7595        database
7596            .record_execution_status(
7597                intent.id,
7598                &tx_hash.to_string(),
7599                TransactionStatus::Broadcast,
7600                None,
7601                None,
7602                None,
7603                None,
7604                None,
7605            )
7606            .await
7607            .unwrap();
7608        (intent, tx_hash, raw_transaction)
7609    }
7610
7611    async fn persist_test_payload(
7612        database: &BlockchainCacheDatabase,
7613        keys: Option<&PayloadKeySet>,
7614        intent: &ExecutionIntentRow,
7615        tx_hash: B256,
7616        raw_transaction: &[u8],
7617    ) {
7618        if let Some(keys) = keys {
7619            database.reserve_execution_payload_seal(keys).await.unwrap();
7620            let transaction_hash = tx_hash.to_string();
7621            let context =
7622                payload_context_identity(intent, &transaction_hash, 42_161, keys.deployment_id())
7623                    .unwrap();
7624            let envelope = keys.seal(raw_transaction, &context).unwrap();
7625            database
7626                .add_execution_transaction_envelope(intent.id, 42_161, &transaction_hash, &envelope)
7627                .await
7628                .unwrap();
7629        } else {
7630            database
7631                .add_execution_transaction_hash(
7632                    intent.id,
7633                    42_161,
7634                    &tx_hash.to_string(),
7635                    raw_transaction,
7636                )
7637                .await
7638                .unwrap();
7639        }
7640    }
7641
7642    async fn later_reconnect(
7643        previous: BlockchainExecutionClient,
7644        http_rpc_url: String,
7645    ) -> anyhow::Error {
7646        let database = previous.cache.database.as_ref().unwrap().clone();
7647        let payload_keys = previous.payload_keys.clone();
7648        drop(previous);
7649        let mut next = test_client(http_rpc_url);
7650        next.cache.database = Some(database);
7651        next.payload_keys = payload_keys;
7652        next.signer = Some(Arc::new(
7653            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
7654        ));
7655        next.reconcile_unresolved_execution().await.unwrap_err()
7656    }
7657
7658    fn start_with_events(
7659        client: &mut BlockchainExecutionClient,
7660    ) -> tokio::sync::mpsc::UnboundedReceiver<ExecutionEvent> {
7661        let (sender, receiver) = tokio::sync::mpsc::unbounded_channel();
7662        replace_exec_event_sender(sender);
7663        client.start().unwrap();
7664        receiver
7665    }
7666
7667    async fn await_pending_tasks(client: &BlockchainExecutionClient) {
7668        tokio::time::timeout(TEST_TIMEOUT, async {
7669            while !client.pending_tasks.all_finished() {
7670                tokio::time::sleep(Duration::from_millis(1)).await;
7671            }
7672        })
7673        .await
7674        .unwrap();
7675    }
7676
7677    fn collect_order_events(
7678        receiver: &mut tokio::sync::mpsc::UnboundedReceiver<ExecutionEvent>,
7679    ) -> Vec<OrderEventAny> {
7680        let mut events = Vec::new();
7681
7682        while let Ok(event) = receiver.try_recv() {
7683            if let ExecutionEvent::Order(order_event) = event {
7684                events.push(order_event);
7685            }
7686        }
7687        events
7688    }
7689
7690    fn assert_swap_quarantined_without_terminal_event(events: &[OrderEventAny]) {
7691        assert_eq!(events.len(), 1, "was: {events:?}");
7692        assert!(matches!(&events[0], OrderEventAny::Submitted(_)));
7693    }
7694
7695    fn assert_swap_submitted_and_filled(events: &[OrderEventAny]) {
7696        assert_eq!(events.len(), 2, "was: {events:?}");
7697        assert!(matches!(&events[0], OrderEventAny::Submitted(_)));
7698        assert!(matches!(&events[1], OrderEventAny::Filled(_)));
7699    }
7700
7701    fn expected_swap_calldata(min_amount_out: U256) -> Vec<u8> {
7702        UniswapV3SwapRouter::exactInputSingleCall {
7703            params: UniswapV3SwapRouter::ExactInputSingleParams {
7704                tokenIn: WETH_ADDRESS,
7705                tokenOut: address!("af88d065e77c8cC2239327C5EDb3A432268e5831"),
7706                fee: U24::try_from(500u32).unwrap(),
7707                recipient: address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266"),
7708                deadline: U256::from(FIXTURE_BLOCK_TIMESTAMP + 300),
7709                amountIn: U256::from(1_000_000_000_000_000u64),
7710                amountOutMinimum: min_amount_out,
7711                sqrtPriceLimitX96: U160::ZERO,
7712            },
7713        }
7714        .abi_encode()
7715    }
7716
7717    fn expected_min_amount_out(slippage_bps: u32) -> U256 {
7718        expected_min_amount_out_for(&test_pool(), true, slippage_bps)
7719    }
7720
7721    fn expected_min_amount_out_for(pool: &Pool, zero_for_one: bool, slippage_bps: u32) -> U256 {
7722        let profiler = test_profiler(pool, FIXTURE_BLOCK);
7723        let quote = profiler
7724            .swap_exact_in(U256::from(1_000_000_000_000_000u64), zero_for_one, None)
7725            .unwrap();
7726        let quoted = exact_output_amount(&quote, zero_for_one).unwrap();
7727        derive_min_amount_out(quoted, slippage_bps).unwrap()
7728    }
7729
7730    fn expected_sell_quote_amount() -> U256 {
7731        let profiler = test_profiler(&test_pool(), FIXTURE_BLOCK);
7732        let quote = profiler
7733            .swap_exact_in(U256::from(1_000_000_000_000_000u64), true, None)
7734            .unwrap();
7735        exact_output_amount(&quote, true).unwrap()
7736    }
7737
7738    fn quote_response(amount: U256) -> String {
7739        let result = (amount, U160::from(1u128 << 96), 0u32, U256::from(50_000u64)).abi_encode();
7740        serde_json::json!({
7741            "jsonrpc": "2.0",
7742            "id": 1,
7743            "result": hex::encode_prefixed(result),
7744        })
7745        .to_string()
7746    }
7747
7748    async fn expected_swap_tx(min_amount_out: U256) -> (B256, String) {
7749        // The orchestration's policy math is deterministic: nonce 7 from the fixture,
7750        // buffered gas 78000, buffered max fee 130000000, priority fee 10000000
7751        let expected_tx = build_eip1559_transaction(
7752            42161,
7753            7,
7754            78_000,
7755            130_000_000,
7756            10_000_000,
7757            ROUTER_ADDRESS,
7758            U256::ZERO,
7759            Bytes::from(expected_swap_calldata(min_amount_out)),
7760        );
7761        sign_eip1559_transaction(
7762            expected_tx,
7763            &PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
7764        )
7765        .await
7766        .map(|(hash, raw)| (hash, nautilus_core::hex::encode_prefixed(&raw)))
7767        .unwrap()
7768    }
7769
7770    fn expected_buy_base_amount() -> U256 {
7771        U256::from(1_000_000_000_000_000u64)
7772    }
7773
7774    fn expected_buy_amount_in() -> U256 {
7775        let profiler = test_profiler(&test_pool(), FIXTURE_BLOCK);
7776        profiler
7777            .swap_exact_out(expected_buy_base_amount(), false, None)
7778            .unwrap()
7779            .get_input_amount()
7780    }
7781
7782    fn expected_buy_min_amount_out(slippage_bps: u32) -> U256 {
7783        derive_min_amount_out(expected_buy_base_amount(), slippage_bps).unwrap()
7784    }
7785
7786    fn expected_buy_swap_calldata(min_amount_out: U256, amount_in: U256) -> Vec<u8> {
7787        UniswapV3SwapRouter::exactInputSingleCall {
7788            params: UniswapV3SwapRouter::ExactInputSingleParams {
7789                tokenIn: USDC_ADDRESS,
7790                tokenOut: WETH_ADDRESS,
7791                fee: U24::try_from(500u32).unwrap(),
7792                recipient: address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266"),
7793                deadline: U256::from(FIXTURE_BLOCK_TIMESTAMP + 300),
7794                amountIn: amount_in,
7795                amountOutMinimum: min_amount_out,
7796                sqrtPriceLimitX96: U160::ZERO,
7797            },
7798        }
7799        .abi_encode()
7800    }
7801
7802    async fn expected_buy_swap_tx(min_amount_out: U256, amount_in: U256) -> (B256, String) {
7803        let expected_tx = build_eip1559_transaction(
7804            42161,
7805            7,
7806            78_000,
7807            130_000_000,
7808            10_000_000,
7809            ROUTER_ADDRESS,
7810            U256::ZERO,
7811            Bytes::from(expected_buy_swap_calldata(min_amount_out, amount_in)),
7812        );
7813        sign_eip1559_transaction(
7814            expected_tx,
7815            &PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
7816        )
7817        .await
7818        .map(|(hash, raw)| (hash, nautilus_core::hex::encode_prefixed(&raw)))
7819        .unwrap()
7820    }
7821
7822    fn finalized_buy_swap_receipt(tx_hash: B256, amount_in: U256) -> String {
7823        finalized_buy_swap_receipt_with_base_out(tx_hash, amount_in, expected_buy_base_amount())
7824    }
7825
7826    fn finalized_buy_swap_receipt_with_base_out(
7827        tx_hash: B256,
7828        amount_in: U256,
7829        base_out: U256,
7830    ) -> String {
7831        let data = (
7832            -I256::try_from(u128::try_from(base_out).unwrap()).unwrap(),
7833            I256::try_from(u128::try_from(amount_in).unwrap()).unwrap(),
7834            U160::from(1_u128 << 96),
7835            TEST_LIQUIDITY,
7836            I24::try_from(0).unwrap(),
7837        )
7838            .abi_encode();
7839        serde_json::json!({
7840            "jsonrpc": "2.0",
7841            "id": 1,
7842            "result": {
7843                "transactionHash": tx_hash.to_string(),
7844                "blockHash": "0x2222222222222222222222222222222222222222222222222222222222222222",
7845                "blockNumber": "0x1cf0d41",
7846                "transactionIndex": "0x2",
7847                "gasUsed": "0xc3c0",
7848                "effectiveGasPrice": "0x5f5e100",
7849                "status": "0x1",
7850                "logs": [{
7851                    "removed": false,
7852                    "logIndex": "0x6",
7853                    "transactionIndex": "0x2",
7854                    "transactionHash": tx_hash.to_string(),
7855                    "blockHash": "0x2222222222222222222222222222222222222222222222222222222222222222",
7856                    "blockNumber": "0x1cf0d41",
7857                    "address": test_pool().address.to_string(),
7858                    "data": hex::encode_prefixed(data),
7859                    "topics": [
7860                        "0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67",
7861                        "0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266",
7862                        "0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266"
7863                    ]
7864                }]
7865            }
7866        })
7867        .to_string()
7868    }
7869
7870    fn finalized_buy_swap_block(tx_hash: B256, min_amount_out: U256, amount_in: U256) -> String {
7871        serde_json::json!({
7872            "jsonrpc": "2.0",
7873            "id": 1,
7874            "result": {
7875                "number": "0x1cf0d41",
7876                "hash": "0x2222222222222222222222222222222222222222222222222222222222222222",
7877                "parentHash": FIXTURE_BLOCK_HASH,
7878                "timestamp": "0x69044a21",
7879                "baseFeePerGas": "0x5f5e100",
7880                "transactions": [{
7881                    "hash": tx_hash.to_string(),
7882                    "from": WALLET,
7883                    "nonce": "0x7",
7884                    "chainId": "0xa4b1",
7885                    "type": "0x2",
7886                    "to": ROUTER,
7887                    "input": hex::encode_prefixed(expected_buy_swap_calldata(min_amount_out, amount_in)),
7888                    "value": "0x0",
7889                    "gas": "0x130b0",
7890                    "maxFeePerGas": "0x7bfa480",
7891                    "maxPriorityFeePerGas": "0x989680"
7892                }]
7893            }
7894        })
7895        .to_string()
7896    }
7897
7898    fn finalized_buy_swap_rpc_state(
7899        tx_hash: B256,
7900        min_amount_out: U256,
7901        amount_in: U256,
7902    ) -> MockRpcState {
7903        let receipt = finalized_buy_swap_receipt(tx_hash, amount_in);
7904        let block = finalized_buy_swap_block(tx_hash, min_amount_out, amount_in);
7905        with_finalized_identity(
7906            signing_rpc_state()
7907                .with_response("eth_getTransactionReceipt", &receipt)
7908                .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block)
7909                .with_send_raw_transaction_echo()
7910                .with_call_response(BALANCE_OF_SELECTOR, CALL_BALANCE)
7911                .with_call_response(ALLOWANCE_SELECTOR, CALL_ALLOWANCE),
7912            &block,
7913            &receipt,
7914        )
7915    }
7916
7917    fn finalized_swap_receipt(tx_hash: B256) -> String {
7918        let data = (
7919            I256::try_from(1_000_000_000_000_000_i128).unwrap(),
7920            I256::try_from(-1_000_000_i128).unwrap(),
7921            U160::from(1_u128 << 96),
7922            TEST_LIQUIDITY,
7923            I24::try_from(0).unwrap(),
7924        )
7925            .abi_encode();
7926        serde_json::json!({
7927            "jsonrpc": "2.0",
7928            "id": 1,
7929            "result": {
7930                "transactionHash": tx_hash.to_string(),
7931                "blockHash": "0x2222222222222222222222222222222222222222222222222222222222222222",
7932                "blockNumber": "0x1cf0d41",
7933                "transactionIndex": "0x2",
7934                "gasUsed": "0xc3c0",
7935                "effectiveGasPrice": "0x5f5e100",
7936                "status": "0x1",
7937                "logs": [{
7938                    "removed": false,
7939                    "logIndex": "0x6",
7940                    "transactionIndex": "0x2",
7941                    "transactionHash": tx_hash.to_string(),
7942                    "blockHash": "0x2222222222222222222222222222222222222222222222222222222222222222",
7943                    "blockNumber": "0x1cf0d41",
7944                    "address": test_pool().address.to_string(),
7945                    "data": hex::encode_prefixed(data),
7946                    "topics": [
7947                        "0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67",
7948                        "0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266",
7949                        "0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266"
7950                    ]
7951                }]
7952            }
7953        })
7954        .to_string()
7955    }
7956
7957    fn finalized_swap_receipt_with_unrelated_swap(tx_hash: B256) -> String {
7958        let mut receipt: serde_json::Value =
7959            serde_json::from_str(&finalized_swap_receipt(tx_hash)).unwrap();
7960        let logs = receipt["result"]["logs"].as_array_mut().unwrap();
7961        let mut unrelated = logs[0].clone();
7962        unrelated["address"] = serde_json::json!(ROUTER);
7963        unrelated["logIndex"] = serde_json::json!("0x7");
7964        logs.push(unrelated);
7965        receipt.to_string()
7966    }
7967
7968    fn finalized_swap_block(tx_hash: B256, min_amount_out: U256) -> String {
7969        serde_json::json!({
7970            "jsonrpc": "2.0",
7971            "id": 1,
7972            "result": {
7973                "number": "0x1cf0d41",
7974                "hash": "0x2222222222222222222222222222222222222222222222222222222222222222",
7975                "parentHash": FIXTURE_BLOCK_HASH,
7976                "timestamp": "0x69044a21",
7977                "baseFeePerGas": "0x5f5e100",
7978                "transactions": [{
7979                    "hash": tx_hash.to_string(),
7980                    "from": WALLET,
7981                    "nonce": "0x7",
7982                    "chainId": "0xa4b1",
7983                    "type": "0x2",
7984                    "to": ROUTER,
7985                    "input": hex::encode_prefixed(expected_swap_calldata(min_amount_out)),
7986                    "value": "0x0",
7987                    "gas": "0x130b0",
7988                    "maxFeePerGas": "0x7bfa480",
7989                    "maxPriorityFeePerGas": "0x989680"
7990                }]
7991            }
7992        })
7993        .to_string()
7994    }
7995
7996    fn finalized_swap_rpc_state(tx_hash: B256, min_amount_out: U256) -> MockRpcState {
7997        let receipt = finalized_swap_receipt(tx_hash);
7998        let block = finalized_swap_block(tx_hash, min_amount_out);
7999        with_finalized_identity(
8000            signing_rpc_state()
8001                .with_response("eth_getTransactionReceipt", &receipt)
8002                .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block)
8003                .with_send_raw_transaction_echo()
8004                .with_call_response(BALANCE_OF_SELECTOR, CALL_BALANCE)
8005                .with_call_response(ALLOWANCE_SELECTOR, CALL_ALLOWANCE),
8006            &block,
8007            &receipt,
8008        )
8009    }
8010
8011    fn with_finalized_identity(
8012        state: MockRpcState,
8013        block_response: &str,
8014        receipt_response: &str,
8015    ) -> MockRpcState {
8016        let block: serde_json::Value = serde_json::from_str(block_response).unwrap();
8017        let receipt: serde_json::Value = serde_json::from_str(receipt_response).unwrap();
8018        let transaction = block["result"]["transactions"][0].clone();
8019        let transaction_response = serde_json::json!({
8020            "jsonrpc": "2.0",
8021            "id": 1,
8022            "result": transaction,
8023        })
8024        .to_string();
8025        let success = receipt["result"]["status"] == "0x1";
8026        let mut trace = serde_json::json!({
8027            "type": "CALL",
8028            "from": transaction["from"],
8029            "to": transaction["to"],
8030            "value": transaction["value"],
8031            "gas": transaction["gas"],
8032            "gasUsed": receipt["result"]["gasUsed"],
8033            "input": transaction["input"],
8034            "output": "0x",
8035            "calls": [],
8036        });
8037
8038        if !success {
8039            trace["error"] = serde_json::json!("execution reverted");
8040        }
8041        let trace_response = serde_json::json!({
8042            "jsonrpc": "2.0",
8043            "id": 1,
8044            "result": trace,
8045        })
8046        .to_string();
8047        state
8048            .with_response("eth_getTransactionByHash", &transaction_response)
8049            .with_response("debug_traceTransaction", &trace_response)
8050    }
8051
8052    fn receipt_with_transaction_hash(receipt_response: &str, tx_hash: B256) -> String {
8053        let mut receipt: serde_json::Value = serde_json::from_str(receipt_response).unwrap();
8054        receipt["result"]["transactionHash"] = serde_json::json!(tx_hash.to_string());
8055        receipt.to_string()
8056    }
8057
8058    fn replacement_head_block(tx_hash: B256) -> String {
8059        serde_json::json!({
8060            "jsonrpc": "2.0",
8061            "id": 1,
8062            "result": {
8063                "number": "0x1cf0d40",
8064                "hash": "0x1111111111111111111111111111111111111111111111111111111111111111",
8065                "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000001",
8066                "timestamp": "0x69044a20",
8067                "baseFeePerGas": "0x5f5e100",
8068                "transactions": [{
8069                    "hash": "0x3333333333333333333333333333333333333333333333333333333333333333",
8070                    "from": "0x0000000000000000000000000000000000000001",
8071                    "nonce": "0x1",
8072                    "type": "0x0",
8073                    "to": WETH,
8074                    "input": "0x",
8075                    "value": "0x0",
8076                    "gas": "0x5208"
8077                }, {
8078                    "hash": tx_hash.to_string(),
8079                    "from": WALLET,
8080                    "nonce": "0x7",
8081                    "chainId": "0xa4b1",
8082                    "type": "0x2",
8083                    "to": WETH,
8084                    "input": "0xd0e30db0",
8085                    "value": "0x38d7ea4c68000",
8086                    "gas": "0x130b0",
8087                    "maxFeePerGas": "0x7bfa480",
8088                    "maxPriorityFeePerGas": "0x989680"
8089                }]
8090            }
8091        })
8092        .to_string()
8093    }
8094
8095    fn finalized_wrap_block(tx_hash: B256) -> String {
8096        serde_json::json!({
8097            "jsonrpc": "2.0",
8098            "id": 1,
8099            "result": {
8100                "number": "0x1cf0d41",
8101                "hash": "0x2222222222222222222222222222222222222222222222222222222222222222",
8102                "parentHash": FIXTURE_BLOCK_HASH,
8103                "timestamp": "0x69044a21",
8104                "baseFeePerGas": "0x5f5e100",
8105                "transactions": [{
8106                    "hash": tx_hash.to_string(),
8107                    "from": WALLET,
8108                    "nonce": "0x7",
8109                    "chainId": "0xa4b1",
8110                    "type": "0x2",
8111                    "to": WETH,
8112                    "input": "0xd0e30db0",
8113                    "value": "0x38d7ea4c68000",
8114                    "gas": "0x130b0",
8115                    "maxFeePerGas": "0x7bfa480",
8116                    "maxPriorityFeePerGas": "0x989680"
8117                }]
8118            }
8119        })
8120        .to_string()
8121    }
8122
8123    fn finalized_approve_block(tx_hash: B256, amount: U256) -> String {
8124        let calldata = ERC20::approveCall {
8125            spender: ROUTER_ADDRESS,
8126            amount,
8127        }
8128        .abi_encode();
8129        serde_json::json!({
8130            "jsonrpc": "2.0",
8131            "id": 1,
8132            "result": {
8133                "number": "0x1cf0d41",
8134                "hash": "0x2222222222222222222222222222222222222222222222222222222222222222",
8135                "parentHash": FIXTURE_BLOCK_HASH,
8136                "timestamp": "0x69044a21",
8137                "baseFeePerGas": "0x5f5e100",
8138                "transactions": [{
8139                    "hash": tx_hash.to_string(),
8140                    "from": WALLET,
8141                    "nonce": "0x7",
8142                    "chainId": "0xa4b1",
8143                    "type": "0x2",
8144                    "to": WETH,
8145                    "input": hex::encode_prefixed(calldata),
8146                    "value": "0x0",
8147                    "gas": "0x130b0",
8148                    "maxFeePerGas": "0x7bfa480",
8149                    "maxPriorityFeePerGas": "0x989680"
8150                }]
8151            }
8152        })
8153        .to_string()
8154    }
8155
8156    fn fixture_sell_plan() -> SwapPlan {
8157        let pool = test_pool();
8158        let order = test_market_sell_order(pool.instrument_id);
8159        let quote_token = pool.get_quote_token();
8160        SwapPlan {
8161            order,
8162            quote_currency: Currency::new_checked(
8163                &quote_token.symbol,
8164                quote_token.decimals,
8165                0,
8166                &quote_token.name,
8167                CurrencyType::Crypto,
8168            )
8169            .unwrap(),
8170            instrument_id: pool.instrument_id,
8171            pool_address: pool.address,
8172            router: ROUTER_ADDRESS,
8173            factory: UNISWAP_V3.dex.factory,
8174            weth: WETH_ADDRESS,
8175            token_in: WETH_ADDRESS,
8176            token_out: USDC_ADDRESS,
8177            fee: U24::try_from(500u32).unwrap(),
8178            amount_in: U256::from(1_000_000_000_000_000u64),
8179            min_amount_out: expected_min_amount_out(50),
8180            slippage_bps: 50,
8181            quote_spend_ceiling: None,
8182            profiler_position: Some(profiler_event_position()),
8183            pool,
8184        }
8185    }
8186
8187    fn fixture_block_response(number: u64, hash: B256) -> String {
8188        let parent_hash = if number == FIXTURE_BLOCK {
8189            B256::from_str("0x0000000000000000000000000000000000000000000000000000000000000001")
8190                .unwrap()
8191        } else {
8192            B256::from_str(FIXTURE_BLOCK_HASH).unwrap()
8193        };
8194        serde_json::json!({
8195            "jsonrpc": "2.0",
8196            "id": 1,
8197            "result": {
8198                "number": format!("0x{number:x}"),
8199                "hash": hash.to_string(),
8200                "parentHash": parent_hash.to_string(),
8201                "timestamp": format!("0x{:x}", FIXTURE_BLOCK_TIMESTAMP + number - FIXTURE_BLOCK),
8202                "baseFeePerGas": "0x5f5e100",
8203                "transactions": []
8204            }
8205        })
8206        .to_string()
8207    }
8208
8209    fn profiler_event_receipt(pool_address: Address) -> String {
8210        let transaction_hash = B256::from([0x33; 32]);
8211        serde_json::json!({
8212            "jsonrpc": "2.0",
8213            "id": 1,
8214            "result": {
8215                "transactionHash": transaction_hash.to_string(),
8216                "blockHash": FIXTURE_BLOCK_HASH,
8217                "blockNumber": "0x1cf0d40",
8218                "transactionIndex": "0x2",
8219                "gasUsed": "0xc3c0",
8220                "effectiveGasPrice": "0x5f5e100",
8221                "status": "0x1",
8222                "logs": [{
8223                    "removed": false,
8224                    "logIndex": "0x6",
8225                    "transactionIndex": "0x2",
8226                    "transactionHash": transaction_hash.to_string(),
8227                    "blockHash": FIXTURE_BLOCK_HASH,
8228                    "blockNumber": "0x1cf0d40",
8229                    "address": pool_address.to_string(),
8230                    "data": "0x",
8231                    "topics": [test_pool().dex.swap_created_event.as_ref()]
8232                }]
8233            }
8234        })
8235        .to_string()
8236    }
8237
8238    fn profiler_event_position() -> BlockPosition {
8239        BlockPosition::new(FIXTURE_BLOCK, B256::from([0x33; 32]).to_string(), 2, 6)
8240            .with_block_hash(Some(FIXTURE_BLOCK_HASH.to_string()))
8241    }
8242
8243    #[rstest]
8244    fn quantity_to_raw_amount_scales_by_token_decimals() {
8245        assert_eq!(
8246            quantity_to_raw_amount(Quantity::from("0.001"), 18).unwrap(),
8247            U256::from(1_000_000_000_000_000u64)
8248        );
8249        assert_eq!(
8250            quantity_to_raw_amount(Quantity::from("1.5"), 18).unwrap(),
8251            U256::from(1_500_000_000_000_000_000u128)
8252        );
8253        assert_eq!(
8254            quantity_to_raw_amount(Quantity::from("12.5"), 6).unwrap(),
8255            U256::from(12_500_000u64)
8256        );
8257    }
8258
8259    #[rstest]
8260    fn quantity_to_raw_amount_uses_defi_quantity_precision() {
8261        let amount = U256::from(10_000_000_000_000_000u64);
8262        let quantity = Quantity::from_u256(amount, 18).unwrap();
8263
8264        assert_eq!(quantity_to_raw_amount(quantity, 18).unwrap(), amount);
8265    }
8266
8267    #[rstest]
8268    fn quantity_to_raw_amount_rejects_zero() {
8269        let error = quantity_to_raw_amount(Quantity::from("0.0"), 18).unwrap_err();
8270
8271        assert_eq!(error.to_string(), "Order quantity must be positive");
8272    }
8273
8274    #[rstest]
8275    fn quantity_to_raw_amount_rejects_inexact_token_units() {
8276        let error = quantity_to_raw_amount(Quantity::from("0.0000001"), 6).unwrap_err();
8277
8278        assert!(
8279            error
8280                .to_string()
8281                .contains("is not exactly representable in 6 base token decimals"),
8282            "was: {error}"
8283        );
8284    }
8285
8286    #[rstest]
8287    fn raw_amount_to_quantity_inverts_base_quantity() {
8288        let quantity = Quantity::from("0.001");
8289        let amount = quantity_to_raw_amount(quantity, 18).unwrap();
8290
8291        assert_eq!(raw_amount_to_quantity(amount, 18).unwrap(), quantity);
8292    }
8293
8294    #[rstest]
8295    fn raw_amount_to_quantity_rejects_positive_amount_truncated_to_zero() {
8296        let error = raw_amount_to_quantity(U256::from(99u64), 18).unwrap_err();
8297
8298        assert!(
8299            error
8300                .to_string()
8301                .contains("is below representable quantity precision"),
8302            "was: {error}"
8303        );
8304    }
8305
8306    #[rstest]
8307    fn fill_price_from_quote_recovers_spent_quote_after_quantity_truncation() {
8308        let last_qty = raw_amount_to_quantity(U256::from(1_000_000_403_079_044u64), 18).unwrap();
8309        let quote_amount = U256::from(1_891_348u64);
8310        let quote = Currency::new_checked("USDC", 6, 0, "USD Coin", CurrencyType::Crypto).unwrap();
8311        let last_px = fill_price_from_quote(last_qty, quote_amount, quote).unwrap();
8312
8313        assert_eq!(
8314            Money::from_decimal(last_qty.as_decimal() * last_px.as_decimal(), quote).unwrap(),
8315            Money::from_u256(quote_amount, quote).unwrap()
8316        );
8317    }
8318
8319    #[rstest]
8320    fn swap_token_pair_is_directional() {
8321        assert_eq!(
8322            swap_token_pair(OrderSide::Sell, WETH_ADDRESS, USDC_ADDRESS),
8323            (WETH_ADDRESS, USDC_ADDRESS)
8324        );
8325        assert_eq!(
8326            swap_token_pair(OrderSide::Buy, WETH_ADDRESS, USDC_ADDRESS),
8327            (USDC_ADDRESS, WETH_ADDRESS)
8328        );
8329    }
8330
8331    #[rstest]
8332    fn restore_swap_plan_buy_uses_quote_input() {
8333        let (client, cache) =
8334            swap_client_with_cache(buy_test_config("http://127.0.0.1:1".to_string()));
8335        let order = test_market_buy_order(test_pool().instrument_id);
8336        cache
8337            .borrow_mut()
8338            .add_order(order.clone(), None, None, true)
8339            .unwrap();
8340        let amount_in = U256::from(2_345_678u64);
8341        let intent = ExecutionIntentRow {
8342            id: 7,
8343            schema_version: crate::execution::transaction::EXECUTION_SCHEMA_VERSION,
8344            chain_id: 42161,
8345            wallet_address: WALLET.to_string(),
8346            nonce: Some(7),
8347            purpose: "swap".to_string(),
8348            status: "finalized".to_string(),
8349            client_order_id: Some(order.client_order_id().to_string()),
8350            trader_id: Some(order.trader_id().to_string()),
8351            strategy_id: Some(order.strategy_id().to_string()),
8352            account_id: Some("BLOCKCHAIN-001".to_string()),
8353            instrument_id: Some(order.instrument_id().to_string()),
8354            pool_address: Some(test_pool().address.to_string()),
8355            transaction_to: ROUTER.to_string(),
8356            transaction_input: "0x".to_string(),
8357            transaction_value: "0".to_string(),
8358            amount_in: Some(amount_in.to_string()),
8359            created_block: FIXTURE_BLOCK,
8360            acknowledgement_emitted: true,
8361            fill_emitted: false,
8362            terminal_emitted: false,
8363            active: true,
8364        };
8365
8366        let plan = client.restore_swap_plan(&intent).unwrap();
8367
8368        assert_eq!(plan.token_in, USDC_ADDRESS);
8369        assert_eq!(plan.token_out, WETH_ADDRESS);
8370        assert_eq!(plan.amount_in, amount_in);
8371    }
8372
8373    #[rstest]
8374    #[case(1_000_000, 50, 995_000)]
8375    #[case(1_000_000, 0, 1_000_000)]
8376    #[case(1_000_000, 200, 980_000)]
8377    #[case(10_000, 9_999, 1)]
8378    fn derive_min_amount_out_applies_slippage(
8379        #[case] quoted: u64,
8380        #[case] slippage_bps: u32,
8381        #[case] expected: u64,
8382    ) {
8383        assert_eq!(
8384            derive_min_amount_out(U256::from(quoted), slippage_bps).unwrap(),
8385            U256::from(expected)
8386        );
8387    }
8388
8389    #[rstest]
8390    fn derive_min_amount_out_rejects_zero_result() {
8391        let error = derive_min_amount_out(U256::from(9_999u64), 9_999).unwrap_err();
8392
8393        assert!(
8394            error.to_string().contains("Derived minimum output is zero"),
8395            "was: {error}"
8396        );
8397    }
8398
8399    #[rstest]
8400    fn derive_min_amount_out_rejects_full_slippage() {
8401        let error = derive_min_amount_out(U256::from(1_000_000u64), 10_000).unwrap_err();
8402
8403        assert!(
8404            error.to_string().contains("must be below 10000"),
8405            "was: {error}"
8406        );
8407    }
8408
8409    #[rstest]
8410    fn replacement_scan_range_is_bounded_and_checked() {
8411        assert_eq!(
8412            replacement_scan_range(10, 10).unwrap(),
8413            RangeInclusive::new(10, 10)
8414        );
8415        assert_eq!(
8416            replacement_scan_range(10, 10 + MAX_REPLACEMENT_SCAN_BLOCKS).unwrap(),
8417            RangeInclusive::new(10, 10 + MAX_REPLACEMENT_SCAN_BLOCKS - 1)
8418        );
8419        assert!(
8420            replacement_scan_range(11, 10)
8421                .unwrap_err()
8422                .to_string()
8423                .contains("is behind execution creation block")
8424        );
8425        assert_eq!(
8426            replacement_scan_range(u64::MAX - 1, u64::MAX).unwrap(),
8427            RangeInclusive::new(u64::MAX - 1, u64::MAX)
8428        );
8429    }
8430
8431    #[rstest]
8432    fn terminal_execution_event_ids_are_stable_and_kind_specific() {
8433        let transaction_hash = B256::from([0x42; 32]);
8434
8435        let fill = execution_event_id(transaction_hash, b"fill");
8436        let fill_retry = execution_event_id(transaction_hash, b"fill");
8437        let reverted = execution_event_id(transaction_hash, b"reverted");
8438
8439        assert_eq!(fill, fill_retry);
8440        assert_ne!(fill, reverted);
8441    }
8442
8443    #[rstest]
8444    fn prepared_transaction_debug_redacts_raw_transaction() {
8445        let prepared = PreparedTransaction {
8446            intent_id: 1,
8447            created_block: 2,
8448            nonce: 3,
8449            tx_hash: B256::ZERO,
8450            raw_tx: vec![0xde, 0xad, 0xbe, 0xef],
8451            payload_lease: None,
8452        };
8453
8454        let debug = format!("{prepared:?}");
8455
8456        assert!(debug.contains("raw_tx: \"<redacted>\""));
8457        assert!(!debug.contains("[222, 173, 190, 239]"));
8458    }
8459
8460    #[rstest]
8461    fn call_trace_rejects_unreviewed_internal_edge() {
8462        let signed = crate::execution::transaction::DecodedSignedTransaction {
8463            hash: B256::from([1; 32]),
8464            signer: Address::from_str(WALLET).unwrap(),
8465            chain_id: 42_161,
8466            nonce: 7,
8467            to: ROUTER_ADDRESS,
8468            value: U256::ZERO,
8469            input: Bytes::from(expected_swap_calldata(expected_min_amount_out(50))),
8470            gas_limit: 78_000,
8471            max_fee_per_gas: 130_000_000,
8472            max_priority_fee_per_gas: 10_000_000,
8473        };
8474        let input_digest = keccak256(&signed.input);
8475        let trace = VerifiedCallTrace {
8476            call_type: RpcCallType::Call,
8477            from: signed.signer,
8478            to: Some(signed.to),
8479            value: signed.value,
8480            input_selector: signed
8481                .input
8482                .get(..4)
8483                .map(|selector| selector.try_into().unwrap()),
8484            input_digest,
8485            success: true,
8486            calls: vec![VerifiedCallTrace {
8487                call_type: RpcCallType::Call,
8488                from: ROUTER_ADDRESS,
8489                to: Some(WETH_ADDRESS),
8490                value: U256::ZERO,
8491                input_selector: None,
8492                input_digest: B256::ZERO,
8493                success: true,
8494                calls: Vec::new(),
8495            }],
8496        };
8497        let manifest = &test_config("http://127.0.0.1:1".to_string())
8498            .verification
8499            .unwrap()
8500            .deployment_manifest;
8501
8502        let error = validate_call_trace(&trace, &signed, true, "swap_sell", manifest).unwrap_err();
8503
8504        assert!(
8505            error.to_string().contains("unreviewed call edge"),
8506            "was: {error}"
8507        );
8508    }
8509
8510    #[rstest]
8511    fn call_trace_rejects_unlisted_precompile_target() {
8512        let manifest = test_config("http://127.0.0.1:1".to_string())
8513            .verification
8514            .unwrap()
8515            .deployment_manifest;
8516        let calls = [VerifiedCallTrace {
8517            call_type: RpcCallType::Call,
8518            from: ROUTER_ADDRESS,
8519            to: Some(address!("0000000000000000000000000000000000000007")),
8520            value: U256::ZERO,
8521            input_selector: None,
8522            input_digest: B256::ZERO,
8523            success: true,
8524            calls: Vec::new(),
8525        }];
8526
8527        let error =
8528            validate_internal_calls(&calls, ROUTER_ADDRESS, "swap_sell", &manifest).unwrap_err();
8529
8530        assert_eq!(
8531            error.to_string(),
8532            format!(
8533                "Verified call trace contains an unreviewed call edge {ROUTER_ADDRESS} -> {} for swap_sell",
8534                address!("0000000000000000000000000000000000000007")
8535            )
8536        );
8537    }
8538
8539    #[rstest]
8540    fn call_trace_requires_exact_call_type() {
8541        let manifest = test_config("http://127.0.0.1:1".to_string())
8542            .verification
8543            .unwrap()
8544            .deployment_manifest;
8545        let calls = [VerifiedCallTrace {
8546            call_type: RpcCallType::Staticcall,
8547            from: ROUTER_ADDRESS,
8548            to: Some(test_pool().address),
8549            value: U256::ZERO,
8550            input_selector: None,
8551            input_digest: B256::ZERO,
8552            success: true,
8553            calls: Vec::new(),
8554        }];
8555
8556        let error =
8557            validate_internal_calls(&calls, ROUTER_ADDRESS, "swap_sell", &manifest).unwrap_err();
8558
8559        assert!(
8560            error.to_string().contains("unreviewed staticcall edge"),
8561            "was: {error}"
8562        );
8563    }
8564
8565    #[rstest]
8566    fn call_trace_requires_exact_caller() {
8567        let manifest = test_config("http://127.0.0.1:1".to_string())
8568            .verification
8569            .unwrap()
8570            .deployment_manifest;
8571        let calls = [VerifiedCallTrace {
8572            call_type: RpcCallType::Call,
8573            from: WETH_ADDRESS,
8574            to: Some(test_pool().address),
8575            value: U256::ZERO,
8576            input_selector: None,
8577            input_digest: B256::ZERO,
8578            success: true,
8579            calls: Vec::new(),
8580        }];
8581
8582        let error =
8583            validate_internal_calls(&calls, ROUTER_ADDRESS, "swap_sell", &manifest).unwrap_err();
8584
8585        assert_eq!(
8586            error.to_string(),
8587            "Verified call trace child has an invalid caller context"
8588        );
8589    }
8590
8591    #[rstest]
8592    fn call_trace_rejects_contract_creation() {
8593        let signed = crate::execution::transaction::DecodedSignedTransaction {
8594            hash: B256::from([1; 32]),
8595            signer: Address::from_str(WALLET).unwrap(),
8596            chain_id: 42_161,
8597            nonce: 7,
8598            to: ROUTER_ADDRESS,
8599            value: U256::ZERO,
8600            input: Bytes::from(expected_swap_calldata(expected_min_amount_out(50))),
8601            gas_limit: 78_000,
8602            max_fee_per_gas: 130_000_000,
8603            max_priority_fee_per_gas: 10_000_000,
8604        };
8605        let trace = VerifiedCallTrace {
8606            call_type: RpcCallType::Call,
8607            from: signed.signer,
8608            to: Some(signed.to),
8609            value: signed.value,
8610            input_selector: signed
8611                .input
8612                .get(..4)
8613                .map(|selector| selector.try_into().unwrap()),
8614            input_digest: keccak256(&signed.input),
8615            success: true,
8616            calls: vec![VerifiedCallTrace {
8617                call_type: RpcCallType::Create,
8618                from: ROUTER_ADDRESS,
8619                to: Some(address!("0000000000000000000000000000000000000007")),
8620                value: U256::ZERO,
8621                input_selector: None,
8622                input_digest: B256::ZERO,
8623                success: true,
8624                calls: Vec::new(),
8625            }],
8626        };
8627        let manifest = &test_config("http://127.0.0.1:1".to_string())
8628            .verification
8629            .unwrap()
8630            .deployment_manifest;
8631
8632        let error = validate_call_trace(&trace, &signed, true, "swap_sell", manifest).unwrap_err();
8633
8634        assert!(
8635            error
8636                .to_string()
8637                .contains("forbidden state-changing operation"),
8638            "was: {error}"
8639        );
8640    }
8641
8642    #[tokio::test]
8643    async fn swap_quote_rejects_missing_ingestion_block_hash() {
8644        let (client, state) = client_with_mock_rpc(execution_rpc_state()).await;
8645        let plan = fixture_sell_plan();
8646        let position = BlockPosition::new(
8647            FIXTURE_BLOCK,
8648            FIXTURE_BLOCK_HASH.to_string(),
8649            BLOCK_SCOPED_SNAPSHOT_INDEX,
8650            BLOCK_SCOPED_SNAPSHOT_INDEX,
8651        );
8652
8653        let error = validate_swap_quote(
8654            &position,
8655            &plan,
8656            100,
8657            &client.verification,
8658            &client
8659                .config
8660                .verification
8661                .as_ref()
8662                .unwrap()
8663                .deployment_manifest,
8664        )
8665        .await
8666        .unwrap_err();
8667
8668        assert!(
8669            error.to_string().contains("no ingestion-time block hash"),
8670            "was: {error}"
8671        );
8672        assert!(state.recorded_requests().is_empty());
8673    }
8674
8675    #[tokio::test]
8676    async fn swap_quote_rejects_replaced_ingestion_block() {
8677        let changed = fixture_block_response(FIXTURE_BLOCK, B256::from([0x44; 32]));
8678        let state = execution_rpc_state().with_parameter_response(
8679            "eth_getBlockByNumber",
8680            "0x1cf0d40",
8681            &changed,
8682        );
8683        let (client, _) = client_with_mock_rpc(state).await;
8684        let plan = fixture_sell_plan();
8685        let position = BlockPosition::new(
8686            FIXTURE_BLOCK,
8687            FIXTURE_BLOCK_HASH.to_string(),
8688            BLOCK_SCOPED_SNAPSHOT_INDEX,
8689            BLOCK_SCOPED_SNAPSHOT_INDEX,
8690        )
8691        .with_block_hash(Some(FIXTURE_BLOCK_HASH.to_string()));
8692
8693        let error = validate_swap_quote(
8694            &position,
8695            &plan,
8696            100,
8697            &client.verification,
8698            &client
8699                .config
8700                .verification
8701                .as_ref()
8702                .unwrap()
8703                .deployment_manifest,
8704        )
8705        .await
8706        .unwrap_err();
8707
8708        assert!(error.to_string().contains("changed from"), "was: {error}");
8709    }
8710
8711    #[tokio::test]
8712    async fn swap_quote_accepts_exact_canonical_event_watermark() {
8713        let pool = test_pool();
8714        let receipt = profiler_event_receipt(pool.address);
8715        let state = execution_rpc_state().with_response("eth_getTransactionReceipt", &receipt);
8716        let (client, _) = client_with_mock_rpc(state).await;
8717        let plan = fixture_sell_plan();
8718
8719        let anchors = validate_swap_quote(
8720            &profiler_event_position(),
8721            &plan,
8722            100,
8723            &client.verification,
8724            &client
8725                .config
8726                .verification
8727                .as_ref()
8728                .unwrap()
8729                .deployment_manifest,
8730        )
8731        .await
8732        .unwrap();
8733
8734        assert_eq!(anchors.watermark.number, profiler_event_position().number);
8735        assert_eq!(anchors.state.number, FIXTURE_BLOCK);
8736        assert_eq!(anchors.state.hash, B256::from([0x11; 32]));
8737        assert_eq!(anchors.state.timestamp, FIXTURE_BLOCK_TIMESTAMP);
8738    }
8739
8740    #[tokio::test]
8741    async fn swap_quote_rejects_mismatched_receipt_position() {
8742        let pool = test_pool();
8743        let mut receipt: serde_json::Value =
8744            serde_json::from_str(&profiler_event_receipt(pool.address)).unwrap();
8745        receipt["result"]["transactionIndex"] = serde_json::json!("0x3");
8746        let state =
8747            execution_rpc_state().with_response("eth_getTransactionReceipt", &receipt.to_string());
8748        let (client, _) = client_with_mock_rpc(state).await;
8749        let plan = fixture_sell_plan();
8750
8751        let error = validate_swap_quote(
8752            &profiler_event_position(),
8753            &plan,
8754            100,
8755            &client.verification,
8756            &client
8757                .config
8758                .verification
8759                .as_ref()
8760                .unwrap()
8761                .deployment_manifest,
8762        )
8763        .await
8764        .unwrap_err();
8765
8766        assert_eq!(
8767            error.to_string(),
8768            "Profiler receipt position does not match its ingestion watermark"
8769        );
8770    }
8771
8772    #[tokio::test]
8773    async fn swap_quote_rejects_watermark_from_different_pool() {
8774        let receipt = profiler_event_receipt(ROUTER_ADDRESS);
8775        let state = execution_rpc_state().with_response("eth_getTransactionReceipt", &receipt);
8776        let (client, _) = client_with_mock_rpc(state).await;
8777        let plan = fixture_sell_plan();
8778
8779        let error = validate_swap_quote(
8780            &profiler_event_position(),
8781            &plan,
8782            100,
8783            &client.verification,
8784            &client
8785                .config
8786                .verification
8787                .as_ref()
8788                .unwrap()
8789                .deployment_manifest,
8790        )
8791        .await
8792        .unwrap_err();
8793
8794        assert!(
8795            error
8796                .to_string()
8797                .contains("did not come from expected pool"),
8798            "was: {error}"
8799        );
8800    }
8801
8802    #[rstest]
8803    fn verified_sell_quote_sets_signed_amounts() {
8804        let plan = fixture_sell_plan();
8805        let quote = UniswapV3Quote {
8806            amount: expected_sell_quote_amount(),
8807            sqrt_price_x96_after: U160::from(1u128 << 96),
8808            initialized_ticks_crossed: 0,
8809            gas_estimate: U256::from(50_000u64),
8810        };
8811
8812        let (amount_in, min_amount_out) = verified_swap_amounts(&plan, quote).unwrap();
8813
8814        assert_eq!(amount_in, U256::from(1_000_000_000_000_000u64));
8815        assert_eq!(min_amount_out, expected_min_amount_out(50));
8816    }
8817
8818    #[rstest]
8819    fn exact_output_amount_extracts_negative_leg() {
8820        let pool = test_pool();
8821        let profiler = test_profiler(&pool, FIXTURE_BLOCK);
8822        let quote = profiler
8823            .swap_exact_in(U256::from(1_000_000_000_000_000u64), true, None)
8824            .unwrap();
8825
8826        let amount = exact_output_amount(&quote, true).unwrap();
8827
8828        assert!(amount < U256::from(1_000_000_000_000_000u64));
8829        assert!(amount > U256::from(990_000_000_000_000u64));
8830
8831        let error = exact_output_amount(&quote, false).unwrap_err();
8832        assert!(
8833            error.to_string().contains("is not a positive output"),
8834            "was: {error}"
8835        );
8836    }
8837
8838    #[rstest]
8839    #[case("0xC6962004f452bE9203591991D15f6b388e09E8D0.Arbitrum:UniswapV3", true)]
8840    #[case("0xC6962004f452bE9203591991D15f6b388e09E8D0.Ethereum:UniswapV3", false)]
8841    #[case("0xC6962004f452bE9203591991D15f6b388e09E8D0.Arbitrum:UniswapV4", false)]
8842    #[case("ETHUSDT-PERP.BINANCE", false)]
8843    fn handles_order_venue_matches_chain_and_dex(#[case] instrument: &str, #[case] expected: bool) {
8844        let client = test_client("http://127.0.0.1:1".to_string());
8845        let instrument_id: InstrumentId = instrument.parse().unwrap();
8846
8847        assert_eq!(client.handles_order_venue(instrument_id.venue), expected);
8848    }
8849
8850    #[tokio::test]
8851    async fn submit_order_denies_buy_without_allowlisted_pair() {
8852        let (mut client, cache) =
8853            swap_client_with_cache(test_config("http://127.0.0.1:1".to_string()));
8854        let pool = test_pool();
8855        let order = test_market_buy_order(pool.instrument_id);
8856        cache
8857            .borrow_mut()
8858            .add_order(order.clone(), None, None, true)
8859            .unwrap();
8860        let mut receiver = start_with_events(&mut client);
8861
8862        client.submit_order(submit_order_cmd(&order)).unwrap();
8863
8864        let events = collect_order_events(&mut receiver);
8865        assert_eq!(events.len(), 1);
8866        let OrderEventAny::Denied(denied) = &events[0] else {
8867            panic!("expected OrderDenied, was {:?}", events[0]);
8868        };
8869        assert!(
8870            denied
8871                .reason
8872                .contains("not in the `allowed_token_pairs` allowlist"),
8873            "was: {}",
8874            denied.reason
8875        );
8876        assert!(
8877            denied.reason.contains(&USDC_ADDRESS.to_string()),
8878            "was: {}",
8879            denied.reason
8880        );
8881    }
8882
8883    #[tokio::test]
8884    async fn submit_order_denies_buy_quote_denominated_quantity() {
8885        let (mut client, cache) =
8886            swap_client_with_cache(buy_test_config("http://127.0.0.1:1".to_string()));
8887        let pool = test_pool();
8888        let order = OrderTestBuilder::new(OrderType::Market)
8889            .trader_id(TraderId::from("TRADER-001"))
8890            .strategy_id(StrategyId::from("S-001"))
8891            .instrument_id(pool.instrument_id)
8892            .client_order_id(ClientOrderId::from("O-SWAP-BUY-001"))
8893            .side(OrderSide::Buy)
8894            .quantity(Quantity::from("0.001"))
8895            .quote_quantity(true)
8896            .build();
8897        cache
8898            .borrow_mut()
8899            .add_order(order.clone(), None, None, true)
8900            .unwrap();
8901        let mut receiver = start_with_events(&mut client);
8902
8903        client.submit_order(submit_order_cmd(&order)).unwrap();
8904
8905        let events = collect_order_events(&mut receiver);
8906        assert_eq!(events.len(), 1);
8907        let OrderEventAny::Denied(denied) = &events[0] else {
8908            panic!("expected OrderDenied, was {:?}", events[0]);
8909        };
8910
8911        assert_eq!(denied.reason.as_str(), "UNSUPPORTED_QUOTE_QUANTITY");
8912    }
8913
8914    #[tokio::test]
8915    async fn submit_order_denies_buy_amount_above_max_order_amount() {
8916        let mut config = buy_test_config("http://127.0.0.1:1".to_string());
8917        config.max_order_amount = Some(999_999_999_999_999);
8918        let (mut client, cache) = swap_client_with_cache(config);
8919        let order = test_market_buy_order(test_pool().instrument_id);
8920        cache
8921            .borrow_mut()
8922            .add_order(order.clone(), None, None, true)
8923            .unwrap();
8924        let mut receiver = start_with_events(&mut client);
8925
8926        client.submit_order(submit_order_cmd(&order)).unwrap();
8927
8928        let events = collect_order_events(&mut receiver);
8929        assert_eq!(events.len(), 1);
8930        let OrderEventAny::Denied(denied) = &events[0] else {
8931            panic!("expected OrderDenied, was {:?}", events[0]);
8932        };
8933        assert!(
8934            denied
8935                .reason
8936                .contains("exceeds the configured `max_order_amount`"),
8937            "was: {}",
8938            denied.reason
8939        );
8940    }
8941
8942    #[tokio::test]
8943    async fn submit_order_denies_buy_without_quote_spend_limit() {
8944        let mut config = buy_test_config("http://127.0.0.1:1".to_string());
8945        config.quote_spend_limits = None;
8946        let (mut client, cache) = swap_client_with_cache(config);
8947        let order = test_market_buy_order(test_pool().instrument_id);
8948        cache
8949            .borrow_mut()
8950            .add_order(order.clone(), None, None, true)
8951            .unwrap();
8952        let mut receiver = start_with_events(&mut client);
8953
8954        client.submit_order(submit_order_cmd(&order)).unwrap();
8955
8956        let events = collect_order_events(&mut receiver);
8957        assert_eq!(events.len(), 1);
8958        let OrderEventAny::Denied(denied) = &events[0] else {
8959            panic!("expected OrderDenied, was {:?}", events[0]);
8960        };
8961        assert!(
8962            denied
8963                .reason
8964                .contains("No `quote_spend_limits` entry for BUY token pair"),
8965            "was: {}",
8966            denied.reason
8967        );
8968    }
8969
8970    #[tokio::test]
8971    async fn submit_order_uses_pair_specific_quote_spend_limit() {
8972        let mut config = buy_test_config("http://127.0.0.1:1".to_string());
8973        config.quote_spend_limits = Some(vec![quote_spend_limit(
8974            WETH,
8975            USDC,
8976            18,
8977            "1000000000000000000",
8978        )]);
8979        let (mut client, cache) = swap_client_with_cache(config);
8980        let order = test_market_buy_order(test_pool().instrument_id);
8981        cache
8982            .borrow_mut()
8983            .add_order(order.clone(), None, None, true)
8984            .unwrap();
8985        let mut receiver = start_with_events(&mut client);
8986
8987        client.submit_order(submit_order_cmd(&order)).unwrap();
8988
8989        let events = collect_order_events(&mut receiver);
8990        assert_eq!(events.len(), 1);
8991        let OrderEventAny::Denied(denied) = &events[0] else {
8992            panic!("expected OrderDenied, was {:?}", events[0]);
8993        };
8994        assert!(
8995            denied
8996                .reason
8997                .contains("No `quote_spend_limits` entry for BUY token pair"),
8998            "was: {}",
8999            denied.reason
9000        );
9001    }
9002
9003    #[tokio::test]
9004    async fn submit_order_denies_buy_with_quote_spend_precision_mismatch() {
9005        let mut config = buy_test_config("http://127.0.0.1:1".to_string());
9006        config.quote_spend_limits.as_mut().unwrap()[0].spend_token_decimals = 18;
9007        let error = test_client_result(config, test_pool()).unwrap_err();
9008        assert!(
9009            error
9010                .to_string()
9011                .contains("Quote spend limit decimals do not match the deployment manifest"),
9012            "was: {error}"
9013        );
9014    }
9015
9016    #[tokio::test]
9017    async fn submit_order_denies_buy_with_zero_quote_spend_limit() {
9018        let mut config = buy_test_config("http://127.0.0.1:1".to_string());
9019        config.quote_spend_limits.as_mut().unwrap()[0].max_amount = "0".to_string();
9020        let (mut client, cache) = swap_client_with_cache(config);
9021        let order = test_market_buy_order(test_pool().instrument_id);
9022        cache
9023            .borrow_mut()
9024            .add_order(order.clone(), None, None, true)
9025            .unwrap();
9026        let mut receiver = start_with_events(&mut client);
9027
9028        client.submit_order(submit_order_cmd(&order)).unwrap();
9029
9030        let events = collect_order_events(&mut receiver);
9031        assert_eq!(events.len(), 1);
9032        let OrderEventAny::Denied(denied) = &events[0] else {
9033            panic!("expected OrderDenied, was {:?}", events[0]);
9034        };
9035        assert!(
9036            denied
9037                .reason
9038                .contains("exceeds the configured `quote_spend_limits` maximum 0"),
9039            "was: {}",
9040            denied.reason
9041        );
9042    }
9043
9044    #[tokio::test]
9045    async fn submit_order_denies_buy_one_raw_unit_above_quote_spend_limit_before_readiness() {
9046        let amount_in = expected_buy_amount_in();
9047        let mut config = buy_test_config("http://127.0.0.1:1".to_string());
9048        config.quote_spend_limits.as_mut().unwrap()[0].max_amount =
9049            (amount_in - U256::from(1u8)).to_string();
9050        let (mut client, cache) = swap_client_with_cache(config);
9051        let order = test_market_buy_order(test_pool().instrument_id);
9052        cache
9053            .borrow_mut()
9054            .add_order(order.clone(), None, None, true)
9055            .unwrap();
9056        let mut receiver = start_with_events(&mut client);
9057
9058        client.submit_order(submit_order_cmd(&order)).unwrap();
9059
9060        let events = collect_order_events(&mut receiver);
9061        assert_eq!(events.len(), 1);
9062        let OrderEventAny::Denied(denied) = &events[0] else {
9063            panic!("expected OrderDenied, was {:?}", events[0]);
9064        };
9065        assert!(
9066            denied.reason.contains(&format!(
9067                "BUY quote amount {amount_in} exceeds the configured `quote_spend_limits`"
9068            )),
9069            "was: {}",
9070            denied.reason
9071        );
9072        assert!(!client.core.is_connected());
9073        assert!(!client.cache.has_database());
9074        assert!(client.signer.is_none());
9075        assert!(client.pending_tasks.is_empty());
9076    }
9077
9078    #[tokio::test]
9079    async fn submit_order_denies_non_market_order_type() {
9080        let (mut client, cache) =
9081            swap_client_with_cache(test_config("http://127.0.0.1:1".to_string()));
9082        let pool = test_pool();
9083        let order = OrderTestBuilder::new(OrderType::Limit)
9084            .trader_id(TraderId::from("TRADER-001"))
9085            .strategy_id(StrategyId::from("S-001"))
9086            .instrument_id(pool.instrument_id)
9087            .client_order_id(ClientOrderId::from("O-SWAP-001"))
9088            .side(OrderSide::Sell)
9089            .quantity(Quantity::from("0.001"))
9090            .price(Price::from("2000"))
9091            .build();
9092        cache
9093            .borrow_mut()
9094            .add_order(order.clone(), None, None, true)
9095            .unwrap();
9096        let mut receiver = start_with_events(&mut client);
9097
9098        client.submit_order(submit_order_cmd(&order)).unwrap();
9099
9100        let events = collect_order_events(&mut receiver);
9101        assert_eq!(events.len(), 1);
9102        let OrderEventAny::Denied(denied) = &events[0] else {
9103            panic!("expected OrderDenied, was {:?}", events[0]);
9104        };
9105        assert!(
9106            denied.reason.contains("only Market is supported"),
9107            "was: {}",
9108            denied.reason
9109        );
9110    }
9111
9112    #[tokio::test]
9113    async fn submit_order_denies_quote_denominated_quantity() {
9114        let (mut client, cache) =
9115            swap_client_with_cache(test_config("http://127.0.0.1:1".to_string()));
9116        let pool = test_pool();
9117        let order = OrderTestBuilder::new(OrderType::Market)
9118            .trader_id(TraderId::from("TRADER-001"))
9119            .strategy_id(StrategyId::from("S-001"))
9120            .instrument_id(pool.instrument_id)
9121            .client_order_id(ClientOrderId::from("O-SWAP-001"))
9122            .side(OrderSide::Sell)
9123            .quantity(Quantity::from("0.001"))
9124            .quote_quantity(true)
9125            .build();
9126        cache
9127            .borrow_mut()
9128            .add_order(order.clone(), None, None, true)
9129            .unwrap();
9130        let mut receiver = start_with_events(&mut client);
9131
9132        client.submit_order(submit_order_cmd(&order)).unwrap();
9133
9134        let events = collect_order_events(&mut receiver);
9135        assert_eq!(events.len(), 1);
9136        let OrderEventAny::Denied(denied) = &events[0] else {
9137            panic!("expected OrderDenied, was {:?}", events[0]);
9138        };
9139
9140        assert_eq!(denied.reason.as_str(), "UNSUPPORTED_QUOTE_QUANTITY");
9141    }
9142
9143    #[tokio::test]
9144    async fn submit_order_denies_unknown_pool() {
9145        let (mut client, cache) =
9146            swap_client_with_cache(test_config("http://127.0.0.1:1".to_string()));
9147        let unknown: InstrumentId = "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45.Arbitrum:UniswapV3"
9148            .parse()
9149            .unwrap();
9150        let order = OrderTestBuilder::new(OrderType::Market)
9151            .trader_id(TraderId::from("TRADER-001"))
9152            .strategy_id(StrategyId::from("S-001"))
9153            .instrument_id(unknown)
9154            .client_order_id(ClientOrderId::from("O-SWAP-001"))
9155            .side(OrderSide::Sell)
9156            .quantity(Quantity::from("0.001"))
9157            .build();
9158        cache
9159            .borrow_mut()
9160            .add_order(order.clone(), None, None, true)
9161            .unwrap();
9162        let mut receiver = start_with_events(&mut client);
9163
9164        client.submit_order(submit_order_cmd(&order)).unwrap();
9165
9166        let events = collect_order_events(&mut receiver);
9167        assert_eq!(events.len(), 1);
9168        let OrderEventAny::Denied(denied) = &events[0] else {
9169            panic!("expected OrderDenied, was {:?}", events[0]);
9170        };
9171        assert!(
9172            denied.reason.contains("Unknown pool"),
9173            "was: {}",
9174            denied.reason
9175        );
9176    }
9177
9178    #[tokio::test]
9179    async fn submit_order_denies_sell_when_only_buy_pair_allowlisted() {
9180        let mut config = test_config("http://127.0.0.1:1".to_string());
9181        config.allowed_token_pairs = Some(vec![(USDC.to_string(), WETH.to_string())]);
9182        let (mut client, _) = swap_client_with_cache(config);
9183        let order = test_market_sell_order(test_pool().instrument_id);
9184        let mut receiver = start_with_events(&mut client);
9185
9186        client.submit_order(submit_order_cmd(&order)).unwrap();
9187
9188        let events = collect_order_events(&mut receiver);
9189        assert_eq!(events.len(), 1);
9190        let OrderEventAny::Denied(denied) = &events[0] else {
9191            panic!("expected OrderDenied, was {:?}", events[0]);
9192        };
9193        assert!(
9194            denied
9195                .reason
9196                .contains("not in the `allowed_token_pairs` allowlist"),
9197            "was: {}",
9198            denied.reason
9199        );
9200        assert!(
9201            denied.reason.contains(&WETH_ADDRESS.to_string()),
9202            "was: {}",
9203            denied.reason
9204        );
9205    }
9206
9207    #[tokio::test]
9208    async fn submit_order_sell_ignores_quote_spend_limits() {
9209        let mut config = test_config("http://127.0.0.1:1".to_string());
9210        config.quote_spend_limits = Some(vec![quote_spend_limit(WETH, USDC, 18, "0")]);
9211        let (mut client, _) = swap_client_with_cache(config);
9212        let order = test_market_sell_order(test_pool().instrument_id);
9213        let mut receiver = start_with_events(&mut client);
9214
9215        client.submit_order(submit_order_cmd(&order)).unwrap();
9216
9217        let events = collect_order_events(&mut receiver);
9218        assert_eq!(events.len(), 1);
9219        let OrderEventAny::Denied(denied) = &events[0] else {
9220            panic!("expected OrderDenied, was {:?}", events[0]);
9221        };
9222        assert!(
9223            denied
9224                .reason
9225                .contains("Blockchain execution client is not connected"),
9226            "was: {}",
9227            denied.reason
9228        );
9229    }
9230
9231    #[tokio::test]
9232    async fn submit_order_denies_token_pair_outside_allowlist() {
9233        let mut config = test_config("http://127.0.0.1:1".to_string());
9234        config.allowed_token_pairs = Some(Vec::new());
9235        let (mut client, _) = swap_client_with_cache(config);
9236        let order = test_market_sell_order(test_pool().instrument_id);
9237        let mut receiver = start_with_events(&mut client);
9238
9239        client.submit_order(submit_order_cmd(&order)).unwrap();
9240
9241        let events = collect_order_events(&mut receiver);
9242        assert_eq!(events.len(), 1);
9243        let OrderEventAny::Denied(denied) = &events[0] else {
9244            panic!("expected OrderDenied, was {:?}", events[0]);
9245        };
9246        assert!(
9247            denied
9248                .reason
9249                .contains("not in the `allowed_token_pairs` allowlist"),
9250            "was: {}",
9251            denied.reason
9252        );
9253    }
9254
9255    #[tokio::test]
9256    async fn submit_order_denies_amount_above_max_order_amount() {
9257        let mut config = test_config("http://127.0.0.1:1".to_string());
9258        config.max_order_amount = Some(999_999_999_999_999); // 0.001 WETH in raw units minus one
9259        let (mut client, _) = swap_client_with_cache(config);
9260        let order = test_market_sell_order(test_pool().instrument_id);
9261        let mut receiver = start_with_events(&mut client);
9262
9263        client.submit_order(submit_order_cmd(&order)).unwrap();
9264
9265        let events = collect_order_events(&mut receiver);
9266        assert_eq!(events.len(), 1);
9267        let OrderEventAny::Denied(denied) = &events[0] else {
9268            panic!("expected OrderDenied, was {:?}", events[0]);
9269        };
9270        assert!(
9271            denied
9272                .reason
9273                .contains("exceeds the configured `max_order_amount`"),
9274            "was: {}",
9275            denied.reason
9276        );
9277    }
9278
9279    #[tokio::test]
9280    async fn submit_order_denies_slippage_param_above_ceiling() {
9281        let (mut client, _) = swap_client_with_cache(test_config("http://127.0.0.1:1".to_string()));
9282        let order = test_market_sell_order(test_pool().instrument_id);
9283        let mut cmd = submit_order_cmd(&order);
9284        cmd.params = Some(serde_json::from_str(r#"{"slippage_bps": 201}"#).unwrap());
9285        let mut receiver = start_with_events(&mut client);
9286
9287        client.submit_order(cmd).unwrap();
9288
9289        let events = collect_order_events(&mut receiver);
9290        assert_eq!(events.len(), 1);
9291        let OrderEventAny::Denied(denied) = &events[0] else {
9292            panic!("expected OrderDenied, was {:?}", events[0]);
9293        };
9294        assert!(
9295            denied
9296                .reason
9297                .contains("exceeds the configured `max_slippage_bps`"),
9298            "was: {}",
9299            denied.reason
9300        );
9301    }
9302
9303    #[tokio::test]
9304    async fn submit_order_denies_pool_without_fee_tier() {
9305        let addr = start_mock_rpc_server(MockRpcState::default()).await;
9306        let mut pool = test_pool();
9307        pool.fee = None;
9308        let cache = Rc::new(RefCell::new(Cache::default()));
9309        cache.borrow_mut().add_pool(pool.clone()).unwrap();
9310        let order = test_market_sell_order(pool.instrument_id);
9311        cache
9312            .borrow_mut()
9313            .add_order(order.clone(), None, None, false)
9314            .unwrap();
9315        let core = ExecutionClientCore::new(
9316            TraderId::from("TRADER-001"),
9317            ClientId::from("BLOCKCHAIN-001"),
9318            *BLOCKCHAIN_VENUE,
9319            OmsType::Netting,
9320            AccountId::from("BLOCKCHAIN-001"),
9321            AccountType::Wallet,
9322            None,
9323            cache,
9324        );
9325        let mut client =
9326            BlockchainExecutionClient::new(core, test_config(format!("http://{addr}"))).unwrap();
9327        let mut receiver = start_with_events(&mut client);
9328
9329        client.submit_order(submit_order_cmd(&order)).unwrap();
9330
9331        let events = collect_order_events(&mut receiver);
9332        assert_eq!(events.len(), 1);
9333        let OrderEventAny::Denied(denied) = &events[0] else {
9334            panic!("expected OrderDenied, was {:?}", events[0]);
9335        };
9336        assert!(
9337            denied.reason.contains("no fee tier"),
9338            "was: {}",
9339            denied.reason
9340        );
9341    }
9342
9343    #[tokio::test]
9344    async fn submit_order_denies_without_live_profiler() {
9345        let addr = start_mock_rpc_server(MockRpcState::default()).await;
9346        let cache = Rc::new(RefCell::new(Cache::default()));
9347        let pool = test_pool();
9348        cache.borrow_mut().add_pool(pool.clone()).unwrap();
9349        let order = test_market_sell_order(pool.instrument_id);
9350        cache
9351            .borrow_mut()
9352            .add_order(order.clone(), None, None, false)
9353            .unwrap();
9354        let core = ExecutionClientCore::new(
9355            TraderId::from("TRADER-001"),
9356            ClientId::from("BLOCKCHAIN-001"),
9357            *BLOCKCHAIN_VENUE,
9358            OmsType::Netting,
9359            AccountId::from("BLOCKCHAIN-001"),
9360            AccountType::Wallet,
9361            None,
9362            cache,
9363        );
9364        let mut client =
9365            BlockchainExecutionClient::new(core, test_config(format!("http://{addr}"))).unwrap();
9366        let mut receiver = start_with_events(&mut client);
9367
9368        client.submit_order(submit_order_cmd(&order)).unwrap();
9369
9370        let events = collect_order_events(&mut receiver);
9371        assert_eq!(events.len(), 1);
9372        let OrderEventAny::Denied(denied) = &events[0] else {
9373            panic!("expected OrderDenied, was {:?}", events[0]);
9374        };
9375        assert!(
9376            denied.reason.contains("No pool profiler"),
9377            "was: {}",
9378            denied.reason
9379        );
9380    }
9381
9382    #[tokio::test]
9383    async fn submit_order_denies_when_not_connected() {
9384        let (mut client, _) = swap_client_with_cache(test_config("http://127.0.0.1:1".to_string()));
9385        let order = test_market_sell_order(test_pool().instrument_id);
9386        let mut receiver = start_with_events(&mut client);
9387
9388        client.submit_order(submit_order_cmd(&order)).unwrap();
9389
9390        let events = collect_order_events(&mut receiver);
9391        assert_eq!(events.len(), 1);
9392        let OrderEventAny::Denied(denied) = &events[0] else {
9393            panic!("expected OrderDenied, was {:?}", events[0]);
9394        };
9395        assert!(
9396            denied.reason.contains("is not connected"),
9397            "was: {}",
9398            denied.reason
9399        );
9400    }
9401
9402    #[tokio::test]
9403    async fn submit_order_denies_without_durable_store() {
9404        let (mut client, _) = swap_client_with_cache(test_config("http://127.0.0.1:1".to_string()));
9405        client.core.set_connected();
9406        let order = test_market_sell_order(test_pool().instrument_id);
9407        let mut receiver = start_with_events(&mut client);
9408
9409        client.submit_order(submit_order_cmd(&order)).unwrap();
9410
9411        let events = collect_order_events(&mut receiver);
9412        assert_eq!(events.len(), 1);
9413        let OrderEventAny::Denied(denied) = &events[0] else {
9414            panic!("expected OrderDenied, was {:?}", events[0]);
9415        };
9416        assert!(
9417            denied.reason.contains("No durable store configured"),
9418            "was: {}",
9419            denied.reason
9420        );
9421    }
9422
9423    #[tokio::test]
9424    async fn submit_order_denies_when_transaction_in_flight() {
9425        let (mut client, _) = swap_client_with_cache(test_config("http://127.0.0.1:1".to_string()));
9426        client.core.set_connected();
9427        *client.in_flight.lock() = Some(InFlightSlot::AwaitingFinality(InFlightTransaction {
9428            intent_id: 1,
9429            nonce: 7,
9430            tx_hash: B256::ZERO,
9431            purpose: TransactionPurpose::Wrap,
9432        }));
9433        let order = test_market_sell_order(test_pool().instrument_id);
9434        let mut receiver = start_with_events(&mut client);
9435
9436        client.submit_order(submit_order_cmd(&order)).unwrap();
9437
9438        let events = collect_order_events(&mut receiver);
9439        assert_eq!(events.len(), 1);
9440        let OrderEventAny::Denied(denied) = &events[0] else {
9441            panic!("expected OrderDenied, was {:?}", events[0]);
9442        };
9443        assert!(
9444            denied.reason.contains("still awaiting finality"),
9445            "was: {}",
9446            denied.reason
9447        );
9448    }
9449
9450    #[tokio::test]
9451    async fn submit_order_denies_without_signer() {
9452        let Some((admin_pool, schema, mut client, _, _)) =
9453            swap_client_with_database("execution_submit_no_signer_test", swap_rpc_state().await)
9454                .await
9455        else {
9456            return;
9457        };
9458        client.signer = None;
9459        let order = test_market_sell_order(test_pool().instrument_id);
9460        let mut receiver = start_with_events(&mut client);
9461
9462        client.submit_order(submit_order_cmd(&order)).unwrap();
9463
9464        let events = collect_order_events(&mut receiver);
9465        assert_eq!(events.len(), 1);
9466        let OrderEventAny::Denied(denied) = &events[0] else {
9467            panic!("expected OrderDenied, was {:?}", events[0]);
9468        };
9469        assert!(
9470            denied.reason.contains("Signer not initialized"),
9471            "was: {}",
9472            denied.reason
9473        );
9474
9475        drop_execution_schema(&admin_pool, &schema).await;
9476    }
9477
9478    #[tokio::test]
9479    async fn submit_order_broadcasts_swap_and_records_client_order_id() {
9480        let Some((admin_pool, schema, mut client, state, _)) =
9481            swap_client_with_database("execution_submit_success_test", swap_rpc_state().await)
9482                .await
9483        else {
9484            return;
9485        };
9486        let order = test_market_sell_order(test_pool().instrument_id);
9487        let mut receiver = start_with_events(&mut client);
9488        let expected_min_out = expected_min_amount_out(50);
9489
9490        client.submit_order(submit_order_cmd(&order)).unwrap();
9491        await_pending_tasks(&client).await;
9492
9493        let events = collect_order_events(&mut receiver);
9494        assert_swap_submitted_and_filled(&events);
9495        let OrderEventAny::Submitted(submitted) = &events[0] else {
9496            panic!("expected OrderSubmitted, was {:?}", events[0]);
9497        };
9498        assert_eq!(submitted.client_order_id, order.client_order_id());
9499
9500        // The recorded broadcast matches the fully signed expected transaction
9501        let (expected_hash, expected_raw) = expected_swap_tx(expected_min_out).await;
9502        let broadcasts: Vec<_> = state
9503            .recorded_requests()
9504            .into_iter()
9505            .filter(|request| request["method"] == "eth_sendRawTransaction")
9506            .collect();
9507        assert_eq!(broadcasts.len(), 1);
9508        assert_eq!(broadcasts[0]["params"][0].as_str().unwrap(), expected_raw);
9509
9510        let record = client
9511            .cache
9512            .get_execution_transaction(42161, &expected_hash.to_string())
9513            .await
9514            .unwrap()
9515            .unwrap();
9516        assert_eq!(record.nonce, 7);
9517        assert_eq!(record.purpose, "swap");
9518        assert_eq!(record.status, "finalized");
9519        assert_eq!(
9520            record.client_order_id.as_deref(),
9521            Some(order.client_order_id().as_str())
9522        );
9523        assert!(client.in_flight.lock().is_none());
9524        let nonce_state = sqlx::query_as::<_, (i64, i64)>(sqlx::AssertSqlSafe(format!(
9525            "SELECT next_canonical_nonce, revision FROM {schema}.execution_verification_nonce"
9526        )))
9527        .fetch_one(&admin_pool)
9528        .await
9529        .unwrap();
9530        let decision_count = sqlx::query_scalar::<_, i64>(sqlx::AssertSqlSafe(format!(
9531            "SELECT COUNT(*) FROM {schema}.execution_verification_decision \
9532             WHERE outcome = 'verified'"
9533        )))
9534        .fetch_one(&admin_pool)
9535        .await
9536        .unwrap();
9537        let connect_decision_count = sqlx::query_scalar::<_, i64>(sqlx::AssertSqlSafe(format!(
9538            "SELECT COUNT(*) FROM {schema}.execution_verification_decision \
9539                 WHERE decision_class = 'connect' AND outcome = 'verified'"
9540        )))
9541        .fetch_one(&admin_pool)
9542        .await
9543        .unwrap();
9544        assert_eq!(nonce_state, (8, 1));
9545        assert_eq!(decision_count, 35);
9546        assert_eq!(connect_decision_count, 1);
9547
9548        drop_execution_schema(&admin_pool, &schema).await;
9549    }
9550
9551    #[tokio::test]
9552    async fn submit_order_pins_every_pre_sign_state_read_to_swap_anchor() {
9553        let Some((admin_pool, schema, mut client, state, _)) =
9554            swap_client_with_database("execution_submit_anchor_reads_test", swap_rpc_state().await)
9555                .await
9556        else {
9557            return;
9558        };
9559        let order = test_market_sell_order(test_pool().instrument_id);
9560        let mut receiver = start_with_events(&mut client);
9561
9562        client.submit_order(submit_order_cmd(&order)).unwrap();
9563        await_pending_tasks(&client).await;
9564
9565        let events = collect_order_events(&mut receiver);
9566        assert_swap_submitted_and_filled(&events);
9567        let requests = state.recorded_requests();
9568        let broadcast_index = requests
9569            .iter()
9570            .position(|request| request["method"] == "eth_sendRawTransaction")
9571            .unwrap();
9572        let pre_broadcast = &requests[..broadcast_index];
9573        let latest_reads: Vec<_> = pre_broadcast
9574            .iter()
9575            .filter(|request| {
9576                request["method"] == "eth_getBlockByNumber" && request["params"][0] == "latest"
9577            })
9578            .collect();
9579        assert_eq!(latest_reads.len(), 3);
9580        assert!(
9581            latest_reads
9582                .iter()
9583                .all(|request| request["params"] == serde_json::json!(["latest", false]))
9584        );
9585
9586        for method in [
9587            "eth_getCode",
9588            "eth_call",
9589            "eth_estimateGas",
9590            "eth_getBalance",
9591        ] {
9592            let pinned: Vec<_> = pre_broadcast
9593                .iter()
9594                .filter(|request| request["method"] == method)
9595                .collect();
9596            assert!(!pinned.is_empty(), "method {method}");
9597            assert_eq!(pinned.len() % 3, 0, "method {method}: {pinned:?}");
9598            assert!(
9599                pinned
9600                    .iter()
9601                    .all(|request| request["params"][1] == FIXTURE_BLOCK_PARAM),
9602                "method {method}: {pinned:?}"
9603            );
9604        }
9605
9606        let numbered_blocks: Vec<_> = pre_broadcast
9607            .iter()
9608            .filter(|request| {
9609                request["method"] == "eth_getBlockByNumber"
9610                    && request["params"][0] == FIXTURE_BLOCK_PARAM
9611            })
9612            .collect();
9613        assert!(!numbered_blocks.is_empty());
9614        assert_eq!(numbered_blocks.len() % 3, 0);
9615        assert!(
9616            numbered_blocks
9617                .iter()
9618                .all(|request| request["params"][1] == false)
9619        );
9620
9621        let chain_ids: Vec<_> = pre_broadcast
9622            .iter()
9623            .filter(|request| request["method"] == "eth_chainId")
9624            .collect();
9625        assert_eq!(chain_ids.len(), 3);
9626        assert!(
9627            chain_ids
9628                .iter()
9629                .all(|request| request["params"] == serde_json::json!([]))
9630        );
9631        let nonces: Vec<_> = pre_broadcast
9632            .iter()
9633            .filter(|request| request["method"] == "eth_getTransactionCount")
9634            .collect();
9635        assert_eq!(nonces.len(), 6);
9636        assert_eq!(
9637            nonces
9638                .iter()
9639                .filter(|request| {
9640                    request["params"] == serde_json::json!([WALLET.to_ascii_lowercase(), "pending"])
9641                })
9642                .count(),
9643            3
9644        );
9645        assert_eq!(
9646            nonces
9647                .iter()
9648                .filter(|request| {
9649                    request["params"]
9650                        == serde_json::json!([WALLET.to_ascii_lowercase(), FIXTURE_BLOCK_PARAM])
9651                })
9652                .count(),
9653            3
9654        );
9655        let priority_fees: Vec<_> = pre_broadcast
9656            .iter()
9657            .filter(|request| request["method"] == "eth_maxPriorityFeePerGas")
9658            .collect();
9659        assert_eq!(priority_fees.len(), 3);
9660        assert!(
9661            priority_fees
9662                .iter()
9663                .all(|request| request["params"] == serde_json::json!([]))
9664        );
9665
9666        let (_, expected_raw) = expected_swap_tx(expected_min_amount_out(50)).await;
9667        assert_eq!(
9668            requests[broadcast_index]["params"][0].as_str().unwrap(),
9669            expected_raw
9670        );
9671        let created_block: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
9672            "SELECT created_block FROM {schema}.execution_intent"
9673        )))
9674        .fetch_one(&admin_pool)
9675        .await
9676        .unwrap();
9677        assert_eq!(created_block, i64::try_from(FIXTURE_BLOCK).unwrap());
9678
9679        drop_execution_schema(&admin_pool, &schema).await;
9680    }
9681
9682    #[tokio::test]
9683    async fn submit_order_denies_changed_swap_anchor_before_signing() {
9684        let canonical = fixture_block_response(FIXTURE_BLOCK, B256::from([0x11; 32]));
9685        let changed = fixture_block_response(FIXTURE_BLOCK, B256::from([0x44; 32]));
9686        let state = swap_rpc_state().await.with_parameter_response_sequence(
9687            "eth_getBlockByNumber",
9688            FIXTURE_BLOCK_PARAM,
9689            &[
9690                &canonical, &canonical, &canonical, &canonical, &canonical, &canonical, &canonical,
9691                &canonical, &canonical, &canonical, &canonical, &canonical, &canonical, &canonical,
9692                &canonical, &changed, &changed, &changed,
9693            ],
9694        );
9695        let Some((admin_pool, schema, mut client, state, _)) =
9696            swap_client_with_database("execution_submit_anchor_change_test", state).await
9697        else {
9698            return;
9699        };
9700        let order = test_market_sell_order(test_pool().instrument_id);
9701        let mut receiver = start_with_events(&mut client);
9702
9703        client.submit_order(submit_order_cmd(&order)).unwrap();
9704        await_pending_tasks(&client).await;
9705
9706        let events = collect_order_events(&mut receiver);
9707        assert_eq!(events.len(), 1, "was: {events:?}");
9708        let OrderEventAny::Denied(denied) = &events[0] else {
9709            panic!("expected OrderDenied, was {:?}", events[0]);
9710        };
9711        assert!(
9712            denied
9713                .reason
9714                .contains("pre-sign checkpoint reread verification disagreed"),
9715            "was: {}",
9716            denied.reason
9717        );
9718        assert_eq!(
9719            execution_intent_markers(&admin_pool, &schema).await,
9720            vec![("swap".into(), "recoverable".into(), false, false)]
9721        );
9722        let signed_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
9723            "SELECT COUNT(*) FROM {schema}.execution_transaction_hash"
9724        )))
9725        .fetch_one(&admin_pool)
9726        .await
9727        .unwrap();
9728        assert_eq!(signed_count, 0);
9729        assert!(
9730            state
9731                .recorded_requests()
9732                .iter()
9733                .all(|request| { request["method"] != "eth_sendRawTransaction" })
9734        );
9735        assert!(client.in_flight.lock().is_none());
9736
9737        drop_execution_schema(&admin_pool, &schema).await;
9738    }
9739
9740    #[tokio::test]
9741    async fn submit_order_keeps_verified_swap_anchor_when_latest_advances() {
9742        let canonical = fixture_block_response(FIXTURE_BLOCK, B256::from([0x11; 32]));
9743        let newer = fixture_block_response(FIXTURE_BLOCK + 1, B256::from([0x44; 32]));
9744        let state = swap_rpc_state().await.with_parameter_response_sequence(
9745            "eth_getBlockByNumber",
9746            "latest",
9747            &[&canonical, &canonical, &canonical, &newer, &newer, &newer],
9748        );
9749        let Some((admin_pool, schema, mut client, state, _)) =
9750            swap_client_with_database("execution_submit_advancing_head_test", state).await
9751        else {
9752            return;
9753        };
9754        let order = test_market_sell_order(test_pool().instrument_id);
9755        let mut receiver = start_with_events(&mut client);
9756
9757        client.submit_order(submit_order_cmd(&order)).unwrap();
9758        await_pending_tasks(&client).await;
9759
9760        let events = collect_order_events(&mut receiver);
9761        assert_swap_submitted_and_filled(&events);
9762        let latest_reads = state
9763            .recorded_requests()
9764            .iter()
9765            .filter(|request| {
9766                request["method"] == "eth_getBlockByNumber" && request["params"][0] == "latest"
9767            })
9768            .count();
9769        assert_eq!(latest_reads, 3);
9770
9771        drop_execution_schema(&admin_pool, &schema).await;
9772    }
9773
9774    #[tokio::test]
9775    async fn submit_order_denies_changed_quote_watermark_before_signing() {
9776        let watermark_number = FIXTURE_BLOCK;
9777        let watermark_hash = B256::from([0x11; 32]);
9778        let canonical = fixture_block_response(watermark_number, watermark_hash);
9779        let changed = fixture_block_response(watermark_number, B256::from([0x66; 32]));
9780        let watermark_param = format!("0x{watermark_number:x}");
9781        let min_amount_out = expected_min_amount_out(50);
9782        let (tx_hash, _) = expected_swap_tx(min_amount_out).await;
9783        let head = finalized_swap_block(tx_hash, min_amount_out);
9784        let state = swap_rpc_state()
9785            .await
9786            .with_response("eth_getBlockByNumber", &head)
9787            .with_parameter_response_sequence(
9788                "eth_getBlockByNumber",
9789                &watermark_param,
9790                &[
9791                    &canonical, &canonical, &canonical, &canonical, &canonical, &canonical,
9792                    &canonical, &canonical, &canonical, &canonical, &canonical, &canonical,
9793                    &changed, &changed, &changed,
9794                ],
9795            );
9796        let Some((admin_pool, schema, mut client, state, cache)) =
9797            swap_client_with_database("execution_submit_watermark_change_test", state).await
9798        else {
9799            return;
9800        };
9801        let pool = test_pool();
9802        cache
9803            .borrow_mut()
9804            .add_pool_profiler(test_profiler_at_block(
9805                &pool,
9806                watermark_number,
9807                &watermark_hash.to_string(),
9808            ))
9809            .unwrap();
9810        let order = test_market_sell_order(pool.instrument_id);
9811        let mut receiver = start_with_events(&mut client);
9812
9813        client.submit_order(submit_order_cmd(&order)).unwrap();
9814        await_pending_tasks(&client).await;
9815
9816        let events = collect_order_events(&mut receiver);
9817        assert_eq!(events.len(), 1, "was: {events:?}");
9818        let OrderEventAny::Denied(denied) = &events[0] else {
9819            panic!("expected OrderDenied, was {:?}", events[0]);
9820        };
9821        assert!(
9822            denied.reason.contains("changed before signing"),
9823            "was: {}",
9824            denied.reason
9825        );
9826        assert_eq!(
9827            execution_intent_markers(&admin_pool, &schema).await,
9828            vec![("swap".into(), "recoverable".into(), false, false)]
9829        );
9830        let signed_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
9831            "SELECT COUNT(*) FROM {schema}.execution_transaction_hash"
9832        )))
9833        .fetch_one(&admin_pool)
9834        .await
9835        .unwrap();
9836        assert_eq!(signed_count, 0);
9837        assert!(
9838            state
9839                .recorded_requests()
9840                .iter()
9841                .all(|request| { request["method"] != "eth_sendRawTransaction" })
9842        );
9843        assert!(client.in_flight.lock().is_none());
9844
9845        drop_execution_schema(&admin_pool, &schema).await;
9846    }
9847
9848    #[tokio::test]
9849    async fn changed_swap_anchor_retains_ownership_when_recovery_commit_fails() {
9850        let canonical = fixture_block_response(FIXTURE_BLOCK, B256::from([0x11; 32]));
9851        let changed = fixture_block_response(FIXTURE_BLOCK, B256::from([0x44; 32]));
9852        let state = swap_rpc_state().await.with_parameter_response_sequence(
9853            "eth_getBlockByNumber",
9854            FIXTURE_BLOCK_PARAM,
9855            &[
9856                &canonical, &canonical, &canonical, &canonical, &canonical, &canonical, &canonical,
9857                &canonical, &canonical, &canonical, &canonical, &canonical, &canonical, &canonical,
9858                &canonical, &changed, &changed, &changed,
9859            ],
9860        );
9861        let Some((admin_pool, schema, mut client, state, _)) =
9862            swap_client_with_database("execution_submit_anchor_recovery_fail_test", state).await
9863        else {
9864            return;
9865        };
9866        install_recoverable_commit_rejection(&admin_pool, &schema).await;
9867        let order = test_market_sell_order(test_pool().instrument_id);
9868        let mut receiver = start_with_events(&mut client);
9869
9870        client.submit_order(submit_order_cmd(&order)).unwrap();
9871        await_pending_tasks(&client).await;
9872
9873        let events = collect_order_events(&mut receiver);
9874        assert_eq!(events.len(), 1, "was: {events:?}");
9875        let OrderEventAny::Denied(denied) = &events[0] else {
9876            panic!("expected OrderDenied, was {:?}", events[0]);
9877        };
9878        assert!(
9879            denied
9880                .reason
9881                .contains("pre-sign checkpoint reread verification disagreed"),
9882            "was: {}",
9883            denied.reason
9884        );
9885        assert_eq!(
9886            execution_intent_markers(&admin_pool, &schema).await,
9887            vec![("swap".into(), "prepared".into(), false, true)]
9888        );
9889        let signed_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
9890            "SELECT COUNT(*) FROM {schema}.execution_transaction_hash"
9891        )))
9892        .fetch_one(&admin_pool)
9893        .await
9894        .unwrap();
9895        assert_eq!(signed_count, 0);
9896        assert!(
9897            state
9898                .recorded_requests()
9899                .iter()
9900                .all(|request| { request["method"] != "eth_sendRawTransaction" })
9901        );
9902        assert!(matches!(
9903            *client.in_flight.lock(),
9904            Some(InFlightSlot::Preparing(TransactionPurpose::Swap))
9905        ));
9906
9907        drop_execution_schema(&admin_pool, &schema).await;
9908    }
9909
9910    #[rstest]
9911    #[case::filled("execution_inflight_finality_fill_test", false)]
9912    #[case::reverted("execution_inflight_finality_revert_test", true)]
9913    #[tokio::test]
9914    async fn swap_awaiting_finality_survives_inflight_exhaustion(
9915        #[case] test_name: &str,
9916        #[case] reverted: bool,
9917    ) {
9918        let min_amount_out = expected_min_amount_out(50);
9919        let (expected_hash, _) = expected_swap_tx(min_amount_out).await;
9920
9921        let state = if reverted {
9922            let block = finalized_swap_block(expected_hash, min_amount_out);
9923            let receipt = receipt_with_transaction_hash(RECEIPT_REVERTED, expected_hash);
9924            with_finalized_identity(
9925                swap_rpc_state()
9926                    .await
9927                    .with_response("eth_getTransactionReceipt", &receipt),
9928                &block,
9929                &receipt,
9930            )
9931        } else {
9932            finalized_swap_rpc_state(expected_hash, min_amount_out)
9933        };
9934
9935        let Some((admin_pool, schema, mut client, _state, cache)) =
9936            swap_client_with_database(test_name, state).await
9937        else {
9938            return;
9939        };
9940
9941        let order = test_market_sell_order(test_pool().instrument_id);
9942        let client_order_id = order.client_order_id();
9943        let mut receiver = start_with_events(&mut client);
9944
9945        let plan = client
9946            .prepare_swap(&submit_order_cmd(&order), &order)
9947            .unwrap();
9948        execute_swap(
9949            plan,
9950            client.transaction_executor().unwrap(),
9951            client.emitter.clone(),
9952            client.transaction_limits.max_quote_age_blocks,
9953            client.transaction_limits.deadline_seconds,
9954        )
9955        .await
9956        .unwrap();
9957        let swap_events = collect_order_events(&mut receiver);
9958        assert_eq!(swap_events.len(), 2, "was: {swap_events:?}");
9959        assert!(matches!(&swap_events[0], OrderEventAny::Submitted(_)));
9960
9961        let clock: Rc<RefCell<dyn Clock>> = Rc::new(RefCell::new(VirtualClock::new()));
9962        let config = ExecutionManagerConfig::default();
9963        let max_retries = config.inflight_max_retries;
9964        let mut manager = ExecutionManager::new(clock, Rc::clone(&cache), config).unwrap();
9965
9966        // The open-order check registers the client's retention requirement, as LiveNode does
9967        assert!(manager.check_open_orders(&[&client]).await.is_empty());
9968        cache.borrow_mut().update_order(&swap_events[0]).unwrap();
9969        tokio::time::pause();
9970        manager.register_submission(order.init_event(), Some(client.client_id()));
9971
9972        // Hold the finality outcome until well past the default in-flight budget
9973        let mut inflight_events = Vec::new();
9974        let mut queries_sent = 0;
9975
9976        for _ in 0..60 {
9977            tokio::time::advance(Duration::from_secs(1)).await;
9978            let result = manager.check_inflight_orders();
9979            inflight_events.extend(result.events);
9980
9981            for query in result.queries {
9982                let TradingCommand::QueryOrder(query) = query else {
9983                    panic!("expected QueryOrder, was {query:?}");
9984                };
9985
9986                client.query_order(query).unwrap();
9987                queries_sent += 1;
9988            }
9989        }
9990
9991        let exhaustions = manager.take_submission_recovery_exhaustions();
9992        let status_before_finality = cache.borrow().order(&client_order_id).unwrap().status();
9993        cache.borrow_mut().update_order(&swap_events[1]).unwrap();
9994        tokio::time::resume();
9995        drop_execution_schema(&admin_pool, &schema).await;
9996
9997        assert!(inflight_events.is_empty(), "was: {inflight_events:?}");
9998        assert_eq!(queries_sent, max_retries - 1);
9999        assert!(collect_order_events(&mut receiver).is_empty());
10000        assert_eq!(
10001            exhaustions,
10002            vec![SubmissionRecoveryExhausted {
10003                trader_id: order.trader_id(),
10004                client_id: Some(client.client_id()),
10005                strategy_id: order.strategy_id(),
10006                instrument_id: order.instrument_id(),
10007                client_order_id,
10008                source: SubmissionRecoverySource::Inflight,
10009                retry_count: max_retries,
10010                ts_event: UnixNanos::default(),
10011            }]
10012        );
10013        assert_eq!(status_before_finality, OrderStatus::Submitted);
10014        let cache = cache.borrow();
10015        let resolved = cache.order(&client_order_id).unwrap();
10016
10017        if reverted {
10018            let OrderEventAny::Rejected(rejected) = &swap_events[1] else {
10019                panic!("expected OrderRejected, was {:?}", swap_events[1]);
10020            };
10021
10022            assert_eq!(
10023                rejected.reason.as_str(),
10024                format!("Transaction {expected_hash} reverted on-chain")
10025            );
10026            assert!(!rejected.reconciliation);
10027            assert_eq!(resolved.status(), OrderStatus::Rejected);
10028            assert_eq!(
10029                resolved.filled_qty(),
10030                Quantity::zero(order.quantity().precision)
10031            );
10032        } else {
10033            assert!(matches!(&swap_events[1], OrderEventAny::Filled(_)));
10034            assert_eq!(resolved.status(), OrderStatus::Filled);
10035            assert_eq!(resolved.filled_qty(), order.quantity());
10036        }
10037    }
10038
10039    #[tokio::test]
10040    async fn finalized_swap_emits_exact_fill_once_and_refreshes_wallet() {
10041        let min_amount_out = expected_min_amount_out(50);
10042        let (expected_hash, _) = expected_swap_tx(min_amount_out).await;
10043        let state = finalized_swap_rpc_state(expected_hash, min_amount_out);
10044        let Some((admin_pool, schema, mut client, state, _)) =
10045            swap_client_with_database("execution_submit_fill_test", state).await
10046        else {
10047            return;
10048        };
10049        let order = test_market_sell_order(test_pool().instrument_id);
10050        let mut receiver = start_with_events(&mut client);
10051
10052        let plan = client
10053            .prepare_swap(&submit_order_cmd(&order), &order)
10054            .unwrap();
10055        execute_swap(
10056            plan,
10057            client.transaction_executor().unwrap(),
10058            client.emitter.clone(),
10059            client.transaction_limits.max_quote_age_blocks,
10060            client.transaction_limits.deadline_seconds,
10061        )
10062        .await
10063        .unwrap();
10064
10065        let (nonce, wallet_address, transaction_to, transaction_input, transaction_value): (
10066            i64,
10067            String,
10068            String,
10069            String,
10070            String,
10071        ) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
10072            "SELECT nonce, wallet_address, transaction_to, transaction_input, transaction_value \
10073             FROM {schema}.execution_intent"
10074        )))
10075        .fetch_one(&admin_pool)
10076        .await
10077        .unwrap();
10078        let finalized_block = client
10079            .http_rpc_client
10080            .block_by_number(FIXTURE_BLOCK + 1, true)
10081            .await
10082            .unwrap();
10083        let finalized_transaction = finalized_block
10084            .transactions
10085            .iter()
10086            .find(|transaction| transaction.hash == expected_hash)
10087            .unwrap();
10088        assert_eq!(finalized_transaction.from.to_string(), wallet_address);
10089        assert_eq!(finalized_transaction.nonce, u64::try_from(nonce).unwrap());
10090        assert_eq!(
10091            finalized_transaction.to,
10092            Some(Address::from_str(&transaction_to).unwrap())
10093        );
10094        assert_eq!(
10095            finalized_transaction.input.as_ref(),
10096            hex::decode(transaction_input.strip_prefix("0x").unwrap()).unwrap()
10097        );
10098        assert_eq!(
10099            finalized_transaction.value,
10100            U256::from_str(&transaction_value).unwrap()
10101        );
10102
10103        let mut order_events = Vec::new();
10104        let mut account_states = Vec::new();
10105
10106        while let Ok(event) = receiver.try_recv() {
10107            match event {
10108                ExecutionEvent::Order(event) => order_events.push(event),
10109                ExecutionEvent::Account(state) => account_states.push(state),
10110                other => panic!("unexpected execution event: {other:?}"),
10111            }
10112        }
10113        assert_eq!(order_events.len(), 2, "was: {order_events:?}");
10114        assert!(matches!(&order_events[0], OrderEventAny::Submitted(_)));
10115        let OrderEventAny::Filled(fill) = &order_events[1] else {
10116            panic!("expected OrderFilled, was {:?}", order_events[1]);
10117        };
10118        let expected_commission = Money::from_u256(
10119            U256::from(50_112_u64) * U256::from(100_000_000_u64),
10120            test_pool().chain.native_currency(),
10121        )
10122        .unwrap();
10123        assert_eq!(fill.client_order_id, order.client_order_id());
10124        assert_eq!(fill.venue_order_id.as_str(), expected_hash.to_string());
10125        assert_eq!(fill.order_side, OrderSide::Sell);
10126        assert_eq!(fill.last_qty, Quantity::from("0.001"));
10127        assert_eq!(fill.last_px, Price::from("1000"));
10128        assert_eq!(fill.currency.code, "USDC");
10129        assert_eq!(fill.commission, Some(expected_commission));
10130        assert_eq!(fill.liquidity_side, LiquiditySide::Taker);
10131        assert_eq!(account_states.len(), 1);
10132        let account_state = &account_states[0];
10133        assert_eq!(account_state.account_id, AccountId::from("BLOCKCHAIN-001"));
10134        assert_eq!(account_state.account_type, AccountType::Wallet);
10135        assert_eq!(account_state.base_currency, None);
10136        assert_eq!(account_state.balances.len(), 3);
10137        assert!(account_state.margins.is_empty());
10138        assert!(account_state.is_reported);
10139        assert_eq!(
10140            account_state.balances,
10141            client.wallet_balance.lock().as_account_balances().unwrap()
10142        );
10143
10144        let (fill_emitted, active): (bool, bool) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
10145            "SELECT fill_emitted, active FROM {schema}.execution_intent"
10146        )))
10147        .fetch_one(&admin_pool)
10148        .await
10149        .unwrap();
10150        assert!(fill_emitted);
10151        assert!(!active);
10152
10153        let database = client.cache.database.as_ref().unwrap().clone();
10154        let payload_keys = client.payload_keys.clone();
10155        let restart_config = client.config.clone();
10156        drop(client);
10157        let (mut restarted, _) = swap_client_with_cache(restart_config);
10158        restarted.cache.database = Some(database);
10159        restarted.payload_keys = payload_keys;
10160        restarted.signer = Some(Arc::new(
10161            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
10162        ));
10163        let mut restart_receiver = start_with_events(&mut restarted);
10164        restarted.reconcile_unresolved_execution().await.unwrap();
10165        restarted.reconcile_unresolved_execution().await.unwrap();
10166        assert!(collect_order_events(&mut restart_receiver).is_empty());
10167        let requests = state.recorded_requests();
10168        assert_eq!(
10169            requests
10170                .iter()
10171                .filter(|request| request["method"] == "eth_sendRawTransaction")
10172                .count(),
10173            1
10174        );
10175        assert_eq!(
10176            requests
10177                .iter()
10178                .filter(|request| {
10179                    request["method"] == "eth_call"
10180                        && request["params"][0]["data"]
10181                            .as_str()
10182                            .is_some_and(|data| data.starts_with(BALANCE_OF_SELECTOR))
10183                })
10184                .count(),
10185            9
10186        );
10187        assert_eq!(
10188            requests
10189                .iter()
10190                .filter(|request| request["method"] == "eth_getBalance")
10191                .count(),
10192            6
10193        );
10194
10195        drop_execution_schema(&admin_pool, &schema).await;
10196    }
10197
10198    #[tokio::test]
10199    async fn restart_emits_committed_swap_from_verified_inclusion_header() {
10200        let min_amount_out = expected_min_amount_out(50);
10201        let (expected_hash, _) = expected_swap_tx(min_amount_out).await;
10202        let state = finalized_swap_rpc_state(expected_hash, min_amount_out);
10203        let Some((admin_pool, schema, mut client, _, _)) =
10204            swap_client_with_database("execution_committed_fill_restart_test", state).await
10205        else {
10206            return;
10207        };
10208        let order = test_market_sell_order(test_pool().instrument_id);
10209        let mut receiver = start_with_events(&mut client);
10210        let plan = client
10211            .prepare_swap(&submit_order_cmd(&order), &order)
10212            .unwrap();
10213
10214        execute_swap(
10215            plan,
10216            client.transaction_executor().unwrap(),
10217            client.emitter.clone(),
10218            client.transaction_limits.max_quote_age_blocks,
10219            client.transaction_limits.deadline_seconds,
10220        )
10221        .await
10222        .unwrap();
10223        assert_swap_submitted_and_filled(&collect_order_events(&mut receiver));
10224        let ancestry_range: (i64, i64) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
10225            "SELECT height_start, height_end FROM {schema}.execution_verification_decision \
10226             WHERE decision_class = 'finality' AND read_class = 'numbered_block' \
10227               AND height_end > height_start"
10228        )))
10229        .fetch_one(&admin_pool)
10230        .await
10231        .unwrap();
10232        assert_eq!(
10233            ancestry_range,
10234            ((FIXTURE_BLOCK + 1) as i64, (FIXTURE_BLOCK + 2) as i64)
10235        );
10236
10237        sqlx::query(sqlx::AssertSqlSafe(format!(
10238            "UPDATE {schema}.execution_intent SET fill_emitted = FALSE, active = TRUE"
10239        )))
10240        .execute(&admin_pool)
10241        .await
10242        .unwrap();
10243        let database = client.cache.database.as_ref().unwrap().clone();
10244        let payload_keys = client.payload_keys.clone();
10245        let restart_config = client.config.clone();
10246        drop(client);
10247        let (mut restarted, _) = swap_client_with_cache(restart_config);
10248        restarted.cache.database = Some(database);
10249        restarted.payload_keys = payload_keys;
10250        restarted.signer = Some(Arc::new(
10251            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
10252        ));
10253        let mut restart_receiver = start_with_events(&mut restarted);
10254
10255        restarted.reconcile_unresolved_execution().await.unwrap();
10256
10257        let events = collect_order_events(&mut restart_receiver);
10258        let (fill_emitted, active): (bool, bool) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
10259            "SELECT fill_emitted, active FROM {schema}.execution_intent"
10260        )))
10261        .fetch_one(&admin_pool)
10262        .await
10263        .unwrap();
10264        assert_eq!(events.len(), 1, "was: {events:?}");
10265        assert!(matches!(&events[0], OrderEventAny::Filled(_)));
10266        assert!(fill_emitted);
10267        assert!(!active);
10268
10269        drop_execution_schema(&admin_pool, &schema).await;
10270    }
10271
10272    #[tokio::test]
10273    async fn finalized_swap_ignores_unrelated_swap_logs() {
10274        let min_amount_out = expected_min_amount_out(50);
10275        let (expected_hash, _) = expected_swap_tx(min_amount_out).await;
10276        let receipt = finalized_swap_receipt_with_unrelated_swap(expected_hash);
10277        let state = finalized_swap_rpc_state(expected_hash, min_amount_out)
10278            .with_response("eth_getTransactionReceipt", &receipt);
10279        let Some((admin_pool, schema, mut client, _, _)) =
10280            swap_client_with_database("execution_submit_unrelated_log_test", state).await
10281        else {
10282            return;
10283        };
10284        let order = test_market_sell_order(test_pool().instrument_id);
10285        let mut receiver = start_with_events(&mut client);
10286
10287        client.submit_order(submit_order_cmd(&order)).unwrap();
10288        await_pending_tasks(&client).await;
10289
10290        let events = collect_order_events(&mut receiver);
10291        assert_swap_submitted_and_filled(&events);
10292
10293        drop_execution_schema(&admin_pool, &schema).await;
10294    }
10295
10296    #[tokio::test]
10297    async fn prepare_swap_buy_accepts_quote_spend_exact_boundary() {
10298        let amount_in = expected_buy_amount_in();
10299        let min_amount_out = expected_buy_min_amount_out(50);
10300        let (expected_hash, _) = expected_buy_swap_tx(min_amount_out, amount_in).await;
10301        let state = finalized_buy_swap_rpc_state(expected_hash, min_amount_out, amount_in);
10302        let max_amount = amount_in.to_string();
10303        let Some((admin_pool, schema, client, _, cache)) = swap_client_with_database_config(
10304            "execution_prepare_buy_test",
10305            state,
10306            move |http_rpc_url| {
10307                let mut config = buy_test_config(http_rpc_url);
10308                config.quote_spend_limits.as_mut().unwrap()[0].max_amount = max_amount;
10309                config
10310            },
10311        )
10312        .await
10313        else {
10314            return;
10315        };
10316        let order = test_market_buy_order(test_pool().instrument_id);
10317        cache
10318            .borrow_mut()
10319            .add_order(order.clone(), None, None, true)
10320            .unwrap();
10321
10322        let plan = client
10323            .prepare_swap(&submit_order_cmd(&order), &order)
10324            .unwrap();
10325
10326        assert_eq!(plan.token_in, USDC_ADDRESS);
10327        assert_eq!(plan.token_out, WETH_ADDRESS);
10328        assert_eq!(plan.amount_in, amount_in);
10329        assert_eq!(plan.min_amount_out, min_amount_out);
10330        assert_ne!(plan.token_in, WETH_ADDRESS);
10331
10332        drop_execution_schema(&admin_pool, &schema).await;
10333    }
10334
10335    #[tokio::test]
10336    async fn submit_order_broadcasts_buy_swap() {
10337        let amount_in = expected_buy_amount_in();
10338        let min_amount_out = expected_buy_min_amount_out(50);
10339        let (expected_hash, expected_raw) = expected_buy_swap_tx(min_amount_out, amount_in).await;
10340        let state = finalized_buy_swap_rpc_state(expected_hash, min_amount_out, amount_in);
10341        let Some((admin_pool, schema, mut client, state, cache)) =
10342            swap_client_with_buy_database("execution_submit_buy_success_test", state).await
10343        else {
10344            return;
10345        };
10346        let order = test_market_buy_order(test_pool().instrument_id);
10347        cache
10348            .borrow_mut()
10349            .add_order(order.clone(), None, None, true)
10350            .unwrap();
10351        let mut receiver = start_with_events(&mut client);
10352
10353        client.submit_order(submit_order_cmd(&order)).unwrap();
10354        await_pending_tasks(&client).await;
10355
10356        let events = collect_order_events(&mut receiver);
10357        assert_swap_submitted_and_filled(&events);
10358        let broadcasts: Vec<_> = state
10359            .recorded_requests()
10360            .into_iter()
10361            .filter(|request| request["method"] == "eth_sendRawTransaction")
10362            .collect();
10363        assert_eq!(broadcasts.len(), 1);
10364        assert_eq!(broadcasts[0]["params"][0].as_str().unwrap(), expected_raw);
10365
10366        drop_execution_schema(&admin_pool, &schema).await;
10367    }
10368
10369    #[tokio::test]
10370    async fn finalized_buy_swap_emits_fill_from_output_leg() {
10371        let amount_in = expected_buy_amount_in();
10372        let min_amount_out = expected_buy_min_amount_out(50);
10373        let (expected_hash, _) = expected_buy_swap_tx(min_amount_out, amount_in).await;
10374        let state = finalized_buy_swap_rpc_state(expected_hash, min_amount_out, amount_in);
10375        let Some((admin_pool, schema, mut client, state, cache)) =
10376            swap_client_with_buy_database("execution_submit_buy_fill_test", state).await
10377        else {
10378            return;
10379        };
10380        let order = test_market_buy_order(test_pool().instrument_id);
10381        cache
10382            .borrow_mut()
10383            .add_order(order.clone(), None, None, true)
10384            .unwrap();
10385        let mut receiver = start_with_events(&mut client);
10386        let plan = client
10387            .prepare_swap(&submit_order_cmd(&order), &order)
10388            .unwrap();
10389
10390        execute_swap(
10391            plan,
10392            client.transaction_executor().unwrap(),
10393            client.emitter.clone(),
10394            client.transaction_limits.max_quote_age_blocks,
10395            client.transaction_limits.deadline_seconds,
10396        )
10397        .await
10398        .unwrap();
10399
10400        let mut order_events = Vec::new();
10401
10402        while let Ok(event) = receiver.try_recv() {
10403            if let ExecutionEvent::Order(event) = event {
10404                order_events.push(event);
10405            }
10406        }
10407        assert_eq!(order_events.len(), 2, "was: {order_events:?}");
10408        assert!(matches!(&order_events[0], OrderEventAny::Submitted(_)));
10409        let OrderEventAny::Filled(fill) = &order_events[1] else {
10410            panic!("expected OrderFilled, was {:?}", order_events[1]);
10411        };
10412        let expected_commission = Money::from_u256(
10413            U256::from(50_112_u64) * U256::from(100_000_000_u64),
10414            test_pool().chain.native_currency(),
10415        )
10416        .unwrap();
10417        assert_eq!(fill.client_order_id, order.client_order_id());
10418        assert_eq!(fill.venue_order_id.as_str(), expected_hash.to_string());
10419        assert_eq!(fill.order_side, OrderSide::Buy);
10420        assert_eq!(fill.last_qty, Quantity::from("0.001"));
10421        assert_eq!(fill.currency.code, "USDC");
10422        assert_eq!(fill.commission, Some(expected_commission));
10423        assert_eq!(fill.liquidity_side, LiquiditySide::Taker);
10424        assert_eq!(
10425            Money::from_decimal(
10426                fill.last_qty.as_decimal() * fill.last_px.as_decimal(),
10427                fill.currency,
10428            )
10429            .unwrap(),
10430            Money::from_u256(amount_in, fill.currency).unwrap()
10431        );
10432
10433        let (fill_emitted, active): (bool, bool) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
10434            "SELECT fill_emitted, active FROM {schema}.execution_intent"
10435        )))
10436        .fetch_one(&admin_pool)
10437        .await
10438        .unwrap();
10439        assert!(fill_emitted);
10440        assert!(!active);
10441
10442        let database = client.cache.database.as_ref().unwrap().clone();
10443        let payload_keys = client.payload_keys.clone();
10444        let restart_config = client.config.clone();
10445        drop(client);
10446        let (mut restarted, _) = swap_client_with_cache(restart_config);
10447        restarted.cache.database = Some(database);
10448        restarted.payload_keys = payload_keys;
10449        restarted.signer = Some(Arc::new(
10450            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
10451        ));
10452        let mut restart_receiver = start_with_events(&mut restarted);
10453        restarted.reconcile_unresolved_execution().await.unwrap();
10454        restarted.reconcile_unresolved_execution().await.unwrap();
10455        assert!(collect_order_events(&mut restart_receiver).is_empty());
10456        assert_eq!(
10457            state
10458                .recorded_requests()
10459                .iter()
10460                .filter(|request| request["method"] == "eth_sendRawTransaction")
10461                .count(),
10462            1
10463        );
10464
10465        drop_execution_schema(&admin_pool, &schema).await;
10466    }
10467
10468    #[tokio::test]
10469    async fn finalized_buy_swap_cancels_remainder_when_output_is_short() {
10470        let amount_in = expected_buy_amount_in();
10471        let min_amount_out = expected_buy_min_amount_out(50);
10472        let (expected_hash, _) = expected_buy_swap_tx(min_amount_out, amount_in).await;
10473        let short_base_out = min_amount_out;
10474        let mut state = finalized_buy_swap_rpc_state(expected_hash, min_amount_out, amount_in);
10475        state = state.with_response(
10476            "eth_getTransactionReceipt",
10477            &finalized_buy_swap_receipt_with_base_out(expected_hash, amount_in, short_base_out),
10478        );
10479        let Some((admin_pool, schema, mut client, _, cache)) =
10480            swap_client_with_buy_database("execution_submit_buy_short_fill_test", state).await
10481        else {
10482            return;
10483        };
10484        let order = test_market_buy_order(test_pool().instrument_id);
10485        cache
10486            .borrow_mut()
10487            .add_order(order.clone(), None, None, true)
10488            .unwrap();
10489        let mut receiver = start_with_events(&mut client);
10490        let plan = client
10491            .prepare_swap(&submit_order_cmd(&order), &order)
10492            .unwrap();
10493
10494        execute_swap(
10495            plan,
10496            client.transaction_executor().unwrap(),
10497            client.emitter.clone(),
10498            client.transaction_limits.max_quote_age_blocks,
10499            client.transaction_limits.deadline_seconds,
10500        )
10501        .await
10502        .unwrap();
10503
10504        let events = collect_order_events(&mut receiver);
10505        assert_eq!(events.len(), 3, "was: {events:?}");
10506        assert!(matches!(&events[0], OrderEventAny::Submitted(_)));
10507        let OrderEventAny::Filled(fill) = &events[1] else {
10508            panic!("expected OrderFilled, was {:?}", events[1]);
10509        };
10510        let expected_qty = raw_amount_to_quantity(short_base_out, 18).unwrap();
10511        assert_eq!(fill.order_side, OrderSide::Buy);
10512        assert_eq!(fill.last_qty, expected_qty);
10513        assert!(fill.last_qty < order.quantity());
10514        let OrderEventAny::Canceled(canceled) = &events[2] else {
10515            panic!("expected OrderCanceled, was {:?}", events[2]);
10516        };
10517        assert_eq!(canceled.client_order_id, order.client_order_id());
10518        assert_eq!(
10519            canceled.venue_order_id.as_ref().map(VenueOrderId::as_str),
10520            Some(expected_hash.to_string().as_str())
10521        );
10522
10523        drop_execution_schema(&admin_pool, &schema).await;
10524    }
10525
10526    #[tokio::test]
10527    async fn finalized_buy_swap_reports_full_output_when_above_order_quantity() {
10528        let amount_in = expected_buy_amount_in();
10529        let min_amount_out = expected_buy_min_amount_out(50);
10530        let (expected_hash, _) = expected_buy_swap_tx(min_amount_out, amount_in).await;
10531        let overshoot_base_out = expected_buy_base_amount() * U256::from(2) + U256::from(44);
10532        let mut state = finalized_buy_swap_rpc_state(expected_hash, min_amount_out, amount_in);
10533        state = state.with_response(
10534            "eth_getTransactionReceipt",
10535            &finalized_buy_swap_receipt_with_base_out(expected_hash, amount_in, overshoot_base_out),
10536        );
10537        let Some((admin_pool, schema, mut client, _, cache)) =
10538            swap_client_with_buy_database("execution_submit_buy_overshoot_fill_test", state).await
10539        else {
10540            return;
10541        };
10542        let order = test_market_buy_order(test_pool().instrument_id);
10543        cache
10544            .borrow_mut()
10545            .add_order(order.clone(), None, None, true)
10546            .unwrap();
10547        let mut receiver = start_with_events(&mut client);
10548        let plan = client
10549            .prepare_swap(&submit_order_cmd(&order), &order)
10550            .unwrap();
10551
10552        execute_swap(
10553            plan,
10554            client.transaction_executor().unwrap(),
10555            client.emitter.clone(),
10556            client.transaction_limits.max_quote_age_blocks,
10557            client.transaction_limits.deadline_seconds,
10558        )
10559        .await
10560        .unwrap();
10561
10562        let events = collect_order_events(&mut receiver);
10563        assert_eq!(events.len(), 2, "was: {events:?}");
10564        assert!(matches!(&events[0], OrderEventAny::Submitted(_)));
10565        let OrderEventAny::Filled(fill) = &events[1] else {
10566            panic!("expected OrderFilled, was {:?}", events[1]);
10567        };
10568        let expected_qty = raw_amount_to_quantity(overshoot_base_out, 18).unwrap();
10569        assert_eq!(fill.order_side, OrderSide::Buy);
10570        assert_eq!(fill.last_qty, expected_qty);
10571        assert!(fill.last_qty > order.quantity());
10572        assert_eq!(
10573            Money::from_decimal(
10574                fill.last_qty.as_decimal() * fill.last_px.as_decimal(),
10575                fill.currency,
10576            )
10577            .unwrap(),
10578            Money::from_u256(amount_in, fill.currency).unwrap()
10579        );
10580
10581        drop_execution_schema(&admin_pool, &schema).await;
10582    }
10583
10584    #[tokio::test]
10585    async fn finalized_buy_swap_quarantines_sell_oriented_log() {
10586        let amount_in = expected_buy_amount_in();
10587        let min_amount_out = expected_buy_min_amount_out(50);
10588        let (expected_hash, _) = expected_buy_swap_tx(min_amount_out, amount_in).await;
10589        let state = finalized_buy_swap_rpc_state(expected_hash, min_amount_out, amount_in)
10590            .with_response(
10591                "eth_getTransactionReceipt",
10592                &finalized_swap_receipt(expected_hash),
10593            );
10594        let Some((admin_pool, schema, mut client, _, cache)) =
10595            swap_client_with_buy_database("execution_submit_buy_sell_log_test", state).await
10596        else {
10597            return;
10598        };
10599        let order = test_market_buy_order(test_pool().instrument_id);
10600        cache
10601            .borrow_mut()
10602            .add_order(order.clone(), None, None, true)
10603            .unwrap();
10604        let mut receiver = start_with_events(&mut client);
10605        let plan = client
10606            .prepare_swap(&submit_order_cmd(&order), &order)
10607            .unwrap();
10608
10609        let error = execute_swap(
10610            plan,
10611            client.transaction_executor().unwrap(),
10612            client.emitter.clone(),
10613            client.transaction_limits.max_quote_age_blocks,
10614            client.transaction_limits.deadline_seconds,
10615        )
10616        .await
10617        .unwrap_err();
10618
10619        let events = collect_order_events(&mut receiver);
10620        assert_swap_quarantined_without_terminal_event(&events);
10621        assert!(
10622            error
10623                .to_string()
10624                .contains("does not match the persisted amount")
10625                || error.to_string().contains("is not a BUY output"),
10626            "was: {error}"
10627        );
10628        assert!(client.in_flight.lock().is_some());
10629
10630        drop_execution_schema(&admin_pool, &schema).await;
10631    }
10632
10633    #[tokio::test]
10634    async fn submit_order_applies_buy_slippage_to_base_output() {
10635        let amount_in = expected_buy_amount_in();
10636        let min_amount_out = expected_buy_min_amount_out(200);
10637        let (expected_hash, expected_raw) = expected_buy_swap_tx(min_amount_out, amount_in).await;
10638        let state = finalized_buy_swap_rpc_state(expected_hash, min_amount_out, amount_in);
10639        let Some((admin_pool, schema, mut client, state, cache)) =
10640            swap_client_with_buy_database("execution_submit_buy_slippage_test", state).await
10641        else {
10642            return;
10643        };
10644        let order = test_market_buy_order(test_pool().instrument_id);
10645        cache
10646            .borrow_mut()
10647            .add_order(order.clone(), None, None, true)
10648            .unwrap();
10649        let mut cmd = submit_order_cmd(&order);
10650        cmd.params = Some(serde_json::from_str(r#"{"slippage_bps": 200}"#).unwrap());
10651        let mut receiver = start_with_events(&mut client);
10652
10653        client.submit_order(cmd).unwrap();
10654        await_pending_tasks(&client).await;
10655
10656        let events = collect_order_events(&mut receiver);
10657        assert_swap_submitted_and_filled(&events);
10658        let broadcasts: Vec<_> = state
10659            .recorded_requests()
10660            .into_iter()
10661            .filter(|request| request["method"] == "eth_sendRawTransaction")
10662            .collect();
10663        assert_eq!(broadcasts.len(), 1);
10664        assert_eq!(broadcasts[0]["params"][0].as_str().unwrap(), expected_raw);
10665
10666        drop_execution_schema(&admin_pool, &schema).await;
10667    }
10668
10669    #[tokio::test]
10670    async fn submit_order_denies_buy_on_insufficient_quote_balance() {
10671        let amount_in = expected_buy_amount_in();
10672        let min_amount_out = expected_buy_min_amount_out(50);
10673        let (expected_hash, _) = expected_buy_swap_tx(min_amount_out, amount_in).await;
10674        let state = finalized_buy_swap_rpc_state(expected_hash, min_amount_out, amount_in)
10675            .with_call_response(BALANCE_OF_SELECTOR, CALL_ZERO);
10676        let Some((admin_pool, schema, mut client, _, cache)) =
10677            swap_client_with_buy_database("execution_submit_buy_balance_test", state).await
10678        else {
10679            return;
10680        };
10681        let order = test_market_buy_order(test_pool().instrument_id);
10682        cache
10683            .borrow_mut()
10684            .add_order(order.clone(), None, None, true)
10685            .unwrap();
10686        let mut receiver = start_with_events(&mut client);
10687
10688        client.submit_order(submit_order_cmd(&order)).unwrap();
10689        await_pending_tasks(&client).await;
10690
10691        let events = collect_order_events(&mut receiver);
10692        assert_eq!(events.len(), 1);
10693        let OrderEventAny::Denied(denied) = &events[0] else {
10694            panic!("expected OrderDenied, was {:?}", events[0]);
10695        };
10696        assert!(
10697            denied.reason.contains("is below the swap amount"),
10698            "was: {}",
10699            denied.reason
10700        );
10701
10702        drop_execution_schema(&admin_pool, &schema).await;
10703    }
10704
10705    #[tokio::test]
10706    async fn finalized_swap_without_log_stays_quarantined() {
10707        let state = swap_rpc_state()
10708            .await
10709            .with_response("eth_getTransactionReceipt", RECEIPT_SUCCESS);
10710        let Some((admin_pool, schema, mut client, _, _)) =
10711            swap_client_with_database("execution_submit_missing_log_test", state).await
10712        else {
10713            return;
10714        };
10715        let order = test_market_sell_order(test_pool().instrument_id);
10716        let mut receiver = start_with_events(&mut client);
10717
10718        client.submit_order(submit_order_cmd(&order)).unwrap();
10719        await_pending_tasks(&client).await;
10720
10721        let events = collect_order_events(&mut receiver);
10722        let (status, terminal_emitted, active): (String, bool, bool) =
10723            sqlx::query_as(sqlx::AssertSqlSafe(format!(
10724                "SELECT status, terminal_emitted, active FROM {schema}.execution_intent"
10725            )))
10726            .fetch_one(&admin_pool)
10727            .await
10728            .unwrap();
10729
10730        assert_swap_quarantined_without_terminal_event(&events);
10731        assert_eq!(status, "broadcast");
10732        assert!(!terminal_emitted);
10733        assert!(active);
10734        assert!(client.in_flight.lock().is_some());
10735
10736        drop_execution_schema(&admin_pool, &schema).await;
10737    }
10738
10739    #[tokio::test]
10740    async fn finalized_swap_refresh_failure_stays_owned_for_reconciliation() {
10741        let min_amount_out = expected_min_amount_out(50);
10742        let (expected_hash, _) = expected_swap_tx(min_amount_out).await;
10743        let state = finalized_swap_rpc_state(expected_hash, min_amount_out).with_response_sequence(
10744            "eth_getBalance",
10745            &[
10746                GET_BALANCE,
10747                GET_BALANCE,
10748                GET_BALANCE,
10749                RPC_METHOD_NOT_FOUND,
10750                RPC_METHOD_NOT_FOUND,
10751                RPC_METHOD_NOT_FOUND,
10752            ],
10753        );
10754        let Some((admin_pool, schema, mut client, _state, _)) =
10755            swap_client_with_database("execution_submit_refresh_fail_test", state).await
10756        else {
10757            return;
10758        };
10759        let order = test_market_sell_order(test_pool().instrument_id);
10760        let mut receiver = start_with_events(&mut client);
10761        let plan = client
10762            .prepare_swap(&submit_order_cmd(&order), &order)
10763            .unwrap();
10764
10765        let error = execute_swap(
10766            plan,
10767            client.transaction_executor().unwrap(),
10768            client.emitter.clone(),
10769            client.transaction_limits.max_quote_age_blocks,
10770            client.transaction_limits.deadline_seconds,
10771        )
10772        .await
10773        .unwrap_err();
10774        let events = collect_order_events(&mut receiver);
10775        let (status, fill_emitted, active): (String, bool, bool) =
10776            sqlx::query_as(sqlx::AssertSqlSafe(format!(
10777                "SELECT status, fill_emitted, active FROM {schema}.execution_intent"
10778            )))
10779            .fetch_one(&admin_pool)
10780            .await
10781            .unwrap();
10782
10783        assert!(
10784            error
10785                .to_string()
10786                .contains("finalized native balance verification is locally invalid"),
10787            "was: {error}"
10788        );
10789        assert_eq!(events.len(), 1, "was: {events:?}");
10790        assert!(matches!(&events[0], OrderEventAny::Submitted(_)));
10791        assert_eq!(status, "broadcast");
10792        assert!(!fill_emitted);
10793        assert!(active);
10794        assert!(client.in_flight.lock().is_some());
10795
10796        drop_execution_schema(&admin_pool, &schema).await;
10797    }
10798
10799    #[tokio::test]
10800    async fn submit_order_denies_router_factory_mismatch_before_signing() {
10801        let state = swap_rpc_state()
10802            .await
10803            .with_call_response(FACTORY_SELECTOR, CALL_ZERO);
10804        let Some((admin_pool, schema, mut client, state, _)) =
10805            swap_client_with_database("execution_submit_factory_test", state).await
10806        else {
10807            return;
10808        };
10809        let order = test_market_sell_order(test_pool().instrument_id);
10810        let mut receiver = start_with_events(&mut client);
10811
10812        client.submit_order(submit_order_cmd(&order)).unwrap();
10813        await_pending_tasks(&client).await;
10814
10815        let events = collect_order_events(&mut receiver);
10816        assert_eq!(events.len(), 1);
10817        let OrderEventAny::Denied(denied) = &events[0] else {
10818            panic!("expected OrderDenied, was {:?}", events[0]);
10819        };
10820        assert!(
10821            denied
10822                .reason
10823                .contains("swap deployment manifest verification disagreed"),
10824            "was: {}",
10825            denied.reason
10826        );
10827        let requests = state.recorded_requests();
10828        assert!(
10829            requests
10830                .iter()
10831                .all(|request| request["method"] != "eth_getTransactionCount")
10832        );
10833        assert!(
10834            requests
10835                .iter()
10836                .all(|request| request["method"] != "eth_sendRawTransaction")
10837        );
10838
10839        drop_execution_schema(&admin_pool, &schema).await;
10840    }
10841
10842    #[tokio::test]
10843    async fn submit_order_denies_router_weth_mismatch_before_signing() {
10844        let state = swap_rpc_state()
10845            .await
10846            .with_call_response(WETH9_SELECTOR, CALL_ZERO);
10847        let Some((admin_pool, schema, mut client, state, _)) =
10848            swap_client_with_database("execution_submit_weth_test", state).await
10849        else {
10850            return;
10851        };
10852        let order = test_market_sell_order(test_pool().instrument_id);
10853        let mut receiver = start_with_events(&mut client);
10854
10855        client.submit_order(submit_order_cmd(&order)).unwrap();
10856        await_pending_tasks(&client).await;
10857
10858        let events = collect_order_events(&mut receiver);
10859        assert_eq!(events.len(), 1);
10860        let OrderEventAny::Denied(denied) = &events[0] else {
10861            panic!("expected OrderDenied, was {:?}", events[0]);
10862        };
10863        assert!(
10864            denied
10865                .reason
10866                .contains("swap deployment manifest verification disagreed"),
10867            "was: {}",
10868            denied.reason
10869        );
10870        let requests = state.recorded_requests();
10871        assert!(
10872            requests
10873                .iter()
10874                .all(|request| request["method"] != "eth_getTransactionCount")
10875        );
10876        assert!(
10877            requests
10878                .iter()
10879                .all(|request| request["method"] != "eth_sendRawTransaction")
10880        );
10881
10882        drop_execution_schema(&admin_pool, &schema).await;
10883    }
10884
10885    #[tokio::test]
10886    async fn submit_order_denies_factory_pool_mismatch_before_signing() {
10887        let state = swap_rpc_state()
10888            .await
10889            .with_call_response(GET_POOL_SELECTOR, CALL_ZERO);
10890        let Some((admin_pool, schema, mut client, state, _)) =
10891            swap_client_with_database("execution_submit_pool_identity_test", state).await
10892        else {
10893            return;
10894        };
10895        let order = test_market_sell_order(test_pool().instrument_id);
10896        let mut receiver = start_with_events(&mut client);
10897
10898        client.submit_order(submit_order_cmd(&order)).unwrap();
10899        await_pending_tasks(&client).await;
10900
10901        let events = collect_order_events(&mut receiver);
10902        assert_eq!(events.len(), 1);
10903        let OrderEventAny::Denied(denied) = &events[0] else {
10904            panic!("expected OrderDenied, was {:?}", events[0]);
10905        };
10906        assert!(
10907            denied
10908                .reason
10909                .contains("swap deployment manifest verification disagreed"),
10910            "was: {}",
10911            denied.reason
10912        );
10913        let requests = state.recorded_requests();
10914        assert!(
10915            requests
10916                .iter()
10917                .all(|request| request["method"] != "eth_getTransactionCount")
10918        );
10919        assert!(
10920            requests
10921                .iter()
10922                .all(|request| request["method"] != "eth_sendRawTransaction")
10923        );
10924
10925        drop_execution_schema(&admin_pool, &schema).await;
10926    }
10927
10928    #[tokio::test]
10929    async fn submit_order_denies_cached_token_decimal_mismatch_before_signing() {
10930        let state = swap_rpc_state().await.with_contract_call_response(
10931            WETH,
10932            DECIMALS_SELECTOR,
10933            CALL_DECIMALS_6,
10934        );
10935        let Some((admin_pool, schema, mut client, state, _)) =
10936            swap_client_with_database("execution_submit_decimals_test", state).await
10937        else {
10938            return;
10939        };
10940        let order = test_market_sell_order(test_pool().instrument_id);
10941        let mut receiver = start_with_events(&mut client);
10942
10943        client.submit_order(submit_order_cmd(&order)).unwrap();
10944        await_pending_tasks(&client).await;
10945
10946        let events = collect_order_events(&mut receiver);
10947        assert_eq!(events.len(), 1);
10948        let OrderEventAny::Denied(denied) = &events[0] else {
10949            panic!("expected OrderDenied, was {:?}", events[0]);
10950        };
10951        assert!(
10952            denied
10953                .reason
10954                .contains("swap deployment manifest verification disagreed"),
10955            "was: {}",
10956            denied.reason
10957        );
10958        let requests = state.recorded_requests();
10959        assert!(
10960            requests
10961                .iter()
10962                .all(|request| request["method"] != "eth_getTransactionCount")
10963        );
10964        assert!(
10965            requests
10966                .iter()
10967                .all(|request| request["method"] != "eth_sendRawTransaction")
10968        );
10969
10970        drop_execution_schema(&admin_pool, &schema).await;
10971    }
10972
10973    #[tokio::test]
10974    async fn submit_order_denies_on_insufficient_router_allowance() {
10975        let state = swap_rpc_state()
10976            .await
10977            .with_call_response(ALLOWANCE_SELECTOR, CALL_ZERO);
10978        let Some((admin_pool, schema, mut client, state, _)) =
10979            swap_client_with_database("execution_submit_allowance_test", state).await
10980        else {
10981            return;
10982        };
10983        let order = test_market_sell_order(test_pool().instrument_id);
10984        let mut receiver = start_with_events(&mut client);
10985
10986        client.submit_order(submit_order_cmd(&order)).unwrap();
10987        await_pending_tasks(&client).await;
10988
10989        let events = collect_order_events(&mut receiver);
10990        assert_eq!(events.len(), 1);
10991        let OrderEventAny::Denied(denied) = &events[0] else {
10992            panic!("expected OrderDenied, was {:?}", events[0]);
10993        };
10994        assert!(
10995            denied.reason.contains("below the swap amount"),
10996            "was: {}",
10997            denied.reason
10998        );
10999        let requests = state.recorded_requests();
11000        assert!(
11001            requests
11002                .iter()
11003                .all(|request| request["method"] != "eth_sendRawTransaction"),
11004            "no broadcast may follow a pre-trade denial"
11005        );
11006        let row_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
11007            "SELECT COUNT(*) FROM {schema}.execution_intent"
11008        )))
11009        .fetch_one(&admin_pool)
11010        .await
11011        .unwrap();
11012        assert_eq!(row_count, 0);
11013        assert!(client.in_flight.lock().is_none());
11014
11015        drop_execution_schema(&admin_pool, &schema).await;
11016    }
11017
11018    #[tokio::test]
11019    async fn submit_order_denies_on_insufficient_input_balance() {
11020        let state = swap_rpc_state()
11021            .await
11022            .with_call_response(BALANCE_OF_SELECTOR, CALL_ZERO);
11023        let Some((admin_pool, schema, mut client, _, _)) =
11024            swap_client_with_database("execution_submit_balance_test", state).await
11025        else {
11026            return;
11027        };
11028        let order = test_market_sell_order(test_pool().instrument_id);
11029        let mut receiver = start_with_events(&mut client);
11030
11031        client.submit_order(submit_order_cmd(&order)).unwrap();
11032        await_pending_tasks(&client).await;
11033
11034        let events = collect_order_events(&mut receiver);
11035        assert_eq!(events.len(), 1);
11036        let OrderEventAny::Denied(denied) = &events[0] else {
11037            panic!("expected OrderDenied, was {:?}", events[0]);
11038        };
11039        assert!(
11040            denied.reason.contains("is below the swap amount"),
11041            "was: {}",
11042            denied.reason
11043        );
11044
11045        drop_execution_schema(&admin_pool, &schema).await;
11046    }
11047
11048    #[tokio::test]
11049    async fn submit_order_denies_on_insufficient_native_balance() {
11050        let state = swap_rpc_state()
11051            .await
11052            .with_response("eth_getBalance", GET_BALANCE_INSUFFICIENT);
11053        let Some((admin_pool, schema, mut client, state, _)) =
11054            swap_client_with_database("execution_submit_native_balance_test", state).await
11055        else {
11056            return;
11057        };
11058        let order = test_market_sell_order(test_pool().instrument_id);
11059        let mut receiver = start_with_events(&mut client);
11060
11061        client.submit_order(submit_order_cmd(&order)).unwrap();
11062        await_pending_tasks(&client).await;
11063
11064        let events = collect_order_events(&mut receiver);
11065        assert_eq!(events.len(), 1);
11066        let OrderEventAny::Denied(denied) = &events[0] else {
11067            panic!("expected OrderDenied, was {:?}", events[0]);
11068        };
11069        assert!(
11070            denied.reason.contains("below maximum transaction cost"),
11071            "was: {}",
11072            denied.reason
11073        );
11074        assert!(
11075            state
11076                .recorded_requests()
11077                .iter()
11078                .all(|request| request["method"] != "eth_sendRawTransaction"),
11079            "no broadcast may follow an insufficient native balance"
11080        );
11081        let (row_count, status): (i64, Option<String>) = sqlx::query_as(sqlx::AssertSqlSafe(
11082            format!("SELECT COUNT(*), MAX(status) FROM {schema}.execution_intent"),
11083        ))
11084        .fetch_one(&admin_pool)
11085        .await
11086        .unwrap();
11087        assert_eq!(row_count, 1);
11088        assert_eq!(status.as_deref(), Some("recoverable"));
11089        assert!(client.in_flight.lock().is_none());
11090
11091        drop_execution_schema(&admin_pool, &schema).await;
11092    }
11093
11094    #[tokio::test]
11095    async fn submit_order_denies_on_stale_quote() {
11096        let Some((admin_pool, schema, mut client, state, cache)) =
11097            swap_client_with_database("execution_submit_stale_quote_test", swap_rpc_state().await)
11098                .await
11099        else {
11100            return;
11101        };
11102        let pool = test_pool();
11103        cache
11104            .borrow_mut()
11105            .add_pool_profiler(test_profiler(&pool, FIXTURE_BLOCK - 101))
11106            .unwrap();
11107        let order = test_market_sell_order(pool.instrument_id);
11108        let mut receiver = start_with_events(&mut client);
11109
11110        client.submit_order(submit_order_cmd(&order)).unwrap();
11111        await_pending_tasks(&client).await;
11112
11113        let events = collect_order_events(&mut receiver);
11114        assert_eq!(events.len(), 1);
11115        let OrderEventAny::Denied(denied) = &events[0] else {
11116            panic!("expected OrderDenied, was {:?}", events[0]);
11117        };
11118        assert!(
11119            denied.reason.contains("Stale quote"),
11120            "was: {}",
11121            denied.reason
11122        );
11123        let requests = state.recorded_requests();
11124        assert!(
11125            requests
11126                .iter()
11127                .all(|request| request["method"] != "eth_sendRawTransaction"),
11128            "no broadcast may follow a pre-trade denial"
11129        );
11130
11131        drop_execution_schema(&admin_pool, &schema).await;
11132    }
11133
11134    #[tokio::test]
11135    async fn submit_order_denies_quote_ahead_of_chain_head() {
11136        let Some((admin_pool, schema, mut client, state, cache)) =
11137            swap_client_with_database("execution_submit_ahead_quote_test", swap_rpc_state().await)
11138                .await
11139        else {
11140            return;
11141        };
11142        let pool = test_pool();
11143        cache
11144            .borrow_mut()
11145            .add_pool_profiler(test_profiler(&pool, FIXTURE_BLOCK + 1))
11146            .unwrap();
11147        let order = test_market_sell_order(pool.instrument_id);
11148        let mut receiver = start_with_events(&mut client);
11149
11150        client.submit_order(submit_order_cmd(&order)).unwrap();
11151        await_pending_tasks(&client).await;
11152
11153        let events = collect_order_events(&mut receiver);
11154        assert_eq!(events.len(), 1);
11155        let OrderEventAny::Denied(denied) = &events[0] else {
11156            panic!("expected OrderDenied, was {:?}", events[0]);
11157        };
11158        assert!(
11159            denied.reason.contains("is ahead of the latest block"),
11160            "was: {}",
11161            denied.reason
11162        );
11163        let requests = state.recorded_requests();
11164        assert!(
11165            requests
11166                .iter()
11167                .all(|request| request["method"] != "eth_sendRawTransaction"),
11168            "no broadcast may follow a pre-trade denial"
11169        );
11170
11171        drop_execution_schema(&admin_pool, &schema).await;
11172    }
11173
11174    #[tokio::test]
11175    async fn submit_order_denies_on_deadline_overflow() {
11176        let Some((admin_pool, schema, mut client, state, _)) = swap_client_with_database(
11177            "execution_submit_deadline_overflow_test",
11178            swap_rpc_state().await,
11179        )
11180        .await
11181        else {
11182            return;
11183        };
11184        client.transaction_limits.deadline_seconds = u64::MAX;
11185        let order = test_market_sell_order(test_pool().instrument_id);
11186        let mut receiver = start_with_events(&mut client);
11187
11188        client.submit_order(submit_order_cmd(&order)).unwrap();
11189        await_pending_tasks(&client).await;
11190
11191        let events = collect_order_events(&mut receiver);
11192        assert_eq!(events.len(), 1);
11193        let OrderEventAny::Denied(denied) = &events[0] else {
11194            panic!("expected OrderDenied, was {:?}", events[0]);
11195        };
11196        assert!(
11197            denied.reason.contains("deadline overflow"),
11198            "was: {}",
11199            denied.reason
11200        );
11201        let requests = state.recorded_requests();
11202        assert!(
11203            requests
11204                .iter()
11205                .all(|request| request["method"] != "eth_sendRawTransaction"),
11206            "no broadcast may follow a pre-trade denial"
11207        );
11208        assert!(client.in_flight.lock().is_none());
11209
11210        drop_execution_schema(&admin_pool, &schema).await;
11211    }
11212
11213    #[tokio::test]
11214    async fn submit_order_reconciles_node_rejection_to_finalized_receipt() {
11215        let state = swap_rpc_state()
11216            .await
11217            .with_response("eth_sendRawTransaction", SEND_RAW_TRANSACTION_REJECTED);
11218        let Some((admin_pool, schema, mut client, _state, _)) =
11219            swap_client_with_database("execution_submit_node_rejected_test", state).await
11220        else {
11221            return;
11222        };
11223        let order = test_market_sell_order(test_pool().instrument_id);
11224        let mut receiver = start_with_events(&mut client);
11225
11226        client.submit_order(submit_order_cmd(&order)).unwrap();
11227        await_pending_tasks(&client).await;
11228
11229        let events = collect_order_events(&mut receiver);
11230        assert_swap_submitted_and_filled(&events);
11231        let (purpose, status, client_order_id): (String, String, Option<String>) =
11232            sqlx::query_as(sqlx::AssertSqlSafe(format!(
11233                "SELECT purpose, status, client_order_id FROM {schema}.execution_intent"
11234            )))
11235            .fetch_one(&admin_pool)
11236            .await
11237            .unwrap();
11238        assert_eq!(purpose, "swap");
11239        assert_eq!(status, "finalized");
11240        assert_eq!(
11241            client_order_id.as_deref(),
11242            Some(order.client_order_id().as_str())
11243        );
11244        assert!(client.in_flight.lock().is_none());
11245
11246        drop_execution_schema(&admin_pool, &schema).await;
11247    }
11248
11249    #[tokio::test]
11250    async fn submit_order_acknowledges_uncertain_nonce_too_low() {
11251        let state = swap_rpc_state()
11252            .await
11253            .with_response("eth_sendRawTransaction", SEND_RAW_TRANSACTION_NONCE_TOO_LOW);
11254        let Some((admin_pool, schema, mut client, _state, _)) =
11255            swap_client_with_database("execution_submit_nonce_too_low_test", state).await
11256        else {
11257            return;
11258        };
11259        let order = test_market_sell_order(test_pool().instrument_id);
11260        let mut receiver = start_with_events(&mut client);
11261
11262        client.submit_order(submit_order_cmd(&order)).unwrap();
11263        await_pending_tasks(&client).await;
11264
11265        let events = collect_order_events(&mut receiver);
11266        assert_swap_submitted_and_filled(&events);
11267        let (purpose, status, client_order_id): (String, String, Option<String>) =
11268            sqlx::query_as(sqlx::AssertSqlSafe(format!(
11269                "SELECT purpose, status, client_order_id FROM {schema}.execution_intent"
11270            )))
11271            .fetch_one(&admin_pool)
11272            .await
11273            .unwrap();
11274        assert_eq!(purpose, "swap");
11275        assert_eq!(status, "finalized");
11276        assert_eq!(
11277            client_order_id.as_deref(),
11278            Some(order.client_order_id().as_str())
11279        );
11280        assert!(client.in_flight.lock().is_none());
11281
11282        drop_execution_schema(&admin_pool, &schema).await;
11283    }
11284
11285    #[tokio::test]
11286    async fn submit_order_rejected_on_reverted_receipt() {
11287        let expected_min_out = expected_min_amount_out(50);
11288        let (expected_hash, _) = expected_swap_tx(expected_min_out).await;
11289        let block = finalized_swap_block(expected_hash, expected_min_out);
11290        let receipt = receipt_with_transaction_hash(RECEIPT_REVERTED, expected_hash);
11291        let state = with_finalized_identity(
11292            swap_rpc_state()
11293                .await
11294                .with_response("eth_getTransactionReceipt", &receipt),
11295            &block,
11296            &receipt,
11297        );
11298        let Some((admin_pool, schema, mut client, _state, _)) =
11299            swap_client_with_database("execution_submit_reverted_test", state).await
11300        else {
11301            return;
11302        };
11303        let order = test_market_sell_order(test_pool().instrument_id);
11304        let mut receiver = start_with_events(&mut client);
11305        client.submit_order(submit_order_cmd(&order)).unwrap();
11306        await_pending_tasks(&client).await;
11307
11308        let events = collect_order_events(&mut receiver);
11309        assert_eq!(events.len(), 2);
11310        assert!(
11311            matches!(&events[0], OrderEventAny::Submitted(_)),
11312            "was: {:?}",
11313            events[0]
11314        );
11315        let OrderEventAny::Rejected(rejected) = &events[1] else {
11316            panic!("expected OrderRejected, was {:?}", events[1]);
11317        };
11318        assert!(
11319            rejected.reason.contains("reverted on-chain"),
11320            "was: {}",
11321            rejected.reason
11322        );
11323
11324        let record = client
11325            .cache
11326            .get_execution_transaction(42161, &expected_hash.to_string())
11327            .await
11328            .unwrap()
11329            .unwrap();
11330        assert_eq!(record.purpose, "swap");
11331        assert_eq!(record.status, "reverted");
11332        assert_eq!(
11333            record.client_order_id.as_deref(),
11334            Some(order.client_order_id().as_str())
11335        );
11336        assert!(client.in_flight.lock().is_none());
11337
11338        drop_execution_schema(&admin_pool, &schema).await;
11339    }
11340
11341    #[tokio::test]
11342    async fn submit_order_acknowledges_ambiguous_broadcast() {
11343        // A response the client cannot parse classifies as an ambiguous broadcast failure:
11344        // the transaction may be live, so the durable intent remains submitted and occupied
11345        let state = swap_rpc_state()
11346            .await
11347            .with_response("eth_sendRawTransaction", "not json");
11348        let Some((admin_pool, schema, mut client, _state, _)) =
11349            swap_client_with_database("execution_submit_ambiguous_test", state).await
11350        else {
11351            return;
11352        };
11353        let order = test_market_sell_order(test_pool().instrument_id);
11354        let mut receiver = start_with_events(&mut client);
11355
11356        client.submit_order(submit_order_cmd(&order)).unwrap();
11357        await_pending_tasks(&client).await;
11358
11359        let events = collect_order_events(&mut receiver);
11360        assert_swap_submitted_and_filled(&events);
11361
11362        let record = client
11363            .cache
11364            .get_execution_transaction(
11365                42161,
11366                &expected_swap_tx(expected_min_amount_out(50))
11367                    .await
11368                    .0
11369                    .to_string(),
11370            )
11371            .await
11372            .unwrap()
11373            .unwrap();
11374        assert_eq!(record.purpose, "swap");
11375        assert_eq!(record.status, "finalized");
11376        assert!(client.in_flight.lock().is_none());
11377
11378        drop_execution_schema(&admin_pool, &schema).await;
11379    }
11380
11381    #[tokio::test]
11382    async fn submit_order_acknowledges_broadcast_hash_mismatch() {
11383        // The static fixture's hash differs from the locally signed hash, so acceptance of the
11384        // signed transaction is unverified until the independent receipt reaches finality
11385        let state = swap_rpc_state_for_mismatch().await;
11386        let Some((admin_pool, schema, mut client, _state, _)) =
11387            swap_client_with_database("execution_submit_hash_mismatch_test", state).await
11388        else {
11389            return;
11390        };
11391        let order = test_market_sell_order(test_pool().instrument_id);
11392        let mut receiver = start_with_events(&mut client);
11393
11394        client.submit_order(submit_order_cmd(&order)).unwrap();
11395        await_pending_tasks(&client).await;
11396
11397        let events = collect_order_events(&mut receiver);
11398        assert_swap_submitted_and_filled(&events);
11399
11400        let record = client
11401            .cache
11402            .get_execution_transaction(
11403                42161,
11404                &expected_swap_tx(expected_min_amount_out(50))
11405                    .await
11406                    .0
11407                    .to_string(),
11408            )
11409            .await
11410            .unwrap()
11411            .unwrap();
11412        assert_eq!(record.purpose, "swap");
11413        assert_eq!(record.status, "finalized");
11414        assert!(client.in_flight.lock().is_none());
11415
11416        drop_execution_schema(&admin_pool, &schema).await;
11417    }
11418
11419    #[tokio::test]
11420    async fn submit_order_persists_authenticated_envelope_before_broadcast() {
11421        let state = swap_rpc_state().await;
11422        let Some((admin_pool, schema, mut client, state, _)) =
11423            swap_client_with_database("protected_submit_test", state).await
11424        else {
11425            return;
11426        };
11427        let order = test_market_sell_order(test_pool().instrument_id);
11428        let mut receiver = start_with_events(&mut client);
11429
11430        client.submit_order(submit_order_cmd(&order)).unwrap();
11431        await_pending_tasks(&client).await;
11432
11433        let events = collect_order_events(&mut receiver);
11434        assert_swap_submitted_and_filled(&events);
11435        let representations: Vec<(bool, bool)> = sqlx::query_as(sqlx::AssertSqlSafe(format!(
11436            "SELECT raw_transaction IS NULL, sealed_transaction IS NOT NULL \
11437             FROM {schema}.execution_transaction_hash WHERE payload_expected"
11438        )))
11439        .fetch_all(&admin_pool)
11440        .await
11441        .unwrap();
11442        let broadcasts = state
11443            .recorded_requests()
11444            .into_iter()
11445            .filter(|request| request["method"] == "eth_sendRawTransaction")
11446            .count();
11447        assert_eq!(representations, vec![(true, true)]);
11448        assert_eq!(broadcasts, 1);
11449
11450        drop_execution_schema(&admin_pool, &schema).await;
11451    }
11452
11453    #[tokio::test]
11454    async fn protected_persistence_failure_prevents_broadcast_and_acknowledgment() {
11455        let state = swap_rpc_state().await;
11456        let Some((admin_pool, schema, mut client, state, _)) =
11457            swap_client_with_database("protected_persist_failure_test", state).await
11458        else {
11459            return;
11460        };
11461
11462        for statement in [
11463            format!(
11464                "CREATE FUNCTION {schema}.reject_protected_payload() RETURNS trigger \
11465                 LANGUAGE plpgsql AS 'BEGIN RAISE EXCEPTION ''test payload rejection''; END'"
11466            ),
11467            format!(
11468                "CREATE TRIGGER reject_protected_payload BEFORE INSERT ON \
11469                 {schema}.execution_transaction_hash FOR EACH ROW \
11470                 EXECUTE FUNCTION {schema}.reject_protected_payload()"
11471            ),
11472        ] {
11473            sqlx::query(sqlx::AssertSqlSafe(statement))
11474                .execute(&admin_pool)
11475                .await
11476                .unwrap();
11477        }
11478        let order = test_market_sell_order(test_pool().instrument_id);
11479        let mut receiver = start_with_events(&mut client);
11480
11481        client.submit_order(submit_order_cmd(&order)).unwrap();
11482        await_pending_tasks(&client).await;
11483
11484        let events = collect_order_events(&mut receiver);
11485        assert!(
11486            events
11487                .iter()
11488                .all(|event| !matches!(event, OrderEventAny::Submitted(_)))
11489        );
11490        let broadcasts = state
11491            .recorded_requests()
11492            .into_iter()
11493            .filter(|request| request["method"] == "eth_sendRawTransaction")
11494            .count();
11495        let payload_rows: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
11496            "SELECT COUNT(*) FROM {schema}.execution_transaction_hash"
11497        )))
11498        .fetch_one(&admin_pool)
11499        .await
11500        .unwrap();
11501        assert_eq!(broadcasts, 0);
11502        assert_eq!(payload_rows, 0);
11503
11504        drop_execution_schema(&admin_pool, &schema).await;
11505    }
11506
11507    #[tokio::test]
11508    async fn submit_order_reservation_failure_denies_without_broadcast_and_releases_slot() {
11509        let Some((admin_pool, schema, mut client, state, cache)) =
11510            swap_client_with_database("execution_submit_persist_fail_test", swap_rpc_state().await)
11511                .await
11512        else {
11513            return;
11514        };
11515        sqlx::query(sqlx::AssertSqlSafe(format!(
11516            "DROP TABLE {schema}.execution_intent CASCADE"
11517        )))
11518        .execute(&admin_pool)
11519        .await
11520        .unwrap();
11521        let pool = test_pool();
11522        let first = test_market_sell_order(pool.instrument_id);
11523        let second = market_sell_order_with_id(pool.instrument_id, "O-SWAP-002");
11524        cache
11525            .borrow_mut()
11526            .add_order(second.clone(), None, None, false)
11527            .unwrap();
11528        let mut receiver = start_with_events(&mut client);
11529
11530        client.submit_order(submit_order_cmd(&first)).unwrap();
11531        await_pending_tasks(&client).await;
11532
11533        let events = collect_order_events(&mut receiver);
11534        assert_eq!(events.len(), 1);
11535        let OrderEventAny::Denied(denied) = &events[0] else {
11536            panic!("expected OrderDenied, was {:?}", events[0]);
11537        };
11538        assert!(
11539            denied
11540                .reason
11541                .contains("Execution intent reservation failed before commit"),
11542            "was: {}",
11543            denied.reason
11544        );
11545        client.submit_order(submit_order_cmd(&second)).unwrap();
11546        await_pending_tasks(&client).await;
11547        let retry_events = collect_order_events(&mut receiver);
11548        assert_eq!(retry_events.len(), 1);
11549        let OrderEventAny::Denied(retry_denied) = &retry_events[0] else {
11550            panic!("expected OrderDenied, was {:?}", retry_events[0]);
11551        };
11552        assert_eq!(
11553            retry_denied.reason,
11554            "Execution intent reservation failed before commit"
11555        );
11556        let broadcasts = state
11557            .recorded_requests()
11558            .into_iter()
11559            .filter(|request| request["method"] == "eth_sendRawTransaction")
11560            .count();
11561        assert_eq!(broadcasts, 0);
11562        assert!(client.in_flight.lock().is_none());
11563
11564        drop_execution_schema(&admin_pool, &schema).await;
11565    }
11566
11567    #[tokio::test]
11568    async fn submit_order_reservation_commit_failure_keeps_preparing_slot() {
11569        let Some((admin_pool, schema, mut client, state, cache)) = swap_client_with_database(
11570            "execution_submit_reservation_commit_fail_test",
11571            swap_rpc_state().await,
11572        )
11573        .await
11574        else {
11575            return;
11576        };
11577        install_reservation_commit_rejection(&admin_pool, &schema).await;
11578        let pool = test_pool();
11579        let first = test_market_sell_order(pool.instrument_id);
11580        let second = market_sell_order_with_id(pool.instrument_id, "O-SWAP-002");
11581        cache
11582            .borrow_mut()
11583            .add_order(second.clone(), None, None, false)
11584            .unwrap();
11585        let mut receiver = start_with_events(&mut client);
11586
11587        client.submit_order(submit_order_cmd(&first)).unwrap();
11588        await_pending_tasks(&client).await;
11589        client.submit_order(submit_order_cmd(&second)).unwrap();
11590        await_pending_tasks(&client).await;
11591
11592        let events = collect_order_events(&mut receiver);
11593        let submitted = events
11594            .iter()
11595            .filter(|event| matches!(event, OrderEventAny::Submitted(_)))
11596            .count();
11597        let denied_commit = events
11598            .iter()
11599            .filter(|event| {
11600                matches!(event, OrderEventAny::Denied(denied) if denied.reason == "Execution intent reservation commit outcome is unknown; reconciliation is required")
11601            })
11602            .count();
11603        let denied_in_flight = events
11604            .iter()
11605            .filter(|event| {
11606                matches!(event, OrderEventAny::Denied(denied) if denied.reason.contains("at most one transaction can be in flight"))
11607            })
11608            .count();
11609        let requests = state.recorded_requests();
11610        let intent_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
11611            "SELECT COUNT(*) FROM {schema}.execution_intent"
11612        )))
11613        .fetch_one(&admin_pool)
11614        .await
11615        .unwrap();
11616        let signed_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
11617            "SELECT COUNT(*) FROM {schema}.execution_transaction_hash"
11618        )))
11619        .fetch_one(&admin_pool)
11620        .await
11621        .unwrap();
11622
11623        assert_eq!(submitted, 0, "was: {events:?}");
11624        assert_eq!(denied_commit, 1, "was: {events:?}");
11625        assert_eq!(denied_in_flight, 1, "was: {events:?}");
11626        assert!(matches!(
11627            *client.in_flight.lock(),
11628            Some(InFlightSlot::Preparing(TransactionPurpose::Swap))
11629        ));
11630        assert_eq!(intent_count, 0);
11631        assert_eq!(signed_count, 0);
11632        assert!(
11633            requests
11634                .iter()
11635                .all(|request| request["method"] != "eth_getTransactionCount")
11636        );
11637        assert!(
11638            requests
11639                .iter()
11640                .all(|request| request["method"] != "eth_sendRawTransaction")
11641        );
11642
11643        drop_execution_schema(&admin_pool, &schema).await;
11644    }
11645
11646    #[tokio::test]
11647    async fn protected_payload_migration_failure_keeps_plaintext_and_blocks_ready() {
11648        let Some((admin_pool, schema, client, _)) = execution_client_with_unprotected_database(
11649            "protected_payload_migration_failure",
11650            execution_rpc_state(),
11651        )
11652        .await
11653        else {
11654            return;
11655        };
11656        let database = client.cache.database.as_ref().unwrap();
11657        let (intent, _) = persist_invalid_test_swap(database, None).await;
11658        let keys = payload_test_keys([0x31; 32], vec![], "migration-failure");
11659        let error = database
11660            .ensure_execution_payload_storage(&keys)
11661            .await
11662            .unwrap_err();
11663
11664        assert!(
11665            error
11666                .to_string()
11667                .contains("failed to authenticate execution payload")
11668        );
11669        let row = database
11670            .get_execution_transaction_hashes(intent.id)
11671            .await
11672            .unwrap()
11673            .pop()
11674            .unwrap();
11675        let operation: String = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
11676            "SELECT operation FROM {schema}.execution_payload_state"
11677        )))
11678        .fetch_one(&admin_pool)
11679        .await
11680        .unwrap();
11681        assert_eq!(operation, "migrate");
11682        assert!(row.raw_transaction.is_some());
11683        assert!(row.sealed_transaction.is_none());
11684
11685        drop_execution_schema(&admin_pool, &schema).await;
11686    }
11687
11688    #[tokio::test]
11689    async fn protected_payload_migration_restart_restore_rewrap_and_rollback() {
11690        let Some((admin_pool, pg_config)) =
11691            connect_test_postgres("protected payload lifecycle").await
11692        else {
11693            return;
11694        };
11695        let schema = format!("protected_payload_lifecycle_{}", std::process::id());
11696        setup_execution_schema(&admin_pool, &schema).await;
11697        let options: sqlx::postgres::PgConnectOptions = pg_config.into();
11698        let options = options.options([("search_path", schema.clone())]);
11699        let database = connect_test_database(options.clone()).await.unwrap();
11700        database
11701            .ensure_execution_transaction_schema()
11702            .await
11703            .unwrap();
11704        let intent = reserve_test_wrap_intent(&database).await;
11705        database
11706            .assign_execution_intent_nonce(intent.id, 7)
11707            .await
11708            .unwrap();
11709        let transaction = build_eip1559_transaction(
11710            42161,
11711            7,
11712            78_000,
11713            130_000_000,
11714            10_000_000,
11715            WETH_ADDRESS,
11716            U256::from(1_u64),
11717            Bytes::from(hex::decode("d0e30db0").unwrap()),
11718        );
11719        let (transaction_hash, raw_transaction) = sign_eip1559_transaction(
11720            transaction,
11721            &PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
11722        )
11723        .await
11724        .unwrap();
11725        database
11726            .add_execution_transaction_hash(
11727                intent.id,
11728                42161,
11729                &transaction_hash.to_string(),
11730                &raw_transaction,
11731            )
11732            .await
11733            .unwrap();
11734        let keys = payload_test_keys([0x41; 32], vec![], "restore-a");
11735
11736        database
11737            .ensure_execution_payload_storage(&keys)
11738            .await
11739            .unwrap();
11740        let operation: String = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
11741            "SELECT operation FROM {schema}.execution_payload_state"
11742        )))
11743        .fetch_one(&admin_pool)
11744        .await
11745        .unwrap();
11746        assert_eq!(operation, "ready");
11747        drop(database);
11748
11749        let restarted = connect_test_database(options).await.unwrap();
11750        restarted
11751            .ensure_execution_payload_storage(&keys)
11752            .await
11753            .unwrap();
11754        let row = restarted
11755            .get_execution_transaction_hashes(intent.id)
11756            .await
11757            .unwrap()
11758            .pop()
11759            .unwrap();
11760        assert!(row.raw_transaction.is_none());
11761        let sealed_transaction = row.sealed_transaction.clone().unwrap();
11762        let stored_intent = restarted.get_execution_intent(intent.id).await.unwrap();
11763        let context = payload_context(&stored_intent, &row, keys.deployment_id()).unwrap();
11764        let alternate_envelope = keys.seal(&raw_transaction, &context).unwrap();
11765        assert_ne!(alternate_envelope, sealed_transaction);
11766        let repeated = restarted
11767            .add_execution_transaction_envelope(
11768                intent.id,
11769                42161,
11770                &transaction_hash.to_string(),
11771                &alternate_envelope,
11772            )
11773            .await
11774            .unwrap();
11775        let check = restarted
11776            .check_execution_payload_storage(Some(&keys), None, 1)
11777            .await
11778            .unwrap();
11779        assert_eq!(
11780            repeated.sealed_transaction,
11781            Some(sealed_transaction.clone())
11782        );
11783        assert_eq!(check.plaintext_rows, 0);
11784        assert_eq!(check.original_rows, 1);
11785        assert_eq!(check.replacement_rows, 0);
11786        assert_eq!(check.authenticated_rows, 1);
11787        assert!(!check.read_roles.is_empty());
11788
11789        sqlx::query(sqlx::AssertSqlSafe(format!(
11790            "UPDATE {schema}.execution_payload_key_state SET seals = 4294967295"
11791        )))
11792        .execute(&admin_pool)
11793        .await
11794        .unwrap();
11795        let exhausted = restarted
11796            .reserve_execution_payload_seal(&keys)
11797            .await
11798            .unwrap_err();
11799        assert!(exhausted.to_string().contains("seal limit"));
11800        let seals: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
11801            "SELECT seals FROM {schema}.execution_payload_key_state"
11802        )))
11803        .fetch_one(&admin_pool)
11804        .await
11805        .unwrap();
11806        assert_eq!(seals, 4_294_967_295);
11807        sqlx::query(sqlx::AssertSqlSafe(format!(
11808            "UPDATE {schema}.execution_payload_key_state SET seals = 1"
11809        )))
11810        .execute(&admin_pool)
11811        .await
11812        .unwrap();
11813
11814        let missing_key = restarted
11815            .check_execution_payload_storage(None, None, 1)
11816            .await
11817            .unwrap_err();
11818        assert!(
11819            missing_key
11820                .to_string()
11821                .contains("no payload key is configured")
11822        );
11823        let restored_elsewhere = payload_test_keys([0x41; 32], vec![], "restore-b");
11824        let wrong_context = restarted
11825            .ensure_execution_payload_storage(&restored_elsewhere)
11826            .await
11827            .unwrap_err();
11828        assert!(wrong_context.to_string().contains("deployment ID"));
11829
11830        sqlx::query(sqlx::AssertSqlSafe(format!(
11831            "UPDATE {schema}.execution_transaction_hash \
11832             SET sealed_transaction = set_byte(sealed_transaction, octet_length(sealed_transaction) - 1, \
11833                 get_byte(sealed_transaction, octet_length(sealed_transaction) - 1) # 1)"
11834        )))
11835        .execute(&admin_pool)
11836        .await
11837        .unwrap();
11838        assert!(
11839            restarted
11840                .check_execution_payload_storage(Some(&keys), None, 1)
11841                .await
11842                .is_err()
11843        );
11844        sqlx::query(sqlx::AssertSqlSafe(format!(
11845            "UPDATE {schema}.execution_transaction_hash SET sealed_transaction = $1"
11846        )))
11847        .bind(&sealed_transaction)
11848        .execute(&admin_pool)
11849        .await
11850        .unwrap();
11851
11852        for statement in [
11853            format!(
11854                "ALTER TABLE {schema}.execution_transaction_hash \
11855                 DROP CONSTRAINT execution_transaction_payload_protected_check"
11856            ),
11857            format!("UPDATE {schema}.execution_transaction_hash SET sealed_transaction = NULL"),
11858        ] {
11859            sqlx::query(sqlx::AssertSqlSafe(statement))
11860                .execute(&admin_pool)
11861                .await
11862                .unwrap();
11863        }
11864        assert!(
11865            restarted
11866                .check_execution_payload_storage(Some(&keys), None, 1)
11867                .await
11868                .is_err()
11869        );
11870        sqlx::query(sqlx::AssertSqlSafe(format!(
11871            "UPDATE {schema}.execution_transaction_hash SET sealed_transaction = $1"
11872        )))
11873        .bind(&sealed_transaction)
11874        .execute(&admin_pool)
11875        .await
11876        .unwrap();
11877        sqlx::query(sqlx::AssertSqlSafe(format!(
11878            "ALTER TABLE {schema}.execution_transaction_hash \
11879             ADD CONSTRAINT execution_transaction_payload_protected_check CHECK ( \
11880                 (payload_expected AND raw_transaction IS NULL AND sealed_transaction IS NOT NULL) \
11881                 OR (NOT payload_expected AND raw_transaction IS NULL AND sealed_transaction IS NULL) \
11882             )"
11883        )))
11884        .execute(&admin_pool)
11885        .await
11886        .unwrap();
11887
11888        let rotated = payload_test_keys([0x52; 32], vec![[0x41; 32]], "restore-a");
11889        restarted
11890            .rewrap_execution_payload_storage(&rotated, 1)
11891            .await
11892            .unwrap();
11893        let rotated_check = restarted
11894            .check_execution_payload_storage(Some(&rotated), None, 1)
11895            .await
11896            .unwrap();
11897        assert_eq!(rotated_check.authenticated_rows, 1);
11898        assert_eq!(rotated_check.key_ids.len(), 1);
11899
11900        restarted
11901            .rollback_execution_payload_storage(&rotated, 1)
11902            .await
11903            .unwrap();
11904        let rolled_back = restarted
11905            .get_execution_transaction_hashes(intent.id)
11906            .await
11907            .unwrap()
11908            .pop()
11909            .unwrap();
11910        assert_eq!(
11911            rolled_back.raw_transaction.as_deref(),
11912            Some(raw_transaction.as_slice())
11913        );
11914        assert!(rolled_back.sealed_transaction.is_none());
11915        let legacy_check = restarted
11916            .check_execution_payload_storage(None, None, 1)
11917            .await
11918            .unwrap();
11919        assert!(!legacy_check.protected);
11920        assert_eq!(legacy_check.plaintext_rows, 1);
11921        assert_eq!(legacy_check.authenticated_rows, 1);
11922
11923        drop_execution_schema(&admin_pool, &schema).await;
11924    }
11925
11926    #[tokio::test]
11927    async fn protected_payload_storage_authenticates_multiple_execution_identities() {
11928        let Some((admin_pool, pg_config)) =
11929            connect_test_postgres("protected payload multiple identities").await
11930        else {
11931            return;
11932        };
11933        let schema = format!("protected_payload_identities_{}", std::process::id());
11934        setup_execution_schema(&admin_pool, &schema).await;
11935        let options: sqlx::postgres::PgConnectOptions = pg_config.into();
11936        let database = connect_test_database(options.options([("search_path", schema.clone())]))
11937            .await
11938            .unwrap();
11939        database
11940            .ensure_execution_transaction_schema()
11941            .await
11942            .unwrap();
11943
11944        let private_keys = [
11945            TEST_PRIVATE_KEY,
11946            "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d",
11947        ];
11948
11949        for (index, private_key) in private_keys.into_iter().enumerate() {
11950            let signer = PrivateKeySigner::from_str(private_key).unwrap();
11951            let nonce = 7 + u64::try_from(index).unwrap();
11952            let intent =
11953                reserve_test_wrap_intent_for_wallet(&database, &signer.address().to_string()).await;
11954            database
11955                .assign_execution_intent_nonce(intent.id, nonce)
11956                .await
11957                .unwrap();
11958            let transaction = build_eip1559_transaction(
11959                42161,
11960                nonce,
11961                78_000,
11962                130_000_000,
11963                10_000_000,
11964                WETH_ADDRESS,
11965                U256::from(1_u64),
11966                Bytes::from(hex::decode("d0e30db0").unwrap()),
11967            );
11968            let (transaction_hash, raw_transaction) =
11969                sign_eip1559_transaction(transaction, &signer)
11970                    .await
11971                    .unwrap();
11972            database
11973                .add_execution_transaction_hash(
11974                    intent.id,
11975                    42161,
11976                    &transaction_hash.to_string(),
11977                    &raw_transaction,
11978                )
11979                .await
11980                .unwrap();
11981        }
11982
11983        let keys = payload_test_keys([0x61; 32], vec![], "multiple-identities");
11984        database
11985            .ensure_execution_payload_storage(&keys)
11986            .await
11987            .unwrap();
11988        let lease = database
11989            .require_execution_payload_storage(
11990                &keys,
11991                PayloadPolicy {
11992                    chain_id: 42161,
11993                    signer: Address::from_str(WALLET).unwrap(),
11994                    gas_limit: 1_000_000,
11995                    max_fee_per_gas: 1_000_000_000,
11996                },
11997                1,
11998            )
11999            .await
12000            .unwrap();
12001        drop(lease);
12002        let check = database
12003            .check_execution_payload_storage(Some(&keys), None, 1)
12004            .await
12005            .unwrap();
12006
12007        assert!(check.protected);
12008        assert_eq!(check.plaintext_rows, 0);
12009        assert_eq!(check.original_rows, 2);
12010        assert_eq!(check.replacement_rows, 0);
12011        assert_eq!(check.authenticated_rows, 2);
12012        assert_eq!(check.key_ids.len(), 1);
12013
12014        drop_execution_schema(&admin_pool, &schema).await;
12015    }
12016
12017    #[rstest]
12018    #[case::finalized(TransactionStatus::Finalized)]
12019    #[case::reverted(TransactionStatus::Reverted)]
12020    #[tokio::test]
12021    async fn protected_payload_storage_ignores_current_policy_for_released_terminal_history(
12022        #[case] status: TransactionStatus,
12023    ) {
12024        let Some((admin_pool, pg_config)) =
12025            connect_test_postgres("protected payload terminal history").await
12026        else {
12027            return;
12028        };
12029        let schema = format!(
12030            "protected_payload_terminal_{}_{}",
12031            status.as_str(),
12032            std::process::id()
12033        );
12034        setup_execution_schema(&admin_pool, &schema).await;
12035        let options: sqlx::postgres::PgConnectOptions = pg_config.into();
12036        let database = connect_test_database(options.options([("search_path", schema.clone())]))
12037            .await
12038            .unwrap();
12039        database
12040            .ensure_execution_transaction_schema()
12041            .await
12042            .unwrap();
12043        let (intent, transaction_hash, _) = persist_test_wrap_broadcast(&database, None).await;
12044        database
12045            .record_execution_status(
12046                intent.id,
12047                &transaction_hash.to_string(),
12048                status,
12049                None,
12050                None,
12051                None,
12052                None,
12053                None,
12054            )
12055            .await
12056            .unwrap();
12057        database
12058            .mark_execution_event_emitted(intent.id, "terminal")
12059            .await
12060            .unwrap();
12061        let keys = payload_test_keys([0x62; 32], vec![], "terminal-history");
12062        database
12063            .ensure_execution_payload_storage(&keys)
12064            .await
12065            .unwrap();
12066
12067        let lease = database
12068            .require_execution_payload_storage(
12069                &keys,
12070                PayloadPolicy {
12071                    chain_id: 42161,
12072                    signer: Address::from_str(WALLET).unwrap(),
12073                    gas_limit: 1,
12074                    max_fee_per_gas: 1,
12075                },
12076                1,
12077            )
12078            .await
12079            .unwrap();
12080        drop(lease);
12081        let check = database
12082            .check_execution_payload_storage(Some(&keys), None, 1)
12083            .await
12084            .unwrap();
12085        let active_intent = reserve_test_wrap_intent(&database).await;
12086        database
12087            .assign_execution_intent_nonce(active_intent.id, 8)
12088            .await
12089            .unwrap();
12090        let active_intent = database
12091            .get_execution_intent(active_intent.id)
12092            .await
12093            .unwrap();
12094        let active_transaction = build_eip1559_transaction(
12095            42161,
12096            8,
12097            78_000,
12098            130_000_000,
12099            10_000_000,
12100            WETH_ADDRESS,
12101            U256::from(1_u64),
12102            Bytes::from(hex::decode("d0e30db0").unwrap()),
12103        );
12104        let (active_hash, active_raw_transaction) = sign_eip1559_transaction(
12105            active_transaction,
12106            &PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
12107        )
12108        .await
12109        .unwrap();
12110        persist_test_payload(
12111            &database,
12112            Some(&keys),
12113            &active_intent,
12114            active_hash,
12115            &active_raw_transaction,
12116        )
12117        .await;
12118        let active_error = match database
12119            .require_execution_payload_storage(
12120                &keys,
12121                PayloadPolicy {
12122                    chain_id: 42161,
12123                    signer: Address::from_str(WALLET).unwrap(),
12124                    gas_limit: 1,
12125                    max_fee_per_gas: 1,
12126                },
12127                1,
12128            )
12129            .await
12130        {
12131            Ok(_) => panic!("active execution payload unexpectedly passed current policy"),
12132            Err(e) => e,
12133        };
12134        let active_error = format!("{active_error:#}");
12135
12136        assert!(check.protected);
12137        assert_eq!(check.authenticated_rows, 1);
12138        assert!(
12139            active_error.contains(&format!(
12140                "execution intent {} transaction {active_hash} violates current execution policy",
12141                active_intent.id
12142            )),
12143            "was: {active_error}"
12144        );
12145        assert!(
12146            active_error.contains("gas limit 78000 exceeds configured ceiling 1"),
12147            "was: {active_error}"
12148        );
12149
12150        drop_execution_schema(&admin_pool, &schema).await;
12151    }
12152
12153    #[allow(unsafe_code)] // env-var mutation uses the test client's dedicated signer variable
12154    #[tokio::test]
12155    async fn rollback_blocks_execution_until_protection_and_full_check_succeed() {
12156        let state = ready_rpc_state();
12157        let Some((admin_pool, schema, mut client, state)) =
12158            execution_client_with_database("payload_rollback_reactivation", state).await
12159        else {
12160            return;
12161        };
12162        // SAFETY: this test binary owns the dedicated test signer variable
12163        unsafe { std::env::set_var("BLOCKCHAIN_TEST_PRIVATE_KEY", TEST_PRIVATE_KEY) };
12164        client.disconnect().await.unwrap();
12165        let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel();
12166        replace_exec_event_sender(sender);
12167        client.start().unwrap();
12168
12169        client.rollback_payload_storage(1).await.unwrap();
12170        let unprotected = client.check_payload_storage(1).await.unwrap();
12171        let error = client.connect().await.unwrap_err();
12172
12173        assert!(!unprotected.protected);
12174        assert_eq!(unprotected.plaintext_rows, 0);
12175        assert_eq!(unprotected.authenticated_rows, 0);
12176        assert!(
12177            error
12178                .to_string()
12179                .contains("Postgres execution requires protected payload storage"),
12180            "was: {error}"
12181        );
12182        assert!(client.signer.is_none());
12183        assert!(state.recorded_requests().is_empty());
12184
12185        client.protect_payload_storage().await.unwrap();
12186        let protected = client.check_payload_storage(1).await.unwrap();
12187        assert!(protected.protected);
12188        assert_eq!(protected.plaintext_rows, 0);
12189        assert_eq!(protected.authenticated_rows, 0);
12190
12191        client.connect().await.unwrap();
12192
12193        assert!(client.is_connected());
12194        assert!(client.transaction_executor().is_ok());
12195        assert!(!state.recorded_requests().is_empty());
12196
12197        drop(client);
12198        drop_execution_schema(&admin_pool, &schema).await;
12199    }
12200
12201    #[rstest]
12202    #[case::active_key(true)]
12203    #[case::deployment_identity(false)]
12204    #[tokio::test]
12205    async fn postgres_connect_rejects_missing_payload_identity_before_rpc(
12206        #[case] remove_active_key: bool,
12207    ) {
12208        let test_name = if remove_active_key {
12209            "payload_connect_missing_active_key"
12210        } else {
12211            "payload_connect_missing_deployment"
12212        };
12213        let Some((admin_pool, schema, mut client, state)) =
12214            execution_client_with_database(test_name, ready_rpc_state()).await
12215        else {
12216            return;
12217        };
12218        client.disconnect().await.unwrap();
12219        client.payload_keys = None;
12220        if remove_active_key {
12221            client.config.payload_key_env = None;
12222            client.config.payload_deployment_id = None;
12223        } else {
12224            client.config.payload_deployment_id = None;
12225        }
12226
12227        let error = client.connect().await.unwrap_err();
12228
12229        assert!(
12230            error.to_string().contains(if remove_active_key {
12231                "Postgres execution requires an active payload key and deployment identity"
12232            } else {
12233                "Payload deployment ID is required"
12234            }),
12235            "was: {error}"
12236        );
12237        assert!(client.signer.is_none());
12238        assert!(client.payload_keys.is_none());
12239        assert!(state.recorded_requests().is_empty());
12240
12241        drop(client);
12242        drop_execution_schema(&admin_pool, &schema).await;
12243    }
12244
12245    #[tokio::test]
12246    async fn postgres_connect_rejects_missing_envelope_key_before_rpc() {
12247        let Some((admin_pool, schema, mut client, state)) = execution_client_with_database(
12248            "payload_connect_missing_envelope_key",
12249            ready_rpc_state(),
12250        )
12251        .await
12252        else {
12253            return;
12254        };
12255        let database = client.cache.database.as_ref().unwrap().clone();
12256        persist_test_wrap_broadcast(&database, client.payload_keys.as_deref()).await;
12257        client.disconnect().await.unwrap();
12258        client.payload_keys = None;
12259        let keys = set_test_payload_key(&mut client, [0xb6; 32], &schema);
12260        sqlx::query(sqlx::AssertSqlSafe(format!(
12261            "UPDATE {schema}.execution_payload_state SET active_key_id = $1 \
12262             WHERE component = 'signed_transactions'"
12263        )))
12264        .bind(keys.active_key_id().as_slice())
12265        .execute(&admin_pool)
12266        .await
12267        .unwrap();
12268
12269        let error = client.connect().await.unwrap_err();
12270
12271        assert!(
12272            error
12273                .to_string()
12274                .contains("Stored execution payload requires unavailable key"),
12275            "was: {error}"
12276        );
12277        assert!(client.signer.is_none());
12278        assert!(client.payload_keys.is_none());
12279        assert!(state.recorded_requests().is_empty());
12280
12281        drop(client);
12282        drop_execution_schema(&admin_pool, &schema).await;
12283    }
12284
12285    #[tokio::test]
12286    async fn payload_action_lease_blocks_rewrap_transition_until_release() {
12287        let Some((admin_pool, schema, client, _)) = execution_client_with_unprotected_database(
12288            "payload_action_lease",
12289            execution_rpc_state(),
12290        )
12291        .await
12292        else {
12293            return;
12294        };
12295        let database = client.cache.database.as_ref().unwrap();
12296        let keys = payload_test_keys([0x71; 32], vec![], "action-lease");
12297        database
12298            .ensure_execution_payload_storage(&keys)
12299            .await
12300            .unwrap();
12301        let lease = database
12302            .acquire_execution_payload_lease(&keys)
12303            .await
12304            .unwrap();
12305        let rotated = payload_test_keys([0x72; 32], vec![[0x71; 32]], "action-lease");
12306        let mut operation = Box::pin(database.rewrap_execution_payload_storage(&rotated, 1));
12307
12308        assert!(
12309            tokio::time::timeout(Duration::from_millis(50), &mut operation)
12310                .await
12311                .is_err()
12312        );
12313        let state: String = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
12314            "SELECT operation FROM {schema}.execution_payload_state"
12315        )))
12316        .fetch_one(&admin_pool)
12317        .await
12318        .unwrap();
12319        assert_eq!(state, "ready");
12320
12321        drop(lease);
12322        tokio::time::timeout(Duration::from_secs(2), operation)
12323            .await
12324            .unwrap()
12325            .unwrap();
12326        let state: String = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
12327            "SELECT operation FROM {schema}.execution_payload_state"
12328        )))
12329        .fetch_one(&admin_pool)
12330        .await
12331        .unwrap();
12332        assert_eq!(state, "ready");
12333
12334        drop_execution_schema(&admin_pool, &schema).await;
12335    }
12336
12337    #[tokio::test]
12338    async fn payload_infrastructure_checks_are_scoped_to_the_execution_schema() {
12339        let Some((admin_pool, pg_config)) = connect_test_postgres("payload schema isolation").await
12340        else {
12341            return;
12342        };
12343        let schema_a = format!("payload_schema_a_{}", std::process::id());
12344        let schema_b = format!("payload_schema_b_{}", std::process::id());
12345        setup_execution_schema(&admin_pool, &schema_a).await;
12346        setup_execution_schema(&admin_pool, &schema_b).await;
12347        let options: sqlx::postgres::PgConnectOptions = pg_config.into();
12348        let database_a =
12349            connect_test_database(options.clone().options([("search_path", schema_a.clone())]))
12350                .await
12351                .unwrap();
12352        let database_b =
12353            connect_test_database(options.options([("search_path", schema_b.clone())]))
12354                .await
12355                .unwrap();
12356        database_a
12357            .ensure_execution_transaction_schema()
12358            .await
12359            .unwrap();
12360        database_b
12361            .ensure_execution_transaction_schema()
12362            .await
12363            .unwrap();
12364        let keys_a = payload_test_keys([0x91; 32], vec![], "schema-a");
12365        let keys_b = payload_test_keys([0x92; 32], vec![], "schema-b");
12366
12367        database_a
12368            .ensure_execution_payload_storage(&keys_a)
12369            .await
12370            .unwrap();
12371        database_b
12372            .ensure_execution_payload_storage(&keys_b)
12373            .await
12374            .unwrap();
12375
12376        for statement in [
12377            format!(
12378                "DROP TRIGGER execution_transaction_payload_fence ON \
12379                 {schema_a}.execution_transaction_hash"
12380            ),
12381            format!(
12382                "ALTER TABLE {schema_a}.execution_transaction_hash \
12383                 DROP CONSTRAINT execution_transaction_payload_protected_check"
12384            ),
12385        ] {
12386            sqlx::query(sqlx::AssertSqlSafe(statement))
12387                .execute(&admin_pool)
12388                .await
12389                .unwrap();
12390        }
12391
12392        let error = database_a
12393            .ensure_execution_payload_storage(&keys_a)
12394            .await
12395            .unwrap_err();
12396
12397        assert!(
12398            error
12399                .to_string()
12400                .contains("without its write fence or constraint"),
12401            "was: {error}"
12402        );
12403
12404        drop_execution_schema(&admin_pool, &schema_a).await;
12405        drop_execution_schema(&admin_pool, &schema_b).await;
12406    }
12407
12408    #[tokio::test]
12409    async fn execution_fresh_schema_preserves_nullable_wallet_address() {
12410        let Some((admin_pool, _)) = connect_test_postgres("fresh execution schema").await else {
12411            return;
12412        };
12413        let schema = format!("execution_fresh_test_{}", std::process::id());
12414        let mut transaction = admin_pool.begin().await.unwrap();
12415        sqlx::query(sqlx::AssertSqlSafe(format!("CREATE SCHEMA {schema}")))
12416            .execute(&mut *transaction)
12417            .await
12418            .unwrap();
12419        sqlx::query(sqlx::AssertSqlSafe(format!(
12420            "SET LOCAL search_path TO {schema}"
12421        )))
12422        .execute(&mut *transaction)
12423        .await
12424        .unwrap();
12425        sqlx::query("CREATE TABLE chain (chain_id INTEGER PRIMARY KEY)")
12426            .execute(&mut *transaction)
12427            .await
12428            .unwrap();
12429        sqlx::query("INSERT INTO chain (chain_id) VALUES (42161)")
12430            .execute(&mut *transaction)
12431            .await
12432            .unwrap();
12433        sqlx::query(sqlx::AssertSqlSafe(execution_transaction_create_sql()))
12434            .execute(&mut *transaction)
12435            .await
12436            .unwrap();
12437        sqlx::query(
12438            "INSERT INTO execution_transaction \
12439             (chain_id, nonce, transaction_hash, purpose, status) \
12440             VALUES (42161, 7, '0xfresh-legacy', 'wrap', 'rejected')",
12441        )
12442        .execute(&mut *transaction)
12443        .await
12444        .unwrap();
12445
12446        let is_nullable: String = sqlx::query_scalar(
12447            "SELECT is_nullable FROM information_schema.columns \
12448             WHERE table_schema = $1 AND table_name = 'execution_transaction' \
12449             AND column_name = 'wallet_address'",
12450        )
12451        .bind(&schema)
12452        .fetch_one(&mut *transaction)
12453        .await
12454        .unwrap();
12455        let wallet_address: Option<String> = sqlx::query_scalar(
12456            "SELECT wallet_address FROM execution_transaction \
12457             WHERE transaction_hash = '0xfresh-legacy'",
12458        )
12459        .fetch_one(&mut *transaction)
12460        .await
12461        .unwrap();
12462
12463        assert_eq!(is_nullable, "YES");
12464        assert_eq!(wallet_address, None);
12465        transaction.rollback().await.unwrap();
12466    }
12467
12468    #[tokio::test]
12469    async fn execution_schema_migration_preserves_existing_rows() {
12470        let Some((admin_pool, pg_config)) =
12471            connect_test_postgres("execution schema migration").await
12472        else {
12473            return;
12474        };
12475        let schema = format!("execution_migration_test_{}", std::process::id());
12476        setup_execution_schema(&admin_pool, &schema).await;
12477        sqlx::query(sqlx::AssertSqlSafe(format!(
12478            "INSERT INTO {schema}.execution_transaction \
12479             (chain_id, nonce, transaction_hash, purpose, status) \
12480             VALUES (42161, 7, '0xlegacy', 'wrap', 'rejected')"
12481        )))
12482        .execute(&admin_pool)
12483        .await
12484        .unwrap();
12485
12486        let db_options: sqlx::postgres::PgConnectOptions = pg_config.into();
12487        let db_options = db_options.options([("search_path", schema.clone())]);
12488        let database = connect_test_database(db_options).await.unwrap();
12489        database
12490            .ensure_execution_transaction_schema()
12491            .await
12492            .unwrap();
12493        let legacy = database
12494            .get_execution_transaction(42161, "0xlegacy")
12495            .await
12496            .unwrap()
12497            .unwrap();
12498        assert_eq!(legacy.purpose, "wrap");
12499        assert_eq!(legacy.status, "rejected");
12500        assert_eq!(legacy.client_order_id, None);
12501        assert_eq!(legacy.wallet_address, None);
12502        let is_nullable: String = sqlx::query_scalar(
12503            "SELECT is_nullable FROM information_schema.columns \
12504             WHERE table_schema = $1 AND table_name = 'execution_transaction' \
12505             AND column_name = 'wallet_address'",
12506        )
12507        .bind(&schema)
12508        .fetch_one(&admin_pool)
12509        .await
12510        .unwrap();
12511        let fence_error = database
12512            .add_execution_transaction(
12513                42161,
12514                WALLET,
12515                8,
12516                "0xswap",
12517                "swap",
12518                "pending",
12519                Some("O-SWAP-001"),
12520            )
12521            .await
12522            .err()
12523            .unwrap();
12524        assert!(
12525            fence_error
12526                .to_string()
12527                .contains("Legacy execution writer refused"),
12528            "was: {fence_error}"
12529        );
12530        assert_eq!(is_nullable, "YES");
12531
12532        drop_execution_schema(&admin_pool, &schema).await;
12533    }
12534
12535    #[tokio::test]
12536    async fn execution_schema_migration_drops_legacy_wallet_not_null() {
12537        let Some((admin_pool, pg_config)) =
12538            connect_test_postgres("legacy wallet address schema migration").await
12539        else {
12540            return;
12541        };
12542        let schema = format!("execution_wallet_migration_test_{}", std::process::id());
12543        setup_execution_schema(&admin_pool, &schema).await;
12544        sqlx::query(sqlx::AssertSqlSafe(format!(
12545            "ALTER TABLE {schema}.execution_transaction \
12546             ADD COLUMN wallet_address TEXT NOT NULL"
12547        )))
12548        .execute(&admin_pool)
12549        .await
12550        .unwrap();
12551        sqlx::query(sqlx::AssertSqlSafe(format!(
12552            "INSERT INTO {schema}.execution_transaction \
12553             (chain_id, wallet_address, nonce, transaction_hash, purpose, status) \
12554             VALUES (42161, '{WALLET}', 7, '0xlegacy-wallet', 'wrap', 'rejected')"
12555        )))
12556        .execute(&admin_pool)
12557        .await
12558        .unwrap();
12559
12560        let db_options: sqlx::postgres::PgConnectOptions = pg_config.into();
12561        let db_options = db_options.options([("search_path", schema.clone())]);
12562        let database = connect_test_database(db_options).await.unwrap();
12563        database
12564            .ensure_execution_transaction_schema()
12565            .await
12566            .unwrap();
12567        let legacy = database
12568            .get_execution_transaction(42161, "0xlegacy-wallet")
12569            .await
12570            .unwrap()
12571            .unwrap();
12572        let is_nullable: String = sqlx::query_scalar(
12573            "SELECT is_nullable FROM information_schema.columns \
12574             WHERE table_schema = $1 AND table_name = 'execution_transaction' \
12575             AND column_name = 'wallet_address'",
12576        )
12577        .bind(&schema)
12578        .fetch_one(&admin_pool)
12579        .await
12580        .unwrap();
12581
12582        assert_eq!(legacy.wallet_address.as_deref(), Some(WALLET));
12583        assert_eq!(is_nullable, "YES");
12584        drop_execution_schema(&admin_pool, &schema).await;
12585    }
12586
12587    #[tokio::test]
12588    async fn execution_schema_migration_refuses_unresolved_legacy_rows() {
12589        let Some((admin_pool, pg_config)) =
12590            connect_test_postgres("unsafe execution schema migration").await
12591        else {
12592            return;
12593        };
12594        let schema = format!("execution_unsafe_migration_test_{}", std::process::id());
12595        setup_execution_schema(&admin_pool, &schema).await;
12596        sqlx::query(sqlx::AssertSqlSafe(format!(
12597            "INSERT INTO {schema}.execution_transaction \
12598             (chain_id, nonce, transaction_hash, purpose, status) \
12599             VALUES (42161, 7, '0xunresolved', 'wrap', 'pending')"
12600        )))
12601        .execute(&admin_pool)
12602        .await
12603        .unwrap();
12604        let db_options: sqlx::postgres::PgConnectOptions = pg_config.into();
12605        let db_options = db_options.options([("search_path", schema.clone())]);
12606        let database = connect_test_database(db_options).await.unwrap();
12607
12608        let error = database
12609            .ensure_execution_transaction_schema()
12610            .await
12611            .unwrap_err();
12612        let legacy_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
12613            "SELECT COUNT(*) FROM {schema}.execution_transaction \
12614             WHERE transaction_hash = '0xunresolved' AND status = 'pending'"
12615        )))
12616        .fetch_one(&admin_pool)
12617        .await
12618        .unwrap();
12619        let v2_table: Option<String> = sqlx::query_scalar("SELECT to_regclass($1)::TEXT")
12620            .bind(format!("{schema}.execution_intent"))
12621            .fetch_one(&admin_pool)
12622            .await
12623            .unwrap();
12624
12625        assert!(
12626            error
12627                .to_string()
12628                .contains("Cannot safely migrate 1 unresolved execution schema version 1"),
12629            "was: {error}"
12630        );
12631        assert_eq!(legacy_count, 1);
12632        assert_eq!(v2_table, None);
12633
12634        drop_execution_schema(&admin_pool, &schema).await;
12635    }
12636
12637    #[rstest]
12638    fn receipt_max_polls_derives_from_timeout() {
12639        assert_eq!(receipt_max_polls(0), 1);
12640        assert_eq!(receipt_max_polls(1), 1);
12641        assert_eq!(receipt_max_polls(60), 60);
12642        assert_eq!(receipt_max_polls(u64::MAX), u32::MAX);
12643        assert_eq!(receipt_timeout(0), Duration::from_secs(1));
12644        assert_eq!(receipt_timeout(60), Duration::from_secs(60));
12645        assert_eq!(
12646            receipt_timeout(u64::MAX),
12647            Duration::from_secs(u64::from(u32::MAX))
12648        );
12649    }
12650
12651    #[rstest]
12652    fn submit_order_errors_when_order_not_cached() {
12653        let client = swap_client_with_cache(test_config("http://127.0.0.1:1".to_string())).0;
12654        let mut cmd = submit_order_cmd(&test_market_sell_order(test_pool().instrument_id));
12655        cmd.client_order_id = ClientOrderId::from("O-UNKNOWN");
12656
12657        let error = client.submit_order(cmd).unwrap_err();
12658
12659        assert!(!error.to_string().is_empty());
12660    }
12661
12662    #[tokio::test]
12663    async fn submit_order_denies_pool_fee_above_uint24() {
12664        let cache = Rc::new(RefCell::new(Cache::default()));
12665        let mut pool = test_pool();
12666        pool.fee = Some(16_777_216); // 2^24, above the uint24 calldata range
12667        cache.borrow_mut().add_pool(pool.clone()).unwrap();
12668        let order = test_market_sell_order(pool.instrument_id);
12669        cache
12670            .borrow_mut()
12671            .add_order(order.clone(), None, None, false)
12672            .unwrap();
12673        let core = ExecutionClientCore::new(
12674            TraderId::from("TRADER-001"),
12675            ClientId::from("BLOCKCHAIN-001"),
12676            *BLOCKCHAIN_VENUE,
12677            OmsType::Netting,
12678            AccountId::from("BLOCKCHAIN-001"),
12679            AccountType::Wallet,
12680            None,
12681            cache,
12682        );
12683        let mut client =
12684            BlockchainExecutionClient::new(core, test_config("http://127.0.0.1:1".to_string()))
12685                .unwrap();
12686        let mut receiver = start_with_events(&mut client);
12687
12688        client.submit_order(submit_order_cmd(&order)).unwrap();
12689
12690        let events = collect_order_events(&mut receiver);
12691        assert_eq!(events.len(), 1);
12692        let OrderEventAny::Denied(denied) = &events[0] else {
12693            panic!("expected OrderDenied, was {:?}", events[0]);
12694        };
12695        assert!(
12696            denied.reason.contains("exceeds uint24"),
12697            "was: {}",
12698            denied.reason
12699        );
12700    }
12701
12702    #[tokio::test]
12703    async fn submit_order_denies_uninitialized_profiler() {
12704        let cache = Rc::new(RefCell::new(Cache::default()));
12705        let pool = test_pool();
12706        cache.borrow_mut().add_pool(pool.clone()).unwrap();
12707        let order = test_market_sell_order(pool.instrument_id);
12708        cache
12709            .borrow_mut()
12710            .add_order(order.clone(), None, None, false)
12711            .unwrap();
12712        cache
12713            .borrow_mut()
12714            .add_pool_profiler(PoolProfiler::new(Arc::new(pool)))
12715            .unwrap();
12716        let core = ExecutionClientCore::new(
12717            TraderId::from("TRADER-001"),
12718            ClientId::from("BLOCKCHAIN-001"),
12719            *BLOCKCHAIN_VENUE,
12720            OmsType::Netting,
12721            AccountId::from("BLOCKCHAIN-001"),
12722            AccountType::Wallet,
12723            None,
12724            cache,
12725        );
12726        let mut client =
12727            BlockchainExecutionClient::new(core, test_config("http://127.0.0.1:1".to_string()))
12728                .unwrap();
12729        let mut receiver = start_with_events(&mut client);
12730
12731        client.submit_order(submit_order_cmd(&order)).unwrap();
12732
12733        let events = collect_order_events(&mut receiver);
12734        assert_eq!(events.len(), 1);
12735        let OrderEventAny::Denied(denied) = &events[0] else {
12736            panic!("expected OrderDenied, was {:?}", events[0]);
12737        };
12738        assert!(
12739            denied.reason.contains("is not initialized"),
12740            "was: {}",
12741            denied.reason
12742        );
12743    }
12744
12745    #[tokio::test]
12746    async fn submit_order_denies_when_quote_cannot_fill_order() {
12747        let cache = Rc::new(RefCell::new(Cache::default()));
12748        let pool = test_pool();
12749        cache.borrow_mut().add_pool(pool.clone()).unwrap();
12750        let order = test_market_sell_order(pool.instrument_id);
12751        cache
12752            .borrow_mut()
12753            .add_order(order.clone(), None, None, false)
12754            .unwrap();
12755        // A narrow one-unit-of-liquidity position cannot fill the order
12756        cache
12757            .borrow_mut()
12758            .add_pool_profiler(test_profiler_with_range(
12759                &pool,
12760                FIXTURE_BLOCK,
12761                FIXTURE_BLOCK_HASH,
12762                U160::from(1u128 << 96),
12763                -10,
12764                10,
12765                1,
12766            ))
12767            .unwrap();
12768        let core = ExecutionClientCore::new(
12769            TraderId::from("TRADER-001"),
12770            ClientId::from("BLOCKCHAIN-001"),
12771            *BLOCKCHAIN_VENUE,
12772            OmsType::Netting,
12773            AccountId::from("BLOCKCHAIN-001"),
12774            AccountType::Wallet,
12775            None,
12776            cache,
12777        );
12778        let mut client =
12779            BlockchainExecutionClient::new(core, test_config("http://127.0.0.1:1".to_string()))
12780                .unwrap();
12781        let mut receiver = start_with_events(&mut client);
12782
12783        client.submit_order(submit_order_cmd(&order)).unwrap();
12784
12785        let events = collect_order_events(&mut receiver);
12786        assert_eq!(events.len(), 1);
12787        let OrderEventAny::Denied(denied) = &events[0] else {
12788            panic!("expected OrderDenied, was {:?}", events[0]);
12789        };
12790        assert!(
12791            denied.reason.contains("cannot fill the order"),
12792            "was: {}",
12793            denied.reason
12794        );
12795    }
12796
12797    #[tokio::test]
12798    async fn submit_order_applies_slippage_param_override_at_ceiling() {
12799        let Some((admin_pool, schema, mut client, state, _)) = swap_client_with_database(
12800            "execution_submit_slippage_override_test",
12801            swap_rpc_state_with_min_amount_out(expected_min_amount_out(200)).await,
12802        )
12803        .await
12804        else {
12805            return;
12806        };
12807        let order = test_market_sell_order(test_pool().instrument_id);
12808        let mut cmd = submit_order_cmd(&order);
12809        // Exactly at the configured ceiling (200 bps): accepted, not denied
12810        cmd.params = Some(serde_json::from_str(r#"{"slippage_bps": 200}"#).unwrap());
12811        let mut receiver = start_with_events(&mut client);
12812        let expected_min_out = expected_min_amount_out(200);
12813
12814        client.submit_order(cmd).unwrap();
12815        await_pending_tasks(&client).await;
12816
12817        let events = collect_order_events(&mut receiver);
12818        assert_swap_submitted_and_filled(&events);
12819
12820        let (_, expected_raw) = expected_swap_tx(expected_min_out).await;
12821        let broadcasts: Vec<_> = state
12822            .recorded_requests()
12823            .into_iter()
12824            .filter(|request| request["method"] == "eth_sendRawTransaction")
12825            .collect();
12826        assert_eq!(broadcasts.len(), 1);
12827        assert_eq!(broadcasts[0]["params"][0].as_str().unwrap(), expected_raw);
12828
12829        drop_execution_schema(&admin_pool, &schema).await;
12830    }
12831
12832    #[tokio::test]
12833    async fn submit_order_accepts_quote_fresh_at_max_age_boundary() {
12834        let min_amount_out = expected_min_amount_out(50);
12835        let (tx_hash, _) = expected_swap_tx(min_amount_out).await;
12836        let head = finalized_swap_block(tx_hash, min_amount_out);
12837        let state = swap_rpc_state()
12838            .await
12839            .with_response("eth_getBlockByNumber", &head)
12840            .with_parameter_response("eth_getBlockByNumber", FIXTURE_BLOCK_PARAM, BLOCK_BY_NUMBER);
12841        let Some((admin_pool, schema, mut client, state, cache)) =
12842            swap_client_with_database_config(
12843                "execution_submit_fresh_boundary_test",
12844                state,
12845                |http_rpc_url| {
12846                    let mut config = test_config(http_rpc_url);
12847                    config.max_quote_age_blocks = Some(1);
12848                    config
12849                },
12850            )
12851            .await
12852        else {
12853            return;
12854        };
12855        let pool = test_pool();
12856        cache
12857            .borrow_mut()
12858            .add_pool_profiler(test_profiler_at_block(
12859                &pool,
12860                FIXTURE_BLOCK,
12861                FIXTURE_BLOCK_HASH,
12862            ))
12863            .unwrap();
12864        let order = test_market_sell_order(pool.instrument_id);
12865        let mut receiver = start_with_events(&mut client);
12866
12867        client.submit_order(submit_order_cmd(&order)).unwrap();
12868        await_pending_tasks(&client).await;
12869
12870        let events = collect_order_events(&mut receiver);
12871        assert_eq!(events.len(), 1, "was: {events:?}");
12872        assert!(matches!(&events[0], OrderEventAny::Submitted(_)));
12873        assert_eq!(
12874            state
12875                .recorded_requests()
12876                .iter()
12877                .filter(|request| request["method"] == "eth_sendRawTransaction")
12878                .count(),
12879            1
12880        );
12881
12882        drop_execution_schema(&admin_pool, &schema).await;
12883    }
12884
12885    #[tokio::test]
12886    async fn submit_order_sells_base_token_from_token1_position() {
12887        // A pool with the token order reversed relative to the convention: USDC is token0
12888        // and WETH is token1, so the base token (WETH, by token priority) sits in the
12889        // token1 position and the swap quotes zero_for_one = false
12890        let chain = Arc::new(chains::ARBITRUM.clone());
12891        let dex = Arc::clone(&UNISWAP_V3.dex);
12892        let usdc = Token::new(
12893            Arc::clone(&chain),
12894            address!("af88d065e77c8cC2239327C5EDb3A432268e5831"),
12895            "USD Coin".to_string(),
12896            "USDC".to_string(),
12897            6,
12898        );
12899        let weth = Token::new(
12900            Arc::clone(&chain),
12901            address!("82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
12902            "Wrapped Ether".to_string(),
12903            "WETH".to_string(),
12904            18,
12905        );
12906        let pool = Pool::new(
12907            chain,
12908            dex,
12909            address!("C6962004f452bE9203591991D15f6b388e09E8D0"),
12910            PoolIdentifier::from_address(address!("C6962004f452bE9203591991D15f6b388e09E8D0")),
12911            55_000_000,
12912            usdc,
12913            weth,
12914            Some(500),
12915            Some(10),
12916            UnixNanos::default(),
12917        );
12918
12919        let cache = Rc::new(RefCell::new(Cache::default()));
12920        cache.borrow_mut().add_pool(pool.clone()).unwrap();
12921        let order = test_market_sell_order(pool.instrument_id);
12922        cache
12923            .borrow_mut()
12924            .add_order(order.clone(), None, None, false)
12925            .unwrap();
12926        // Asymmetric price so the zero_for_one direction changes the quote
12927        cache
12928            .borrow_mut()
12929            .add_pool_profiler(test_profiler_with_state(
12930                &pool,
12931                FIXTURE_BLOCK,
12932                U160::from(2u128 << 96),
12933                TEST_LIQUIDITY,
12934            ))
12935            .unwrap();
12936
12937        let (admin_pool, pg_config) = match connect_test_postgres("orientation").await {
12938            Some(setup) => setup,
12939            None => return,
12940        };
12941        let schema = format!("execution_submit_orientation_test_{}", std::process::id());
12942        setup_execution_schema(&admin_pool, &schema).await;
12943        let db_options: sqlx::postgres::PgConnectOptions = pg_config.into();
12944        let db_options = db_options.options([("search_path", schema.clone())]);
12945        let database = connect_test_database(db_options).await.unwrap();
12946
12947        let state = swap_rpc_state()
12948            .await
12949            .with_call_response(POOL_TOKEN0_SELECTOR, CALL_USDC)
12950            .with_call_response(POOL_TOKEN1_SELECTOR, CALL_WETH)
12951            .with_response("eth_getTransactionReceipt", RECEIPT_NULL);
12952        let addr = start_mock_rpc_server(state.clone()).await;
12953        let core = ExecutionClientCore::new(
12954            TraderId::from("TRADER-001"),
12955            ClientId::from("BLOCKCHAIN-001"),
12956            *BLOCKCHAIN_VENUE,
12957            OmsType::Netting,
12958            AccountId::from("BLOCKCHAIN-001"),
12959            AccountType::Wallet,
12960            None,
12961            cache,
12962        );
12963        let mut config = test_config(format!("http://{addr}"));
12964        let result = |response: &str| {
12965            serde_json::from_str::<serde_json::Value>(response).unwrap()["result"]
12966                .as_str()
12967                .unwrap()
12968                .to_string()
12969        };
12970        {
12971            let verification = config.verification.as_mut().unwrap();
12972            let pool_identity = &mut verification.deployment_manifest.pools[0];
12973            pool_identity.token0 = USDC.to_string();
12974            pool_identity.token1 = WETH.to_string();
12975            let pool_contract = verification
12976                .deployment_manifest
12977                .contracts
12978                .iter_mut()
12979                .find(|contract| contract.role == BlockchainContractRole::Pool)
12980                .unwrap();
12981
12982            for probe in &mut pool_contract.probes {
12983                if probe.call_data.starts_with(POOL_TOKEN0_SELECTOR) {
12984                    probe.expected_output = result(CALL_USDC);
12985                } else if probe.call_data.starts_with(POOL_TOKEN1_SELECTOR) {
12986                    probe.expected_output = result(CALL_WETH);
12987                }
12988            }
12989        }
12990        refresh_test_manifest_digest(&mut config);
12991        let mut client = BlockchainExecutionClient::new(core, config).unwrap();
12992        client.cache.database = Some(database);
12993        client
12994            .cache
12995            .ensure_execution_transaction_schema()
12996            .await
12997            .unwrap();
12998        protect_test_storage(&mut client, &schema).await;
12999        initialize_test_verification_ledger(&client).await;
13000        client.signer = Some(Arc::new(
13001            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
13002        ));
13003        client.core.set_connected();
13004        let mut receiver = start_with_events(&mut client);
13005
13006        let profiler = test_profiler_with_state(
13007            &pool,
13008            FIXTURE_BLOCK,
13009            U160::from(2u128 << 96),
13010            TEST_LIQUIDITY,
13011        );
13012        let quote = profiler
13013            .swap_exact_in(U256::from(1_000_000_000_000_000u64), false, None)
13014            .unwrap();
13015        let quoted = exact_output_amount(&quote, false).unwrap();
13016        let expected_min_out = derive_min_amount_out(quoted, 50).unwrap();
13017        let wrong_direction_quote = profiler
13018            .swap_exact_in(U256::from(1_000_000_000_000_000u64), true, None)
13019            .unwrap();
13020        let wrong_direction_out = exact_output_amount(&wrong_direction_quote, true).unwrap();
13021        assert_ne!(
13022            quoted, wrong_direction_out,
13023            "the asymmetric price must make the quote direction observable"
13024        );
13025        assert_ne!(
13026            expected_min_out,
13027            expected_min_amount_out(50),
13028            "the profiler quote must remain distinct from the independent quote fixture"
13029        );
13030
13031        client.submit_order(submit_order_cmd(&order)).unwrap();
13032        await_pending_tasks(&client).await;
13033
13034        let events = collect_order_events(&mut receiver);
13035        assert_eq!(events.len(), 1);
13036        assert!(
13037            matches!(&events[0], OrderEventAny::Submitted(_)),
13038            "was: {:?}",
13039            events[0]
13040        );
13041
13042        let (_, expected_raw) = expected_swap_tx(expected_min_amount_out(50)).await;
13043        let broadcasts: Vec<_> = state
13044            .recorded_requests()
13045            .into_iter()
13046            .filter(|request| request["method"] == "eth_sendRawTransaction")
13047            .collect();
13048        assert_eq!(broadcasts.len(), 1);
13049        assert_eq!(broadcasts[0]["params"][0].as_str().unwrap(), expected_raw);
13050
13051        drop_execution_schema(&admin_pool, &schema).await;
13052    }
13053
13054    #[tokio::test]
13055    async fn submit_order_denies_on_chain_mismatch() {
13056        let state = swap_rpc_state()
13057            .await
13058            .with_response("eth_chainId", CHAIN_ID_ETHEREUM);
13059        let Some((admin_pool, schema, mut client, state, _)) =
13060            swap_client_with_database("execution_submit_chain_mismatch_test", state).await
13061        else {
13062            return;
13063        };
13064        let order = test_market_sell_order(test_pool().instrument_id);
13065        let mut receiver = start_with_events(&mut client);
13066
13067        client.submit_order(submit_order_cmd(&order)).unwrap();
13068        await_pending_tasks(&client).await;
13069
13070        let events = collect_order_events(&mut receiver);
13071        assert_eq!(events.len(), 1);
13072        let OrderEventAny::Denied(denied) = &events[0] else {
13073            panic!("expected OrderDenied, was {:?}", events[0]);
13074        };
13075        assert!(
13076            denied
13077                .reason
13078                .contains("pre-sign chain ID verification disagreed"),
13079            "was: {}",
13080            denied.reason
13081        );
13082        let requests = state.recorded_requests();
13083        assert!(
13084            requests
13085                .iter()
13086                .all(|request| request["method"] != "eth_sendRawTransaction"),
13087            "no broadcast may follow a chain mismatch"
13088        );
13089        assert!(client.in_flight.lock().is_none());
13090
13091        drop_execution_schema(&admin_pool, &schema).await;
13092    }
13093
13094    #[tokio::test]
13095    async fn submit_order_finality_timeout_marks_dropped_and_keeps_ownership() {
13096        let receipt_release = Arc::new(tokio::sync::Semaphore::new(0));
13097        let state = swap_rpc_state()
13098            .await
13099            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
13100            .with_response_release("eth_getTransactionReceipt", Arc::clone(&receipt_release));
13101        let Some((admin_pool, schema, mut client, state, _)) =
13102            swap_client_with_database("execution_submit_inclusion_timeout_test", state).await
13103        else {
13104            return;
13105        };
13106        client.transaction_limits.receipt_timeout_secs = 1;
13107        let order = test_market_sell_order(test_pool().instrument_id);
13108        let mut receiver = start_with_events(&mut client);
13109
13110        client.submit_order(submit_order_cmd(&order)).unwrap();
13111        await_recorded_requests(&state, "eth_getTransactionReceipt", 3).await;
13112        tokio::time::timeout(Duration::from_secs(3), await_pending_tasks(&client))
13113            .await
13114            .unwrap();
13115        receipt_release.add_permits(3);
13116
13117        // Broadcast acceptance was observed, so the order is submitted; the unobserved
13118        // inclusion justifies no further event
13119        let events = collect_order_events(&mut receiver);
13120        assert_eq!(events.len(), 1);
13121        assert!(
13122            matches!(&events[0], OrderEventAny::Submitted(_)),
13123            "was: {:?}",
13124            events[0]
13125        );
13126
13127        let record = client
13128            .cache
13129            .get_execution_transaction(
13130                42161,
13131                &expected_swap_tx(expected_min_amount_out(50))
13132                    .await
13133                    .0
13134                    .to_string(),
13135            )
13136            .await
13137            .unwrap()
13138            .unwrap();
13139        assert_eq!(record.status, "dropped");
13140        assert!(client.in_flight.lock().is_some());
13141
13142        drop_execution_schema(&admin_pool, &schema).await;
13143    }
13144
13145    #[tokio::test]
13146    async fn included_receipt_finality_timeout_marks_dropped_and_keeps_ownership() {
13147        let state = execution_rpc_state()
13148            .with_response("eth_getTransactionCount", TRANSACTION_COUNT)
13149            .with_response("eth_estimateGas", ESTIMATE_GAS)
13150            .with_response("eth_call", CALL_BALANCE)
13151            .with_response("eth_getTransactionReceipt", RECEIPT_SUCCESS)
13152            .with_parameter_response("eth_getBlockByNumber", "finalized", BLOCK_BY_NUMBER)
13153            .with_send_raw_transaction_echo();
13154        let Some((admin_pool, schema, mut client, _)) =
13155            execution_client_with_database("execution_included_timeout_test", state).await
13156        else {
13157            return;
13158        };
13159        client.transaction_limits.receipt_timeout_secs = 1;
13160
13161        let error = client
13162            .wrap(U256::from(1_000_000_000_000_000_u64))
13163            .await
13164            .unwrap_err();
13165        let transitions: Vec<String> = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
13166            "SELECT to_status FROM {schema}.execution_transaction_transition ORDER BY id"
13167        )))
13168        .fetch_all(&admin_pool)
13169        .await
13170        .unwrap();
13171
13172        assert!(
13173            error.to_string().contains("Timed out awaiting finality"),
13174            "was: {error}"
13175        );
13176        assert_eq!(transitions, ["prepared", "signed", "broadcast", "dropped"]);
13177        assert!(client.in_flight.lock().is_some());
13178
13179        drop_execution_schema(&admin_pool, &schema).await;
13180    }
13181
13182    #[tokio::test]
13183    async fn submit_order_single_in_flight_rejects_concurrent_swap() {
13184        let broadcast_release = Arc::new(tokio::sync::Semaphore::new(0));
13185        let state = swap_rpc_state()
13186            .await
13187            .with_response_release("eth_sendRawTransaction", Arc::clone(&broadcast_release));
13188        let Some((admin_pool, schema, mut client, state, cache)) =
13189            swap_client_with_database("execution_submit_concurrent_test", state).await
13190        else {
13191            return;
13192        };
13193        let pool = test_pool();
13194        let first = test_market_sell_order(pool.instrument_id);
13195        let second = OrderTestBuilder::new(OrderType::Market)
13196            .trader_id(TraderId::from("TRADER-001"))
13197            .strategy_id(StrategyId::from("S-001"))
13198            .instrument_id(pool.instrument_id)
13199            .client_order_id(ClientOrderId::from("O-SWAP-002"))
13200            .side(OrderSide::Sell)
13201            .quantity(Quantity::from("0.001"))
13202            .build();
13203        cache
13204            .borrow_mut()
13205            .add_order(second.clone(), None, None, false)
13206            .unwrap();
13207        let mut receiver = start_with_events(&mut client);
13208
13209        client.submit_order(submit_order_cmd(&first)).unwrap();
13210        await_recorded_requests(&state, "eth_sendRawTransaction", 1).await;
13211        client.submit_order(submit_order_cmd(&second)).unwrap();
13212        let event = tokio::time::timeout(TEST_TIMEOUT, receiver.recv())
13213            .await
13214            .unwrap()
13215            .unwrap();
13216        let ExecutionEvent::Order(OrderEventAny::Denied(denied)) = event else {
13217            panic!("expected OrderDenied, was {event:?}");
13218        };
13219        assert_eq!(denied.client_order_id, second.client_order_id());
13220        assert!(
13221            denied
13222                .reason
13223                .contains("at most one transaction can be in flight"),
13224            "was: {}",
13225            denied.reason
13226        );
13227        broadcast_release.add_permits(1);
13228        await_pending_tasks(&client).await;
13229
13230        let events = collect_order_events(&mut receiver);
13231        assert_swap_submitted_and_filled(&events);
13232
13233        let broadcasts = state
13234            .recorded_requests()
13235            .into_iter()
13236            .filter(|request| request["method"] == "eth_sendRawTransaction")
13237            .count();
13238        assert_eq!(broadcasts, 1);
13239        let row_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
13240            "SELECT COUNT(*) FROM {schema}.execution_intent"
13241        )))
13242        .fetch_one(&admin_pool)
13243        .await
13244        .unwrap();
13245        assert_eq!(row_count, 1);
13246        assert!(client.in_flight.lock().is_none());
13247
13248        drop_execution_schema(&admin_pool, &schema).await;
13249    }
13250
13251    #[tokio::test]
13252    async fn preflight_ready_when_all_checks_pass() {
13253        let (client, state) = client_with_mock_rpc(ready_rpc_state()).await;
13254        let pool = test_pool();
13255
13256        let report = client.preflight(&pool.instrument_id).await.unwrap();
13257
13258        assert!(report.ready, "issues: {:?}", report.issues);
13259        assert!(report.issues.is_empty());
13260        assert_eq!(report.expected_chain_id, 42161);
13261        assert_eq!(report.actual_chain_id, 42161);
13262        assert!(report.chain_id_matches);
13263        assert_eq!(
13264            report.pool.address,
13265            address!("C6962004f452bE9203591991D15f6b388e09E8D0")
13266        );
13267        assert!(report.pool.has_deployed_code);
13268        assert_eq!(report.pool.fee, Some(500));
13269        assert_eq!(
13270            report.pool.base_token,
13271            address!("82aF49447D8a07e3bd95BD0d56f35241523fBab1")
13272        );
13273        assert_eq!(
13274            report.pool.quote_token,
13275            address!("af88d065e77c8cC2239327C5EDb3A432268e5831")
13276        );
13277        assert_eq!(report.routers.len(), 1);
13278        assert!(report.routers[0].has_deployed_code);
13279        assert_eq!(report.tokens.len(), 2);
13280        assert_eq!(
13281            report.tokens[0].wallet_balance,
13282            U256::from(500_000_000_000_000_000u64)
13283        );
13284        assert_eq!(
13285            report.tokens[0].router_allowances,
13286            vec![(
13287                address!("E592427A0AEce92De3Edee1F18E0157C05861564"),
13288                U256::from(1_000_000_000_000_000_000u64)
13289            )]
13290        );
13291        assert_eq!(
13292            report.native_balance_wei,
13293            U256::from(1_000_000_000_000_000_000u64)
13294        );
13295        assert_eq!(report.base_fee_per_gas_wei, 100_000_000);
13296        assert_eq!(report.max_priority_fee_per_gas_wei, 10_000_000);
13297        assert_eq!(report.derived_max_fee_per_gas_wei, 130_000_000);
13298        assert!(report.fee_within_ceiling);
13299
13300        let requests = state.recorded_requests();
13301        for method in ["eth_getCode", "eth_call", "eth_getBalance"] {
13302            let matching: Vec<_> = requests
13303                .iter()
13304                .filter(|request| request["method"] == method)
13305                .collect();
13306            assert!(!matching.is_empty(), "method {method}");
13307            assert!(
13308                matching
13309                    .iter()
13310                    .all(|request| request["params"][1] == "latest"),
13311                "method {method}: {matching:?}"
13312            );
13313        }
13314    }
13315
13316    #[tokio::test]
13317    async fn preflight_not_ready_without_pool_fee() {
13318        let addr = start_mock_rpc_server(ready_rpc_state()).await;
13319        let mut pool = test_pool();
13320        pool.fee = None;
13321        let client = test_client_from_config(test_config(format!("http://{addr}")), pool.clone());
13322
13323        let report = client.preflight(&pool.instrument_id).await.unwrap();
13324
13325        assert!(!report.ready);
13326        assert_eq!(report.pool.fee, None);
13327        assert_eq!(report.issues, vec!["Pool fee tier is missing"]);
13328    }
13329
13330    #[tokio::test]
13331    async fn preflight_not_ready_on_wrong_chain() {
13332        let state = ready_rpc_state().with_response("eth_chainId", CHAIN_ID_ETHEREUM);
13333        let (client, _) = client_with_mock_rpc(state).await;
13334        let pool = test_pool();
13335
13336        let report = client.preflight(&pool.instrument_id).await.unwrap();
13337
13338        assert!(!report.ready);
13339        assert!(!report.chain_id_matches);
13340        assert!(
13341            report
13342                .issues
13343                .iter()
13344                .any(|issue| issue.contains("Chain ID mismatch"))
13345        );
13346    }
13347
13348    #[tokio::test]
13349    async fn preflight_not_ready_without_deployed_code() {
13350        let state = ready_rpc_state().with_response("eth_getCode", GET_CODE_EMPTY);
13351        let (client, _) = client_with_mock_rpc(state).await;
13352        let pool = test_pool();
13353
13354        let report = client.preflight(&pool.instrument_id).await.unwrap();
13355
13356        assert!(!report.ready);
13357        assert!(!report.pool.has_deployed_code);
13358        assert!(!report.routers[0].has_deployed_code);
13359        assert!(report.tokens.iter().all(|t| !t.has_deployed_code));
13360        assert!(
13361            report
13362                .issues
13363                .iter()
13364                .any(|issue| issue.contains("No deployed bytecode at router address"))
13365        );
13366    }
13367
13368    #[tokio::test]
13369    async fn preflight_not_ready_with_zero_balances_and_allowance() {
13370        let state = ready_rpc_state()
13371            .with_response("eth_getBalance", GET_BALANCE_ZERO)
13372            .with_call_response(BALANCE_OF_SELECTOR, CALL_ZERO)
13373            .with_call_response(ALLOWANCE_SELECTOR, CALL_ZERO);
13374        let (client, _) = client_with_mock_rpc(state).await;
13375        let pool = test_pool();
13376
13377        let report = client.preflight(&pool.instrument_id).await.unwrap();
13378
13379        assert!(!report.ready);
13380        assert!(
13381            report
13382                .issues
13383                .iter()
13384                .any(|issue| issue.contains("Native currency balance is zero"))
13385        );
13386        assert!(
13387            report
13388                .issues
13389                .iter()
13390                .any(|issue| issue.contains("balance is zero"))
13391        );
13392        assert!(
13393            report
13394                .issues
13395                .iter()
13396                .any(|issue| issue.contains("No router allowance"))
13397        );
13398    }
13399
13400    #[tokio::test]
13401    async fn preflight_not_ready_when_fees_exceed_ceiling() {
13402        let addr = start_mock_rpc_server(ready_rpc_state()).await;
13403        let mut config = test_config(format!("http://{addr}"));
13404        config.max_fee_per_gas_wei = 1;
13405        let pool = test_pool();
13406        let client = test_client_from_config(config, pool.clone());
13407
13408        let report = client.preflight(&pool.instrument_id).await.unwrap();
13409
13410        assert!(!report.ready);
13411        assert!(!report.fee_within_ceiling);
13412        assert_eq!(report.derived_max_fee_per_gas_wei, 130_000_000);
13413        assert!(
13414            report
13415                .issues
13416                .iter()
13417                .any(|issue| issue.contains("exceeds ceiling"))
13418        );
13419    }
13420
13421    #[rstest]
13422    fn resolve_pool_rejects_unknown_pool() {
13423        let client = test_client("http://127.0.0.1:1".to_string());
13424        let unknown: InstrumentId = "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45.Arbitrum:UniswapV3"
13425            .parse()
13426            .unwrap();
13427
13428        let error = client.resolve_pool(&unknown).unwrap_err();
13429
13430        assert!(error.to_string().contains("Unknown pool"), "was: {error}");
13431    }
13432
13433    #[rstest]
13434    fn resolve_pool_rejects_mismatched_chain() {
13435        let client = test_client("http://127.0.0.1:1".to_string());
13436        let ethereum_pool: InstrumentId =
13437            "0xC6962004f452bE9203591991D15f6b388e09E8D0.Ethereum:UniswapV3"
13438                .parse()
13439                .unwrap();
13440
13441        let error = client.resolve_pool(&ethereum_pool).unwrap_err();
13442
13443        assert!(
13444            error
13445                .to_string()
13446                .contains("does not match the client chain"),
13447            "was: {error}"
13448        );
13449    }
13450
13451    #[rstest]
13452    fn resolve_pool_rejects_unsupported_dex() {
13453        let client = test_client("http://127.0.0.1:1".to_string());
13454        let v4_pool: InstrumentId = "0xC6962004f452bE9203591991D15f6b388e09E8D0.Arbitrum:UniswapV4"
13455            .parse()
13456            .unwrap();
13457
13458        let error = client.resolve_pool(&v4_pool).unwrap_err();
13459
13460        assert!(
13461            error.to_string().contains("only UniswapV3 is supported"),
13462            "was: {error}"
13463        );
13464    }
13465
13466    #[rstest]
13467    fn resolve_pool_rejects_pool_id_identifier() {
13468        let client = test_client("http://127.0.0.1:1".to_string());
13469        let pool_id: InstrumentId =
13470            "0x0000000000000000000000000000000000000000000000000000000000000000.Arbitrum:UniswapV3"
13471                .parse()
13472                .unwrap();
13473
13474        let error = client.resolve_pool(&pool_id).unwrap_err();
13475
13476        assert!(
13477            error
13478                .to_string()
13479                .contains("only address identifiers are supported"),
13480            "was: {error}"
13481        );
13482    }
13483
13484    #[rstest]
13485    fn resolve_pool_rejects_ambiguous_token_priority() {
13486        let chain = Arc::new(chains::ARBITRUM.clone());
13487        let dex = Arc::clone(&UNISWAP_V3.dex);
13488        let token_a = Token::new(
13489            Arc::clone(&chain),
13490            address!("1111111111111111111111111111111111111111"),
13491            "Token A".to_string(),
13492            "TOKA".to_string(),
13493            18,
13494        );
13495        let token_b = Token::new(
13496            Arc::clone(&chain),
13497            address!("2222222222222222222222222222222222222222"),
13498            "Token B".to_string(),
13499            "TOKB".to_string(),
13500            18,
13501        );
13502        let pool = Pool::new(
13503            chain,
13504            dex,
13505            address!("3333333333333333333333333333333333333333"),
13506            PoolIdentifier::from_address(address!("3333333333333333333333333333333333333333")),
13507            55_000_000,
13508            token_a,
13509            token_b,
13510            Some(500),
13511            Some(10),
13512            UnixNanos::default(),
13513        );
13514        let client =
13515            test_client_from_config(test_config("http://127.0.0.1:1".to_string()), pool.clone());
13516
13517        let error = client.resolve_pool(&pool.instrument_id).unwrap_err();
13518
13519        assert!(error.to_string().contains("ambiguous"), "was: {error}");
13520    }
13521
13522    #[rstest]
13523    fn new_parses_pair_specific_quote_spend_limits_with_distinct_precisions() {
13524        let mut config = buy_test_config("http://127.0.0.1:1".to_string());
13525        config.quote_spend_limits = Some(vec![
13526            quote_spend_limit(WETH, USDC, 18, &U256::MAX.to_string()),
13527            quote_spend_limit(USDC, WETH, 6, "1000000000"),
13528        ]);
13529
13530        let client = test_client_from_config(config, test_pool());
13531        let sell_ceiling = client
13532            .transaction_limits
13533            .quote_spend_limits
13534            .get(&(WETH_ADDRESS, USDC_ADDRESS))
13535            .unwrap();
13536        let buy_ceiling = client
13537            .transaction_limits
13538            .quote_spend_limits
13539            .get(&(USDC_ADDRESS, WETH_ADDRESS))
13540            .unwrap();
13541
13542        assert_eq!(sell_ceiling.spend_token, WETH_ADDRESS);
13543        assert_eq!(sell_ceiling.spend_token_decimals, 18);
13544        assert_eq!(sell_ceiling.max_amount, U256::MAX);
13545        assert_eq!(buy_ceiling.spend_token, USDC_ADDRESS);
13546        assert_eq!(buy_ceiling.spend_token_decimals, 6);
13547        assert_eq!(buy_ceiling.max_amount, U256::from(1_000_000_000u64));
13548    }
13549
13550    #[rstest]
13551    fn new_rejects_quote_spend_limit_token_pair_mismatch() {
13552        let mut config = buy_test_config("http://127.0.0.1:1".to_string());
13553        config.quote_spend_limits.as_mut().unwrap()[0].spend_token = WETH.to_string();
13554
13555        let error = test_client_result(config, test_pool()).unwrap_err();
13556
13557        assert!(
13558            error
13559                .to_string()
13560                .contains("`spend_token` must match `token_in`"),
13561            "was: {error}"
13562        );
13563    }
13564
13565    #[rstest]
13566    fn new_rejects_quote_spend_limit_pair_outside_allowlist() {
13567        let mut config = buy_test_config("http://127.0.0.1:1".to_string());
13568        config.quote_spend_limits = Some(vec![quote_spend_limit(
13569            USDC,
13570            "0x1111111111111111111111111111111111111111",
13571            6,
13572            "1000000000",
13573        )]);
13574
13575        let error = test_client_result(config, test_pool()).unwrap_err();
13576
13577        assert!(
13578            error
13579                .to_string()
13580                .contains("is not in the `allowed_token_pairs` allowlist"),
13581            "was: {error}"
13582        );
13583    }
13584
13585    #[rstest]
13586    fn new_rejects_duplicate_quote_spend_pairs() {
13587        let mut config = buy_test_config("http://127.0.0.1:1".to_string());
13588        config.quote_spend_limits = Some(vec![
13589            quote_spend_limit(USDC, WETH, 6, "1000000000"),
13590            quote_spend_limit(USDC, WETH, 6, "2000000000"),
13591        ]);
13592
13593        let error = test_client_result(config, test_pool()).unwrap_err();
13594
13595        assert!(
13596            error
13597                .to_string()
13598                .contains("Duplicate quote spend limit for token pair"),
13599            "was: {error}"
13600        );
13601    }
13602
13603    #[rstest]
13604    #[case::empty("")]
13605    #[case::signed("-1")]
13606    #[case::fractional("1.5")]
13607    #[case::hexadecimal("0x10")]
13608    #[case::overflow(
13609        "115792089237316195423570985008687907853269984665640564039457584007913129639936"
13610    )]
13611    fn new_rejects_invalid_quote_spend_max_amount(#[case] max_amount: &str) {
13612        let mut config = buy_test_config("http://127.0.0.1:1".to_string());
13613        config.quote_spend_limits.as_mut().unwrap()[0].max_amount = max_amount.to_string();
13614
13615        let error = test_client_result(config, test_pool()).unwrap_err();
13616
13617        assert!(
13618            error.to_string().contains("Quote spend limit `max_amount`"),
13619            "was: {error}"
13620        );
13621    }
13622
13623    #[rstest]
13624    #[case::allowed_token_pairs("allowed_token_pairs")]
13625    #[case::slippage_bps("slippage_bps")]
13626    #[case::max_slippage_bps("max_slippage_bps")]
13627    #[case::max_order_amount("max_order_amount")]
13628    #[case::deadline_seconds("deadline_seconds")]
13629    #[case::max_quote_age_blocks("max_quote_age_blocks")]
13630    #[case::receipt_timeout_secs("receipt_timeout_secs")]
13631    fn new_rejects_each_missing_transaction_limit(#[case] missing: &str) {
13632        let mut config = test_config("http://127.0.0.1:1".to_string());
13633        match missing {
13634            "allowed_token_pairs" => config.allowed_token_pairs = None,
13635            "slippage_bps" => config.slippage_bps = None,
13636            "max_slippage_bps" => config.max_slippage_bps = None,
13637            "max_order_amount" => config.max_order_amount = None,
13638            "deadline_seconds" => config.deadline_seconds = None,
13639            "max_quote_age_blocks" => config.max_quote_age_blocks = None,
13640            "receipt_timeout_secs" => config.receipt_timeout_secs = None,
13641            _ => unreachable!(),
13642        }
13643
13644        let error = test_client_result(config, test_pool()).unwrap_err();
13645
13646        assert_eq!(
13647            error.to_string(),
13648            "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"
13649        );
13650    }
13651
13652    #[rstest]
13653    fn new_rejects_empty_router_allowlist() {
13654        let mut config = test_config("http://127.0.0.1:1".to_string());
13655        config.router_addresses = Vec::new();
13656
13657        let error = test_client_result(config, test_pool()).unwrap_err();
13658
13659        assert!(
13660            error.to_string().contains("at least one router address"),
13661            "was: {error}"
13662        );
13663    }
13664
13665    #[tokio::test]
13666    async fn wrap_refuses_without_durable_store() {
13667        let (mut client, _) = client_with_mock_rpc(ready_rpc_state()).await;
13668        client.core.set_connected();
13669
13670        let error = client.wrap(U256::from(1_000u64)).await.unwrap_err();
13671
13672        assert!(
13673            error.to_string().contains("No durable store configured"),
13674            "was: {error}"
13675        );
13676    }
13677
13678    #[tokio::test]
13679    async fn approve_rejects_router_outside_allowlist() {
13680        let (mut client, state) = client_with_mock_rpc(ready_rpc_state()).await;
13681
13682        let error = client
13683            .approve(
13684                WETH_ADDRESS,
13685                U256::from(1_000u64),
13686                address!("68b3465833fb72A70ecDF485E0e4C7bD8665Fc45"),
13687            )
13688            .await
13689            .err()
13690            .unwrap();
13691
13692        assert!(
13693            error
13694                .to_string()
13695                .contains("not in the configured `router_addresses` allowlist"),
13696            "was: {error}"
13697        );
13698        assert!(state.recorded_requests().is_empty());
13699    }
13700
13701    #[tokio::test]
13702    async fn approve_rejects_token_outside_input_allowlist() {
13703        let (mut client, state) = client_with_mock_rpc(ready_rpc_state()).await;
13704
13705        let error = client
13706            .approve(USDC_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
13707            .await
13708            .unwrap_err();
13709
13710        assert!(
13711            error
13712                .to_string()
13713                .contains("is not an input token in the configured `allowed_token_pairs`"),
13714            "was: {error}"
13715        );
13716        assert!(state.recorded_requests().is_empty());
13717    }
13718
13719    #[tokio::test]
13720    async fn in_flight_guard_rejects_second_transaction() {
13721        let (mut client, _) = client_with_mock_rpc(ready_rpc_state()).await;
13722        client.core.set_connected();
13723        *client.in_flight.lock() = Some(InFlightSlot::AwaitingFinality(InFlightTransaction {
13724            intent_id: 1,
13725            nonce: 7,
13726            tx_hash: B256::ZERO,
13727            purpose: TransactionPurpose::Wrap,
13728        }));
13729
13730        let error = client.wrap(U256::from(1_000u64)).await.unwrap_err();
13731
13732        assert!(
13733            error.to_string().contains("still awaiting finality"),
13734            "was: {error}"
13735        );
13736    }
13737
13738    #[tokio::test]
13739    async fn wrap_rejects_zero_amount() {
13740        let (mut client, _) = client_with_mock_rpc(ready_rpc_state()).await;
13741
13742        let error = client.wrap(U256::ZERO).await.unwrap_err();
13743
13744        assert!(
13745            error.to_string().contains("Wrap amount must be positive"),
13746            "was: {error}"
13747        );
13748    }
13749
13750    #[tokio::test]
13751    async fn wrap_rejects_code_free_target_before_broadcast() {
13752        let state = execution_rpc_state().with_response("eth_getCode", GET_CODE_EMPTY);
13753        let Some((admin_pool, schema, mut client, state)) =
13754            execution_client_with_database("execution_wrap_code_free_test", state).await
13755        else {
13756            return;
13757        };
13758
13759        let error = client
13760            .wrap(U256::from(1_000_000_000_000_000u64))
13761            .await
13762            .unwrap_err();
13763
13764        assert!(
13765            error
13766                .to_string()
13767                .contains("pre-sign deployment manifest verification disagreed"),
13768            "was: {error}"
13769        );
13770        assert!(client.in_flight.lock().is_none());
13771        let requests = state.recorded_requests();
13772        assert_eq!(
13773            requests
13774                .iter()
13775                .filter(|request| request["method"] == "eth_getCode")
13776                .count(),
13777            3
13778        );
13779        assert!(
13780            requests
13781                .iter()
13782                .all(|request| request["method"] != "eth_sendRawTransaction")
13783        );
13784
13785        drop_execution_schema(&admin_pool, &schema).await;
13786    }
13787
13788    #[tokio::test]
13789    async fn wrap_rejects_unrelated_target_before_broadcast() {
13790        let state = execution_rpc_state().with_response("eth_call", CALL_EMPTY);
13791        let Some((admin_pool, schema, mut client, state)) =
13792            execution_client_with_database("execution_wrap_unrelated_test", state).await
13793        else {
13794            return;
13795        };
13796
13797        let error = client
13798            .wrap(U256::from(1_000_000_000_000_000u64))
13799            .await
13800            .unwrap_err();
13801
13802        assert!(
13803            error
13804                .to_string()
13805                .contains("pre-sign wrapped token probe verification is unavailable"),
13806            "was: {error}"
13807        );
13808        assert!(client.in_flight.lock().is_none());
13809        let requests = state.recorded_requests();
13810        assert!(
13811            requests
13812                .iter()
13813                .all(|request| request["method"] != "eth_sendRawTransaction")
13814        );
13815
13816        drop_execution_schema(&admin_pool, &schema).await;
13817    }
13818
13819    #[tokio::test]
13820    async fn wrap_rejects_included_transaction_without_balance_delta() {
13821        let state = broadcast_rpc_state().with_response_sequence("eth_call", &[CALL_BALANCE; 9]);
13822        let Some((admin_pool, schema, mut client, state)) =
13823            execution_client_with_database("execution_wrap_no_delta_test", state).await
13824        else {
13825            return;
13826        };
13827
13828        let error = client
13829            .wrap(U256::from(1_000_000_000_000_000u64))
13830            .await
13831            .unwrap_err();
13832
13833        assert!(
13834            error.to_string().contains("did not increase"),
13835            "was: {error}"
13836        );
13837        let in_flight = awaiting_in_flight(&client);
13838        assert_eq!(in_flight.purpose, TransactionPurpose::Wrap);
13839        assert_eq!(
13840            execution_intent_markers(&admin_pool, &schema).await,
13841            vec![("wrap".into(), "broadcast".into(), false, true)]
13842        );
13843        let nonce_state = sqlx::query_as::<_, (i64, i64)>(sqlx::AssertSqlSafe(format!(
13844            "SELECT next_canonical_nonce, revision FROM {schema}.execution_verification_nonce"
13845        )))
13846        .fetch_one(&admin_pool)
13847        .await
13848        .unwrap();
13849        assert_eq!(nonce_state, (7, 0));
13850        let broadcasts = state
13851            .recorded_requests()
13852            .into_iter()
13853            .filter(|request| request["method"] == "eth_sendRawTransaction")
13854            .count();
13855        assert_eq!(broadcasts, 1);
13856
13857        let expected_hash = expected_wrap_tx_hash(U256::from(1_000_000_000_000_000u64)).await;
13858        let block = finalized_wrap_block(expected_hash);
13859        let receipt = receipt_with_transaction_hash(RECEIPT_SUCCESS, expected_hash);
13860        let restart_state = with_finalized_identity(
13861            execution_rpc_state()
13862                .with_response("eth_getTransactionReceipt", &receipt)
13863                .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block)
13864                .with_response_sequence("eth_call", &[CALL_BALANCE; 6]),
13865            &block,
13866            &receipt,
13867        );
13868        let addr = start_mock_rpc_server(restart_state).await;
13869        let error = later_reconnect(client, format!("http://{addr}")).await;
13870        assert!(
13871            error.to_string().contains("did not increase"),
13872            "was: {error}"
13873        );
13874        assert_eq!(
13875            execution_intent_markers(&admin_pool, &schema).await,
13876            vec![("wrap".into(), "broadcast".into(), false, true)]
13877        );
13878        let nonce_state = sqlx::query_as::<_, (i64, i64)>(sqlx::AssertSqlSafe(format!(
13879            "SELECT next_canonical_nonce, revision FROM {schema}.execution_verification_nonce"
13880        )))
13881        .fetch_one(&admin_pool)
13882        .await
13883        .unwrap();
13884        assert_eq!(nonce_state, (7, 0));
13885
13886        drop_execution_schema(&admin_pool, &schema).await;
13887    }
13888
13889    #[tokio::test]
13890    async fn wrap_reports_inclusion_when_postcondition_read_fails() {
13891        let state = broadcast_rpc_state().with_response_sequence(
13892            "eth_call",
13893            &[
13894                CALL_BALANCE,
13895                CALL_BALANCE,
13896                CALL_BALANCE,
13897                CALL_BALANCE,
13898                CALL_BALANCE,
13899                CALL_BALANCE,
13900                RPC_METHOD_NOT_FOUND,
13901                RPC_METHOD_NOT_FOUND,
13902                RPC_METHOD_NOT_FOUND,
13903            ],
13904        );
13905        let Some((admin_pool, schema, mut client, _)) =
13906            execution_client_with_database("execution_wrap_postcondition_rpc_test", state).await
13907        else {
13908            return;
13909        };
13910
13911        let error = client
13912            .wrap(U256::from(1_000_000_000_000_000u64))
13913            .await
13914            .unwrap_err();
13915
13916        let message = error.to_string();
13917        assert!(
13918            message.contains("failed to verify WETH balance after included transaction 0x"),
13919            "was: {message}"
13920        );
13921        assert!(message.contains("at block 30346561"), "was: {message}");
13922        let in_flight = awaiting_in_flight(&client);
13923        assert_eq!(in_flight.purpose, TransactionPurpose::Wrap);
13924        assert_eq!(
13925            execution_intent_markers(&admin_pool, &schema).await,
13926            vec![("wrap".into(), "broadcast".into(), false, true)]
13927        );
13928
13929        drop_execution_schema(&admin_pool, &schema).await;
13930    }
13931
13932    #[tokio::test]
13933    async fn approve_rejects_false_return_before_broadcast() {
13934        let state = execution_rpc_state().with_response("eth_call", CALL_ZERO);
13935        let Some((admin_pool, schema, mut client, state)) =
13936            execution_client_with_database("execution_approve_false_test", state).await
13937        else {
13938            return;
13939        };
13940
13941        let error = client
13942            .approve(WETH_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
13943            .await
13944            .unwrap_err();
13945
13946        assert!(error.to_string().contains("returned false"), "was: {error}");
13947        assert!(client.in_flight.lock().is_none());
13948        let requests = state.recorded_requests();
13949        let approval_calls = requests
13950            .iter()
13951            .filter(|request| {
13952                request["method"] == "eth_call"
13953                    && request["params"][0]["data"]
13954                        .as_str()
13955                        .is_some_and(|data| data.starts_with("0x095ea7b3"))
13956            })
13957            .collect::<Vec<_>>();
13958        assert_eq!(approval_calls.len(), 3);
13959        for request in approval_calls {
13960            assert_eq!(
13961                request["params"][0]["from"]
13962                    .as_str()
13963                    .unwrap()
13964                    .parse::<Address>()
13965                    .unwrap(),
13966                WALLET.parse::<Address>().unwrap()
13967            );
13968            assert_eq!(request["params"][1], FIXTURE_BLOCK_PARAM);
13969        }
13970        assert!(
13971            requests
13972                .iter()
13973                .all(|request| request["method"] != "eth_getTransactionCount")
13974        );
13975        assert!(
13976            requests
13977                .iter()
13978                .all(|request| request["method"] != "eth_sendRawTransaction")
13979        );
13980
13981        drop_execution_schema(&admin_pool, &schema).await;
13982    }
13983
13984    #[tokio::test]
13985    async fn approve_rejects_router_with_wrong_factory_before_signing() {
13986        let state = ready_rpc_state().with_call_response(FACTORY_SELECTOR, CALL_ZERO);
13987        let Some((admin_pool, schema, mut client, state)) =
13988            execution_client_with_database("execution_approve_factory_test", state).await
13989        else {
13990            return;
13991        };
13992
13993        let error = client
13994            .approve(WETH_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
13995            .await
13996            .unwrap_err();
13997
13998        assert!(
13999            error
14000                .to_string()
14001                .contains("pre-sign deployment manifest verification disagreed"),
14002            "was: {error}"
14003        );
14004        let requests = state.recorded_requests();
14005        assert!(
14006            requests
14007                .iter()
14008                .all(|request| request["method"] != "eth_getTransactionCount")
14009        );
14010        assert!(
14011            requests
14012                .iter()
14013                .all(|request| request["method"] != "eth_sendRawTransaction")
14014        );
14015
14016        drop_execution_schema(&admin_pool, &schema).await;
14017    }
14018
14019    #[tokio::test]
14020    async fn approve_rejects_router_with_wrong_weth_before_signing() {
14021        let state = ready_rpc_state().with_call_response(WETH9_SELECTOR, CALL_ZERO);
14022        let Some((admin_pool, schema, mut client, state)) =
14023            execution_client_with_database("execution_approve_weth_test", state).await
14024        else {
14025            return;
14026        };
14027
14028        let error = client
14029            .approve(WETH_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
14030            .await
14031            .unwrap_err();
14032
14033        assert!(
14034            error
14035                .to_string()
14036                .contains("pre-sign deployment manifest verification disagreed"),
14037            "was: {error}"
14038        );
14039        let requests = state.recorded_requests();
14040        assert!(
14041            requests
14042                .iter()
14043                .all(|request| request["method"] != "eth_getTransactionCount")
14044        );
14045        assert!(
14046            requests
14047                .iter()
14048                .all(|request| request["method"] != "eth_sendRawTransaction")
14049        );
14050
14051        drop_execution_schema(&admin_pool, &schema).await;
14052    }
14053
14054    #[tokio::test]
14055    async fn approve_rejects_nonzero_to_nonzero_transition_before_signing() {
14056        let state = ready_rpc_state();
14057        let Some((admin_pool, schema, mut client, state)) =
14058            execution_client_with_database("execution_approve_nonzero_test", state).await
14059        else {
14060            return;
14061        };
14062
14063        let error = client
14064            .approve(WETH_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
14065            .await
14066            .unwrap_err();
14067
14068        assert!(
14069            error.to_string().contains("approve zero before setting"),
14070            "was: {error}"
14071        );
14072        let requests = state.recorded_requests();
14073        assert!(
14074            requests
14075                .iter()
14076                .all(|request| request["method"] != "eth_getTransactionCount")
14077        );
14078        assert!(
14079            requests
14080                .iter()
14081                .all(|request| request["method"] != "eth_sendRawTransaction")
14082        );
14083
14084        drop_execution_schema(&admin_pool, &schema).await;
14085    }
14086
14087    #[tokio::test]
14088    async fn approve_zero_revokes_under_unlimited_policy() {
14089        let state = broadcast_rpc_state()
14090            .with_response("eth_call", CALL_BOOL_TRUE)
14091            .with_call_response_sequence(
14092                ALLOWANCE_SELECTOR,
14093                &[
14094                    CALL_ALLOWANCE,
14095                    CALL_ALLOWANCE,
14096                    CALL_ALLOWANCE,
14097                    CALL_ZERO,
14098                    CALL_ZERO,
14099                    CALL_ZERO,
14100                ],
14101            );
14102        let Some((admin_pool, schema, mut client, state)) =
14103            execution_client_with_database("execution_approve_revoke_test", state).await
14104        else {
14105            return;
14106        };
14107        client.config.unlimited_approval = true;
14108        client.transaction_limits.receipt_timeout_secs = TEST_TIMEOUT.as_secs();
14109
14110        let tx_hash = client
14111            .approve(WETH_ADDRESS, U256::ZERO, ROUTER_ADDRESS)
14112            .await
14113            .unwrap();
14114
14115        assert_eq!(tx_hash, expected_approve_tx_hash(U256::ZERO).await);
14116        let approve_data = state
14117            .recorded_requests()
14118            .into_iter()
14119            .find_map(|request| {
14120                (request["method"] == "eth_estimateGas")
14121                    .then(|| request["params"][0]["data"].as_str().map(str::to_owned))
14122                    .flatten()
14123            })
14124            .unwrap();
14125        assert!(approve_data.starts_with("0x095ea7b3"));
14126        assert!(approve_data.ends_with(&"0".repeat(64)));
14127        assert_eq!(
14128            state
14129                .recorded_requests()
14130                .iter()
14131                .filter(|request| {
14132                    request["method"] == "eth_call"
14133                        && request["params"][0]["data"]
14134                            .as_str()
14135                            .is_some_and(|data| data.starts_with(FACTORY_SELECTOR))
14136                })
14137                .count(),
14138            12
14139        );
14140        assert_eq!(
14141            execution_intent_markers(&admin_pool, &schema).await,
14142            vec![("approve".into(), "finalized".into(), true, false)]
14143        );
14144
14145        drop_execution_schema(&admin_pool, &schema).await;
14146    }
14147
14148    #[tokio::test]
14149    async fn approve_accepts_empty_return_with_sufficient_allowance() {
14150        let state = broadcast_rpc_state()
14151            .with_response("eth_call", CALL_EMPTY)
14152            .with_call_response_sequence(
14153                ALLOWANCE_SELECTOR,
14154                &[
14155                    CALL_ZERO,
14156                    CALL_ZERO,
14157                    CALL_ZERO,
14158                    CALL_ALLOWANCE_1000,
14159                    CALL_ALLOWANCE_1000,
14160                    CALL_ALLOWANCE_1000,
14161                ],
14162            );
14163        let Some((admin_pool, schema, mut client, _)) =
14164            execution_client_with_database("execution_approve_empty_test", state).await
14165        else {
14166            return;
14167        };
14168
14169        let tx_hash = client
14170            .approve(WETH_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
14171            .await
14172            .unwrap();
14173
14174        let record = client
14175            .cache
14176            .get_execution_transaction(42161, &tx_hash.to_string())
14177            .await
14178            .unwrap()
14179            .unwrap();
14180        assert_eq!(record.purpose, "approve");
14181        assert_eq!(record.status, "finalized");
14182        assert!(client.in_flight.lock().is_none());
14183        assert_eq!(
14184            execution_intent_markers(&admin_pool, &schema).await,
14185            vec![("approve".into(), "finalized".into(), true, false)]
14186        );
14187
14188        drop_execution_schema(&admin_pool, &schema).await;
14189    }
14190
14191    #[tokio::test]
14192    async fn approve_rejects_empty_return_with_insufficient_allowance() {
14193        let state = broadcast_rpc_state()
14194            .with_response("eth_call", CALL_EMPTY)
14195            .with_call_response_sequence(ALLOWANCE_SELECTOR, &[CALL_ZERO; 6]);
14196        let Some((admin_pool, schema, mut client, _)) =
14197            execution_client_with_database("execution_approve_insufficient_test", state).await
14198        else {
14199            return;
14200        };
14201
14202        let error = client
14203            .approve(WETH_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
14204            .await
14205            .unwrap_err();
14206
14207        assert!(
14208            error
14209                .to_string()
14210                .contains("does not equal the requested amount"),
14211            "was: {error}"
14212        );
14213        let in_flight = awaiting_in_flight(&client);
14214        assert_eq!(in_flight.purpose, TransactionPurpose::Approve);
14215        assert_eq!(
14216            execution_intent_markers(&admin_pool, &schema).await,
14217            vec![("approve".into(), "broadcast".into(), false, true)]
14218        );
14219        let nonce_state = sqlx::query_as::<_, (i64, i64)>(sqlx::AssertSqlSafe(format!(
14220            "SELECT next_canonical_nonce, revision FROM {schema}.execution_verification_nonce"
14221        )))
14222        .fetch_one(&admin_pool)
14223        .await
14224        .unwrap();
14225        assert_eq!(nonce_state, (7, 0));
14226
14227        let expected_hash = expected_approve_tx_hash(U256::from(1_000u64)).await;
14228        let block = finalized_approve_block(expected_hash, U256::from(1_000u64));
14229        let receipt = receipt_with_transaction_hash(RECEIPT_SUCCESS, expected_hash);
14230        let restart_state = with_finalized_identity(
14231            execution_rpc_state()
14232                .with_response("eth_getTransactionReceipt", &receipt)
14233                .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block)
14234                .with_call_response(ALLOWANCE_SELECTOR, CALL_ZERO),
14235            &block,
14236            &receipt,
14237        );
14238        let addr = start_mock_rpc_server(restart_state).await;
14239        let error = later_reconnect(client, format!("http://{addr}")).await;
14240        assert!(
14241            error
14242                .to_string()
14243                .contains("does not equal the requested amount"),
14244            "was: {error}"
14245        );
14246        assert_eq!(
14247            execution_intent_markers(&admin_pool, &schema).await,
14248            vec![("approve".into(), "broadcast".into(), false, true)]
14249        );
14250        let nonce_state = sqlx::query_as::<_, (i64, i64)>(sqlx::AssertSqlSafe(format!(
14251            "SELECT next_canonical_nonce, revision FROM {schema}.execution_verification_nonce"
14252        )))
14253        .fetch_one(&admin_pool)
14254        .await
14255        .unwrap();
14256        assert_eq!(nonce_state, (7, 0));
14257
14258        drop_execution_schema(&admin_pool, &schema).await;
14259    }
14260
14261    #[tokio::test]
14262    async fn approve_reports_inclusion_when_postcondition_read_fails() {
14263        let state = broadcast_rpc_state()
14264            .with_response("eth_call", CALL_BOOL_TRUE)
14265            .with_call_response_sequence(
14266                ALLOWANCE_SELECTOR,
14267                &[
14268                    CALL_ZERO,
14269                    CALL_ZERO,
14270                    CALL_ZERO,
14271                    RPC_METHOD_NOT_FOUND,
14272                    RPC_METHOD_NOT_FOUND,
14273                    RPC_METHOD_NOT_FOUND,
14274                ],
14275            );
14276        let Some((admin_pool, schema, mut client, _)) =
14277            execution_client_with_database("execution_approve_postcondition_rpc_test", state).await
14278        else {
14279            return;
14280        };
14281
14282        let error = client
14283            .approve(WETH_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
14284            .await
14285            .unwrap_err();
14286
14287        let message = error.to_string();
14288        assert!(
14289            message.contains("failed to verify router allowance after included transaction 0x"),
14290            "was: {message}"
14291        );
14292        assert!(message.contains("at block 30346561"), "was: {message}");
14293        let in_flight = awaiting_in_flight(&client);
14294        assert_eq!(in_flight.purpose, TransactionPurpose::Approve);
14295        assert_eq!(
14296            execution_intent_markers(&admin_pool, &schema).await,
14297            vec![("approve".into(), "broadcast".into(), false, true)]
14298        );
14299
14300        drop_execution_schema(&admin_pool, &schema).await;
14301    }
14302
14303    #[tokio::test]
14304    async fn wallet_balance_refresh_replaces_complete_snapshot_with_exact_precision() {
14305        let state = execution_rpc_state()
14306            .with_response_sequence("eth_getBalance", &[GET_BALANCE, GET_BALANCE_ZERO])
14307            .with_response_sequence(
14308                "eth_call",
14309                &[
14310                    CALL_BALANCE_WETH,
14311                    CALL_BALANCE_USDC,
14312                    CALL_BALANCE_WETH_UPDATED,
14313                    CALL_BALANCE_USDC_UPDATED,
14314                ],
14315            );
14316        let (mut client, _, _) =
14317            client_with_token_mock_rpc(state, "BLOCKCHAIN_TEST_BALANCE_REPLACE").await;
14318        let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel();
14319        replace_exec_event_sender(sender);
14320        client.start().unwrap();
14321
14322        client.refresh_wallet_balances().await.unwrap();
14323        let balances = client.wallet_balance.lock().as_account_balances().unwrap();
14324
14325        assert_eq!(balances.len(), 3);
14326        assert_eq!(balances[0].currency.code, "ETH");
14327        assert_eq!(balances[0].currency.name, "Ethereum");
14328        assert_eq!(balances[0].currency.precision, 18);
14329        assert_eq!(balances[0].total.raw(), 1_000_000_000_000_000_000);
14330        assert_eq!(balances[0].free, balances[0].total);
14331        assert_eq!(balances[0].locked, Money::zero(balances[0].currency));
14332        assert_eq!(balances[1].currency.code, "WETH");
14333        assert_eq!(balances[1].currency.name, "Wrapped Ether");
14334        assert_eq!(balances[1].currency.precision, 18);
14335        assert_eq!(balances[1].total.raw(), 1_234_567_890_123_456_789);
14336        assert_eq!(balances[1].free, balances[1].total);
14337        assert_eq!(balances[1].locked, Money::zero(balances[1].currency));
14338        assert_eq!(balances[2].currency.code, "USDC");
14339        assert_eq!(balances[2].currency.name, "USD Coin");
14340        assert_eq!(balances[2].currency.precision, 6);
14341        assert_eq!(balances[2].total.raw(), 9_876_543_210_000_000_000);
14342        assert_eq!(balances[2].free, balances[2].total);
14343        assert_eq!(balances[2].locked, Money::zero(balances[2].currency));
14344
14345        client.refresh_wallet_balances().await.unwrap();
14346        let balances = client.wallet_balance.lock().as_account_balances().unwrap();
14347
14348        assert_eq!(balances.len(), 3);
14349        assert_eq!(client.wallet_balance.lock().token_balances.len(), 2);
14350        assert_eq!(balances[0].total.raw(), 0);
14351        assert_eq!(balances[1].total.raw(), 2_000_000_000_000_000_000);
14352        assert_eq!(balances[2].total.raw(), 12_345_670_000_000_000);
14353    }
14354
14355    #[allow(unsafe_code)] // env-var mutation in tests; unique var names avoid cross-test races
14356    #[tokio::test]
14357    async fn failed_connect_refresh_retains_snapshot_and_publishes_nothing() {
14358        let state = execution_rpc_state()
14359            .with_response_sequence("eth_getBalance", &[GET_BALANCE, GET_BALANCE_ZERO])
14360            .with_response_sequence(
14361                "eth_call",
14362                &[
14363                    CALL_BALANCE_WETH,
14364                    CALL_BALANCE_USDC,
14365                    CALL_BALANCE_WETH_UPDATED,
14366                    RPC_METHOD_NOT_FOUND,
14367                ],
14368            );
14369        let (mut client, _, _) =
14370            client_with_token_mock_rpc(state, "BLOCKCHAIN_TEST_BALANCE_ATOMIC").await;
14371        let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
14372        replace_exec_event_sender(sender);
14373        client.start().unwrap();
14374        client.refresh_wallet_balances().await.unwrap();
14375        let retained = client.wallet_balance.lock().as_account_balances().unwrap();
14376        receiver.try_recv().unwrap();
14377        // SAFETY: this variable name is unique to this test across the test binary
14378        unsafe { std::env::set_var("BLOCKCHAIN_TEST_BALANCE_ATOMIC", TEST_PRIVATE_KEY) };
14379
14380        let error = client.connect().await.unwrap_err();
14381        let failed_address = Address::from_str(USDC).unwrap();
14382
14383        assert!(
14384            error.to_string().contains(&format!(
14385                "failed to fetch token balance for {failed_address}"
14386            )),
14387            "was: {error}"
14388        );
14389        assert!(!client.is_connected());
14390        assert!(client.signer.is_none());
14391        assert_eq!(
14392            client.wallet_balance.lock().as_account_balances().unwrap(),
14393            retained
14394        );
14395        assert!(matches!(
14396            receiver.try_recv(),
14397            Err(tokio::sync::mpsc::error::TryRecvError::Empty)
14398        ));
14399    }
14400
14401    #[allow(unsafe_code)] // env-var mutation in tests; unique var names avoid cross-test races
14402    #[tokio::test]
14403    async fn failed_connect_publication_retains_snapshot() {
14404        let state = execution_rpc_state()
14405            .with_response_sequence("eth_getBalance", &[GET_BALANCE, GET_BALANCE_ZERO])
14406            .with_response_sequence(
14407                "eth_call",
14408                &[
14409                    CALL_BALANCE_WETH,
14410                    CALL_BALANCE_USDC,
14411                    CALL_BALANCE_WETH_UPDATED,
14412                    CALL_BALANCE_USDC_UPDATED,
14413                ],
14414            );
14415        let (mut client, _, _) =
14416            client_with_token_mock_rpc(state, "BLOCKCHAIN_TEST_BALANCE_PUBLICATION").await;
14417        let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
14418        replace_exec_event_sender(sender);
14419        client.start().unwrap();
14420        client.refresh_wallet_balances().await.unwrap();
14421        let retained = client.wallet_balance.lock().as_account_balances().unwrap();
14422        receiver.try_recv().unwrap();
14423        drop(receiver);
14424        // SAFETY: this variable name is unique to this test across the test binary
14425        unsafe { std::env::set_var("BLOCKCHAIN_TEST_BALANCE_PUBLICATION", TEST_PRIVATE_KEY) };
14426
14427        let error = client.connect().await.unwrap_err();
14428
14429        assert!(
14430            error.to_string().contains("Failed to send account state"),
14431            "was: {error}"
14432        );
14433        assert!(!client.is_connected());
14434        assert!(client.signer.is_none());
14435        assert_eq!(
14436            client.wallet_balance.lock().as_account_balances().unwrap(),
14437            retained
14438        );
14439    }
14440
14441    #[allow(unsafe_code)] // env-var mutation in tests; unique var names avoid cross-test races
14442    #[tokio::test]
14443    async fn connect_and_repeated_query_publish_wallet_account_state() {
14444        let state = execution_rpc_state()
14445            .with_response_sequence("eth_call", &[CALL_BALANCE_WETH, CALL_BALANCE_USDC]);
14446        let (mut client, state, cache) =
14447            client_with_token_mock_rpc(state, "BLOCKCHAIN_TEST_ACCOUNT_STATE").await;
14448        let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
14449        replace_exec_event_sender(sender);
14450        client.start().unwrap();
14451        // SAFETY: this variable name is unique to this test across the test binary
14452        unsafe { std::env::set_var("BLOCKCHAIN_TEST_ACCOUNT_STATE", TEST_PRIVATE_KEY) };
14453
14454        client.connect().await.unwrap();
14455
14456        let ExecutionEvent::Account(connected) = receiver.try_recv().unwrap() else {
14457            panic!("expected account state event")
14458        };
14459        assert_eq!(connected.account_id, AccountId::from("BLOCKCHAIN-001"));
14460        assert_eq!(connected.account_type, AccountType::Wallet);
14461        assert_eq!(connected.base_currency, None);
14462        assert_eq!(connected.balances.len(), 3);
14463        assert!(connected.margins.is_empty());
14464        assert!(connected.is_reported);
14465        cache.borrow_mut().update_account_state(&connected).unwrap();
14466
14467        let account = client.get_account().unwrap();
14468        assert!(matches!(account, AccountAny::Wallet(_)));
14469        assert_eq!(account.id(), AccountId::from("BLOCKCHAIN-001"));
14470        assert_eq!(account.last_event(), Some(connected.clone()));
14471
14472        let requests_before = state.recorded_requests().len();
14473        let query = || {
14474            QueryAccount::new(
14475                TraderId::from("TRADER-001"),
14476                Some(ClientId::from("BLOCKCHAIN-001")),
14477                AccountId::from("BLOCKCHAIN-001"),
14478                UUID4::new(),
14479                UnixNanos::default(),
14480                None,
14481                None,
14482            )
14483        };
14484        client.query_account(query()).unwrap();
14485        client.query_account(query()).unwrap();
14486
14487        let ExecutionEvent::Account(first_query) = receiver.try_recv().unwrap() else {
14488            panic!("expected first query account state event")
14489        };
14490        let ExecutionEvent::Account(second_query) = receiver.try_recv().unwrap() else {
14491            panic!("expected second query account state event")
14492        };
14493        assert!(connected.has_same_balances_and_margins(&first_query));
14494        assert!(first_query.has_same_balances_and_margins(&second_query));
14495        assert_eq!(first_query.account_id, connected.account_id);
14496        assert_eq!(first_query.account_type, connected.account_type);
14497        assert_eq!(first_query.base_currency, connected.base_currency);
14498        assert_eq!(first_query.is_reported, connected.is_reported);
14499        assert_ne!(first_query.event_id, second_query.event_id);
14500        assert_eq!(state.recorded_requests().len(), requests_before);
14501
14502        client.stop().unwrap();
14503        assert!(client.core.is_stopped());
14504        assert!(!client.is_connected());
14505        assert!(client.signer.is_none());
14506    }
14507
14508    #[tokio::test]
14509    async fn connect_rejects_on_chain_mismatch() {
14510        let state = ready_rpc_state().with_response("eth_chainId", CHAIN_ID_ETHEREUM);
14511        let (mut client, _) = client_with_mock_rpc(state).await;
14512
14513        let error = client.connect().await.unwrap_err();
14514
14515        assert!(
14516            error
14517                .to_string()
14518                .contains("Blockchain chain ID verification disagreed"),
14519            "was: {error}"
14520        );
14521    }
14522
14523    #[allow(unsafe_code)] // env-var mutation in tests; unique var names avoid cross-test races
14524    #[tokio::test]
14525    async fn connect_rejects_on_signer_wallet_mismatch() {
14526        let addr = start_mock_rpc_server(ready_rpc_state()).await;
14527        let config = test_config_with_signer_env(
14528            format!("http://{addr}"),
14529            "BLOCKCHAIN_TEST_PRIVATE_KEY_MISMATCH",
14530        );
14531        let mut client = test_client_from_config(config, test_pool());
14532        // Anvil development key #1 derives a different address than the configured wallet
14533        // SAFETY: this variable name is unique to this test across the test binary
14534        unsafe {
14535            std::env::set_var(
14536                "BLOCKCHAIN_TEST_PRIVATE_KEY_MISMATCH",
14537                "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d",
14538            );
14539        };
14540
14541        let error = client.connect().await.unwrap_err();
14542
14543        assert!(
14544            error
14545                .to_string()
14546                .contains("does not match configured wallet address"),
14547            "was: {error}"
14548        );
14549    }
14550
14551    #[allow(unsafe_code)] // env-var mutation in tests; unique var names avoid cross-test races
14552    #[tokio::test]
14553    async fn connect_rejects_missing_trace_capability_before_signer_load() {
14554        let state = ready_rpc_state().with_parameter_response(
14555            "debug_traceTransaction",
14556            &B256::ZERO.to_string(),
14557            RPC_METHOD_NOT_FOUND,
14558        );
14559        let addr = start_mock_rpc_server(state.clone()).await;
14560        let config = test_config_with_signer_env(
14561            format!("http://{addr}"),
14562            "BLOCKCHAIN_TEST_TRACE_CAPABILITY",
14563        );
14564        let mut client = test_client_from_config(config, test_pool());
14565        // SAFETY: this variable name is unique to this test across the test binary
14566        unsafe { std::env::set_var("BLOCKCHAIN_TEST_TRACE_CAPABILITY", TEST_PRIVATE_KEY) };
14567
14568        let error = client.connect().await.unwrap_err();
14569
14570        assert!(
14571            error
14572                .to_string()
14573                .contains("Blockchain call trace capability verification is locally invalid"),
14574            "was: {error}"
14575        );
14576        assert!(client.signer.is_none());
14577        assert!(
14578            state
14579                .recorded_requests()
14580                .iter()
14581                .all(|request| request["method"] != "eth_getBalance")
14582        );
14583    }
14584
14585    #[allow(unsafe_code)] // env-var mutation in tests; unique var names avoid cross-test races
14586    #[tokio::test]
14587    async fn connect_initializes_signer_from_env() {
14588        let addr = start_mock_rpc_server(ready_rpc_state()).await;
14589        let config =
14590            test_config_with_signer_env(format!("http://{addr}"), "BLOCKCHAIN_TEST_PRIVATE_KEY_OK");
14591        let mut client = test_client_from_config(config, test_pool());
14592        let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel();
14593        replace_exec_event_sender(sender);
14594        client.start().unwrap();
14595        // SAFETY: this variable name is unique to this test across the test binary
14596        unsafe { std::env::set_var("BLOCKCHAIN_TEST_PRIVATE_KEY_OK", TEST_PRIVATE_KEY) };
14597
14598        client.connect().await.unwrap();
14599
14600        let signer = client.signer.as_ref().unwrap();
14601        assert_eq!(
14602            signer.address(),
14603            address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266")
14604        );
14605    }
14606
14607    #[allow(unsafe_code)] // env-var mutation in tests; unique var names avoid cross-test races
14608    #[tokio::test]
14609    async fn reconnect_resumes_at_the_durable_finalized_tip() {
14610        let Some((admin_pool, pg_config)) =
14611            connect_test_postgres("verification ledger resume").await
14612        else {
14613            return;
14614        };
14615        let schema = format!("verification_ledger_resume_{}", std::process::id());
14616        setup_execution_schema(&admin_pool, &schema).await;
14617        let options: sqlx::postgres::PgConnectOptions = pg_config.into();
14618        let options = options.options([("search_path", schema.clone())]);
14619        let database = connect_test_database(options).await.unwrap();
14620        database
14621            .ensure_execution_transaction_schema()
14622            .await
14623            .unwrap();
14624
14625        let state = ready_rpc_state();
14626        let addr = start_mock_rpc_server(state.clone()).await;
14627        let config = test_config_with_signer_env(
14628            format!("http://{addr}"),
14629            "BLOCKCHAIN_TEST_VERIFICATION_RESUME",
14630        );
14631        let mut client = test_client_from_config(config, test_pool());
14632        client.cache.database = Some(database);
14633        protect_test_storage(&mut client, &schema).await;
14634        let finalized_headers = [
14635            ExecutionVerifiedHeader {
14636                number: FIXTURE_BLOCK,
14637                hash: FIXTURE_BLOCK_HASH.to_string(),
14638                parent_hash: "0x0000000000000000000000000000000000000000000000000000000000000001"
14639                    .to_string(),
14640                timestamp: FIXTURE_BLOCK_TIMESTAMP,
14641                base_fee_per_gas: Some(100_000_000),
14642            },
14643            ExecutionVerifiedHeader {
14644                number: FIXTURE_BLOCK + 1,
14645                hash: B256::from([0x22; 32]).to_string(),
14646                parent_hash: FIXTURE_BLOCK_HASH.to_string(),
14647                timestamp: FIXTURE_BLOCK_TIMESTAMP + 1,
14648                base_fee_per_gas: Some(100_000_000),
14649            },
14650            ExecutionVerifiedHeader {
14651                number: FIXTURE_BLOCK + 2,
14652                hash: B256::from([0x33; 32]).to_string(),
14653                parent_hash: B256::from([0x22; 32]).to_string(),
14654                timestamp: FIXTURE_BLOCK_TIMESTAMP + 2,
14655                base_fee_per_gas: Some(100_000_000),
14656            },
14657        ];
14658        initialize_test_verification_ledger_with_headers(&client, &finalized_headers).await;
14659        let verification = client.config.verification.as_ref().unwrap();
14660        let position = client
14661            .cache
14662            .database
14663            .as_ref()
14664            .unwrap()
14665            .load_execution_verification_position(
14666                42_161,
14667                WALLET,
14668                &verification.manifest_version,
14669                &verification.manifest_digest,
14670            )
14671            .await
14672            .unwrap()
14673            .unwrap();
14674        let resume = client
14675            .cache
14676            .database
14677            .as_ref()
14678            .unwrap()
14679            .load_execution_verification_resume(
14680                42_161,
14681                WALLET,
14682                &verification.manifest_version,
14683                &verification.manifest_digest,
14684            )
14685            .await
14686            .unwrap()
14687            .unwrap();
14688        assert_eq!(position.next_canonical_nonce, 7);
14689        assert_eq!(position.revision, 0);
14690        assert_eq!(position.finalized_tip, finalized_headers[2]);
14691        assert_eq!(resume.next_canonical_nonce, 7);
14692        assert_eq!(resume.revision, 0);
14693        assert_eq!(resume.finalized_headers, finalized_headers);
14694
14695        let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel();
14696        replace_exec_event_sender(sender);
14697        client.start().unwrap();
14698        // SAFETY: this variable name is unique to this test across the test binary
14699        unsafe { std::env::set_var("BLOCKCHAIN_TEST_VERIFICATION_RESUME", TEST_PRIVATE_KEY) };
14700
14701        client.connect().await.unwrap();
14702
14703        let skipped_height = format!("0x{:x}", FIXTURE_BLOCK + 1);
14704        assert!(client.is_connected());
14705        assert!(state.recorded_requests().iter().all(|request| {
14706            request["method"] != "eth_getBlockByNumber"
14707                || request["params"][0].as_str() != Some(&skipped_height)
14708        }));
14709        let header_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
14710            "SELECT COUNT(*) FROM {schema}.execution_verified_finalized_header"
14711        )))
14712        .fetch_one(&admin_pool)
14713        .await
14714        .unwrap();
14715        assert_eq!(header_count, 3);
14716
14717        drop(client);
14718        drop_execution_schema(&admin_pool, &schema).await;
14719    }
14720
14721    #[tokio::test]
14722    async fn disconnect_revokes_signer_and_blocks_execution() {
14723        let (mut client, state) = client_with_mock_rpc(ready_rpc_state()).await;
14724        client.core.set_connected();
14725        client.signer = Some(Arc::new(
14726            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
14727        ));
14728
14729        client.disconnect().await.unwrap();
14730        let error = client.wrap(U256::from(1_000u64)).await.unwrap_err();
14731
14732        assert!(!client.is_connected());
14733        assert!(client.signer.is_none());
14734        assert!(
14735            error.to_string().contains("is not connected"),
14736            "was: {error}"
14737        );
14738        assert!(state.recorded_requests().is_empty());
14739    }
14740
14741    #[allow(unsafe_code)] // env-var mutation in tests; unique var names avoid cross-test races
14742    #[tokio::test]
14743    async fn connect_releases_stale_preparing_claim_after_tasks_finish() {
14744        let addr = start_mock_rpc_server(ready_rpc_state()).await;
14745        let config = test_config_with_signer_env(
14746            format!("http://{addr}"),
14747            "BLOCKCHAIN_TEST_RECONNECT_CLAIM",
14748        );
14749        let mut client = test_client_from_config(config, test_pool());
14750        let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel();
14751        replace_exec_event_sender(sender);
14752        client.start().unwrap();
14753        // SAFETY: this variable name is unique to this test across the test binary
14754        unsafe { std::env::set_var("BLOCKCHAIN_TEST_RECONNECT_CLAIM", TEST_PRIVATE_KEY) };
14755        *client.in_flight.lock() = Some(InFlightSlot::Preparing(TransactionPurpose::Swap));
14756        client
14757            .pending_tasks
14758            .spawn(async {})
14759            .expect("stale task spawn");
14760
14761        client.connect().await.unwrap();
14762
14763        assert!(client.in_flight.lock().is_none());
14764    }
14765
14766    #[allow(unsafe_code)] // env-var mutation in tests; unique var names avoid cross-test races
14767    #[tokio::test]
14768    async fn reconnect_after_aborted_submission_releases_preparing_claim() {
14769        // The swap task blocks in its first RPC; disconnect aborts it mid-preparation, and the
14770        // following connect awaits termination and releases the stale claim
14771        let state = ready_rpc_state().with_sleep("eth_getBlockByNumber", Duration::from_secs(30));
14772        let Some((admin_pool, schema, mut client, state, _)) =
14773            swap_client_with_database("execution_reconnect_claim_test", state).await
14774        else {
14775            return;
14776        };
14777        let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel();
14778        replace_exec_event_sender(sender);
14779        client.start().unwrap();
14780        // SAFETY: this variable name is unique to this test across the test binary
14781        unsafe { std::env::set_var("BLOCKCHAIN_TEST_PRIVATE_KEY", TEST_PRIVATE_KEY) };
14782
14783        let order = test_market_sell_order(test_pool().instrument_id);
14784        client.submit_order(submit_order_cmd(&order)).unwrap();
14785        await_recorded_requests(&state, "eth_getBlockByNumber", 1).await;
14786        assert!(client.in_flight.lock().is_some());
14787
14788        client.disconnect().await.unwrap();
14789        let ready_addr = start_mock_rpc_server(ready_rpc_state()).await;
14790        let ready = test_client(format!("http://{ready_addr}"));
14791        client.http_rpc_client = ready.http_rpc_client;
14792        client.verification = ready.verification;
14793        client.connect().await.unwrap();
14794
14795        assert!(client.in_flight.lock().is_none());
14796
14797        drop_execution_schema(&admin_pool, &schema).await;
14798    }
14799
14800    #[tokio::test]
14801    async fn poll_for_receipt_returns_none_after_exhaustion() {
14802        let state = ready_rpc_state().with_response("eth_getTransactionReceipt", RECEIPT_NULL);
14803        let (client, state) = client_with_mock_rpc(state).await;
14804
14805        let receipt = poll_for_receipt(&client.http_rpc_client, &B256::ZERO, 3, Duration::ZERO)
14806            .await
14807            .unwrap();
14808
14809        assert!(receipt.is_none());
14810        let requests = state.recorded_requests();
14811        assert_eq!(requests.len(), 3);
14812    }
14813
14814    #[tokio::test]
14815    async fn poll_for_receipt_returns_last_error_when_every_poll_fails() {
14816        let state =
14817            ready_rpc_state().with_response("eth_getTransactionReceipt", RPC_METHOD_NOT_FOUND);
14818        let (client, state) = client_with_mock_rpc(state).await;
14819
14820        let error = poll_for_receipt(&client.http_rpc_client, &B256::ZERO, 3, Duration::ZERO)
14821            .await
14822            .unwrap_err();
14823
14824        assert_eq!(
14825            error.to_string(),
14826            "eth_getTransactionReceipt RPC error -32601"
14827        );
14828        let requests = state.recorded_requests();
14829        assert_eq!(requests.len(), 3);
14830    }
14831
14832    #[tokio::test]
14833    async fn poll_for_receipt_returns_none_after_pending_then_errors() {
14834        let state = ready_rpc_state().with_response_sequence(
14835            "eth_getTransactionReceipt",
14836            &[RECEIPT_NULL, RPC_METHOD_NOT_FOUND, RPC_METHOD_NOT_FOUND],
14837        );
14838        let (client, state) = client_with_mock_rpc(state).await;
14839
14840        let receipt = poll_for_receipt(&client.http_rpc_client, &B256::ZERO, 3, Duration::ZERO)
14841            .await
14842            .unwrap();
14843
14844        assert!(receipt.is_none());
14845        let requests = state.recorded_requests();
14846        assert_eq!(requests.len(), 3);
14847    }
14848
14849    #[tokio::test]
14850    async fn cancellation_during_persistence_keeps_in_flight_slot() {
14851        let Some((admin_pool, pg_config)) = connect_test_postgres("persistence cancellation").await
14852        else {
14853            return;
14854        };
14855
14856        let schema = format!("execution_persist_cancel_test_{}", std::process::id());
14857        setup_execution_schema(&admin_pool, &schema).await;
14858
14859        let db_options: sqlx::postgres::PgConnectOptions = pg_config.into();
14860        let db_options = db_options.options([("search_path", schema.clone())]);
14861        let database = connect_test_database(db_options).await.unwrap();
14862
14863        let state = signing_rpc_state();
14864        let addr = start_mock_rpc_server(state.clone()).await;
14865
14866        let mut client = test_client(format!("http://{addr}"));
14867        client.cache.database = Some(database);
14868        // Mirror the connect-time migration: tests create the pre-submission table shape
14869        client
14870            .cache
14871            .ensure_execution_transaction_schema()
14872            .await
14873            .unwrap();
14874        protect_test_storage(&mut client, &schema).await;
14875        initialize_test_verification_ledger(&client).await;
14876        client.signer = Some(Arc::new(
14877            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
14878        ));
14879        client.core.set_connected();
14880
14881        let advisory_lock = i64::from(std::process::id());
14882
14883        for statement in [
14884            format!(
14885                "CREATE FUNCTION {schema}.block_execution_hash_insert() RETURNS trigger \
14886                 LANGUAGE plpgsql AS 'BEGIN PERFORM pg_advisory_xact_lock({advisory_lock}); \
14887                 RETURN NEW; END'"
14888            ),
14889            format!(
14890                "CREATE TRIGGER block_execution_hash_insert BEFORE INSERT ON \
14891                 {schema}.execution_transaction_hash FOR EACH ROW EXECUTE FUNCTION \
14892                 {schema}.block_execution_hash_insert()"
14893            ),
14894        ] {
14895            sqlx::query(sqlx::AssertSqlSafe(statement))
14896                .execute(&admin_pool)
14897                .await
14898                .unwrap();
14899        }
14900        let mut lock_transaction = admin_pool.begin().await.unwrap();
14901        sqlx::query("SELECT pg_advisory_xact_lock($1)")
14902            .bind(advisory_lock)
14903            .execute(&mut *lock_transaction)
14904            .await
14905            .unwrap();
14906
14907        let value = U256::from(1_000_000_000_000_000u64);
14908        let in_flight = Arc::clone(&client.in_flight);
14909        let mut wrap = Box::pin(client.wrap(value));
14910        tokio::time::timeout(Duration::from_secs(2), async {
14911            loop {
14912                tokio::select! {
14913                    result = &mut wrap => {
14914                        panic!("persistence completed while the table was locked: {result:?}")
14915                    }
14916                    () = tokio::time::sleep(Duration::from_millis(1)) => {}
14917                }
14918
14919                if matches!(*in_flight.lock(), Some(InFlightSlot::AwaitingFinality(_))) {
14920                    break;
14921                }
14922            }
14923        })
14924        .await
14925        .unwrap();
14926        drop(wrap);
14927        lock_transaction.rollback().await.unwrap();
14928
14929        let slot = *client.in_flight.lock();
14930        let second_error = client
14931            .wrap(U256::from(2_000_000_000_000_000u64))
14932            .await
14933            .unwrap_err();
14934        let broadcasts = state
14935            .recorded_requests()
14936            .into_iter()
14937            .filter(|request| request["method"] == "eth_sendRawTransaction")
14938            .count();
14939        let status: String = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
14940            "SELECT status FROM {schema}.execution_intent"
14941        )))
14942        .fetch_one(&admin_pool)
14943        .await
14944        .unwrap();
14945
14946        assert!(matches!(slot, Some(InFlightSlot::AwaitingFinality(_))));
14947        assert_eq!(status, "prepared");
14948        assert!(
14949            second_error.to_string().contains("awaiting finality"),
14950            "was: {second_error}"
14951        );
14952        assert_eq!(broadcasts, 0);
14953
14954        drop_execution_schema(&admin_pool, &schema).await;
14955    }
14956
14957    #[tokio::test]
14958    async fn reservation_failure_releases_preparing_slot() {
14959        let state = signing_rpc_state();
14960        let Some((admin_pool, schema, mut client, state)) =
14961            execution_client_with_database("execution_reservation_fail_test", state).await
14962        else {
14963            return;
14964        };
14965        sqlx::query(sqlx::AssertSqlSafe(format!(
14966            "DROP TABLE {schema}.execution_intent CASCADE"
14967        )))
14968        .execute(&admin_pool)
14969        .await
14970        .unwrap();
14971
14972        let error = client
14973            .wrap(U256::from(1_000_000_000_000_000u64))
14974            .await
14975            .unwrap_err();
14976        let retry_error = client
14977            .wrap(U256::from(2_000_000_000_000_000u64))
14978            .await
14979            .unwrap_err();
14980        let broadcasts = state
14981            .recorded_requests()
14982            .into_iter()
14983            .filter(|request| request["method"] == "eth_sendRawTransaction")
14984            .count();
14985
14986        assert_eq!(
14987            error.to_string(),
14988            "Execution intent reservation failed before commit"
14989        );
14990        assert!(reservation_failure_proven_not_committed(&error));
14991        assert_eq!(
14992            retry_error.to_string(),
14993            "Execution intent reservation failed before commit"
14994        );
14995        assert_eq!(broadcasts, 0);
14996        assert!(client.in_flight.lock().is_none());
14997
14998        drop_execution_schema(&admin_pool, &schema).await;
14999    }
15000
15001    #[tokio::test]
15002    async fn reservation_commit_failure_keeps_preparing_slot() {
15003        let state = signing_rpc_state();
15004        let Some((admin_pool, schema, mut client, state)) =
15005            execution_client_with_database("execution_reservation_commit_fail_test", state).await
15006        else {
15007            return;
15008        };
15009        install_reservation_commit_rejection(&admin_pool, &schema).await;
15010
15011        let error = client
15012            .wrap(U256::from(1_000_000_000_000_000u64))
15013            .await
15014            .unwrap_err();
15015        let slot = *client.in_flight.lock();
15016        let second_error = client
15017            .wrap(U256::from(2_000_000_000_000_000u64))
15018            .await
15019            .unwrap_err();
15020        let requests = state.recorded_requests();
15021        let intent_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
15022            "SELECT COUNT(*) FROM {schema}.execution_intent"
15023        )))
15024        .fetch_one(&admin_pool)
15025        .await
15026        .unwrap();
15027        let signed_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
15028            "SELECT COUNT(*) FROM {schema}.execution_transaction_hash"
15029        )))
15030        .fetch_one(&admin_pool)
15031        .await
15032        .unwrap();
15033
15034        assert_eq!(
15035            error.to_string(),
15036            "Execution intent reservation commit outcome is unknown; reconciliation is required"
15037        );
15038        assert!(!reservation_failure_proven_not_committed(&error));
15039        assert!(matches!(
15040            slot,
15041            Some(InFlightSlot::Preparing(TransactionPurpose::Wrap))
15042        ));
15043        assert!(
15044            second_error.to_string().contains("being prepared"),
15045            "was: {second_error}"
15046        );
15047        assert_eq!(intent_count, 0);
15048        assert_eq!(signed_count, 0);
15049        assert!(
15050            requests
15051                .iter()
15052                .all(|request| request["method"] != "eth_getTransactionCount")
15053        );
15054        assert!(
15055            requests
15056                .iter()
15057                .all(|request| request["method"] != "eth_sendRawTransaction")
15058        );
15059
15060        drop_execution_schema(&admin_pool, &schema).await;
15061    }
15062
15063    #[tokio::test]
15064    async fn persistence_failure_keeps_unbroadcast_slot() {
15065        let state = signing_rpc_state();
15066        let Some((admin_pool, schema, mut client, state)) =
15067            execution_client_with_database("execution_persist_fail_test", state).await
15068        else {
15069            return;
15070        };
15071        sqlx::query(sqlx::AssertSqlSafe(format!(
15072            "ALTER TABLE {schema}.execution_transaction_hash \
15073             ADD CONSTRAINT execution_hash_reject CHECK (FALSE)"
15074        )))
15075        .execute(&admin_pool)
15076        .await
15077        .unwrap();
15078
15079        let value = U256::from(1_000_000_000_000_000u64);
15080        let error = client.wrap(value).await.unwrap_err();
15081        let in_flight = awaiting_in_flight(&client);
15082        let second_error = client
15083            .wrap(U256::from(2_000_000_000_000_000u64))
15084            .await
15085            .unwrap_err();
15086        let broadcasts = state
15087            .recorded_requests()
15088            .into_iter()
15089            .filter(|request| request["method"] == "eth_sendRawTransaction")
15090            .count();
15091        let expected_hash = expected_wrap_tx_hash(value).await;
15092
15093        let error_message = error.to_string();
15094        assert!(error_message.starts_with(&format!(
15095            "Failed to persist transaction {expected_hash}: Failed to persist signed transaction"
15096        )), "was: {error_message}");
15097        assert!(
15098            error_message.ends_with("the in-flight slot stays occupied"),
15099            "was: {error_message}"
15100        );
15101        assert_eq!(in_flight.nonce, 7);
15102        assert_eq!(in_flight.tx_hash, expected_hash);
15103        assert_eq!(in_flight.purpose, TransactionPurpose::Wrap);
15104        assert!(
15105            second_error.to_string().contains("still awaiting finality"),
15106            "was: {second_error}"
15107        );
15108        assert_eq!(broadcasts, 0);
15109
15110        drop_execution_schema(&admin_pool, &schema).await;
15111    }
15112
15113    #[tokio::test]
15114    async fn cancellation_after_dispatch_keeps_record_and_in_flight_slot() {
15115        let state = signing_rpc_state().with_sleep(
15116            "eth_sendRawTransaction",
15117            Duration::from_secs(EXECUTION_RPC_TIMEOUT_SECS + 2),
15118        );
15119        let Some((admin_pool, schema, mut client, state)) =
15120            execution_client_with_database("execution_cancel_test", state).await
15121        else {
15122            return;
15123        };
15124
15125        let mut wrap = Box::pin(client.wrap(U256::from(1_000_000_000_000_000u64)));
15126        tokio::select! {
15127            result = &mut wrap => panic!("broadcast completed before cancellation: {result:?}"),
15128            () = await_recorded_requests(&state, "eth_sendRawTransaction", 1) => {}
15129        }
15130        drop(wrap);
15131
15132        let in_flight = awaiting_in_flight(&client);
15133        let error = client
15134            .wrap(U256::from(2_000_000_000_000_000u64))
15135            .await
15136            .unwrap_err();
15137        let record = client
15138            .cache
15139            .get_execution_transaction(42161, &in_flight.tx_hash.to_string())
15140            .await
15141            .unwrap()
15142            .unwrap();
15143        let broadcasts = state
15144            .recorded_requests()
15145            .into_iter()
15146            .filter(|request| request["method"] == "eth_sendRawTransaction")
15147            .count();
15148
15149        assert!(
15150            error.to_string().contains("still awaiting finality"),
15151            "was: {error}"
15152        );
15153        assert_eq!(record.nonce, 7);
15154        assert_eq!(record.purpose, "wrap");
15155        assert_eq!(record.status, "broadcast");
15156        assert_eq!(broadcasts, 1);
15157
15158        drop_execution_schema(&admin_pool, &schema).await;
15159    }
15160
15161    #[tokio::test]
15162    async fn cancellation_during_receipt_polling_keeps_record_and_in_flight_slot() {
15163        let state = signing_rpc_state()
15164            .with_send_raw_transaction_echo()
15165            .with_response("eth_getTransactionReceipt", RECEIPT_SUCCESS)
15166            .with_sleep(
15167                "eth_getTransactionReceipt",
15168                Duration::from_secs(EXECUTION_RPC_TIMEOUT_SECS + 2),
15169            );
15170        let Some((admin_pool, schema, mut client, state)) =
15171            execution_client_with_database("execution_receipt_cancel_test", state).await
15172        else {
15173            return;
15174        };
15175
15176        let mut wrap = Box::pin(client.wrap(U256::from(1_000_000_000_000_000u64)));
15177        tokio::select! {
15178            result = &mut wrap => panic!("receipt polling completed before cancellation: {result:?}"),
15179            () = await_recorded_requests(&state, "eth_getTransactionReceipt", 3) => {}
15180        }
15181        drop(wrap);
15182
15183        let in_flight = awaiting_in_flight(&client);
15184        let second_error = client
15185            .wrap(U256::from(2_000_000_000_000_000u64))
15186            .await
15187            .unwrap_err();
15188        let record = client
15189            .cache
15190            .get_execution_transaction(42161, &in_flight.tx_hash.to_string())
15191            .await
15192            .unwrap()
15193            .unwrap();
15194        let requests = state.recorded_requests();
15195
15196        assert_eq!(in_flight.nonce, 7);
15197        assert_eq!(in_flight.purpose, TransactionPurpose::Wrap);
15198        assert!(
15199            second_error.to_string().contains("still awaiting finality"),
15200            "was: {second_error}"
15201        );
15202        assert_eq!(record.nonce, 7);
15203        assert_eq!(record.transaction_hash, in_flight.tx_hash.to_string());
15204        assert_eq!(record.purpose, "wrap");
15205        assert_eq!(record.status, "broadcast");
15206        assert_eq!(
15207            requests
15208                .iter()
15209                .filter(|request| request["method"] == "eth_sendRawTransaction")
15210                .count(),
15211            1
15212        );
15213        assert_eq!(
15214            requests
15215                .iter()
15216                .filter(|request| request["method"] == "eth_getTransactionReceipt")
15217                .count(),
15218            3
15219        );
15220
15221        drop_execution_schema(&admin_pool, &schema).await;
15222    }
15223
15224    #[tokio::test]
15225    async fn rejected_broadcast_stays_dropped_and_occupied() {
15226        let state = signing_rpc_state()
15227            .with_response("eth_sendRawTransaction", SEND_RAW_TRANSACTION_REJECTED)
15228            .with_response("eth_getTransactionReceipt", RECEIPT_NULL);
15229        let Some((admin_pool, schema, mut client, state)) =
15230            execution_client_with_database("execution_rejected_test", state).await
15231        else {
15232            return;
15233        };
15234
15235        let error = client
15236            .wrap(U256::from(1_000_000_000_000_000u64))
15237            .await
15238            .unwrap_err();
15239        let (purpose, status): (String, String) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
15240            "SELECT purpose, status FROM {schema}.execution_intent"
15241        )))
15242        .fetch_one(&admin_pool)
15243        .await
15244        .unwrap();
15245        let broadcasts = state
15246            .recorded_requests()
15247            .into_iter()
15248            .filter(|request| request["method"] == "eth_sendRawTransaction")
15249            .count();
15250
15251        assert!(
15252            error.to_string().contains("Timed out awaiting finality"),
15253            "was: {error}"
15254        );
15255        assert!(client.in_flight.lock().is_some());
15256        assert_eq!(purpose, "wrap");
15257        assert_eq!(status, "dropped");
15258        assert_eq!(broadcasts, 1);
15259
15260        drop_execution_schema(&admin_pool, &schema).await;
15261    }
15262
15263    #[tokio::test]
15264    async fn legacy_table_loss_does_not_release_rejected_broadcast() {
15265        let state = signing_rpc_state()
15266            .with_response("eth_sendRawTransaction", SEND_RAW_TRANSACTION_REJECTED)
15267            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
15268            .with_sleep("eth_sendRawTransaction", Duration::from_secs(1));
15269        let Some((admin_pool, schema, mut client, state)) =
15270            execution_client_with_database("execution_rejected_update_test", state).await
15271        else {
15272            return;
15273        };
15274
15275        let mut wrap = Box::pin(client.wrap(U256::from(1_000_000_000_000_000u64)));
15276        tokio::select! {
15277            result = &mut wrap => panic!("broadcast completed before database failure: {result:?}"),
15278            () = await_recorded_requests(&state, "eth_sendRawTransaction", 1) => {}
15279        }
15280        sqlx::query(sqlx::AssertSqlSafe(format!(
15281            "DROP TABLE {schema}.execution_transaction"
15282        )))
15283        .execute(&admin_pool)
15284        .await
15285        .unwrap();
15286
15287        let error = wrap.await.unwrap_err();
15288
15289        assert!(
15290            error.to_string().contains("Timed out awaiting finality"),
15291            "was: {error}"
15292        );
15293        let in_flight = awaiting_in_flight(&client);
15294        assert_eq!(in_flight.nonce, 7);
15295        assert_eq!(in_flight.purpose, TransactionPurpose::Wrap);
15296
15297        drop_execution_schema(&admin_pool, &schema).await;
15298    }
15299
15300    #[tokio::test]
15301    async fn finalized_receipt_keeps_slot_when_status_update_fails() {
15302        let expected_hash = expected_wrap_tx_hash(U256::from(1_000_000_000_000_000u64)).await;
15303        let block = finalized_wrap_block(expected_hash);
15304        let receipt = receipt_with_transaction_hash(RECEIPT_SUCCESS, expected_hash);
15305        let state = with_finalized_identity(
15306            signing_rpc_state()
15307                .with_send_raw_transaction_echo()
15308                .with_response("eth_getTransactionReceipt", &receipt)
15309                .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block)
15310                .with_call_response_sequence(
15311                    BALANCE_OF_SELECTOR,
15312                    &[
15313                        CALL_BALANCE,
15314                        CALL_BALANCE,
15315                        CALL_BALANCE,
15316                        CALL_BALANCE,
15317                        CALL_BALANCE,
15318                        CALL_BALANCE,
15319                        CALL_BALANCE_AFTER_WRAP,
15320                        CALL_BALANCE_AFTER_WRAP,
15321                        CALL_BALANCE_AFTER_WRAP,
15322                    ],
15323                ),
15324            &block,
15325            &receipt,
15326        );
15327        let Some((admin_pool, schema, mut client, state)) =
15328            execution_client_with_database("execution_finalized_update_test", state).await
15329        else {
15330            return;
15331        };
15332
15333        for statement in [
15334            format!(
15335                "CREATE FUNCTION {schema}.reject_finalized_status() RETURNS trigger \
15336                 LANGUAGE plpgsql AS 'BEGIN IF NEW.status = ''finalized'' THEN \
15337                 RAISE EXCEPTION ''test finalized status rejection''; END IF; \
15338                 RETURN NEW; END'"
15339            ),
15340            format!(
15341                "CREATE TRIGGER reject_finalized_status BEFORE UPDATE ON \
15342                 {schema}.execution_transaction_hash FOR EACH ROW \
15343                 EXECUTE FUNCTION {schema}.reject_finalized_status()"
15344            ),
15345        ] {
15346            sqlx::query(sqlx::AssertSqlSafe(statement))
15347                .execute(&admin_pool)
15348                .await
15349                .unwrap();
15350        }
15351
15352        let error = client
15353            .wrap(U256::from(1_000_000_000_000_000u64))
15354            .await
15355            .unwrap_err();
15356        let in_flight = awaiting_in_flight(&client);
15357        let requests = state.recorded_requests();
15358
15359        assert!(
15360            error
15361                .to_string()
15362                .contains("test finalized status rejection"),
15363            "was: {error}"
15364        );
15365        assert_eq!(in_flight.nonce, 7);
15366        assert_eq!(in_flight.purpose, TransactionPurpose::Wrap);
15367        assert_eq!(
15368            requests
15369                .iter()
15370                .filter(|request| request["method"] == "eth_sendRawTransaction")
15371                .count(),
15372            1
15373        );
15374        assert_eq!(
15375            requests
15376                .iter()
15377                .filter(|request| request["method"] == "eth_getTransactionReceipt")
15378                .count(),
15379            3
15380        );
15381
15382        drop_execution_schema(&admin_pool, &schema).await;
15383    }
15384
15385    #[tokio::test]
15386    async fn broadcast_timeout_marks_dropped_and_keeps_ownership() {
15387        let state = signing_rpc_state()
15388            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
15389            .with_sleep(
15390                "eth_sendRawTransaction",
15391                Duration::from_secs(EXECUTION_RPC_TIMEOUT_SECS + 2),
15392            );
15393        let Some((admin_pool, schema, mut client, _)) =
15394            execution_client_with_database("execution_timeout_test", state).await
15395        else {
15396            return;
15397        };
15398
15399        let error = client
15400            .wrap(U256::from(1_000_000_000_000_000u64))
15401            .await
15402            .unwrap_err();
15403
15404        assert!(
15405            error.to_string().contains("Timed out awaiting finality"),
15406            "was: {error}"
15407        );
15408        let in_flight = awaiting_in_flight(&client);
15409        assert_eq!(in_flight.purpose, TransactionPurpose::Wrap);
15410        assert_eq!(in_flight.nonce, 7);
15411
15412        // The persisted record stays active for reconciliation instead of rebroadcasting.
15413        let record = client
15414            .cache
15415            .get_execution_transaction(42161, &in_flight.tx_hash.to_string())
15416            .await
15417            .unwrap()
15418            .unwrap();
15419        assert_eq!(record.status, "dropped");
15420
15421        drop_execution_schema(&admin_pool, &schema).await;
15422    }
15423
15424    #[tokio::test]
15425    async fn restart_reconciles_dropped_transaction_without_rebroadcast() {
15426        let initial_state = broadcast_rpc_state()
15427            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
15428            .with_call_response(BALANCE_OF_SELECTOR, CALL_BALANCE);
15429        let Some((admin_pool, schema, mut first_client, _)) =
15430            execution_client_with_database("execution_restart_test", initial_state).await
15431        else {
15432            return;
15433        };
15434        let expected_hash = expected_wrap_tx_hash(U256::from(1_000_000_000_000_000_u64)).await;
15435        let error = first_client
15436            .wrap(U256::from(1_000_000_000_000_000_u64))
15437            .await
15438            .unwrap_err();
15439        assert!(
15440            error.to_string().contains("Timed out awaiting finality"),
15441            "was: {error}"
15442        );
15443        let database = first_client.cache.database.as_ref().unwrap().clone();
15444        let payload_keys = first_client.payload_keys.clone();
15445        drop(first_client);
15446
15447        let block = finalized_wrap_block(expected_hash);
15448        let receipt = receipt_with_transaction_hash(RECEIPT_SUCCESS, expected_hash);
15449        let restart_state = with_finalized_identity(
15450            execution_rpc_state()
15451                .with_response("eth_getTransactionReceipt", &receipt)
15452                .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block)
15453                .with_response_sequence(
15454                    "eth_call",
15455                    &[
15456                        CALL_BALANCE,
15457                        CALL_BALANCE,
15458                        CALL_BALANCE,
15459                        CALL_BALANCE_AFTER_WRAP,
15460                        CALL_BALANCE_AFTER_WRAP,
15461                        CALL_BALANCE_AFTER_WRAP,
15462                    ],
15463                ),
15464            &block,
15465            &receipt,
15466        );
15467        let addr = start_mock_rpc_server(restart_state.clone()).await;
15468        let mut restarted = test_client(format!("http://{addr}"));
15469        restarted.cache.database = Some(database);
15470        restarted.payload_keys = payload_keys;
15471        restarted.signer = Some(Arc::new(
15472            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
15473        ));
15474
15475        restarted.reconcile_unresolved_execution().await.unwrap();
15476        restarted.reconcile_unresolved_execution().await.unwrap();
15477
15478        let record = restarted
15479            .cache
15480            .get_execution_transaction(42161, &expected_hash.to_string())
15481            .await
15482            .unwrap()
15483            .unwrap();
15484        let requests = restart_state.recorded_requests();
15485        assert_eq!(record.status, "finalized");
15486        assert!(restarted.in_flight.lock().is_none());
15487        assert_eq!(
15488            requests
15489                .iter()
15490                .filter(|request| request["method"] == "eth_getTransactionReceipt")
15491                .count(),
15492            6
15493        );
15494        assert_eq!(
15495            requests
15496                .iter()
15497                .filter(|request| {
15498                    request["method"] == "eth_call"
15499                        && request["params"][0]["data"]
15500                            .as_str()
15501                            .is_some_and(|data| data.starts_with(BALANCE_OF_SELECTOR))
15502                })
15503                .count(),
15504            6
15505        );
15506        assert_eq!(
15507            requests
15508                .iter()
15509                .filter(|request| request["method"] == "eth_sendRawTransaction")
15510                .count(),
15511            0
15512        );
15513
15514        drop_execution_schema(&admin_pool, &schema).await;
15515    }
15516
15517    fn migration_test_intent(id: i64) -> ExecutionIntentRow {
15518        ExecutionIntentRow {
15519            id,
15520            schema_version: crate::execution::transaction::EXECUTION_SCHEMA_VERSION,
15521            chain_id: 42_161,
15522            wallet_address: WALLET.to_string(),
15523            nonce: None,
15524            purpose: "wrap".to_string(),
15525            status: "prepared".to_string(),
15526            client_order_id: None,
15527            trader_id: None,
15528            strategy_id: None,
15529            account_id: None,
15530            instrument_id: None,
15531            pool_address: None,
15532            transaction_to: WETH.to_string(),
15533            transaction_input: "0xd0e30db0".to_string(),
15534            transaction_value: "1".to_string(),
15535            amount_in: None,
15536            created_block: FIXTURE_BLOCK,
15537            acknowledgement_emitted: false,
15538            fill_emitted: false,
15539            terminal_emitted: false,
15540            active: true,
15541        }
15542    }
15543
15544    fn migration_nonce_verification() -> Verified<u64> {
15545        Verified {
15546            value: 7,
15547            read: crate::rpc::verification::VerificationRead::TransactionCount,
15548            provider_ids: [
15549                "authoritative".to_string(),
15550                "verifier-a".to_string(),
15551                "verifier-b".to_string(),
15552            ],
15553            normalized_value_digest: keccak256(7_u64.to_be_bytes()),
15554        }
15555    }
15556
15557    fn migration_finalized_header() -> VerifiedBlockHeader {
15558        VerifiedBlockHeader {
15559            number: FIXTURE_BLOCK,
15560            hash: B256::from_str(FIXTURE_BLOCK_HASH).unwrap(),
15561            parent_hash: B256::from_str(
15562                "0x0000000000000000000000000000000000000000000000000000000000000001",
15563            )
15564            .unwrap(),
15565            timestamp: FIXTURE_BLOCK_TIMESTAMP,
15566            base_fee_per_gas: Some(100_000_000),
15567        }
15568    }
15569
15570    #[tokio::test]
15571    async fn verification_migration_recovers_prepared_unassigned_intent() {
15572        let client = test_client("http://127.0.0.1:1".to_string());
15573        let snapshot = ExecutionVerificationMigrationSnapshot {
15574            intents: vec![migration_test_intent(1)],
15575            hashes: Vec::new(),
15576        };
15577        let finalized = migration_finalized_header();
15578        let migration = client
15579            .build_execution_verification_migration(
15580                snapshot,
15581                finalized,
15582                &[finalized],
15583                &migration_nonce_verification(),
15584            )
15585            .await
15586            .unwrap();
15587
15588        assert_eq!(migration.records.len(), 1);
15589        let record = &migration.records[0];
15590        assert_eq!(record.intent_id, 1);
15591        assert_eq!(record.nonce, None);
15592        assert_eq!(record.transaction_hash, None);
15593        assert!(record.recover_prepared);
15594        assert_eq!(record.decisions.len(), 1);
15595    }
15596
15597    #[tokio::test]
15598    async fn verification_migration_rejects_inconsistent_released_history() {
15599        let client = test_client("http://127.0.0.1:1".to_string());
15600        let finalized = migration_finalized_header();
15601        let nonce_verification = migration_nonce_verification();
15602        let mut missing_marker = migration_test_intent(1);
15603        missing_marker.active = false;
15604        missing_marker.status = "finalized".to_string();
15605        missing_marker.nonce = Some(6);
15606        let error = client
15607            .build_execution_verification_migration(
15608                ExecutionVerificationMigrationSnapshot {
15609                    intents: vec![missing_marker],
15610                    hashes: Vec::new(),
15611                },
15612                finalized,
15613                &[finalized],
15614                &nonce_verification,
15615            )
15616            .await
15617            .err()
15618            .unwrap();
15619        assert!(
15620            error.to_string().contains("has no durable event marker"),
15621            "was: {error}"
15622        );
15623
15624        let mut first = migration_test_intent(1);
15625        first.active = false;
15626        first.status = "recoverable".to_string();
15627        first.nonce = Some(6);
15628        let mut second = first.clone();
15629        second.id = 2;
15630        let error = client
15631            .build_execution_verification_migration(
15632                ExecutionVerificationMigrationSnapshot {
15633                    intents: vec![first, second],
15634                    hashes: Vec::new(),
15635                },
15636                finalized,
15637                &[finalized],
15638                &nonce_verification,
15639            )
15640            .await
15641            .err()
15642            .unwrap();
15643        assert!(
15644            error
15645                .to_string()
15646                .contains("duplicate signer nonce ownership"),
15647            "was: {error}"
15648        );
15649    }
15650
15651    #[tokio::test]
15652    async fn verification_migration_reconstructs_consumed_active_intent() {
15653        let expected_hash = expected_wrap_tx_hash(U256::from(1u64)).await;
15654        let mut block_value: serde_json::Value =
15655            serde_json::from_str(&finalized_wrap_block(expected_hash)).unwrap();
15656        block_value["result"]["transactions"][0]["value"] = serde_json::json!("0x1");
15657        let block = block_value.to_string();
15658        let receipt = receipt_with_transaction_hash(RECEIPT_SUCCESS, expected_hash);
15659        let state = with_finalized_identity(
15660            execution_rpc_state()
15661                .with_response("eth_getTransactionCount", TRANSACTION_COUNT_NEXT)
15662                .with_response("eth_getTransactionReceipt", &receipt)
15663                .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block),
15664            &block,
15665            &receipt,
15666        );
15667        let Some((admin_pool, pg_config)) =
15668            connect_test_postgres("verification_migration_reconstructs_consumed_active_intent")
15669                .await
15670        else {
15671            return;
15672        };
15673        let schema = format!(
15674            "verification_migration_reconstructs_consumed_active_intent_{}",
15675            std::process::id()
15676        );
15677        setup_execution_schema(&admin_pool, &schema).await;
15678        let options: sqlx::postgres::PgConnectOptions = pg_config.into();
15679        let database = connect_test_database(options.options([("search_path", schema.clone())]))
15680            .await
15681            .unwrap();
15682        let addr = start_mock_rpc_server(state).await;
15683        let (mut client, _) = swap_client_with_cache(test_config(format!("http://{addr}")));
15684        client.cache.database = Some(database.clone());
15685        client
15686            .cache
15687            .ensure_execution_transaction_schema()
15688            .await
15689            .unwrap();
15690        let (intent, persisted_hash, _) = persist_test_wrap_broadcast(&database, None).await;
15691        protect_test_storage(&mut client, &schema).await;
15692        assert_eq!(persisted_hash, expected_hash);
15693
15694        let snapshot = database
15695            .load_execution_verification_migration_snapshot(42_161, WALLET)
15696            .await
15697            .unwrap();
15698        let nonce_verification = required_verification(
15699            client
15700                .verification
15701                .verify_transaction_count(&Address::from_str(WALLET).unwrap(), FIXTURE_BLOCK + 1)
15702                .await,
15703            "test migration nonce",
15704        )
15705        .unwrap();
15706        let headers = [
15707            VerifiedBlockHeader {
15708                number: FIXTURE_BLOCK,
15709                hash: B256::from_str(FIXTURE_BLOCK_HASH).unwrap(),
15710                parent_hash: B256::from_str(
15711                    "0x0000000000000000000000000000000000000000000000000000000000000001",
15712                )
15713                .unwrap(),
15714                timestamp: FIXTURE_BLOCK_TIMESTAMP,
15715                base_fee_per_gas: Some(100_000_000),
15716            },
15717            VerifiedBlockHeader {
15718                number: FIXTURE_BLOCK + 1,
15719                hash: B256::from([0x22; 32]),
15720                parent_hash: B256::from_str(FIXTURE_BLOCK_HASH).unwrap(),
15721                timestamp: FIXTURE_BLOCK_TIMESTAMP + 1,
15722                base_fee_per_gas: Some(100_000_000),
15723            },
15724        ];
15725        let migration = client
15726            .build_execution_verification_migration(
15727                snapshot,
15728                headers[1],
15729                &headers,
15730                &nonce_verification,
15731            )
15732            .await
15733            .unwrap();
15734        let finalized_headers = headers
15735            .iter()
15736            .map(|header| ExecutionVerifiedHeader {
15737                number: header.number,
15738                hash: header.hash.to_string(),
15739                parent_hash: header.parent_hash.to_string(),
15740                timestamp: header.timestamp,
15741                base_fee_per_gas: header.base_fee_per_gas,
15742            })
15743            .collect::<Vec<_>>();
15744        let decisions = [verification_decision(
15745            &nonce_verification,
15746            Some(FIXTURE_BLOCK + 1),
15747            Some(FIXTURE_BLOCK + 1),
15748        )];
15749
15750        initialize_test_verification_migration(
15751            &client,
15752            &finalized_headers,
15753            8,
15754            &decisions,
15755            &migration,
15756        )
15757        .await;
15758
15759        let migrated = database.get_execution_intent(intent.id).await.unwrap();
15760        let resume = database
15761            .load_execution_verification_resume(
15762                42_161,
15763                WALLET,
15764                &client
15765                    .config
15766                    .verification
15767                    .as_ref()
15768                    .unwrap()
15769                    .manifest_version,
15770                &client.config.verification.as_ref().unwrap().manifest_digest,
15771            )
15772            .await
15773            .unwrap()
15774            .unwrap();
15775        let evidence: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
15776            "SELECT COUNT(*) FROM {schema}.execution_verification_decision \
15777             WHERE intent_id = $1 AND decision_class = 'migration'"
15778        )))
15779        .bind(intent.id)
15780        .fetch_one(&admin_pool)
15781        .await
15782        .unwrap();
15783
15784        assert_eq!(migrated.status, "finalized");
15785        assert!(migrated.active);
15786        assert_eq!(resume.next_canonical_nonce, 8);
15787        assert!(evidence >= 5);
15788
15789        drop_execution_schema(&admin_pool, &schema).await;
15790    }
15791
15792    #[tokio::test]
15793    async fn verification_resume_accepts_one_consumed_owned_nonce() {
15794        let Some((admin_pool, schema, client, _)) = execution_client_with_database(
15795            "verification_resume_owned_nonce_test",
15796            ready_rpc_state(),
15797        )
15798        .await
15799        else {
15800            return;
15801        };
15802        let database = client.cache.database.as_ref().unwrap();
15803        persist_test_wrap_broadcast(database, client.payload_keys.as_deref()).await;
15804        let resume = database
15805            .load_execution_verification_resume(
15806                42_161,
15807                WALLET,
15808                &client
15809                    .config
15810                    .verification
15811                    .as_ref()
15812                    .unwrap()
15813                    .manifest_version,
15814                &client.config.verification.as_ref().unwrap().manifest_digest,
15815            )
15816            .await
15817            .unwrap()
15818            .unwrap();
15819
15820        ensure_test_verification_ledger(&client, &resume.finalized_headers, 7, 8)
15821            .await
15822            .unwrap();
15823
15824        let nonce_state = sqlx::query_as::<_, (i64, i64)>(sqlx::AssertSqlSafe(format!(
15825            "SELECT next_canonical_nonce, revision FROM {schema}.execution_verification_nonce"
15826        )))
15827        .fetch_one(&admin_pool)
15828        .await
15829        .unwrap();
15830        assert_eq!(nonce_state, (7, 0));
15831
15832        drop_execution_schema(&admin_pool, &schema).await;
15833    }
15834
15835    #[tokio::test]
15836    async fn verification_resume_rejects_unowned_nonce_advance() {
15837        for (test_name, mutation, observed_canonical_nonce) in [
15838            ("verification_resume_no_active_test", "no_active", 8),
15839            ("verification_resume_excess_test", "unchanged", 9),
15840            ("verification_resume_signed_test", "signed", 8),
15841            ("verification_resume_prepared_test", "prepared", 8),
15842            (
15843                "verification_resume_missing_payload_test",
15844                "missing_payload",
15845                8,
15846            ),
15847        ] {
15848            let Some((admin_pool, schema, client, _)) =
15849                execution_client_with_database(test_name, ready_rpc_state()).await
15850            else {
15851                return;
15852            };
15853            let database = client.cache.database.as_ref().unwrap();
15854            let (intent, _, _) =
15855                persist_test_wrap_broadcast(database, client.payload_keys.as_deref()).await;
15856
15857            match mutation {
15858                "no_active" => {
15859                    sqlx::query(sqlx::AssertSqlSafe(format!(
15860                        "UPDATE {schema}.execution_intent SET active = FALSE WHERE id = $1"
15861                    )))
15862                    .bind(intent.id)
15863                    .execute(&admin_pool)
15864                    .await
15865                    .unwrap();
15866                }
15867                "signed" => {
15868                    sqlx::query(sqlx::AssertSqlSafe(format!(
15869                        "UPDATE {schema}.execution_intent SET status = 'signed' WHERE id = $1"
15870                    )))
15871                    .bind(intent.id)
15872                    .execute(&admin_pool)
15873                    .await
15874                    .unwrap();
15875                    sqlx::query(sqlx::AssertSqlSafe(format!(
15876                        "UPDATE {schema}.execution_transaction_hash SET status = 'signed' \
15877                         WHERE intent_id = $1"
15878                    )))
15879                    .bind(intent.id)
15880                    .execute(&admin_pool)
15881                    .await
15882                    .unwrap();
15883                }
15884                "prepared" => {
15885                    sqlx::query(sqlx::AssertSqlSafe(format!(
15886                        "UPDATE {schema}.execution_intent \
15887                         SET nonce = NULL, status = 'prepared' WHERE id = $1"
15888                    )))
15889                    .bind(intent.id)
15890                    .execute(&admin_pool)
15891                    .await
15892                    .unwrap();
15893                    sqlx::query(sqlx::AssertSqlSafe(format!(
15894                        "UPDATE {schema}.execution_transaction_hash SET current = FALSE \
15895                         WHERE intent_id = $1"
15896                    )))
15897                    .bind(intent.id)
15898                    .execute(&admin_pool)
15899                    .await
15900                    .unwrap();
15901                }
15902                "missing_payload" => {
15903                    sqlx::query(sqlx::AssertSqlSafe(format!(
15904                        "UPDATE {schema}.execution_transaction_hash \
15905                         SET payload_expected = FALSE, sealed_transaction = NULL \
15906                         WHERE intent_id = $1"
15907                    )))
15908                    .bind(intent.id)
15909                    .execute(&admin_pool)
15910                    .await
15911                    .unwrap();
15912                }
15913                "unchanged" => {}
15914                _ => unreachable!(),
15915            }
15916            let resume = database
15917                .load_execution_verification_resume(
15918                    42_161,
15919                    WALLET,
15920                    &client
15921                        .config
15922                        .verification
15923                        .as_ref()
15924                        .unwrap()
15925                        .manifest_version,
15926                    &client.config.verification.as_ref().unwrap().manifest_digest,
15927                )
15928                .await
15929                .unwrap()
15930                .unwrap();
15931
15932            let error = ensure_test_verification_ledger(
15933                &client,
15934                &resume.finalized_headers,
15935                7,
15936                observed_canonical_nonce,
15937            )
15938            .await
15939            .unwrap_err();
15940
15941            assert!(
15942                error
15943                    .to_string()
15944                    .contains(if observed_canonical_nonce == 9 {
15945                        "outside the owned recovery range"
15946                    } else {
15947                        "without"
15948                    }),
15949                "{mutation}: {error}"
15950            );
15951            let nonce_state = sqlx::query_as::<_, (i64, i64)>(sqlx::AssertSqlSafe(format!(
15952                "SELECT next_canonical_nonce, revision FROM {schema}.execution_verification_nonce"
15953            )))
15954            .fetch_one(&admin_pool)
15955            .await
15956            .unwrap();
15957            assert_eq!(nonce_state, (7, 0), "{mutation}");
15958
15959            drop_execution_schema(&admin_pool, &schema).await;
15960        }
15961    }
15962
15963    #[tokio::test]
15964    async fn restart_marks_prepared_intent_recoverable_without_broadcast() {
15965        let Some((admin_pool, schema, client, state)) =
15966            execution_client_with_database("execution_prepared_restart_test", ready_rpc_state())
15967                .await
15968        else {
15969            return;
15970        };
15971        let database = client.cache.database.as_ref().unwrap();
15972        let intent = reserve_test_wrap_intent(database).await;
15973        *client.in_flight.lock() = Some(InFlightSlot::Preparing(TransactionPurpose::Wrap));
15974
15975        client.reconcile_unresolved_execution().await.unwrap();
15976
15977        let (status, active): (String, bool) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
15978            "SELECT status, active FROM {schema}.execution_intent WHERE id = {}",
15979            intent.id
15980        )))
15981        .fetch_one(&admin_pool)
15982        .await
15983        .unwrap();
15984        let transitions: Vec<String> = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
15985            "SELECT to_status FROM {schema}.execution_transaction_transition ORDER BY id"
15986        )))
15987        .fetch_all(&admin_pool)
15988        .await
15989        .unwrap();
15990
15991        assert_eq!(status, "recoverable");
15992        assert!(!active);
15993        assert_eq!(transitions, ["prepared", "recoverable"]);
15994        assert!(client.in_flight.lock().is_none());
15995        assert!(
15996            state
15997                .recorded_requests()
15998                .iter()
15999                .all(|request| request["method"] != "eth_sendRawTransaction")
16000        );
16001
16002        drop_execution_schema(&admin_pool, &schema).await;
16003    }
16004
16005    #[tokio::test]
16006    async fn protected_restart_authenticates_envelope_before_recovery() {
16007        let initial_state = broadcast_rpc_state()
16008            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
16009            .with_call_response(BALANCE_OF_SELECTOR, CALL_BALANCE);
16010        let Some((admin_pool, schema, mut first_client, _)) =
16011            execution_client_with_database("protected_restart_test", initial_state).await
16012        else {
16013            return;
16014        };
16015        let database = first_client.cache.database.as_ref().unwrap().clone();
16016        let value = U256::from(1_000_000_000_000_000_u64);
16017        let expected_hash = expected_wrap_tx_hash(value).await;
16018
16019        let error = first_client.wrap(value).await.unwrap_err();
16020
16021        assert!(error.to_string().contains("Timed out awaiting finality"));
16022        let intent = database
16023            .get_active_execution_intent(42161, WALLET)
16024            .await
16025            .unwrap()
16026            .unwrap();
16027        let payload = database
16028            .get_execution_transaction_hashes(intent.id)
16029            .await
16030            .unwrap()
16031            .pop()
16032            .unwrap();
16033        assert!(payload.raw_transaction.is_none());
16034        assert!(payload.sealed_transaction.is_some());
16035        let keys = first_client.payload_keys.take().unwrap();
16036        drop(first_client);
16037
16038        let block = finalized_wrap_block(expected_hash);
16039        let receipt = receipt_with_transaction_hash(RECEIPT_SUCCESS, expected_hash);
16040        let restart_state = with_finalized_identity(
16041            execution_rpc_state()
16042                .with_response("eth_getTransactionReceipt", &receipt)
16043                .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block)
16044                .with_response_sequence(
16045                    "eth_call",
16046                    &[
16047                        CALL_BALANCE,
16048                        CALL_BALANCE,
16049                        CALL_BALANCE,
16050                        CALL_BALANCE_AFTER_WRAP,
16051                        CALL_BALANCE_AFTER_WRAP,
16052                        CALL_BALANCE_AFTER_WRAP,
16053                    ],
16054                ),
16055            &block,
16056            &receipt,
16057        );
16058        let addr = start_mock_rpc_server(restart_state.clone()).await;
16059        let mut restarted = test_client(format!("http://{addr}"));
16060        restarted.cache.database = Some(database);
16061        restarted.payload_keys = Some(keys);
16062        restarted.signer = Some(Arc::new(
16063            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16064        ));
16065
16066        restarted.reconcile_unresolved_execution().await.unwrap();
16067
16068        let record = restarted
16069            .cache
16070            .get_execution_transaction(42161, &expected_hash.to_string())
16071            .await
16072            .unwrap()
16073            .unwrap();
16074        let requests = restart_state.recorded_requests();
16075        assert_eq!(record.status, "finalized");
16076        assert!(restarted.in_flight.lock().is_none());
16077        assert_eq!(
16078            requests
16079                .iter()
16080                .filter(|request| request["method"] == "eth_sendRawTransaction")
16081                .count(),
16082            0
16083        );
16084
16085        drop_execution_schema(&admin_pool, &schema).await;
16086    }
16087
16088    #[tokio::test]
16089    async fn active_intent_reconciliation_waits_for_reservation_fence() {
16090        let Some((admin_pool, schema, client, _)) = execution_client_with_database(
16091            "execution_active_intent_reservation_fence_test",
16092            ready_rpc_state(),
16093        )
16094        .await
16095        else {
16096            return;
16097        };
16098        let database = client.cache.database.as_ref().unwrap().clone();
16099        let lock = PgAdvisoryLock::new(format!(
16100            "nautilus:blockchain:execution:42161:{}",
16101            WALLET.to_ascii_lowercase()
16102        ));
16103        let PgAdvisoryLockKey::BigInt(lock_key) = lock.key() else {
16104            unreachable!("string advisory locks use the 64-bit key space");
16105        };
16106        let mut reservation = admin_pool.begin().await.unwrap();
16107        sqlx::query("SELECT pg_advisory_xact_lock($1)")
16108            .bind(*lock_key)
16109            .execute(&mut *reservation)
16110            .await
16111            .unwrap();
16112        sqlx::query(sqlx::AssertSqlSafe(format!(
16113            "INSERT INTO {schema}.execution_intent (\
16114                schema_version, chain_id, wallet_address, purpose, status, transaction_to, \
16115                transaction_input, transaction_value, created_block\
16116             ) VALUES (2, $1, $2, 'wrap', 'prepared', $3, '0xd0e30db0', '1', $4)"
16117        )))
16118        .bind(42161_i32)
16119        .bind(WALLET)
16120        .bind(WETH_ADDRESS.to_string())
16121        .bind(i64::try_from(FIXTURE_BLOCK).unwrap())
16122        .execute(&mut *reservation)
16123        .await
16124        .unwrap();
16125
16126        let mut reconciliation = Box::pin(database.get_active_execution_intent(42161, WALLET));
16127        assert!(
16128            tokio::time::timeout(Duration::from_millis(100), &mut reconciliation)
16129                .await
16130                .is_err(),
16131            "reconciliation did not wait for the reservation fence"
16132        );
16133
16134        reservation.commit().await.unwrap();
16135        let intent = tokio::time::timeout(Duration::from_secs(2), reconciliation)
16136            .await
16137            .unwrap()
16138            .unwrap()
16139            .unwrap();
16140
16141        assert_eq!(intent.chain_id, 42161);
16142        assert_eq!(intent.wallet_address, WALLET);
16143        assert_eq!(intent.purpose, "wrap");
16144        assert_eq!(intent.status, "prepared");
16145        assert!(intent.active);
16146
16147        drop_execution_schema(&admin_pool, &schema).await;
16148    }
16149
16150    #[tokio::test]
16151    async fn restart_keeps_unbroadcast_signed_intent_reserved() {
16152        let Some((admin_pool, schema, client, state)) =
16153            execution_client_with_database("execution_signed_restart_test", ready_rpc_state())
16154                .await
16155        else {
16156            return;
16157        };
16158        let database = client.cache.database.as_ref().unwrap();
16159        let intent = reserve_test_wrap_intent(database).await;
16160        let transaction = build_eip1559_transaction(
16161            42161,
16162            7,
16163            78_000,
16164            130_000_000,
16165            10_000_000,
16166            WETH_ADDRESS,
16167            U256::from(1u64),
16168            Bytes::from(nautilus_core::hex::decode("d0e30db0").unwrap()),
16169        );
16170        let (tx_hash, raw_transaction) = sign_eip1559_transaction(
16171            transaction,
16172            &PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16173        )
16174        .await
16175        .unwrap();
16176        database
16177            .assign_execution_intent_nonce(intent.id, 7)
16178            .await
16179            .unwrap();
16180        let intent = database.get_execution_intent(intent.id).await.unwrap();
16181        persist_test_payload(
16182            database,
16183            client.payload_keys.as_deref(),
16184            &intent,
16185            tx_hash,
16186            &raw_transaction,
16187        )
16188        .await;
16189
16190        let error = client.reconcile_unresolved_execution().await.unwrap_err();
16191
16192        assert!(
16193            error
16194                .to_string()
16195                .contains("was not authorized for broadcast"),
16196            "was: {error}"
16197        );
16198        let recovery = recovering_in_flight(&client);
16199        assert_eq!(recovery.intent_id, intent.id);
16200        assert_eq!(recovery.nonce, 7);
16201        assert_eq!(recovery.purpose, TransactionPurpose::Wrap);
16202        assert_eq!(
16203            execution_intent_markers(&admin_pool, &schema).await,
16204            vec![("wrap".into(), "signed".into(), false, true)]
16205        );
16206        assert!(
16207            state
16208                .recorded_requests()
16209                .iter()
16210                .all(|request| request["method"] != "eth_sendRawTransaction")
16211        );
16212
16213        drop_execution_schema(&admin_pool, &schema).await;
16214    }
16215
16216    #[tokio::test]
16217    async fn restart_quarantines_legacy_recoverable_signed_intent() {
16218        let Some((admin_pool, schema, client, state)) = execution_client_with_database(
16219            "execution_legacy_recoverable_restart_test",
16220            ready_rpc_state(),
16221        )
16222        .await
16223        else {
16224            return;
16225        };
16226        let database = client.cache.database.as_ref().unwrap();
16227        let intent = reserve_test_wrap_intent(database).await;
16228        let tx_hash = B256::from([0x55; 32]);
16229        database
16230            .assign_execution_intent_nonce(intent.id, 7)
16231            .await
16232            .unwrap();
16233        let intent = database.get_execution_intent(intent.id).await.unwrap();
16234        persist_test_payload(
16235            database,
16236            client.payload_keys.as_deref(),
16237            &intent,
16238            tx_hash,
16239            &[0x01, 0x02, 0x03],
16240        )
16241        .await;
16242        sqlx::query(sqlx::AssertSqlSafe(format!(
16243            "UPDATE {schema}.execution_intent SET status = 'recoverable', active = FALSE WHERE id = {}",
16244            intent.id
16245        )))
16246        .execute(&admin_pool)
16247        .await
16248        .unwrap();
16249
16250        let error = client.reconcile_unresolved_execution().await.unwrap_err();
16251
16252        assert!(
16253            error
16254                .to_string()
16255                .contains("retains signed transaction bytes"),
16256            "was: {error}"
16257        );
16258        assert!(client.in_flight.lock().is_none());
16259        assert!(
16260            state
16261                .recorded_requests()
16262                .iter()
16263                .all(|request| request["method"] != "eth_sendRawTransaction")
16264        );
16265
16266        drop_execution_schema(&admin_pool, &schema).await;
16267    }
16268
16269    #[rstest]
16270    #[case::current("execution_invalid_signed_restart_test", false, "broadcast")]
16271    #[case::historical("execution_invalid_historical_restart_test", true, "replaced")]
16272    #[tokio::test]
16273    async fn restart_rejects_invalid_signed_bytes_before_recovery_effects(
16274        #[case] test_name: &str,
16275        #[case] historical: bool,
16276        #[case] expected_status: &str,
16277    ) {
16278        let Some((admin_pool, schema, first_client, state, _)) =
16279            swap_client_with_database(test_name, ready_rpc_state()).await
16280        else {
16281            return;
16282        };
16283        let database = first_client.cache.database.as_ref().unwrap().clone();
16284        let (intent, _) =
16285            persist_invalid_test_swap(&database, first_client.payload_keys.as_deref()).await;
16286
16287        if historical {
16288            sqlx::query(sqlx::AssertSqlSafe(format!(
16289                "UPDATE {schema}.execution_transaction_hash \
16290                 SET current = FALSE, status = 'replaced' WHERE intent_id = $1"
16291            )))
16292            .bind(intent.id)
16293            .execute(&admin_pool)
16294            .await
16295            .unwrap();
16296            sqlx::query(sqlx::AssertSqlSafe(format!(
16297                "INSERT INTO {schema}.execution_transaction_hash (\
16298                     intent_id, chain_id, transaction_hash, payload_expected, status, current\
16299                 ) VALUES ($1, 42161, $2, FALSE, 'replaced', TRUE)"
16300            )))
16301            .bind(intent.id)
16302            .bind(B256::from([0x44; 32]).to_string())
16303            .execute(&admin_pool)
16304            .await
16305            .unwrap();
16306            sqlx::query(sqlx::AssertSqlSafe(format!(
16307                "UPDATE {schema}.execution_intent SET status = 'replaced' WHERE id = $1"
16308            )))
16309            .bind(intent.id)
16310            .execute(&admin_pool)
16311            .await
16312            .unwrap();
16313        }
16314        let transitions_before: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
16315            "SELECT COUNT(*) FROM {schema}.execution_transaction_transition"
16316        )))
16317        .fetch_one(&admin_pool)
16318        .await
16319        .unwrap();
16320        let config = first_client.config.clone();
16321        let payload_keys = first_client.payload_keys.clone();
16322        drop(first_client);
16323
16324        let (mut restarted, _) = swap_client_with_cache(config);
16325        restarted.cache.database = Some(database);
16326        restarted.payload_keys = payload_keys;
16327        restarted.signer = Some(Arc::new(
16328            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16329        ));
16330        let mut receiver = start_with_events(&mut restarted);
16331
16332        let error = restarted
16333            .reconcile_unresolved_execution()
16334            .await
16335            .unwrap_err();
16336
16337        let recovery = recovering_in_flight(&restarted);
16338        let (status, acknowledgement_emitted, active): (String, bool, bool) =
16339            sqlx::query_as(sqlx::AssertSqlSafe(format!(
16340                "SELECT status, acknowledgement_emitted, active FROM {schema}.execution_intent"
16341            )))
16342            .fetch_one(&admin_pool)
16343            .await
16344            .unwrap();
16345        let transitions_after: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
16346            "SELECT COUNT(*) FROM {schema}.execution_transaction_transition"
16347        )))
16348        .fetch_one(&admin_pool)
16349        .await
16350        .unwrap();
16351
16352        assert!(
16353            error
16354                .to_string()
16355                .contains("is not a complete EIP-2718 envelope"),
16356            "was: {error}"
16357        );
16358        assert_eq!(recovery.intent_id, intent.id);
16359        assert_eq!(recovery.nonce, 7);
16360        assert_eq!(recovery.purpose, TransactionPurpose::Swap);
16361        assert_eq!(status, expected_status);
16362        assert!(!acknowledgement_emitted);
16363        assert!(active);
16364        assert_eq!(transitions_after, transitions_before);
16365        assert!(collect_order_events(&mut receiver).is_empty());
16366        assert!(
16367            state
16368                .recorded_requests()
16369                .iter()
16370                .all(|request| request["method"] != "eth_sendRawTransaction")
16371        );
16372
16373        drop_execution_schema(&admin_pool, &schema).await;
16374    }
16375
16376    #[tokio::test]
16377    async fn restart_rebroadcasts_only_durably_authorized_bytes() {
16378        let Some((admin_pool, schema, first_client, _)) =
16379            execution_client_with_database("execution_broadcast_restart_test", ready_rpc_state())
16380                .await
16381        else {
16382            return;
16383        };
16384        let database = first_client.cache.database.as_ref().unwrap().clone();
16385        let (_, _, raw_tx) =
16386            persist_test_wrap_broadcast(&database, first_client.payload_keys.as_deref()).await;
16387        let payload_keys = first_client.payload_keys.clone();
16388        drop(first_client);
16389
16390        let restart_state = execution_rpc_state()
16391            .with_response("eth_getTransactionCount", TRANSACTION_COUNT)
16392            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
16393            .with_response("eth_call", CALL_EMPTY)
16394            .with_send_raw_transaction_echo();
16395        let addr = start_mock_rpc_server(restart_state.clone()).await;
16396        let mut restarted = test_client(format!("http://{addr}"));
16397        restarted.cache.database = Some(database);
16398        restarted.payload_keys = payload_keys;
16399        restarted.signer = Some(Arc::new(
16400            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16401        ));
16402
16403        restarted.reconcile_unresolved_execution().await.unwrap();
16404
16405        let broadcasts = restart_state
16406            .recorded_requests()
16407            .into_iter()
16408            .filter(|request| request["method"] == "eth_sendRawTransaction")
16409            .collect::<Vec<_>>();
16410        assert_eq!(broadcasts.len(), 1);
16411        assert_eq!(broadcasts[0]["params"][0], hex::encode_prefixed(&raw_tx));
16412        assert_eq!(
16413            execution_intent_markers(&admin_pool, &schema).await,
16414            vec![("wrap".into(), "dropped".into(), false, true)]
16415        );
16416
16417        drop_execution_schema(&admin_pool, &schema).await;
16418    }
16419
16420    #[tokio::test]
16421    async fn restart_suppresses_rebroadcast_when_canonical_nonce_advanced() {
16422        let Some((admin_pool, schema, first_client, _)) = execution_client_with_database(
16423            "execution_rebroadcast_nonce_advanced_test",
16424            ready_rpc_state(),
16425        )
16426        .await
16427        else {
16428            return;
16429        };
16430        let database = first_client.cache.database.as_ref().unwrap().clone();
16431        let (intent, _, _) =
16432            persist_test_wrap_broadcast(&database, first_client.payload_keys.as_deref()).await;
16433        let payload_keys = first_client.payload_keys.clone();
16434        drop(first_client);
16435
16436        let mut empty_block: serde_json::Value =
16437            serde_json::from_str(&replacement_head_block(B256::from([0x44; 32]))).unwrap();
16438        empty_block["result"]["transactions"] = serde_json::json!([]);
16439        let empty_block = empty_block.to_string();
16440        let restart_state = execution_rpc_state()
16441            .with_response("eth_getTransactionCount", TRANSACTION_COUNT_NEXT)
16442            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
16443            .with_response("eth_call", CALL_EMPTY)
16444            .with_parameter_response("eth_getBlockByNumber", "0x1cf0d40", &empty_block)
16445            .with_send_raw_transaction_echo();
16446        let addr = start_mock_rpc_server(restart_state.clone()).await;
16447        let mut restarted = test_client(format!("http://{addr}"));
16448        restarted.cache.database = Some(database);
16449        restarted.payload_keys = payload_keys;
16450        restarted.signer = Some(Arc::new(
16451            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16452        ));
16453
16454        let error = restarted
16455            .reconcile_unresolved_execution()
16456            .await
16457            .unwrap_err();
16458
16459        let requests = restart_state.recorded_requests();
16460        assert!(
16461            requests
16462                .iter()
16463                .all(|request| request["method"] != "eth_sendRawTransaction")
16464        );
16465        assert!(requests.iter().all(|request| {
16466            request["method"] != "eth_call" || request["params"][0]["data"] != "0xd0e30db0"
16467        }));
16468        let decision_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
16469            "SELECT COUNT(*) FROM {schema}.execution_verification_decision \
16470             WHERE intent_id = $1 AND decision_class = 'rebroadcast'"
16471        )))
16472        .bind(intent.id)
16473        .fetch_one(&admin_pool)
16474        .await
16475        .unwrap();
16476        assert!(
16477            error
16478                .to_string()
16479                .contains("without an authenticated signer transaction"),
16480            "was: {error}"
16481        );
16482        assert_eq!(decision_count, 6);
16483        let replacement_decision_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
16484            "SELECT COUNT(*) FROM {schema}.execution_verification_decision \
16485                 WHERE intent_id = $1 AND decision_class = 'replacement_scan'"
16486        )))
16487        .bind(intent.id)
16488        .fetch_one(&admin_pool)
16489        .await
16490        .unwrap();
16491        assert_eq!(replacement_decision_count, 2);
16492        let nonce_state = sqlx::query_as::<_, (i64, i64)>(sqlx::AssertSqlSafe(format!(
16493            "SELECT next_canonical_nonce, revision FROM {schema}.execution_verification_nonce"
16494        )))
16495        .fetch_one(&admin_pool)
16496        .await
16497        .unwrap();
16498        assert_eq!(nonce_state, (7, 0));
16499
16500        drop_execution_schema(&admin_pool, &schema).await;
16501    }
16502
16503    #[tokio::test]
16504    async fn restart_suppresses_rebroadcast_when_receipt_exists() {
16505        let Some((admin_pool, schema, first_client, _)) = execution_client_with_database(
16506            "execution_rebroadcast_receipt_present_test",
16507            ready_rpc_state(),
16508        )
16509        .await
16510        else {
16511            return;
16512        };
16513        let database = first_client.cache.database.as_ref().unwrap().clone();
16514        let (intent, _, _) =
16515            persist_test_wrap_broadcast(&database, first_client.payload_keys.as_deref()).await;
16516        let payload_keys = first_client.payload_keys.clone();
16517        drop(first_client);
16518
16519        let restart_state = execution_rpc_state()
16520            .with_response("eth_getTransactionCount", TRANSACTION_COUNT)
16521            .with_response("eth_getTransactionReceipt", RECEIPT_SUCCESS)
16522            .with_response("eth_call", CALL_EMPTY)
16523            .with_send_raw_transaction_echo();
16524        let addr = start_mock_rpc_server(restart_state.clone()).await;
16525        let mut restarted = test_client(format!("http://{addr}"));
16526        restarted.cache.database = Some(database);
16527        restarted.payload_keys = payload_keys;
16528        restarted.signer = Some(Arc::new(
16529            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16530        ));
16531
16532        let error = restarted
16533            .reconcile_unresolved_execution()
16534            .await
16535            .unwrap_err();
16536
16537        assert!(
16538            error
16539                .to_string()
16540                .contains("finalized transaction verification is locally invalid"),
16541            "was: {error}"
16542        );
16543        let requests = restart_state.recorded_requests();
16544        assert!(
16545            requests
16546                .iter()
16547                .all(|request| request["method"] != "eth_sendRawTransaction")
16548        );
16549        assert!(requests.iter().all(|request| {
16550            request["method"] != "eth_call" || request["params"][0]["data"] != "0xd0e30db0"
16551        }));
16552        let decision_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
16553            "SELECT COUNT(*) FROM {schema}.execution_verification_decision \
16554             WHERE intent_id = $1 AND decision_class = 'rebroadcast'"
16555        )))
16556        .bind(intent.id)
16557        .fetch_one(&admin_pool)
16558        .await
16559        .unwrap();
16560        assert_eq!(decision_count, 6);
16561
16562        drop_execution_schema(&admin_pool, &schema).await;
16563    }
16564
16565    #[rstest]
16566    #[case("execution_rebroadcast_false_test", CALL_ZERO)]
16567    #[case("execution_rebroadcast_revert_test", CALL_REVERTED)]
16568    #[tokio::test]
16569    async fn restart_suppresses_rebroadcast_when_simulation_denies(
16570        #[case] test_name: &str,
16571        #[case] simulation_response: &str,
16572    ) {
16573        let Some((admin_pool, schema, first_client, _)) =
16574            execution_client_with_database(test_name, ready_rpc_state()).await
16575        else {
16576            return;
16577        };
16578        let database = first_client.cache.database.as_ref().unwrap().clone();
16579        let (intent, _, _) =
16580            persist_test_wrap_broadcast(&database, first_client.payload_keys.as_deref()).await;
16581        let payload_keys = first_client.payload_keys.clone();
16582        drop(first_client);
16583
16584        let restart_state = execution_rpc_state()
16585            .with_response("eth_getTransactionCount", TRANSACTION_COUNT)
16586            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
16587            .with_response("eth_call", simulation_response)
16588            .with_send_raw_transaction_echo();
16589        let addr = start_mock_rpc_server(restart_state.clone()).await;
16590        let mut restarted = test_client(format!("http://{addr}"));
16591        restarted.cache.database = Some(database);
16592        restarted.payload_keys = payload_keys;
16593        restarted.signer = Some(Arc::new(
16594            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16595        ));
16596
16597        restarted.reconcile_unresolved_execution().await.unwrap();
16598
16599        let requests = restart_state.recorded_requests();
16600        assert!(
16601            requests
16602                .iter()
16603                .all(|request| request["method"] != "eth_sendRawTransaction")
16604        );
16605        let simulation_calls = requests
16606            .iter()
16607            .filter(|request| {
16608                request["method"] == "eth_call"
16609                    && request["params"][0]["data"] == "0xd0e30db0"
16610                    && request["params"][1] == FIXTURE_BLOCK_PARAM
16611            })
16612            .count();
16613        assert_eq!(simulation_calls, 3);
16614        let decision_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
16615            "SELECT COUNT(*) FROM {schema}.execution_verification_decision \
16616             WHERE intent_id = $1 AND decision_class = 'rebroadcast'"
16617        )))
16618        .bind(intent.id)
16619        .fetch_one(&admin_pool)
16620        .await
16621        .unwrap();
16622        assert_eq!(decision_count, 7);
16623
16624        drop_execution_schema(&admin_pool, &schema).await;
16625    }
16626
16627    #[tokio::test]
16628    async fn restart_wrap_identity_mismatch_keeps_signer_ownership() {
16629        let initial_state = broadcast_rpc_state()
16630            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
16631            .with_call_response(BALANCE_OF_SELECTOR, CALL_BALANCE);
16632        let Some((admin_pool, schema, mut first_client, _)) =
16633            execution_client_with_database("execution_restart_mismatch_test", initial_state).await
16634        else {
16635            return;
16636        };
16637        let expected_hash = expected_wrap_tx_hash(U256::from(1_000_000_000_000_000_u64)).await;
16638        let error = first_client
16639            .wrap(U256::from(1_000_000_000_000_000_u64))
16640            .await
16641            .unwrap_err();
16642        assert!(
16643            error.to_string().contains("Timed out awaiting finality"),
16644            "was: {error}"
16645        );
16646        let database = first_client.cache.database.as_ref().unwrap().clone();
16647        let payload_keys = first_client.payload_keys.clone();
16648        drop(first_client);
16649
16650        // The finalized transaction carries no value, so it cannot be the persisted wrap
16651        let mismatched_block = serde_json::json!({
16652            "jsonrpc": "2.0",
16653            "id": 1,
16654            "result": {
16655                "number": "0x1cf0d41",
16656                "hash": "0x2222222222222222222222222222222222222222222222222222222222222222",
16657                "parentHash": FIXTURE_BLOCK_HASH,
16658                "timestamp": "0x69044a21",
16659                "baseFeePerGas": "0x5f5e100",
16660                "transactions": [{
16661                    "hash": expected_hash.to_string(),
16662                    "from": WALLET,
16663                    "nonce": "0x7",
16664                    "chainId": "0xa4b1",
16665                    "type": "0x2",
16666                    "to": WETH,
16667                    "input": "0xd0e30db0",
16668                    "value": "0x0",
16669                    "gas": "0x130b0",
16670                    "maxFeePerGas": "0x7bfa480",
16671                    "maxPriorityFeePerGas": "0x989680"
16672                }]
16673            }
16674        })
16675        .to_string();
16676        let receipt = receipt_with_transaction_hash(RECEIPT_SUCCESS, expected_hash);
16677        let restart_state = with_finalized_identity(
16678            execution_rpc_state()
16679                .with_response("eth_getTransactionReceipt", &receipt)
16680                .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &mismatched_block),
16681            &mismatched_block,
16682            &receipt,
16683        );
16684        let addr = start_mock_rpc_server(restart_state.clone()).await;
16685        let mut restarted = test_client(format!("http://{addr}"));
16686        restarted.cache.database = Some(database);
16687        restarted.payload_keys = payload_keys;
16688        restarted.signer = Some(Arc::new(
16689            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16690        ));
16691
16692        let error = restarted
16693            .reconcile_unresolved_execution()
16694            .await
16695            .unwrap_err();
16696
16697        assert!(
16698            error
16699                .to_string()
16700                .contains("finalized transaction identity mismatch"),
16701            "was: {error}"
16702        );
16703        let in_flight = awaiting_in_flight(&restarted);
16704        assert_eq!(in_flight.nonce, 7);
16705        assert_eq!(in_flight.purpose, TransactionPurpose::Wrap);
16706        assert_eq!(in_flight.tx_hash, expected_hash);
16707        let requests = restart_state.recorded_requests();
16708        assert!(
16709            requests.iter().all(|request| {
16710                request["method"] != "eth_call"
16711                    || !request["params"][0]["data"]
16712                        .as_str()
16713                        .is_some_and(|data| data.starts_with(BALANCE_OF_SELECTOR))
16714            }),
16715            "the postcondition must not run when call identity is unproven"
16716        );
16717        assert!(
16718            requests
16719                .iter()
16720                .all(|request| request["method"] != "eth_sendRawTransaction")
16721        );
16722        let (status, active): (String, bool) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
16723            "SELECT status, active FROM {schema}.execution_intent"
16724        )))
16725        .fetch_one(&admin_pool)
16726        .await
16727        .unwrap();
16728        assert_eq!(status, "dropped");
16729        assert!(active);
16730
16731        let database = restarted.cache.database.as_ref().unwrap().clone();
16732        let payload_keys = restarted.payload_keys.clone();
16733        drop(restarted);
16734        let mut second = test_client(format!("http://{addr}"));
16735        second.cache.database = Some(database);
16736        second.payload_keys = payload_keys;
16737        second.signer = Some(Arc::new(
16738            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16739        ));
16740        let error = second.reconcile_unresolved_execution().await.unwrap_err();
16741        assert!(
16742            error
16743                .to_string()
16744                .contains("finalized transaction identity mismatch"),
16745            "was: {error}"
16746        );
16747        let active: bool = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
16748            "SELECT active FROM {schema}.execution_intent"
16749        )))
16750        .fetch_one(&admin_pool)
16751        .await
16752        .unwrap();
16753        assert!(active);
16754
16755        drop_execution_schema(&admin_pool, &schema).await;
16756    }
16757
16758    #[tokio::test]
16759    async fn restart_wrap_postcondition_failure_keeps_signer_ownership() {
16760        let initial_state = broadcast_rpc_state()
16761            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
16762            .with_call_response(BALANCE_OF_SELECTOR, CALL_BALANCE);
16763        let Some((admin_pool, schema, mut first_client, _)) =
16764            execution_client_with_database("execution_restart_postcondition_test", initial_state)
16765                .await
16766        else {
16767            return;
16768        };
16769        let expected_hash = expected_wrap_tx_hash(U256::from(1_000_000_000_000_000_u64)).await;
16770        let error = first_client
16771            .wrap(U256::from(1_000_000_000_000_000_u64))
16772            .await
16773            .unwrap_err();
16774        assert!(
16775            error.to_string().contains("Timed out awaiting finality"),
16776            "was: {error}"
16777        );
16778        let database = first_client.cache.database.as_ref().unwrap().clone();
16779        let payload_keys = first_client.payload_keys.clone();
16780        drop(first_client);
16781
16782        // Call identity matches, but the wrapped balance does not increase
16783        let failing_wrap_state = || {
16784            let block = finalized_wrap_block(expected_hash);
16785            let receipt = receipt_with_transaction_hash(RECEIPT_SUCCESS, expected_hash);
16786            with_finalized_identity(
16787                execution_rpc_state()
16788                    .with_response("eth_getTransactionReceipt", &receipt)
16789                    .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block)
16790                    .with_response_sequence("eth_call", &[CALL_BALANCE; 6]),
16791                &block,
16792                &receipt,
16793            )
16794        };
16795        let restart_state = failing_wrap_state();
16796        let addr = start_mock_rpc_server(restart_state.clone()).await;
16797        let mut restarted = test_client(format!("http://{addr}"));
16798        restarted.cache.database = Some(database);
16799        restarted.payload_keys = payload_keys;
16800        restarted.signer = Some(Arc::new(
16801            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16802        ));
16803
16804        let error = restarted
16805            .reconcile_unresolved_execution()
16806            .await
16807            .unwrap_err();
16808
16809        assert!(
16810            error.to_string().contains("did not increase by"),
16811            "was: {error}"
16812        );
16813        let in_flight = awaiting_in_flight(&restarted);
16814        assert_eq!(in_flight.nonce, 7);
16815        assert_eq!(in_flight.purpose, TransactionPurpose::Wrap);
16816        assert!(
16817            restart_state
16818                .recorded_requests()
16819                .iter()
16820                .all(|request| request["method"] != "eth_sendRawTransaction")
16821        );
16822        assert_eq!(
16823            execution_intent_markers(&admin_pool, &schema).await,
16824            vec![("wrap".into(), "dropped".into(), false, true)]
16825        );
16826
16827        let addr = start_mock_rpc_server(failing_wrap_state()).await;
16828        let error = later_reconnect(restarted, format!("http://{addr}")).await;
16829        assert!(
16830            error.to_string().contains("did not increase by"),
16831            "was: {error}"
16832        );
16833        assert_eq!(
16834            execution_intent_markers(&admin_pool, &schema).await,
16835            vec![("wrap".into(), "dropped".into(), false, true)]
16836        );
16837
16838        drop_execution_schema(&admin_pool, &schema).await;
16839    }
16840
16841    #[tokio::test]
16842    async fn restart_wrap_revert_marks_terminal_and_releases() {
16843        let initial_state = broadcast_rpc_state()
16844            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
16845            .with_call_response(BALANCE_OF_SELECTOR, CALL_BALANCE);
16846        let Some((admin_pool, schema, mut first_client, _)) =
16847            execution_client_with_database("execution_restart_wrap_revert_test", initial_state)
16848                .await
16849        else {
16850            return;
16851        };
16852        let expected_hash = expected_wrap_tx_hash(U256::from(1_000_000_000_000_000_u64)).await;
16853        let error = first_client
16854            .wrap(U256::from(1_000_000_000_000_000_u64))
16855            .await
16856            .unwrap_err();
16857        assert!(
16858            error.to_string().contains("Timed out awaiting finality"),
16859            "was: {error}"
16860        );
16861        let database = first_client.cache.database.as_ref().unwrap().clone();
16862        let payload_keys = first_client.payload_keys.clone();
16863        drop(first_client);
16864
16865        let block = finalized_wrap_block(expected_hash);
16866        let receipt = receipt_with_transaction_hash(RECEIPT_REVERTED, expected_hash);
16867        let restart_state = with_finalized_identity(
16868            execution_rpc_state()
16869                .with_response("eth_getTransactionReceipt", &receipt)
16870                .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block),
16871            &block,
16872            &receipt,
16873        );
16874        let addr = start_mock_rpc_server(restart_state.clone()).await;
16875        let mut restarted = test_client(format!("http://{addr}"));
16876        restarted.cache.database = Some(database);
16877        restarted.payload_keys = payload_keys;
16878        restarted.signer = Some(Arc::new(
16879            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16880        ));
16881
16882        restarted.reconcile_unresolved_execution().await.unwrap();
16883
16884        assert!(restarted.in_flight.lock().is_none());
16885        assert!(
16886            restart_state
16887                .recorded_requests()
16888                .iter()
16889                .all(|request| request["method"] != "eth_sendRawTransaction")
16890        );
16891        assert_eq!(
16892            execution_intent_markers(&admin_pool, &schema).await,
16893            vec![("wrap".into(), "reverted".into(), true, false)]
16894        );
16895
16896        drop_execution_schema(&admin_pool, &schema).await;
16897    }
16898
16899    #[tokio::test]
16900    async fn restart_quarantines_unretained_same_nonce_wrap_replacement() {
16901        let initial_state = broadcast_rpc_state()
16902            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
16903            .with_call_response(BALANCE_OF_SELECTOR, CALL_BALANCE);
16904        let Some((admin_pool, schema, mut first_client, _)) =
16905            execution_client_with_database("execution_restart_replacement_test", initial_state)
16906                .await
16907        else {
16908            return;
16909        };
16910        let original_hash = expected_wrap_tx_hash(U256::from(1_000_000_000_000_000_u64)).await;
16911        let error = first_client
16912            .wrap(U256::from(1_000_000_000_000_000_u64))
16913            .await
16914            .unwrap_err();
16915        assert!(
16916            error.to_string().contains("Timed out awaiting finality"),
16917            "was: {error}"
16918        );
16919        let database = first_client.cache.database.as_ref().unwrap().clone();
16920        let payload_keys = first_client.payload_keys.clone();
16921        drop(first_client);
16922
16923        // Matching call fields do not authenticate an unknown same-nonce payload.
16924        let replacement_hash = B256::from([0x44; 32]);
16925        let restart_state = execution_rpc_state()
16926            .with_response("eth_getTransactionCount", TRANSACTION_COUNT_NEXT)
16927            .with_response_sequence("eth_call", &[CALL_BALANCE, CALL_BALANCE_AFTER_WRAP])
16928            .with_response_sequence(
16929                "eth_getTransactionReceipt",
16930                &[RECEIPT_NULL, RECEIPT_NULL, RECEIPT_NULL],
16931            )
16932            .with_parameter_response(
16933                "eth_getBlockByNumber",
16934                "0x1cf0d40",
16935                &replacement_head_block(replacement_hash),
16936            )
16937            .with_parameter_response(
16938                "eth_getBlockByNumber",
16939                "0x1cf0d41",
16940                &finalized_wrap_block(replacement_hash),
16941            );
16942        let addr = start_mock_rpc_server(restart_state.clone()).await;
16943        let mut restarted = test_client(format!("http://{addr}"));
16944        restarted.cache.database = Some(database);
16945        restarted.payload_keys = payload_keys;
16946        restarted.signer = Some(Arc::new(
16947            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16948        ));
16949        restarted.transaction_limits.receipt_timeout_secs = 2;
16950
16951        let error = restarted
16952            .reconcile_unresolved_execution()
16953            .await
16954            .unwrap_err();
16955
16956        let hashes: Vec<(String, String, bool)> = sqlx::query_as(sqlx::AssertSqlSafe(format!(
16957            "SELECT transaction_hash, status, current FROM \
16958                 {schema}.execution_transaction_hash ORDER BY id"
16959        )))
16960        .fetch_all(&admin_pool)
16961        .await
16962        .unwrap();
16963        let requests = restart_state.recorded_requests();
16964
16965        assert!(
16966            error
16967                .to_string()
16968                .contains("has no authenticated retained payload"),
16969            "was: {error}"
16970        );
16971        assert_eq!(
16972            hashes,
16973            [(original_hash.to_string(), "dropped".to_string(), true)]
16974        );
16975        assert!(restarted.in_flight.lock().is_some());
16976        assert_eq!(
16977            execution_intent_markers(&admin_pool, &schema).await,
16978            vec![("wrap".into(), "dropped".into(), false, true)]
16979        );
16980        assert_eq!(
16981            requests
16982                .iter()
16983                .filter(|request| request["method"] == "eth_getTransactionReceipt")
16984                .count(),
16985            3
16986        );
16987        assert!(requests.iter().all(|request| {
16988            request["method"] != "eth_call" || request["params"][0]["data"] != "0xd0e30db0"
16989        }));
16990        assert!(
16991            requests
16992                .iter()
16993                .all(|request| request["method"] != "eth_sendRawTransaction")
16994        );
16995
16996        drop_execution_schema(&admin_pool, &schema).await;
16997    }
16998
16999    #[tokio::test]
17000    async fn replacement_scan_accepts_only_an_authenticated_retained_payload() {
17001        let expected_hash = expected_wrap_tx_hash(U256::from(1_u64)).await;
17002        let mut replacement_block: serde_json::Value =
17003            serde_json::from_str(&replacement_head_block(expected_hash)).unwrap();
17004        replacement_block["result"]["transactions"][1]["value"] = serde_json::json!("0x1");
17005        let replacement_block = replacement_block.to_string();
17006        let state = execution_rpc_state().with_parameter_response(
17007            "eth_getBlockByNumber",
17008            "0x1cf0d40",
17009            &replacement_block,
17010        );
17011        let Some((admin_pool, schema, client, _)) =
17012            execution_client_with_database("replacement_scan_authenticated", state).await
17013        else {
17014            return;
17015        };
17016        let database = client.cache.database.as_ref().unwrap();
17017        let (intent, persisted_hash, persisted_raw) =
17018            persist_test_wrap_broadcast(database, client.payload_keys.as_deref()).await;
17019        assert_eq!(persisted_hash, expected_hash);
17020        let authenticated_payloads = HashMap::from([(expected_hash, persisted_raw.clone())]);
17021        let head = VerifiedBlockHeader {
17022            number: FIXTURE_BLOCK,
17023            hash: B256::from_str(FIXTURE_BLOCK_HASH).unwrap(),
17024            parent_hash: B256::from_str(
17025                "0x0000000000000000000000000000000000000000000000000000000000000001",
17026            )
17027            .unwrap(),
17028            timestamp: FIXTURE_BLOCK_TIMESTAMP,
17029            base_fee_per_gas: Some(100_000_000),
17030        };
17031
17032        let matched = client
17033            .transaction_executor()
17034            .unwrap()
17035            .scan_canonical_replacement(&intent, 7, head, &authenticated_payloads)
17036            .await
17037            .unwrap()
17038            .unwrap();
17039
17040        assert_eq!(matched, (expected_hash, persisted_raw));
17041        let cursor: (i64, String) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
17042            "SELECT finalized_cursor_number, finalized_cursor_hash \
17043             FROM {schema}.execution_replacement_scan WHERE intent_id = $1"
17044        )))
17045        .bind(intent.id)
17046        .fetch_one(&admin_pool)
17047        .await
17048        .unwrap();
17049        assert_eq!(
17050            cursor,
17051            (FIXTURE_BLOCK as i64, FIXTURE_BLOCK_HASH.to_string())
17052        );
17053        let evidence_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
17054            "SELECT COUNT(*) FROM {schema}.execution_verification_decision \
17055             WHERE intent_id = $1 AND decision_class = 'replacement_scan'"
17056        )))
17057        .bind(intent.id)
17058        .fetch_one(&admin_pool)
17059        .await
17060        .unwrap();
17061        assert_eq!(evidence_count, 2);
17062
17063        drop_execution_schema(&admin_pool, &schema).await;
17064    }
17065
17066    #[tokio::test]
17067    async fn restart_reconciles_finalized_approve_after_validation() {
17068        let initial_state = broadcast_rpc_state()
17069            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
17070            .with_response("eth_call", CALL_BOOL_TRUE)
17071            .with_call_response_sequence(ALLOWANCE_SELECTOR, &[CALL_ZERO; 3])
17072            .with_call_response(ALLOWANCE_SELECTOR, CALL_ZERO);
17073        let Some((admin_pool, schema, mut first_client, _)) =
17074            execution_client_with_database("execution_restart_approve_test", initial_state).await
17075        else {
17076            return;
17077        };
17078        let expected_hash = expected_approve_tx_hash(U256::from(1_000u64)).await;
17079        let error = first_client
17080            .approve(WETH_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
17081            .await
17082            .unwrap_err();
17083        assert!(
17084            error.to_string().contains("Timed out awaiting finality"),
17085            "was: {error}"
17086        );
17087        let database = first_client.cache.database.as_ref().unwrap().clone();
17088        let payload_keys = first_client.payload_keys.clone();
17089        drop(first_client);
17090
17091        let block = finalized_approve_block(expected_hash, U256::from(1_000u64));
17092        let receipt = receipt_with_transaction_hash(RECEIPT_SUCCESS, expected_hash);
17093        let restart_state = with_finalized_identity(
17094            execution_rpc_state()
17095                .with_response("eth_getTransactionReceipt", &receipt)
17096                .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block)
17097                .with_call_response(ALLOWANCE_SELECTOR, CALL_ALLOWANCE_1000),
17098            &block,
17099            &receipt,
17100        );
17101        let addr = start_mock_rpc_server(restart_state.clone()).await;
17102        let mut restarted = test_client(format!("http://{addr}"));
17103        restarted.cache.database = Some(database);
17104        restarted.payload_keys = payload_keys;
17105        restarted.signer = Some(Arc::new(
17106            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
17107        ));
17108
17109        restarted.reconcile_unresolved_execution().await.unwrap();
17110
17111        let record = restarted
17112            .cache
17113            .get_execution_transaction(42161, &expected_hash.to_string())
17114            .await
17115            .unwrap()
17116            .unwrap();
17117        let requests = restart_state.recorded_requests();
17118        assert_eq!(record.status, "finalized");
17119        assert_eq!(record.purpose, "approve");
17120        assert!(restarted.in_flight.lock().is_none());
17121        assert_eq!(
17122            execution_intent_markers(&admin_pool, &schema).await,
17123            vec![("approve".into(), "finalized".into(), true, false)]
17124        );
17125        assert_eq!(
17126            requests
17127                .iter()
17128                .filter(|request| {
17129                    request["method"] == "eth_call"
17130                        && request["params"][0]["data"]
17131                            .as_str()
17132                            .is_some_and(|data| data.starts_with(ALLOWANCE_SELECTOR))
17133                })
17134                .count(),
17135            3
17136        );
17137        assert_eq!(
17138            requests
17139                .iter()
17140                .filter(|request| request["method"] == "eth_sendRawTransaction")
17141                .count(),
17142            0
17143        );
17144
17145        drop_execution_schema(&admin_pool, &schema).await;
17146    }
17147
17148    #[tokio::test]
17149    async fn restart_approve_postcondition_failure_keeps_signer_ownership() {
17150        let initial_state = broadcast_rpc_state()
17151            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
17152            .with_response("eth_call", CALL_BOOL_TRUE)
17153            .with_call_response_sequence(ALLOWANCE_SELECTOR, &[CALL_ZERO; 3])
17154            .with_call_response(ALLOWANCE_SELECTOR, CALL_ZERO);
17155        let Some((admin_pool, schema, mut first_client, _)) =
17156            execution_client_with_database("execution_restart_approve_post_test", initial_state)
17157                .await
17158        else {
17159            return;
17160        };
17161        let expected_hash = expected_approve_tx_hash(U256::from(1_000u64)).await;
17162        let error = first_client
17163            .approve(WETH_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
17164            .await
17165            .unwrap_err();
17166        assert!(
17167            error.to_string().contains("Timed out awaiting finality"),
17168            "was: {error}"
17169        );
17170        let database = first_client.cache.database.as_ref().unwrap().clone();
17171        let payload_keys = first_client.payload_keys.clone();
17172        drop(first_client);
17173
17174        // Call identity matches, but the allowance does not equal the approved amount
17175        let block = finalized_approve_block(expected_hash, U256::from(1_000u64));
17176        let receipt = receipt_with_transaction_hash(RECEIPT_SUCCESS, expected_hash);
17177        let restart_state = with_finalized_identity(
17178            execution_rpc_state()
17179                .with_response("eth_getTransactionReceipt", &receipt)
17180                .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block)
17181                .with_call_response(ALLOWANCE_SELECTOR, CALL_ZERO),
17182            &block,
17183            &receipt,
17184        );
17185        let addr = start_mock_rpc_server(restart_state.clone()).await;
17186        let mut restarted = test_client(format!("http://{addr}"));
17187        restarted.cache.database = Some(database);
17188        restarted.payload_keys = payload_keys;
17189        restarted.signer = Some(Arc::new(
17190            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
17191        ));
17192
17193        let error = restarted
17194            .reconcile_unresolved_execution()
17195            .await
17196            .unwrap_err();
17197
17198        assert!(
17199            error
17200                .to_string()
17201                .contains("does not equal the requested amount"),
17202            "was: {error}"
17203        );
17204        let in_flight = awaiting_in_flight(&restarted);
17205        assert_eq!(in_flight.nonce, 7);
17206        assert_eq!(in_flight.purpose, TransactionPurpose::Approve);
17207        assert!(
17208            restart_state
17209                .recorded_requests()
17210                .iter()
17211                .all(|request| request["method"] != "eth_sendRawTransaction")
17212        );
17213        assert_eq!(
17214            execution_intent_markers(&admin_pool, &schema).await,
17215            vec![("approve".into(), "dropped".into(), false, true)]
17216        );
17217
17218        let error = later_reconnect(restarted, format!("http://{addr}")).await;
17219        assert!(
17220            error
17221                .to_string()
17222                .contains("does not equal the requested amount"),
17223            "was: {error}"
17224        );
17225        assert_eq!(
17226            execution_intent_markers(&admin_pool, &schema).await,
17227            vec![("approve".into(), "dropped".into(), false, true)]
17228        );
17229
17230        drop_execution_schema(&admin_pool, &schema).await;
17231    }
17232
17233    #[tokio::test]
17234    async fn disappearing_unfinalized_receipt_drops_without_committing_inclusion() {
17235        let state = execution_rpc_state()
17236            .with_response("eth_getTransactionCount", TRANSACTION_COUNT)
17237            .with_response("eth_estimateGas", ESTIMATE_GAS)
17238            .with_response("eth_call", CALL_BALANCE)
17239            .with_response_sequence(
17240                "eth_getTransactionReceipt",
17241                &[
17242                    RECEIPT_SUCCESS,
17243                    RECEIPT_SUCCESS,
17244                    RECEIPT_SUCCESS,
17245                    RECEIPT_NULL,
17246                    RECEIPT_NULL,
17247                    RECEIPT_NULL,
17248                ],
17249            )
17250            .with_parameter_response("eth_getBlockByNumber", "finalized", BLOCK_BY_NUMBER)
17251            .with_send_raw_transaction_echo();
17252        let Some((admin_pool, schema, mut client, _)) =
17253            execution_client_with_database("execution_receipt_disappeared_test", state).await
17254        else {
17255            return;
17256        };
17257        client.transaction_limits.receipt_timeout_secs = 2;
17258
17259        let error = client
17260            .wrap(U256::from(1_000_000_000_000_000_u64))
17261            .await
17262            .unwrap_err();
17263        let transitions: Vec<String> = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
17264            "SELECT to_status FROM {schema}.execution_transaction_transition ORDER BY id"
17265        )))
17266        .fetch_all(&admin_pool)
17267        .await
17268        .unwrap();
17269
17270        assert!(
17271            error.to_string().contains("Timed out awaiting finality"),
17272            "was: {error}"
17273        );
17274        assert_eq!(transitions, ["prepared", "signed", "broadcast", "dropped"]);
17275        assert!(client.in_flight.lock().is_some());
17276
17277        drop_execution_schema(&admin_pool, &schema).await;
17278    }
17279
17280    #[tokio::test]
17281    async fn changed_unfinalized_block_drops_without_committing_inclusion() {
17282        let changed_block = serde_json::json!({
17283            "jsonrpc": "2.0",
17284            "id": 1,
17285            "result": {
17286                "number": "0x1cf0d41",
17287                "hash": "0x4444444444444444444444444444444444444444444444444444444444444444",
17288                "parentHash": FIXTURE_BLOCK_HASH,
17289                "timestamp": "0x69044a21",
17290                "baseFeePerGas": "0x5f5e100",
17291                "transactions": []
17292            }
17293        })
17294        .to_string();
17295        let state = execution_rpc_state()
17296            .with_response("eth_getTransactionCount", TRANSACTION_COUNT)
17297            .with_response("eth_estimateGas", ESTIMATE_GAS)
17298            .with_response("eth_call", CALL_BALANCE)
17299            .with_response_sequence(
17300                "eth_getTransactionReceipt",
17301                &[
17302                    RECEIPT_SUCCESS,
17303                    RECEIPT_SUCCESS,
17304                    RECEIPT_SUCCESS,
17305                    RECEIPT_SUCCESS,
17306                    RECEIPT_SUCCESS,
17307                    RECEIPT_SUCCESS,
17308                ],
17309            )
17310            .with_parameter_response_sequence(
17311                "eth_getBlockByNumber",
17312                "0x1cf0d41",
17313                &[
17314                    BLOCK_CANONICAL,
17315                    BLOCK_CANONICAL,
17316                    BLOCK_CANONICAL,
17317                    &changed_block,
17318                    &changed_block,
17319                    &changed_block,
17320                ],
17321            )
17322            .with_parameter_response("eth_getBlockByNumber", "finalized", BLOCK_BY_NUMBER)
17323            .with_send_raw_transaction_echo();
17324        let Some((admin_pool, schema, mut client, _)) =
17325            execution_client_with_database("execution_reorg_test", state).await
17326        else {
17327            return;
17328        };
17329        client.transaction_limits.receipt_timeout_secs = 2;
17330
17331        let error = client
17332            .wrap(U256::from(1_000_000_000_000_000_u64))
17333            .await
17334            .unwrap_err();
17335        let transitions: Vec<String> = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
17336            "SELECT to_status FROM {schema}.execution_transaction_transition ORDER BY id"
17337        )))
17338        .fetch_all(&admin_pool)
17339        .await
17340        .unwrap();
17341
17342        assert!(
17343            error.to_string().contains("Timed out awaiting finality"),
17344            "was: {error}"
17345        );
17346        assert_eq!(transitions, ["prepared", "signed", "broadcast", "dropped"]);
17347        assert!(client.in_flight.lock().is_some());
17348
17349        drop_execution_schema(&admin_pool, &schema).await;
17350    }
17351
17352    #[tokio::test]
17353    async fn pre_sign_pending_nonce_drift_blocks_wrap_before_signature() {
17354        let state = execution_rpc_state()
17355            .with_response_sequence(
17356                "eth_getTransactionCount",
17357                &[
17358                    TRANSACTION_COUNT,
17359                    TRANSACTION_COUNT,
17360                    TRANSACTION_COUNT,
17361                    TRANSACTION_COUNT_NEXT,
17362                    TRANSACTION_COUNT_NEXT,
17363                    TRANSACTION_COUNT_NEXT,
17364                ],
17365            )
17366            .with_response("eth_estimateGas", ESTIMATE_GAS)
17367            .with_call_response(BALANCE_OF_SELECTOR, CALL_BALANCE)
17368            .with_send_raw_transaction_echo();
17369        let Some((admin_pool, schema, mut client, state)) =
17370            execution_client_with_database("pre_sign_pending_nonce_drift", state).await
17371        else {
17372            return;
17373        };
17374
17375        let error = client
17376            .wrap(U256::from(1_000_000_000_000_000_u64))
17377            .await
17378            .unwrap_err();
17379        let signed_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
17380            "SELECT COUNT(*) FROM {schema}.execution_transaction_hash"
17381        )))
17382        .fetch_one(&admin_pool)
17383        .await
17384        .unwrap();
17385
17386        assert!(
17387            error
17388                .to_string()
17389                .contains("Pending nonce does not match the verified canonical nonce"),
17390            "was: {error}"
17391        );
17392        assert_eq!(signed_count, 0);
17393        assert!(
17394            state
17395                .recorded_requests()
17396                .iter()
17397                .all(|request| request["method"] != "eth_sendRawTransaction")
17398        );
17399        assert!(client.in_flight.lock().is_none());
17400
17401        drop_execution_schema(&admin_pool, &schema).await;
17402    }
17403
17404    #[tokio::test]
17405    async fn unknown_same_nonce_swap_replacement_emits_no_rejection() {
17406        let replacement_hash = B256::from([0x44; 32]);
17407        let replacement_block = replacement_head_block(replacement_hash);
17408        let state = execution_rpc_state()
17409            .with_parameter_response("eth_getBlockByNumber", "0x1cf0d40", &replacement_block)
17410            .with_send_raw_transaction_echo();
17411        let Some((admin_pool, schema, mut client, _, _)) =
17412            swap_client_with_database("unknown_same_nonce_swap_replacement", state).await
17413        else {
17414            return;
17415        };
17416        let mut receiver = start_with_events(&mut client);
17417        let database = client.cache.database.as_ref().unwrap();
17418        let (intent, original_hash, original_payload) =
17419            persist_test_swap_broadcast(database, client.payload_keys.as_deref()).await;
17420        let authenticated_payloads = HashMap::from([(original_hash, original_payload)]);
17421        let head = VerifiedBlockHeader {
17422            number: FIXTURE_BLOCK,
17423            hash: B256::from_str(FIXTURE_BLOCK_HASH).unwrap(),
17424            parent_hash: B256::from_str(
17425                "0x0000000000000000000000000000000000000000000000000000000000000001",
17426            )
17427            .unwrap(),
17428            timestamp: FIXTURE_BLOCK_TIMESTAMP,
17429            base_fee_per_gas: Some(100_000_000),
17430        };
17431
17432        let error = client
17433            .transaction_executor()
17434            .unwrap()
17435            .scan_canonical_replacement(&intent, 7, head, &authenticated_payloads)
17436            .await
17437            .unwrap_err();
17438        let events = collect_order_events(&mut receiver);
17439        let (status, active): (String, bool) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
17440            "SELECT status, active FROM {schema}.execution_intent"
17441        )))
17442        .fetch_one(&admin_pool)
17443        .await
17444        .unwrap();
17445
17446        assert!(
17447            error
17448                .to_string()
17449                .contains("has no authenticated retained payload"),
17450            "was: {error}"
17451        );
17452        assert!(events.is_empty(), "was: {events:?}");
17453        assert_eq!(status, "broadcast");
17454        assert!(active);
17455
17456        drop_execution_schema(&admin_pool, &schema).await;
17457    }
17458
17459    #[tokio::test]
17460    async fn replacement_scan_rejects_rpc_fields_that_conflict_with_the_payload() {
17461        let expected_hash = expected_wrap_tx_hash(U256::from(1_u64)).await;
17462        let mut replacement_block: serde_json::Value =
17463            serde_json::from_str(&replacement_head_block(expected_hash)).unwrap();
17464        replacement_block["result"]["transactions"][1]["value"] = serde_json::json!("0x2");
17465        let replacement_block = replacement_block.to_string();
17466        let state = execution_rpc_state().with_parameter_response(
17467            "eth_getBlockByNumber",
17468            "0x1cf0d40",
17469            &replacement_block,
17470        );
17471        let Some((admin_pool, schema, client, _)) =
17472            execution_client_with_database("replacement_scan_payload_mismatch", state).await
17473        else {
17474            return;
17475        };
17476        let database = client.cache.database.as_ref().unwrap();
17477        let (intent, persisted_hash, persisted_payload) =
17478            persist_test_wrap_broadcast(database, client.payload_keys.as_deref()).await;
17479        assert_eq!(persisted_hash, expected_hash);
17480        let authenticated_payloads = HashMap::from([(persisted_hash, persisted_payload)]);
17481        let head = VerifiedBlockHeader {
17482            number: FIXTURE_BLOCK,
17483            hash: B256::from_str(FIXTURE_BLOCK_HASH).unwrap(),
17484            parent_hash: B256::from_str(
17485                "0x0000000000000000000000000000000000000000000000000000000000000001",
17486            )
17487            .unwrap(),
17488            timestamp: FIXTURE_BLOCK_TIMESTAMP,
17489            base_fee_per_gas: Some(100_000_000),
17490        };
17491
17492        let error = client
17493            .transaction_executor()
17494            .unwrap()
17495            .scan_canonical_replacement(&intent, 7, head, &authenticated_payloads)
17496            .await
17497            .unwrap_err();
17498        let (status, active): (String, bool) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
17499            "SELECT status, active FROM {schema}.execution_intent"
17500        )))
17501        .fetch_one(&admin_pool)
17502        .await
17503        .unwrap();
17504
17505        assert!(
17506            error
17507                .to_string()
17508                .contains("failed authenticated payload validation"),
17509            "was: {error}"
17510        );
17511        assert_eq!(status, "broadcast");
17512        assert!(active);
17513
17514        drop_execution_schema(&admin_pool, &schema).await;
17515    }
17516
17517    #[tokio::test]
17518    async fn execution_transaction_constraints_reject_conflicting_identity() {
17519        const TRANSACTION_HASH: &str = "0xduplicate-transaction-hash";
17520        const OTHER_WALLET: &str = "0x0000000000000000000000000000000000000001";
17521        let Some((admin_pool, schema, client, _)) = execution_client_with_unprotected_database(
17522            "execution_duplicate_record_test",
17523            ready_rpc_state(),
17524        )
17525        .await
17526        else {
17527            return;
17528        };
17529        let database = client.cache.database.as_ref().unwrap();
17530        let operator = database
17531            .reserve_execution_intent(&ExecutionIntentInsert {
17532                chain_id: 42161,
17533                wallet_address: WALLET.to_string(),
17534                purpose: "wrap".to_string(),
17535                client_order_id: None,
17536                trader_id: None,
17537                strategy_id: None,
17538                account_id: None,
17539                instrument_id: None,
17540                pool_address: None,
17541                transaction_to: WETH_ADDRESS.to_string(),
17542                transaction_input: "0xd0e30db0".to_string(),
17543                transaction_value: "1".to_string(),
17544                amount_in: None,
17545                created_block: FIXTURE_BLOCK,
17546            })
17547            .await
17548            .unwrap();
17549        database
17550            .assign_execution_intent_nonce(operator.id, 7)
17551            .await
17552            .unwrap();
17553        database
17554            .add_execution_transaction_hash(operator.id, 42161, TRANSACTION_HASH, &[1, 2, 3])
17555            .await
17556            .unwrap();
17557        database
17558            .add_execution_transaction_hash(operator.id, 42161, TRANSACTION_HASH, &[1, 2, 3])
17559            .await
17560            .unwrap();
17561
17562        let signer_conflict = database
17563            .reserve_execution_intent(&ExecutionIntentInsert {
17564                chain_id: 42161,
17565                wallet_address: WALLET.to_string(),
17566                purpose: "approve".to_string(),
17567                client_order_id: None,
17568                trader_id: None,
17569                strategy_id: None,
17570                account_id: None,
17571                instrument_id: None,
17572                pool_address: None,
17573                transaction_to: ROUTER_ADDRESS.to_string(),
17574                transaction_input: "0x01".to_string(),
17575                transaction_value: "0".to_string(),
17576                amount_in: None,
17577                created_block: FIXTURE_BLOCK,
17578            })
17579            .await
17580            .unwrap_err();
17581        let other = database
17582            .reserve_execution_intent(&ExecutionIntentInsert {
17583                chain_id: 42161,
17584                wallet_address: OTHER_WALLET.to_string(),
17585                purpose: "approve".to_string(),
17586                client_order_id: None,
17587                trader_id: None,
17588                strategy_id: None,
17589                account_id: None,
17590                instrument_id: None,
17591                pool_address: None,
17592                transaction_to: ROUTER_ADDRESS.to_string(),
17593                transaction_input: "0x01".to_string(),
17594                transaction_value: "0".to_string(),
17595                amount_in: None,
17596                created_block: FIXTURE_BLOCK,
17597            })
17598            .await
17599            .unwrap();
17600        database
17601            .assign_execution_intent_nonce(other.id, 7)
17602            .await
17603            .unwrap();
17604        let hash_conflict = database
17605            .add_execution_transaction_hash(other.id, 42161, TRANSACTION_HASH, &[4, 5, 6])
17606            .await
17607            .unwrap_err();
17608
17609        let record = database
17610            .get_execution_transaction(42161, TRANSACTION_HASH)
17611            .await
17612            .unwrap()
17613            .unwrap();
17614        let count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
17615            "SELECT COUNT(*) FROM {schema}.execution_intent"
17616        )))
17617        .fetch_one(&admin_pool)
17618        .await
17619        .unwrap();
17620
17621        assert_eq!(record.wallet_address.as_deref(), Some(WALLET));
17622        assert_eq!(record.nonce, 7);
17623        assert_eq!(record.transaction_hash, TRANSACTION_HASH);
17624        assert_eq!(record.purpose, "wrap");
17625        assert_eq!(record.status, "signed");
17626        assert_eq!(record.client_order_id, None);
17627        assert!(
17628            hash_conflict
17629                .to_string()
17630                .contains("conflicts with its persisted identity"),
17631            "was: {hash_conflict}"
17632        );
17633        assert_eq!(
17634            signer_conflict.to_string(),
17635            "Execution intent reservation failed before commit"
17636        );
17637        assert!(
17638            signer_conflict.chain().any(|cause| cause
17639                .to_string()
17640                .contains("execution_intent_active_signer_key")),
17641            "was: {signer_conflict:#}"
17642        );
17643        assert_eq!(count, 2);
17644
17645        drop_execution_schema(&admin_pool, &schema).await;
17646    }
17647
17648    #[tokio::test]
17649    async fn execution_status_transitions_are_idempotent() {
17650        const TRANSACTION_HASH: &str =
17651            "0x5555555555555555555555555555555555555555555555555555555555555555";
17652        let Some((admin_pool, schema, client, _)) = execution_client_with_unprotected_database(
17653            "execution_transition_test",
17654            ready_rpc_state(),
17655        )
17656        .await
17657        else {
17658            return;
17659        };
17660        let database = client.cache.database.as_ref().unwrap();
17661        let intent = database
17662            .reserve_execution_intent(&ExecutionIntentInsert {
17663                chain_id: 42161,
17664                wallet_address: WALLET.to_string(),
17665                purpose: "wrap".to_string(),
17666                client_order_id: None,
17667                trader_id: None,
17668                strategy_id: None,
17669                account_id: None,
17670                instrument_id: None,
17671                pool_address: None,
17672                transaction_to: WETH_ADDRESS.to_string(),
17673                transaction_input: "0xd0e30db0".to_string(),
17674                transaction_value: "1".to_string(),
17675                amount_in: None,
17676                created_block: FIXTURE_BLOCK,
17677            })
17678            .await
17679            .unwrap();
17680        database
17681            .assign_execution_intent_nonce(intent.id, 7)
17682            .await
17683            .unwrap();
17684        database
17685            .add_execution_transaction_hash(intent.id, 42161, TRANSACTION_HASH, &[1, 2, 3])
17686            .await
17687            .unwrap();
17688
17689        for _ in 0..2 {
17690            database
17691                .record_execution_status(
17692                    intent.id,
17693                    TRANSACTION_HASH,
17694                    TransactionStatus::Broadcast,
17695                    None,
17696                    None,
17697                    None,
17698                    None,
17699                    None,
17700                )
17701                .await
17702                .unwrap();
17703        }
17704
17705        for status in [TransactionStatus::Included, TransactionStatus::Included] {
17706            database
17707                .record_execution_status(
17708                    intent.id,
17709                    TRANSACTION_HASH,
17710                    status,
17711                    Some(FIXTURE_BLOCK + 1),
17712                    Some("0x2222222222222222222222222222222222222222222222222222222222222222"),
17713                    Some(true),
17714                    Some(50_112),
17715                    Some("100000000"),
17716                )
17717                .await
17718                .unwrap();
17719        }
17720
17721        for _ in 0..2 {
17722            database
17723                .record_execution_status(
17724                    intent.id,
17725                    TRANSACTION_HASH,
17726                    TransactionStatus::Finalized,
17727                    Some(FIXTURE_BLOCK + 1),
17728                    Some("0x2222222222222222222222222222222222222222222222222222222222222222"),
17729                    Some(true),
17730                    Some(50_112),
17731                    Some("100000000"),
17732                )
17733                .await
17734                .unwrap();
17735        }
17736
17737        let transitions: Vec<String> = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
17738            "SELECT to_status FROM {schema}.execution_transaction_transition ORDER BY id"
17739        )))
17740        .fetch_all(&admin_pool)
17741        .await
17742        .unwrap();
17743        let (status, active): (String, bool) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
17744            "SELECT status, active FROM {schema}.execution_intent"
17745        )))
17746        .fetch_one(&admin_pool)
17747        .await
17748        .unwrap();
17749        let append_only_error = sqlx::query(sqlx::AssertSqlSafe(format!(
17750            "DELETE FROM {schema}.execution_transaction_transition WHERE intent_id = {}",
17751            intent.id
17752        )))
17753        .execute(&admin_pool)
17754        .await
17755        .unwrap_err();
17756
17757        assert_eq!(
17758            transitions,
17759            ["prepared", "signed", "broadcast", "included", "finalized"]
17760        );
17761        assert_eq!(status, "finalized");
17762        assert!(active);
17763        assert!(
17764            append_only_error
17765                .to_string()
17766                .contains("Execution transitions are append-only"),
17767            "was: {append_only_error}"
17768        );
17769
17770        drop_execution_schema(&admin_pool, &schema).await;
17771    }
17772
17773    #[tokio::test]
17774    async fn wrap_then_approve_persists_records_and_clears_in_flight() {
17775        let state = execution_rpc_state()
17776            .with_parameter_response_sequence(
17777                "eth_getBlockByNumber",
17778                "latest",
17779                &[
17780                    BLOCK_BY_NUMBER,
17781                    BLOCK_BY_NUMBER,
17782                    BLOCK_BY_NUMBER,
17783                    BLOCK_FINALIZED,
17784                    BLOCK_FINALIZED,
17785                    BLOCK_FINALIZED,
17786                ],
17787            )
17788            .with_response_sequence(
17789                "eth_call",
17790                &[
17791                    CALL_BALANCE,
17792                    CALL_BALANCE,
17793                    CALL_BALANCE,
17794                    CALL_BALANCE,
17795                    CALL_BALANCE,
17796                    CALL_BALANCE,
17797                    CALL_BALANCE_AFTER_WRAP,
17798                    CALL_BALANCE_AFTER_WRAP,
17799                    CALL_BALANCE_AFTER_WRAP,
17800                    CALL_BOOL_TRUE,
17801                    CALL_BOOL_TRUE,
17802                    CALL_BOOL_TRUE,
17803                ],
17804            )
17805            .with_call_response_sequence(
17806                ALLOWANCE_SELECTOR,
17807                &[
17808                    CALL_ZERO,
17809                    CALL_ZERO,
17810                    CALL_ZERO,
17811                    CALL_ALLOWANCE_MAX,
17812                    CALL_ALLOWANCE_MAX,
17813                    CALL_ALLOWANCE_MAX,
17814                ],
17815            )
17816            .with_response_sequence(
17817                "eth_getTransactionCount",
17818                &[
17819                    TRANSACTION_COUNT,
17820                    TRANSACTION_COUNT,
17821                    TRANSACTION_COUNT,
17822                    TRANSACTION_COUNT,
17823                    TRANSACTION_COUNT,
17824                    TRANSACTION_COUNT,
17825                    TRANSACTION_COUNT_NEXT,
17826                    TRANSACTION_COUNT_NEXT,
17827                    TRANSACTION_COUNT_NEXT,
17828                    TRANSACTION_COUNT_NEXT,
17829                    TRANSACTION_COUNT_NEXT,
17830                    TRANSACTION_COUNT_NEXT,
17831                ],
17832            )
17833            .with_response("eth_estimateGas", ESTIMATE_GAS)
17834            .with_response_sequence(
17835                "eth_getTransactionReceipt",
17836                &[
17837                    RECEIPT_SUCCESS,
17838                    RECEIPT_SUCCESS,
17839                    RECEIPT_SUCCESS,
17840                    RECEIPT_SUCCESS,
17841                    RECEIPT_SUCCESS,
17842                    RECEIPT_SUCCESS,
17843                ],
17844            )
17845            .with_send_raw_transaction_echo();
17846        let Some((admin_pool, schema, mut client, state)) =
17847            execution_client_with_database("execution_client_test", state).await
17848        else {
17849            return;
17850        };
17851        client.config.unlimited_approval = true;
17852        client.transaction_limits.receipt_timeout_secs = 3;
17853
17854        let wrap_hash = client
17855            .wrap(U256::from(1_000_000_000_000_000u64))
17856            .await
17857            .unwrap();
17858
17859        let record = client
17860            .cache
17861            .get_execution_transaction(42161, &wrap_hash.to_string())
17862            .await
17863            .unwrap()
17864            .unwrap();
17865        assert_eq!(record.nonce, 7);
17866        assert_eq!(record.purpose, "wrap");
17867        assert_eq!(record.status, "finalized");
17868        assert_eq!(
17869            execution_intent_markers(&admin_pool, &schema).await,
17870            vec![("wrap".into(), "finalized".into(), true, false)]
17871        );
17872
17873        let approve_hash = client
17874            .approve(WETH_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
17875            .await
17876            .unwrap();
17877
17878        let record = client
17879            .cache
17880            .get_execution_transaction(42161, &approve_hash.to_string())
17881            .await
17882            .unwrap()
17883            .unwrap();
17884        assert_eq!(record.nonce, 8);
17885        assert_eq!(record.purpose, "approve");
17886        assert_eq!(record.status, "finalized");
17887        assert_eq!(
17888            execution_intent_markers(&admin_pool, &schema).await,
17889            vec![
17890                ("wrap".into(), "finalized".into(), true, false),
17891                ("approve".into(), "finalized".into(), true, false),
17892            ]
17893        );
17894
17895        let requests = state.recorded_requests();
17896        let broadcasts: Vec<_> = requests
17897            .iter()
17898            .filter(|request| request["method"] == "eth_sendRawTransaction")
17899            .collect();
17900        assert_eq!(broadcasts.len(), 2);
17901        for broadcast in &broadcasts {
17902            let payload = broadcast["params"][0].as_str().unwrap();
17903            assert!(payload.starts_with("0x02"), "was: {payload}");
17904        }
17905        let broadcast_indexes = requests
17906            .iter()
17907            .enumerate()
17908            .filter_map(|(index, request)| {
17909                (request["method"] == "eth_sendRawTransaction").then_some(index)
17910            })
17911            .collect::<Vec<_>>();
17912
17913        for (index, expected_block) in broadcast_indexes
17914            .into_iter()
17915            .zip([FIXTURE_BLOCK_PARAM, "0x1cf0d42"])
17916        {
17917            let nonce_fence = &requests[index - 6..index];
17918            assert!(nonce_fence[..3].iter().all(|request| {
17919                request["method"] == "eth_getTransactionCount"
17920                    && request["params"][1] == expected_block
17921            }));
17922            assert!(nonce_fence[3..].iter().all(|request| {
17923                request["method"] == "eth_getTransactionCount" && request["params"][1] == "pending"
17924            }));
17925        }
17926        let receipt_polls = requests
17927            .iter()
17928            .filter(|request| request["method"] == "eth_getTransactionReceipt")
17929            .count();
17930        assert_eq!(receipt_polls, 6);
17931        let allowance_calls: Vec<_> = requests
17932            .iter()
17933            .filter(|request| {
17934                request["method"] == "eth_call"
17935                    && request["params"][0]["data"]
17936                        .as_str()
17937                        .is_some_and(|data| data.starts_with(ALLOWANCE_SELECTOR))
17938            })
17939            .collect();
17940        assert_eq!(allowance_calls.len(), 6);
17941        assert!(
17942            allowance_calls[..3]
17943                .iter()
17944                .all(|request| request["params"][1] == "0x1cf0d42")
17945        );
17946        assert!(
17947            allowance_calls[3..]
17948                .iter()
17949                .all(|request| request["params"][1] == "0x1cf0d41")
17950        );
17951
17952        // Unlimited approval policy encoded U256::MAX in the approve calldata
17953        let estimates: Vec<_> = requests
17954            .iter()
17955            .filter(|request| request["method"] == "eth_estimateGas")
17956            .collect();
17957        assert_eq!(estimates.len(), 6);
17958        assert!(estimates[..3].iter().all(|request| {
17959            request["params"].as_array().unwrap().len() == 2
17960                && request["params"][1] == FIXTURE_BLOCK_PARAM
17961        }));
17962        assert!(estimates[3..].iter().all(|request| {
17963            request["params"].as_array().unwrap().len() == 2 && request["params"][1] == "0x1cf0d42"
17964        }));
17965        let approve_data = estimates[3]["params"][0]["data"].as_str().unwrap();
17966        assert!(
17967            approve_data.starts_with("0x095ea7b3"),
17968            "was: {approve_data}"
17969        );
17970        assert!(
17971            approve_data.ends_with(&"f".repeat(64)),
17972            "was: {approve_data}"
17973        );
17974
17975        for selector in [FACTORY_SELECTOR, WETH9_SELECTOR] {
17976            let calls: Vec<_> = requests
17977                .iter()
17978                .filter(|request| {
17979                    request["method"] == "eth_call"
17980                        && request["params"][0]["data"]
17981                            .as_str()
17982                            .is_some_and(|data| data.starts_with(selector))
17983                })
17984                .collect();
17985            assert!(!calls.is_empty(), "selector {selector}");
17986            assert!(
17987                calls.iter().all(|request| request["params"][1]
17988                    .as_str()
17989                    .is_some_and(|block| block.starts_with("0x"))),
17990                "selector {selector}: {calls:?}"
17991            );
17992        }
17993        let latest_blocks = requests
17994            .iter()
17995            .filter(|request| {
17996                request["method"] == "eth_getBlockByNumber"
17997                    && request["params"] == serde_json::json!(["latest", false])
17998            })
17999            .count();
18000        assert_eq!(latest_blocks, 6);
18001
18002        drop_execution_schema(&admin_pool, &schema).await;
18003    }
18004
18005    #[tokio::test]
18006    async fn reverted_receipt_marks_record_reverted_and_errors() {
18007        let state = signing_rpc_state()
18008            .with_response("eth_getTransactionReceipt", RECEIPT_REVERTED)
18009            .with_send_raw_transaction_echo();
18010        let Some((admin_pool, schema, mut client, _)) =
18011            execution_client_with_database("execution_reverted_test", state).await
18012        else {
18013            return;
18014        };
18015
18016        let error = client
18017            .wrap(U256::from(1_000_000_000_000_000u64))
18018            .await
18019            .unwrap_err();
18020
18021        assert!(
18022            error.to_string().contains("reverted on-chain"),
18023            "was: {error}"
18024        );
18025        assert!(client.in_flight.lock().is_none());
18026
18027        let expected_hash = expected_wrap_tx_hash(U256::from(1_000_000_000_000_000u64)).await;
18028
18029        let record = client
18030            .cache
18031            .get_execution_transaction(42161, &expected_hash.to_string())
18032            .await
18033            .unwrap()
18034            .unwrap();
18035        assert_eq!(record.status, "reverted");
18036        assert_eq!(
18037            execution_intent_markers(&admin_pool, &schema).await,
18038            vec![("wrap".into(), "reverted".into(), true, false)]
18039        );
18040
18041        drop_execution_schema(&admin_pool, &schema).await;
18042    }
18043
18044    fn market_sell_order_with_id(instrument_id: InstrumentId, client_order_id: &str) -> OrderAny {
18045        OrderTestBuilder::new(OrderType::Market)
18046            .trader_id(TraderId::from("TRADER-001"))
18047            .strategy_id(StrategyId::from("S-001"))
18048            .instrument_id(instrument_id)
18049            .client_order_id(ClientOrderId::from(client_order_id))
18050            .side(OrderSide::Sell)
18051            .quantity(Quantity::from("0.001"))
18052            .build()
18053    }
18054
18055    fn market_buy_order_with_id(instrument_id: InstrumentId, client_order_id: &str) -> OrderAny {
18056        OrderTestBuilder::new(OrderType::Market)
18057            .trader_id(TraderId::from("TRADER-001"))
18058            .strategy_id(StrategyId::from("S-001"))
18059            .instrument_id(instrument_id)
18060            .client_order_id(ClientOrderId::from(client_order_id))
18061            .side(OrderSide::Buy)
18062            .quantity(Quantity::from("0.001"))
18063            .build()
18064    }
18065
18066    fn submit_order_list_cmd(orders: &[OrderAny]) -> SubmitOrderList {
18067        let order_list = OrderList::new(
18068            OrderListId::from("OL-001"),
18069            orders[0].instrument_id(),
18070            orders[0].strategy_id(),
18071            orders.iter().map(|order| order.client_order_id()).collect(),
18072            UnixNanos::default(),
18073        );
18074        SubmitOrderList::new(
18075            TraderId::from("TRADER-001"),
18076            Some(ClientId::from("BLOCKCHAIN-001")),
18077            orders[0].strategy_id(),
18078            order_list,
18079            orders
18080                .iter()
18081                .map(|order| order.init_event().clone())
18082                .collect(),
18083            None,
18084            None,
18085            None,
18086            UUID4::new(),
18087            UnixNanos::default(),
18088            None,
18089        )
18090    }
18091
18092    fn modify_order_cmd(
18093        instrument_id: InstrumentId,
18094        client_order_id: ClientOrderId,
18095    ) -> ModifyOrder {
18096        ModifyOrder::new(
18097            TraderId::from("TRADER-001"),
18098            Some(ClientId::from("BLOCKCHAIN-001")),
18099            StrategyId::from("S-001"),
18100            instrument_id,
18101            client_order_id,
18102            None,
18103            Some(Quantity::from("0.002")),
18104            None,
18105            None,
18106            UUID4::new(),
18107            UnixNanos::default(),
18108            None,
18109            None,
18110        )
18111    }
18112
18113    fn cancel_order_cmd(
18114        instrument_id: InstrumentId,
18115        client_order_id: ClientOrderId,
18116    ) -> CancelOrder {
18117        CancelOrder::new(
18118            TraderId::from("TRADER-001"),
18119            Some(ClientId::from("BLOCKCHAIN-001")),
18120            StrategyId::from("S-001"),
18121            instrument_id,
18122            client_order_id,
18123            None,
18124            UUID4::new(),
18125            UnixNanos::default(),
18126            None,
18127            None,
18128        )
18129    }
18130
18131    fn cancel_all_orders_cmd(instrument_id: InstrumentId) -> CancelAllOrders {
18132        CancelAllOrders::new(
18133            TraderId::from("TRADER-001"),
18134            Some(ClientId::from("BLOCKCHAIN-001")),
18135            StrategyId::from("S-001"),
18136            instrument_id,
18137            Some(OrderSide::Sell),
18138            UUID4::new(),
18139            UnixNanos::default(),
18140            None,
18141            None,
18142        )
18143    }
18144
18145    fn batch_cancel_orders_cmd(cancels: Vec<CancelOrder>) -> BatchCancelOrders {
18146        BatchCancelOrders::new(
18147            TraderId::from("TRADER-001"),
18148            Some(ClientId::from("BLOCKCHAIN-001")),
18149            StrategyId::from("S-001"),
18150            cancels[0].instrument_id,
18151            cancels,
18152            UUID4::new(),
18153            UnixNanos::default(),
18154            None,
18155            None,
18156        )
18157    }
18158
18159    fn query_order_cmd(instrument_id: InstrumentId, client_order_id: ClientOrderId) -> QueryOrder {
18160        QueryOrder::new(
18161            TraderId::from("TRADER-001"),
18162            Some(ClientId::from("BLOCKCHAIN-001")),
18163            StrategyId::from("S-001"),
18164            instrument_id,
18165            client_order_id,
18166            None,
18167            UUID4::new(),
18168            UnixNanos::default(),
18169            None,
18170            None,
18171        )
18172    }
18173
18174    async fn unsupported_client_with_mock_rpc()
18175    -> (BlockchainExecutionClient, MockRpcState, Rc<RefCell<Cache>>) {
18176        let state = ready_rpc_state();
18177        let addr = start_mock_rpc_server(state.clone()).await;
18178        let (client, cache) = swap_client_with_cache(test_config(format!("http://{addr}")));
18179        (client, state, cache)
18180    }
18181
18182    #[tokio::test]
18183    async fn submit_order_denies_reduce_only_without_side_effects() {
18184        let (mut client, state, cache) = unsupported_client_with_mock_rpc().await;
18185        let order = OrderTestBuilder::new(OrderType::Market)
18186            .trader_id(TraderId::from("TRADER-001"))
18187            .strategy_id(StrategyId::from("S-001"))
18188            .instrument_id(test_pool().instrument_id)
18189            .client_order_id(ClientOrderId::from("O-REDUCE-ONLY"))
18190            .side(OrderSide::Sell)
18191            .quantity(Quantity::from("0.001"))
18192            .reduce_only(true)
18193            .build();
18194        cache
18195            .borrow_mut()
18196            .add_order(order.clone(), None, None, false)
18197            .unwrap();
18198        let mut receiver = start_with_events(&mut client);
18199
18200        client.submit_order(submit_order_cmd(&order)).unwrap();
18201
18202        let events = collect_order_events(&mut receiver);
18203        assert_eq!(events.len(), 1, "was: {events:?}");
18204        let OrderEventAny::Denied(denied) = &events[0] else {
18205            panic!("expected OrderDenied, was {:?}", events[0]);
18206        };
18207        assert_eq!(denied.client_order_id, order.client_order_id());
18208        assert_eq!(denied.reason, "UNSUPPORTED_REDUCE_ONLY");
18209        assert!(state.recorded_requests().is_empty());
18210        assert!(client.in_flight.lock().is_none());
18211    }
18212
18213    #[tokio::test]
18214    async fn submit_order_list_denies_every_order_without_side_effects() {
18215        let (mut client, state, cache) = unsupported_client_with_mock_rpc().await;
18216        let pool = test_pool();
18217        let first = test_market_sell_order(pool.instrument_id);
18218        let second = market_sell_order_with_id(pool.instrument_id, "O-SWAP-002");
18219        cache
18220            .borrow_mut()
18221            .add_order(second.clone(), None, None, false)
18222            .unwrap();
18223        let orders = [first.clone(), second.clone()];
18224        let mut receiver = start_with_events(&mut client);
18225
18226        client
18227            .submit_order_list(submit_order_list_cmd(&orders))
18228            .unwrap();
18229
18230        let events = collect_order_events(&mut receiver);
18231        let mut denied_ids = Vec::new();
18232
18233        for event in &events {
18234            let OrderEventAny::Denied(denied) = event else {
18235                panic!("expected OrderDenied, was {event:?}");
18236            };
18237            assert_eq!(denied.reason, ORDER_LIST_UNSUPPORTED);
18238            denied_ids.push(denied.client_order_id);
18239        }
18240        denied_ids.sort();
18241        assert_eq!(
18242            denied_ids,
18243            [first.client_order_id(), second.client_order_id()]
18244        );
18245        assert!(state.recorded_requests().is_empty());
18246        assert!(client.in_flight.lock().is_none());
18247
18248        for order in &orders {
18249            let cache_ref = cache.borrow();
18250            let cached = cache_ref.order(&order.client_order_id()).unwrap();
18251            assert_eq!(cached.status(), OrderStatus::Initialized);
18252        }
18253    }
18254
18255    #[tokio::test]
18256    async fn modify_order_rejects_without_side_effects() {
18257        let (mut client, state, cache) = unsupported_client_with_mock_rpc().await;
18258        let order = test_market_sell_order(test_pool().instrument_id);
18259        let mut receiver = start_with_events(&mut client);
18260
18261        client
18262            .modify_order(modify_order_cmd(
18263                order.instrument_id(),
18264                order.client_order_id(),
18265            ))
18266            .unwrap();
18267
18268        let events = collect_order_events(&mut receiver);
18269        assert_eq!(events.len(), 1, "was: {events:?}");
18270        let OrderEventAny::ModifyRejected(rejected) = &events[0] else {
18271            panic!("expected OrderModifyRejected, was {:?}", events[0]);
18272        };
18273        assert_eq!(rejected.client_order_id, order.client_order_id());
18274        assert_eq!(rejected.reason, ORDER_MODIFY_UNSUPPORTED);
18275        assert!(state.recorded_requests().is_empty());
18276        assert!(client.in_flight.lock().is_none());
18277        let cache_ref = cache.borrow();
18278        let cached = cache_ref.order(&order.client_order_id()).unwrap();
18279        assert_eq!(cached.status(), OrderStatus::Initialized);
18280    }
18281
18282    #[tokio::test]
18283    async fn cancel_order_rejects_without_side_effects() {
18284        let (mut client, state, cache) = unsupported_client_with_mock_rpc().await;
18285        let order = test_market_sell_order(test_pool().instrument_id);
18286        let mut receiver = start_with_events(&mut client);
18287
18288        client
18289            .cancel_order(cancel_order_cmd(
18290                order.instrument_id(),
18291                order.client_order_id(),
18292            ))
18293            .unwrap();
18294
18295        let events = collect_order_events(&mut receiver);
18296        assert_eq!(events.len(), 1, "was: {events:?}");
18297        let OrderEventAny::CancelRejected(rejected) = &events[0] else {
18298            panic!("expected OrderCancelRejected, was {:?}", events[0]);
18299        };
18300        assert_eq!(rejected.client_order_id, order.client_order_id());
18301        assert_eq!(rejected.reason, ORDER_CANCEL_UNSUPPORTED);
18302        assert!(state.recorded_requests().is_empty());
18303        assert!(client.in_flight.lock().is_none());
18304        let cache_ref = cache.borrow();
18305        let cached = cache_ref.order(&order.client_order_id()).unwrap();
18306        assert_eq!(cached.status(), OrderStatus::Initialized);
18307    }
18308
18309    #[tokio::test]
18310    async fn batch_cancel_orders_rejects_each_order_without_side_effects() {
18311        let (mut client, state, cache) = unsupported_client_with_mock_rpc().await;
18312        let pool = test_pool();
18313        let first = test_market_sell_order(pool.instrument_id);
18314        let second = market_sell_order_with_id(pool.instrument_id, "O-SWAP-002");
18315        cache
18316            .borrow_mut()
18317            .add_order(second.clone(), None, None, false)
18318            .unwrap();
18319        let orders = [first.clone(), second.clone()];
18320        let cancels = orders
18321            .iter()
18322            .map(|order| cancel_order_cmd(order.instrument_id(), order.client_order_id()))
18323            .collect();
18324        let mut receiver = start_with_events(&mut client);
18325
18326        client
18327            .batch_cancel_orders(batch_cancel_orders_cmd(cancels))
18328            .unwrap();
18329
18330        let events = collect_order_events(&mut receiver);
18331        let mut rejected_ids = Vec::new();
18332
18333        for event in &events {
18334            let OrderEventAny::CancelRejected(rejected) = event else {
18335                panic!("expected OrderCancelRejected, was {event:?}");
18336            };
18337            assert_eq!(rejected.reason, ORDER_CANCEL_UNSUPPORTED);
18338            rejected_ids.push(rejected.client_order_id);
18339        }
18340        rejected_ids.sort();
18341        assert_eq!(
18342            rejected_ids,
18343            [first.client_order_id(), second.client_order_id()]
18344        );
18345        assert!(state.recorded_requests().is_empty());
18346        assert!(client.in_flight.lock().is_none());
18347    }
18348
18349    #[tokio::test]
18350    async fn cancel_all_and_query_order_log_unsupported_without_side_effects() {
18351        let (mut client, state, cache) = unsupported_client_with_mock_rpc().await;
18352        let order = test_market_sell_order(test_pool().instrument_id);
18353        let mut receiver = start_with_events(&mut client);
18354
18355        client
18356            .cancel_all_orders(cancel_all_orders_cmd(order.instrument_id()))
18357            .unwrap();
18358        client
18359            .query_order(query_order_cmd(
18360                order.instrument_id(),
18361                order.client_order_id(),
18362            ))
18363            .unwrap();
18364
18365        let events = collect_order_events(&mut receiver);
18366        assert!(events.is_empty(), "was: {events:?}");
18367        assert!(state.recorded_requests().is_empty());
18368        assert!(client.in_flight.lock().is_none());
18369        let cache_ref = cache.borrow();
18370        let cached = cache_ref.order(&order.client_order_id()).unwrap();
18371        assert_eq!(cached.status(), OrderStatus::Initialized);
18372    }
18373
18374    #[tokio::test]
18375    async fn unsupported_commands_handle_unknown_orders_without_panic() {
18376        let (mut client, state, _) = unsupported_client_with_mock_rpc().await;
18377        let instrument_id = test_pool().instrument_id;
18378        let unknown = ClientOrderId::from("O-UNKNOWN");
18379        let mut receiver = start_with_events(&mut client);
18380
18381        client
18382            .modify_order(modify_order_cmd(instrument_id, unknown))
18383            .unwrap();
18384        client
18385            .cancel_order(cancel_order_cmd(instrument_id, unknown))
18386            .unwrap();
18387        client
18388            .batch_cancel_orders(batch_cancel_orders_cmd(vec![cancel_order_cmd(
18389                instrument_id,
18390                unknown,
18391            )]))
18392            .unwrap();
18393        client
18394            .query_order(query_order_cmd(instrument_id, unknown))
18395            .unwrap();
18396
18397        let events = collect_order_events(&mut receiver);
18398        assert!(events.is_empty(), "was: {events:?}");
18399        assert!(state.recorded_requests().is_empty());
18400        assert!(client.in_flight.lock().is_none());
18401    }
18402
18403    #[tokio::test]
18404    async fn report_generators_error_except_mass_status_without_side_effects() {
18405        let (client, state, _) = unsupported_client_with_mock_rpc().await;
18406
18407        let report = client
18408            .generate_order_status_report(&GenerateOrderStatusReport::new(
18409                UUID4::new(),
18410                UnixNanos::default(),
18411                None,
18412                None,
18413                None,
18414                None,
18415                None,
18416            ))
18417            .await
18418            .unwrap_err();
18419        assert_eq!(report.to_string(), VENUE_EXECUTION_REPORTS_UNSUPPORTED);
18420
18421        let reports = client
18422            .generate_order_status_reports(&GenerateOrderStatusReports::new(
18423                UUID4::new(),
18424                UnixNanos::default(),
18425                false,
18426                None,
18427                None,
18428                None,
18429                None,
18430                None,
18431            ))
18432            .await
18433            .unwrap_err();
18434        assert_eq!(reports.to_string(), VENUE_EXECUTION_REPORTS_UNSUPPORTED);
18435
18436        let fills = client
18437            .generate_fill_reports(GenerateFillReports::new(
18438                UUID4::new(),
18439                UnixNanos::default(),
18440                None,
18441                None,
18442                None,
18443                None,
18444                None,
18445                None,
18446            ))
18447            .await
18448            .unwrap_err();
18449        assert_eq!(fills.to_string(), VENUE_EXECUTION_REPORTS_UNSUPPORTED);
18450
18451        let positions = client
18452            .generate_position_status_reports(&GeneratePositionStatusReports::new(
18453                UUID4::new(),
18454                UnixNanos::default(),
18455                None,
18456                None,
18457                None,
18458                None,
18459                None,
18460            ))
18461            .await
18462            .unwrap_err();
18463        assert_eq!(positions.to_string(), VENUE_EXECUTION_REPORTS_UNSUPPORTED);
18464
18465        let mass_status = client.generate_mass_status(None).await.unwrap();
18466        assert!(mass_status.is_none());
18467
18468        let mass_status = client.generate_mass_status(Some(60)).await.unwrap();
18469        assert!(mass_status.is_none());
18470
18471        assert!(state.recorded_requests().is_empty());
18472        assert!(client.in_flight.lock().is_none());
18473    }
18474
18475    async fn connect_test_postgres(
18476        test_name: &str,
18477    ) -> Option<(sqlx::PgPool, PostgresConnectOptions)> {
18478        let pg_config = get_postgres_connect_options(None, None, None, None, None);
18479        let admin_options: sqlx::postgres::PgConnectOptions = pg_config.clone().into();
18480        let admin_pool = match PgPoolOptions::new()
18481            .max_connections(1)
18482            .connect_with(admin_options)
18483            .await
18484        {
18485            Ok(pool) => pool,
18486            Err(e) => {
18487                eprintln!("Postgres unavailable; skipping {test_name} test: {e}");
18488                return None;
18489            }
18490        };
18491
18492        Some((admin_pool, pg_config))
18493    }
18494
18495    async fn execution_client_with_database(
18496        test_name: &str,
18497        state: MockRpcState,
18498    ) -> Option<(
18499        sqlx::PgPool,
18500        String,
18501        BlockchainExecutionClient,
18502        MockRpcState,
18503    )> {
18504        let (admin_pool, schema, mut client, state) =
18505            execution_client_with_unprotected_database(test_name, state).await?;
18506        protect_test_storage(&mut client, &schema).await;
18507        Some((admin_pool, schema, client, state))
18508    }
18509
18510    async fn execution_client_with_unprotected_database(
18511        test_name: &str,
18512        state: MockRpcState,
18513    ) -> Option<(
18514        sqlx::PgPool,
18515        String,
18516        BlockchainExecutionClient,
18517        MockRpcState,
18518    )> {
18519        let (admin_pool, pg_config) = connect_test_postgres(test_name).await?;
18520        let schema = format!("{test_name}_{}", std::process::id());
18521        setup_execution_schema(&admin_pool, &schema).await;
18522
18523        let db_options: sqlx::postgres::PgConnectOptions = pg_config.into();
18524        let db_options = db_options.options([("search_path", schema.clone())]);
18525        let database = connect_test_database(db_options).await.unwrap();
18526        let addr = start_mock_rpc_server(state.clone()).await;
18527        let mut client = test_client(format!("http://{addr}"));
18528        client.cache.database = Some(database);
18529        // Mirror the connect-time migration: tests create the pre-submission table shape
18530        client
18531            .cache
18532            .ensure_execution_transaction_schema()
18533            .await
18534            .unwrap();
18535        initialize_test_verification_ledger(&client).await;
18536        client.signer = Some(Arc::new(
18537            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
18538        ));
18539        client.core.set_connected();
18540
18541        Some((admin_pool, schema, client, state))
18542    }
18543
18544    async fn install_reservation_commit_rejection(admin_pool: &sqlx::PgPool, schema: &str) {
18545        for statement in [
18546            format!(
18547                "CREATE FUNCTION {schema}.reject_execution_reservation_commit() RETURNS trigger \
18548                 LANGUAGE plpgsql AS 'BEGIN RAISE EXCEPTION ''test reservation commit rejection''; \
18549                 RETURN NEW; END'"
18550            ),
18551            format!(
18552                "CREATE CONSTRAINT TRIGGER reject_execution_reservation_commit AFTER INSERT ON \
18553                 {schema}.execution_transaction_transition DEFERRABLE INITIALLY DEFERRED \
18554                 FOR EACH ROW EXECUTE FUNCTION {schema}.reject_execution_reservation_commit()"
18555            ),
18556        ] {
18557            sqlx::query(sqlx::AssertSqlSafe(statement))
18558                .execute(admin_pool)
18559                .await
18560                .unwrap();
18561        }
18562    }
18563
18564    async fn install_recoverable_commit_rejection(admin_pool: &sqlx::PgPool, schema: &str) {
18565        for statement in [
18566            format!(
18567                "CREATE FUNCTION {schema}.reject_recoverable_commit() RETURNS trigger \
18568                 LANGUAGE plpgsql AS 'BEGIN IF NEW.transition_key = ''recoverable'' THEN \
18569                 RAISE EXCEPTION ''test recoverable commit rejection''; END IF; \
18570                 RETURN NEW; END'"
18571            ),
18572            format!(
18573                "CREATE CONSTRAINT TRIGGER reject_recoverable_commit AFTER INSERT ON \
18574                 {schema}.execution_transaction_transition DEFERRABLE INITIALLY DEFERRED \
18575                 FOR EACH ROW EXECUTE FUNCTION {schema}.reject_recoverable_commit()"
18576            ),
18577        ] {
18578            sqlx::query(sqlx::AssertSqlSafe(statement))
18579                .execute(admin_pool)
18580                .await
18581                .unwrap();
18582        }
18583    }
18584
18585    async fn drop_execution_schema(admin_pool: &sqlx::PgPool, schema: &str) {
18586        sqlx::query(sqlx::AssertSqlSafe(format!("DROP SCHEMA {schema} CASCADE")))
18587            .execute(admin_pool)
18588            .await
18589            .unwrap();
18590    }
18591
18592    async fn setup_execution_schema(admin_pool: &sqlx::PgPool, schema: &str) {
18593        for statement in [
18594            format!("CREATE SCHEMA {schema}"),
18595            format!(
18596                r#"CREATE TABLE {schema}."chain" (chain_id INTEGER PRIMARY KEY, name TEXT NOT NULL)"#
18597            ),
18598            format!(r#"INSERT INTO {schema}."chain" (chain_id, name) VALUES (42161, 'Arbitrum')"#),
18599            format!(
18600                r#"CREATE TABLE {schema}."execution_transaction" (
18601                    id BIGSERIAL PRIMARY KEY,
18602                    chain_id INTEGER NOT NULL REFERENCES {schema}."chain"(chain_id) ON DELETE CASCADE,
18603                    nonce BIGINT NOT NULL,
18604                    transaction_hash TEXT NOT NULL,
18605                    purpose TEXT NOT NULL,
18606                    status TEXT NOT NULL,
18607                    UNIQUE (chain_id, transaction_hash)
18608                )"#
18609            ),
18610        ] {
18611            sqlx::query(sqlx::AssertSqlSafe(statement))
18612                .execute(admin_pool)
18613                .await
18614                .unwrap();
18615        }
18616    }
18617
18618    fn execution_transaction_create_sql() -> &'static str {
18619        const TABLES_SQL: &str = include_str!("../../../../../schema/sql/tables.sql");
18620        const START: &str = "CREATE TABLE IF NOT EXISTS \"execution_transaction\"";
18621        let start = TABLES_SQL
18622            .find(START)
18623            .expect("execution_transaction table is missing from tables.sql");
18624        let statement = &TABLES_SQL[start..];
18625        let end = statement
18626            .find(";\n")
18627            .expect("execution_transaction CREATE TABLE is unterminated")
18628            + 1;
18629        &statement[..end]
18630    }
18631}